34e338d0 |
package com.fiddlerwoaroof.experiments.graphql_addressbook
|
5eb9d929 |
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server._
import akka.stream.ActorMaterializer
import sangria.ast.Document
import sangria.execution.{ErrorWithResolver, Executor, QueryAnalysisError}
|
34e338d0 |
import sangria.macros.derive._
|
5eb9d929 |
import sangria.marshalling.sprayJson._
import sangria.parser.QueryParser
|
34e338d0 |
import sangria.schema._
|
5eb9d929 |
import spray.json.{JsObject, JsString, JsValue}
|
34e338d0 |
|
5eb9d929 |
import cats.Monoid
import cats.instances.all._
|
34e338d0 |
|
5eb9d929 |
import scala.util.{Failure, Success}
|
34e338d0 |
trait Identifiable {
def id: String
}
|
5eb9d929 |
class AddressBook {
import AddressBook.PNHelper
private val Contacts = List(
Contact("1",
EnglishName("John", Some("Apple"), "Seed"),
|
964ee21a |
Address("1103 Foo St.", "Ventura", "CA", "93003", "USA"),
pn"333-444-3333"),
|
5eb9d929 |
Contact("1",
EnglishName("Bob", None, "Marley"),
|
964ee21a |
Address("1103 Maricopa Ave.", "Ventura", "CA", "93003", "USA"),
pn"435-2039"),
|
5eb9d929 |
)
def contact(id: String): Option[Contact] =
Contacts find (_.id == id)
def addressesByPartialName(name: String, addressType: String = "home"): Seq[Address] =
Contacts
.filter(_.name.name.toLowerCase contains name)
|
964ee21a |
.map(_.address)
|
5eb9d929 |
|
964ee21a |
def contacts: List[Contact] = Contacts
|
5eb9d929 |
}
object AddressBook {
implicit class PNHelper(private val sc: StringContext) extends AnyVal {
def pn(args: Any*): PhoneNumber = {
val str = sc.parts.head
val parts = str.split('-')
parts match {
case Array(cc, ac, pref, suf) => PhoneNumber(cc.toInt, ac.toInt, pref.toInt, suf.toInt)
case Array(ac, pref, suf) => PhoneNumber(1, ac.toInt, pref.toInt, suf.toInt)
case Array(pref, suf) => PhoneNumber(1, 805, pref.toInt, suf.toInt)
}
}
}
}
|
34e338d0 |
case class Picture(width: Int, height: Int, url: Option[String])
|
964ee21a |
case class PhoneNumber(countryCode: Int, areaCode: Int, prefix: Int, suffix: Int) {
def formatted = f"(${areaCode}%03d) ${prefix}%03d-${suffix}%04d"
def localFormatted = f"${prefix}%03d-${suffix}%04d"
def intlFormatted = f"+$countryCode (${areaCode}%03d) ${prefix}%03d-${suffix}%04d"
}
|
5eb9d929 |
case class Address(address: String, city: String, state: String, zip: String, country: String)
trait Name {
def name: String
def sortName: String
}
case class EnglishName(first: String, middle: Option[String], last: String) extends Name {
def name: String = s"$first ${middle.map(x => s"$x ").getOrElse("")}$last"
def sortName: String = s"$last, $first"
}
|
964ee21a |
case class Contact(id: String, name: Name, address: Address, phoneNumber: PhoneNumber) extends Identifiable {
|
34e338d0 |
def picture(size: Int): Picture =
Picture(
width = size,
height = size,
url = Some(s"//cdn.com/$size/$id.jpg"))
}
|
5eb9d929 |
object Hello extends App {
|
964ee21a |
implicit val PictureType: ObjectType[Unit, Picture] =
|
34e338d0 |
deriveObjectType[Unit, Picture](
ObjectTypeDescription("The product picture"),
DocumentField("url", "Picture CDN URL"))
|
964ee21a |
val IdentifiableType: InterfaceType[Unit, Identifiable] =
|
34e338d0 |
InterfaceType(
"Identifiable",
"Entity that can be identified",
fields[Unit, Identifiable](
Field("id", StringType, resolve = _.value.id)))
|
964ee21a |
implicit val AddressType: ObjectType[Unit, Address] =
|
5eb9d929 |
deriveObjectType[Unit, Address](
|
964ee21a |
ObjectTypeDescription("A Contact's address"))
implicit val PhoneNumberType: ObjectType[Unit, PhoneNumber] =
deriveObjectType[Unit, PhoneNumber](
IncludeMethods("formatted", "localFormatted", "intlFormatted"))
val NameType: InterfaceType[Unit, Name] =
InterfaceType(
"Name",
"An interface for things that represent a name",
fields[Unit, Name](
Field("name", StringType, resolve = _.value.name),
Field("sortName", StringType, resolve = _.value.sortName)))
implicit val EnglishNameType: ObjectType[Unit, EnglishName] =
deriveObjectType[Unit, EnglishName](
Interfaces(NameType))
val ContactType =
deriveObjectType[Unit, Contact](
|
34e338d0 |
Interfaces(IdentifiableType),
|
964ee21a |
IncludeMethods("picture"))
|
34e338d0 |
val Id = Argument("id", StringType)
val QueryType =
|
5eb9d929 |
ObjectType("Query", fields[AddressBook, Unit](
Field("contact", OptionType(ContactType),
|
34e338d0 |
description = Some("Return product with specific `id`."),
arguments = Id :: Nil,
|
5eb9d929 |
resolve = c => c.ctx.contact(c arg Id)),
|
34e338d0 |
|
964ee21a |
Field("contacts", ListType(ContactType),
|
34e338d0 |
description = Some("Returns all products"),
|
964ee21a |
resolve = _.ctx.contacts)
|
34e338d0 |
))
|
5eb9d929 |
val schema = Schema(QueryType)
implicit val system = ActorSystem("sangria-server")
implicit val materializer = ActorMaterializer()
import system.dispatcher
val route: Route =
(post & path("graphql")) {
entity(as[JsValue]) {
requestJson => graphQLEndpoint(requestJson)
}
} ~
get {
getFromResource("graphiql.html")
}
def graphQLEndpoint(requestJson: JsValue) = {
val JsObject(fields) = requestJson
val JsString(query) = fields("query")
val operation = fields.get("operationName") collect {
case JsString(op) => op
}
val vars = fields.get("variables") match {
case Some(obj: JsObject) => obj
case _ => JsObject.empty
}
QueryParser.parse(query) match {
case Success(queryAst) =>
complete(executeGraphQLQuery(queryAst, operation, vars))
case Failure(error) =>
complete(BadRequest, JsObject("error" -> JsString(error.getMessage)))
}
}
|
34e338d0 |
|
5eb9d929 |
def executeGraphQLQuery(query: Document, op: Option[String], vars: JsObject) =
Executor.execute(schema, query, new AddressBook,
variables = vars,
operationName = op)
.map(OK -> _)
.recover {
case error: QueryAnalysisError => BadRequest -> error.resolveError
case error: ErrorWithResolver => InternalServerError -> error.resolveError
}
Http().bindAndHandle(route, "0.0.0.0", 4930)
|
34e338d0 |
}
|