We’re making an API to collect cats. We’ll be using ZIO 2.0, and Caliban

This is what a cat looks like. With a repository to collect them.

case class Cat(name: String, color: Color, pattern: Pattern)

object Cat {
  case class Color(s: String) extends AnyVal

  sealed abstract class Pattern extends EnumEntry
  object Pattern extends Enum[Pattern] {
    case object Stripes extends Pattern
    case object Plain extends Pattern

    val values: IndexedSeq[Pattern] = findValues
  }
}

object CatRepo {
  //in memory database of cats. not threadsafe or reasonable
  var catDb: List[Cat] = List(Cat("Koffie", Color("grey"), Pattern.Stripes), Cat("Minoes", Color("brown"), Pattern.Plain))

  def getAllCats: List[Cat] = catDb

  def addCat(cat: Cat): Cat = {
    catDb = cat :: catDb
    cat
  }
}

Then we can create the API around it. There are adapters for many http libraries. Here we use Akka Http.

case class Queries(cats: () => List[Cat])
case class Mutations(addCat: Cat => Cat)

class Api {
  implicit val runtime = zio.Runtime.default
  val interpreter = runtime.unsafeRun(createInterpreter)
  def route = AkkaHttpAdapter.makeHttpService(interpreter)

  def createInterpreter = {
    GraphQL.graphQL(
      RootResolver(
        Queries(
          () => CatRepo.getAllCats
        ),
        Mutations(
          CatRepo.addCat
        )
      )
    ).interpreter
  }
}

For the complete code check https://github.com/tammosminia/calibanExample

We can run a request using curl to see that it works

$ curl --request POST \
>   --url http://localhost:8080/graphql \
>   --header 'Content-Type: application/json' \
>   --data '{"query":"query cats {\n\tcats {\n\t\tname\n\t\tcolor\n\t\tpattern\n\t}\n}","variables":{},"operationName":"cats"}'
{"data":{"cats":[{"name":"Koffie","color":"grey","pattern":"Stripes"},{"name":"Minoes","color":"brown","pattern":"Plain"}]}}

$ curl --request POST \
>   --url http://localhost:8080/graphql \
>   --header 'Content-Type: application/json' \
>   --data '{"query":"mutation addCat {\n  addCat(name: \"Neighbour\", color: \"black\", pattern: Plain) {\n    name\n    color\n    pattern\n  }\n}","variables":{"name":"Neighbour","color":"black","pattern":"Plain"},"operationName":"addCat"}'
{"data":{"addCat":{"name":"Neighbour","color":"black","pattern":"Plain"}}}

We can see the GraphQl schema with get-graphql-schema

(base) tammo@ananas:~$ get-graphql-schema http:localhost:8080
schema {
  query: Queries
  mutation: Mutations
}

type Cat {
  name: String!
  color: String!
  pattern: Pattern!
}

type Mutations {
  addCat(name: String!, color: String!, pattern: Pattern!): Cat!
}

enum Pattern {
  Plain
  Stripes
}

type Queries {
  cats: [Cat!]!
}

Caliban has some good defaults for generating a schema, but maybe we to have some things in a different way. For example Cat.Color is a value class, which is just shown as String here. I like color to be a type in the schema with some documentation. To do this I can create an implicit for Cat.Color and import it in Api.

object ApiSchema extends GenericSchema[Any] {
  implicit val colorSchema: Schema[Any, Color] = scalarSchema[Color]("CatColor", Some("the color of the cat"), None, c => StringValue(c.s))
}

Resulting in this schema:

type Cat {
  name: String!
  color: CatColor!
  pattern: Pattern!
}

"""the color of the cat"""
scalar CatColor

type Mutations {
  addCat(name: String!, color: CatColor!, pattern: Pattern!): Cat!
}

Let’s make our example a bit more realistic. We’ll move the CatRepo into the Zio environment. Seen from the outside it will remain the same.

class CatRepo {
  //in memory database of cats. not threadsafe or reasonable
  var catDb: List[Cat] = List(Cat("Koffie", Color("grey"), Pattern.Stripes), Cat("Minoes", Color("brown"), Pattern.Plain))

  def getAllCats: Task[List[Cat]] = ZIO.succeed(catDb)

  def addCat(cat: Cat): Task[Cat] = Task {
    catDb = cat :: catDb
    cat
  }
}

/** some convenience methods to use in the API */
object CatRepo {
  def getAllCats: RIO[CatRepo, List[Cat]] = ZIO.environmentWithZIO[CatRepo](_.get.getAllCats)
  def addCat(cat: Cat): RIO[CatRepo, Cat] = ZIO.environmentWithZIO[CatRepo](_.get.addCat(cat))
}

case class Queries(cats: () => RIO[CatRepo, List[Cat]])
case class Mutations(addCat: Cat => RIO[CatRepo, Cat])

class Api {
  val catRepoLayer: ULayer[CatRepo] = ZLayer.succeed(new CatRepo())
  implicit val runtime: Runtime[CatRepo] = zio.Runtime.unsafeFromLayer(catRepoLayer)
  val i = runtime.unsafeRun(createInterpreter)
  def route = AkkaHttpAdapter.makeHttpService(i)

  def createInterpreter = {
    import ApiSchema._
    GraphQL.graphQL(
      RootResolver(
        Queries(
          () => CatRepo.getAllCats
        ),
        Mutations(
          CatRepo.addCat
        )
      )
    ).interpreter
  }
}

object ApiSchema extends GenericSchema[CatRepo] {
  implicit val colorSchema: Schema[Any, Color] = scalarSchema[Color]("CatColor", Some("the color of the cat"), None, c => StringValue(c.s))
}

We can add a field that is only determined on demand. This is great for resource-heavy or circular fields.

class CatRepo {
  def similar(cat: Cat): Task[List[Cat]] = Task {
    catDb.filter(x => x != cat && x.color == cat.color)
  }
}

object ApiSchema extends GenericSchema[CatRepo] {
  implicit val catSchema: Schema[CatRepo, Cat] = obj[CatRepo, Cat]("Cat")(
    implicit ft =>
      List(
        field("name")(_.name),
        field("color")(_.color),
        field("pattern")(_.pattern),
        field("similar")(cat => CatRepo.similar(cat))
      )
  )
}
shadow-left