A simple GraphQL client on top of Sangria, Akka HTTP and Circe.
Clone or download
Latest commit 7ed02f7 Oct 14, 2018
Permalink
Failed to load latest commit information.
project bump deps Oct 13, 2018
src Scala 2.11 work arrounds Oct 14, 2018
.gitignore Initial code Jan 19, 2018
.scalafix.conf Initial code Jan 19, 2018
.scalafmt.conf Initial code Jan 19, 2018
CHANGELOG.md update readme Oct 13, 2018
LICENSE Add license and bintray plugin Jan 19, 2018
README.md update readme Oct 13, 2018
build.sbt release 2.5 Oct 14, 2018
version.sbt release 2.5 Oct 14, 2018

README.md

Drunk

A simple GraphQL client on top of Sangria, Akka HTTP and Circe.

Quickstart

Add the following dependency:

  resolvers += Resolver.bintrayRepo("jarlakxen", "maven")

  "com.github.jarlakxen" %% "drunk" % "2.5.0"

Then, import:

  import com.github.jarlakxen.drunk._
  import io.circe._, io.circe.generic.semiauto._
  import sangria.macros._

There are three ways to create a GraphQLClient:

  1. As Akka Https flow connection
 import akka.http.scaladsl.model.Uri
 
 val uri: Uri = Uri(s"https://$host:$port/api/graphql")

 val http: HttpExt = Http()
 val flow: Flow[HttpRequest, HttpResponse, Future[OutgoingConnection]] = http.outgoingConnectionHttps(uri.authority.host.address(), uri.effectivePort)
 val client = GraphQLClient(uri, flow, clientOptions = ClientOptions.Default, headers = Nil)

  1. As Akka Http flow connection
 import akka.http.scaladsl.model.Uri
 
 val uri: Uri = Uri(s"http://$host:$port/api/graphql")

 val http: HttpExt = Http()
 val flow: Flow[HttpRequest, HttpResponse, Future[OutgoingConnection]] = http.outgoingConnection(uri.authority.host.address(), uri.effectivePort)
 val client = GraphQLClient(uri, flow, clientOptions = ClientOptions.Default, headers = Nil)

  1. As Akka Http single request
  val client = GraphQLClient(s"http://$host:$port/api/graphql")

Then, query:

  val query =
    graphql"""
      query HeroAndFriends {
        hero {
          id
          name
          friends {
            name
          }
          appearsIn
        }
      }
    """
      
  val cursor: GraphQLCursor = client.query[HeroQuery](query)
  val data: Future[GraphQLResponse[HeroQuery]] = cursor.result

Working with the GraphQLCursor

  type HerosQuery = Map[String, List[Hero]]
  
  case class Pagination(offset: Int, size: Int)

  val query =
    graphql"""
      query Heros($offset: Int, $size: Int) {
        heros(offset: $offset, size: $size) {
          id
          name
          friends {
            name
          }
          appearsIn
        }
      }
    """
      
  val page1: GraphQLCursor[HerosQuery, Pagination] = 
    client.query(query, Pagination(0, 10))
  val page2: GraphQLCursor[HerosQuery, Pagination] = 
    page1.fetchMore(lastPage => lastPage.copy(offset = lastPage.offset + lastPage.size ) )

Mutations

  import com.github.jarlakxen.drunk._
  import io.circe._, io.circe.generic.semiauto._
  import sangria.macros._

  case class User(id: String, name: String)
  
  val client = GraphQLClient(s"http://$host:$port/api/graphql")

  val mutation =
    graphql"""
      mutation($user1: String!, $user2: String!) {
        user1: newUser(name: $user1) {
          id
          name
        }
        user2: newUser(name: $user2) {
          id
          name
        }
      }
    """
    val result: Future[GraphQLResponse[Map[String, User]]] = 
      client.query(mutation, Map("user1" -> "123", "user2" -> "456"))

Schema Introspection

  import com.github.jarlakxen.drunk._
  import sangria.introspection.IntrospectionSchema

  val client = GraphQLClient(s"http://$host:$port/api/graphql")
      
  val result: Future[GraphQLResponse[IntrospectionSchema]] = client.schema

Typename Derive

It's very common in GraphQL to have response with polymorphic objects in the responses. One way to discriminate the type of object is to check the __typename field. For that purpose there an special derive decoder in com.github.jarlakxen.drunk.circe._:

  import com.github.jarlakxen.drunk.circe._
  import io.circe._, io.circe.generic.semiauto._

  trait Character {
    def id: String
    def name: Option[String]
    def friends: List[String]
    def appearsIn: List[Episode.Value]
  }
  
  case class Human(
    id: String,
    name: Option[String],
    friends: List[String],
    appearsIn: List[Episode.Value],
    homePlanet: Option[String]) extends Character
  
  case class Droid(
    id: String,
    name: Option[String],
    friends: List[String],
    appearsIn: List[Episode.Value],
    primaryFunction: Option[String]) extends Character
    
  implicit val humanDecoder: Decoder[Human] = deriveDecoder
  implicit val droidDecoder: Decoder[Droid] = deriveDecoder
  
  implicit val characterDecoder: Decoder[Character] = deriveByTypenameDecoder(
    "Human".decodeAs[Human], // for __typename: 'Human' is going to use humanDecoder
    "Droid".decodeAs[Droid] // for __typename: 'Droid' is going to use droidDecoder
  )

This code can be use to parse a response like:

{
  "__typename": "Droid"
  "name": "R2D2"
  ....
}

Take into account the client automatically adds the '__typename' field to every selector, so it's not required to be added in the queries.

Extensions

There are several extension for GraphQL, this client supports:

To get the information of the extensions you can:

  val cursor: GraphQLCursor = client.query[HeroQuery](query)
  val extensions: Future[GraphQLExtensions] = cursor.extensions
  val metrics: Future[Option[GraphQLMetricsExtension]] = extensions.map(_.metrics)
  val cacheControl: Future[Option[GraphQLCacheControlExtension]] = extensions.map(_.cacheControl)

Contributing

If you have a question, or hit a problem, feel free to ask in the issues!

Or, if you encounter a bug, something is unclear in the code or documentation, don’t hesitate and open an issue.