Skip to content

Instantly share code, notes, and snippets.

@cgbystrom
Created July 16, 2015 17:31
Show Gist options
  • Select an option

  • Save cgbystrom/448fd0f2aad91f28c45e to your computer and use it in GitHub Desktop.

Select an option

Save cgbystrom/448fd0f2aad91f28c45e to your computer and use it in GitHub Desktop.

Revisions

  1. cgbystrom created this gist Jul 16, 2015.
    192 changes: 192 additions & 0 deletions StarWarsSchema.scala
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,192 @@

    // An example for how to map a GraphQL schema on to Scala's type system.
    // Work in progress and still lots of unknowns but the great benefit we are after is true type safety at compile time.
    // Tries to mimic the original Star Wars schema found in graphql-js reference impl.
    // The schema below does not deal with Futures at all but it something we definitively need to support.

    import scala.annotation.StaticAnnotation

    // Test fixture data
    object Data {
    val luke = new Human(
    id = "1000",
    name = "Luke Skywalker",
    friendIds = List("1002", "1003", "2000", "2001"),
    appearsInIds = List(4, 5, 6),
    _homePlanet = Some("Tatooine")
    )

    val vader = new Human(
    id = "1001",
    name = "Darth Vader",
    friendIds = List("1004"),
    appearsInIds = List(4, 5, 6),
    _homePlanet = Some("Tatooine")
    )

    val han = new Human(
    id = "1002",
    name = "Han Solo",
    friendIds = List("1000", "1003", "2001"),
    appearsInIds = List(4, 5, 6)
    )

    val leia = new Human(
    id = "1002",
    name = "Leia Organa",
    friendIds = List("1000", "1002", "2000", "2001"),
    appearsInIds = List(4, 5, 6),
    _homePlanet = Some("Alderaan")
    )

    val tarkin = new Human(
    id = "1004",
    name = "Wilhuff Tarkin",
    friendIds = List("1001"),
    appearsInIds = List(4)
    )

    val humanData = Map(
    "1000" -> luke,
    "1001" -> vader,
    "1002" -> han,
    "1003" -> leia,
    "1004" -> tarkin
    )

    val threepio = new Droid(
    id = "2000",
    name = "C-3PO",
    friendIds = List("1000", "1002", "1003", "2001"),
    appearsInIds = List(4, 5, 6),
    _primaryFunction = Some("Protocol")
    )

    val artoo = new Droid(
    id = "2001",
    name = "R2-D2",
    friendIds = List("1000", "1002", "1003"),
    appearsInIds = List(4, 5, 6),
    _primaryFunction = Some("Astromech")
    )

    val droidData = Map(
    "2000" -> threepio,
    "2001" -> artoo
    )

    /** Helper function to get a character by ID. */
    def getCharacter(id: String) = Option(humanData.getOrElse(id, droidData.getOrElse(id, null)))

    /** Helper function to get a character by ID. */
    def getEpisode(id: Int) = Option(Episode.lookup(id))

    /** Allows us to query for a character's friends. */
    def getFriends(friendsIds: List[String]): List[Character] = friendsIds.flatMap(getCharacter)

    /** Allows us to query for a character's friends. */
    def getEpisodes(episodeIds: List[Int]) = episodeIds.flatMap(getEpisode)
    }

    // Placeholders for now. For Scala to retain annotations at runtime, they need to be declared in Java.
    // Annotations can be used to express metadata needed by the GraphQL introspection.
    case class Interface(desc: String = "__notset") extends StaticAnnotation
    case class Field(desc: String = "__notset") extends StaticAnnotation
    case class Object(desc: String = "__notset") extends StaticAnnotation

    // A NonNull annotation might be useful over Scala's Option since GraphQL can guarantee
    // that input are for example, non-null. Hence no need for handling it in end-user code at all.
    case class NonNull() extends StaticAnnotation

    // Ghetto implementation of a GraphQL Enum. Consider it a placeholder.
    // TBD: Use Scala Enumeration or a GraphQLEnum?
    case class Episode(value: Any, description: String)
    object Episode {
    val NEWHOPE = Episode(4, "Released in 1977.")
    val EMPIRE = Episode(5, "Released in 1980.")
    val JEDI = Episode(6, "Released in 1983.")
    val lookup = Map(NEWHOPE.value -> NEWHOPE, EMPIRE.value -> EMPIRE, JEDI.value -> JEDI)
    }

    // Example below uses Scala native data types instead of GraphQLString, GraphQLList etc.
    // Still something we need to figure out.

    @Interface(desc="A character in the Star Wars Trilogy")
    trait Character {
    @Field(desc="The id of the character.")
    def id: String

    @Field(desc="The name of the character.")
    def name: String

    @Field(desc="The friends of the character, or an empty list if they have none.")
    def friends: List[Character]

    @Field(desc="Which movies they appear in.")
    def appearsIn: List[Episode]

    // TBD: How do handle resolveType?
    // In graphql-js, Character is aware of its implementors (why?).
    }

    // Human and Droid are actual classes and instances unlike graphql-js. The reason for this is to enforce type safety.
    // Rather than relying on the untyped "source" parameter JS object/Scala Map,
    // the data required for Human/Droid to do it's job is passed to the constructor.

    // It is of course a fine balance how much data you should require in the constructor.
    // Preferably as little a possible to avoid overfetching from underlying data sources (other services or a database).
    // Additional nesting of fields/objects will likely help address this problem.

    @Object(desc="A humanoid creature in the Star Wars universe.")
    case class Human(id: String, name: String, friendIds: List[String], appearsInIds: List[Int], _homePlanet: Option[String] = None) extends Character {
    // Note how friends and episodes will only be resolved when asked for (by calling the function, aka the resolver)
    // Right now, these are fast in-memory lookups but they can be something slow, say a database query.
    override def friends = Data.getFriends(friendIds)
    override def appearsIn = Data.getEpisodes(appearsInIds)

    @Field(desc="The home planet of the human, or null if unknown.")
    def homePlanet: String = _homePlanet.orNull
    }

    @Object(desc="A mechanical creature in the Star Wars universe.")
    case class Droid(id: String, name: String, friendIds: List[String], appearsInIds: List[Int], _primaryFunction: Option[String] = None) extends Character {
    // Same as in Human. The reason for these being duplicated in each GraphQL object class (Human and Droid)
    // is that a GraphQLInterface cannot have resolve functions.
    override def friends = Data.getFriends(friendIds)
    override def appearsIn = Data.getEpisodes(appearsInIds)

    @Field(desc="The primary function of the droid.")
    def primaryFunction: String = _primaryFunction.orNull
    }

    @Object
    class Query {
    def hero: Character = Data.artoo

    def human(@NonNull @Field("ID of the human") id: String): Human = {
    // Not ideal null checking, but got type erasure if matching on Option[Character].
    val c = Data.getCharacter(id).orNull
    c match {
    case h: Human => h
    case _ => throw new IllegalArgumentException("Invalid human ID given")
    }
    }

    def droid(@NonNull id: String): Droid = {
    val c = Data.getCharacter(id).orNull
    c match {
    case d: Droid => d
    case _ => throw new IllegalArgumentException("Invalid droid ID given")
    }
    }
    }

    object Test {
    def main(args: Array[String]) {
    val q = new Query()
    println(s"The true hero in Star Wars is ${q.hero.name}")
    println(s"A friend of the hero is ${q.hero.friends(0).name}")
    println(s"Darth Vader appears in ${q.human("1001").appearsIn}")
    println(s"C3-PO's primary function is ${q.droid("2000").primaryFunction}")
    }
    }