git.fiddlerwoaroof.com
graphql-addressbook/src/main/scala/com/fiddlerwoaroof/experiments/graphql_addressbook/Hello.scala
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
 }