Skip to main content

Code generation

Writing queries in scala using the dsl is more concise and type-safe than writing out the types and codecs by hand, but still requires a lot of code for non-trivial queries.

gql also features a code generator that transforms a graphql schema file and a set of queries (or fragments) into dsl code.

Setting up

The code generator comes as a stand-alone cli at the maven coordinates:

// build.sbt
"io.github.valdemargr" %% "gql-client-codegen-cli" % "0.3.5"

The code generator can also be integrated into sbt for a smoother development experience:

// project/plugins.sbt
addSbtPlugin("io.github.valdemargr" % "gql-client-codegen-sbt" % "0.3.5")

Sbt integration

By default the sbt integration will look for a schema file in the resources directory at .../resources/schema.graphql and queries in the resources directory at .../resources/queries.

You can, however, override or add more sources at custom locations:

lazy val myBuild = 
...
.settings(
resourceGroups += Gql.resourceGroup(
name="other_resources",
schemaFile= file("path/to/schema.graphql"),
file("path/to/query1.graphql"),
file("path/to/query2.graphql")
)
)

Usage

When the code-generator is invoked it will use the queries and fragments in combination with the schema to generate a set of scala files containing the equivalent query in scala code.

For this demonstration, the code generator will be invoked manually:

import gql.client.codegen.{ GeneratorCli => Gen }
import fs2.io.file.Files
import cats.effect._
import cats.implicits._
import cats.effect.unsafe.implicits.global

def runQuery(queryDef: String) =
Files[IO].tempDirectory.use{ tmp =>
val schemaFile = tmp / "schema.graphql"
val queryFile = tmp / "query.graphql"
val sharedOutFile = tmp / "shared.scala"
val queryOutFile = tmp / "query.scala"

val schemaDef = """
enum HelloEnum {
HELLO,
WORLD
}

type A {
a: String
}

type B {
b: String
}

union HelloUnion = A | B

type Query {
helloEnum(name: String): HelloEnum,
helloUnion(name2: String): HelloUnion
}
"""

val writeSchemaF = fs2.Stream(schemaDef)
.through(fs2.text.utf8.encode)
.through(Files[IO].writeAll(schemaFile))
.compile
.drain

val writeQueryF = fs2.Stream(queryDef)
.through(fs2.text.utf8.encode)
.through(Files[IO].writeAll(queryFile))
.compile
.drain

import io.circe._
import io.circe.syntax._
val jo = Json.obj(
"schema" -> Json.fromString(schemaFile.toString),
"shared" -> Json.fromString(sharedOutFile.toString),
"queries" -> Json.arr(
Json.obj(
"query" -> Json.fromString(queryFile.toString),
"output" -> Json.fromString(queryOutFile.toString)
)
)
)

writeSchemaF >>
writeQueryF >>
Gen.run(List("--validate", "--input",jo.spaces2)) >>
Files[IO].readAll(queryOutFile)
.through(fs2.text.utf8.decode)
.compile
.string
.map(println)
}.unsafeRunSync()

runQuery(
"""
fragment HelloFragment on Query {
helloEnum(name: $name)
}

query HelloQuery($name: String) {
...HelloFragment
helloUnion(name2: "hey") {
... on A {
a
}
... on B {
b
}
}
}
"""
)
// package gql.client.generated
//
// import _root_.gql.client._
// import _root_.gql.client.dsl._
// import _root_.gql.parser.{Value => V, AnyValue, Const}
// import cats.implicits._
//
// final case class HelloFragment(
// helloEnum: Option[HelloEnum]
// )
//
// object HelloFragment {
// implicit val selectionSet: SelectionSet[HelloFragment] = (
// sel.build[Option[HelloEnum]]("helloEnum", x => x.args(arg("name", V.VariableValue("name"))))
// ).map(apply)
//
// implicit val fragdef: Fragment[HelloFragment] = fragment[HelloFragment]("HelloFragment", "Query")
// }
//
// final case class HelloQuery(
// helloFragment: gql.client.generated.HelloFragment,
// helloUnion: Option[HelloQuery.HelloUnion]
// )
//
// object HelloQuery {
// final case class HelloUnion(
// a: Option[HelloUnion.InlineA],
// b: Option[HelloUnion.InlineB]
// ) {
// lazy val variant: Option[HelloUnion.Variant] =
// (a).map(HelloUnion.Variant.OnA.apply) orElse
// (b).map(HelloUnion.Variant.OnB.apply)
// }
//
// object HelloUnion {
// sealed trait Variant extends Product with Serializable
// object Variant {
// final case class OnA(
// a: HelloUnion.InlineA
// ) extends Variant
//
// final case class OnB(
// b: HelloUnion.InlineB
// ) extends Variant
// }
//
// final case class InlineA(
// a: Option[String]
// )
//
// object InlineA {
// implicit val selectionSet: SelectionSet[InlineA] = (
// sel.build[Option[String]]("a", x => x)
// ).map(apply)
// }
//
// final case class InlineB(
// b: Option[String]
// )
//
// object InlineB {
// implicit val selectionSet: SelectionSet[InlineB] = (
// sel.build[Option[String]]("b", x => x)
// ).map(apply)
// }
//
// implicit val selectionSet: SelectionSet[HelloUnion] = (
// inlineFrag.build[HelloUnion.InlineA]("A", x => x),
// inlineFrag.build[HelloUnion.InlineB]("B", x => x)
// ).mapN(apply)
// }
//
// implicit val selectionSet: SelectionSet[HelloQuery] = (
// fragment.spread.build[gql.client.generated.HelloFragment](x => x).requiredFragment("HelloFragment", "Query"),
// sel.build[Option[HelloQuery.HelloUnion]]("helloUnion", x => x.args(arg("name2", V.StringValue("hey"))))
// ).mapN(apply)
//
// final case class Variables(
// name: Option[Option[String]] = None
// ) {
// def setName(value: Option[String]): Variables = copy(name = Some(value))
// }
//
// val queryExpr = (
// omittableVariable[Option[String]]("name")
// ).introduce{ _ =>
// selectionSet
// }
//
// val query = _root_.gql.client.Query.parameterized(_root_.gql.parser.QueryAst.OperationType.Query, "HelloQuery", queryExpr)
// }

When supplying the --validate flag, gql will generate a stub implementation of the schema and run the same code as if running a gql server.

Lets construct a helper to show this:

import scala.util.{Try,Failure}
// We will also remove the ansii color codes from the output, since they don't render well in the docs
def runFail(q: String) =
Try {
runQuery(q)
} match {
case Failure(ex) => println(ex.getMessage().replaceAll("\u001B\\[[;\\d]*m", ""))
}

Now with a parsing error:

runFail(
"""
query MyQuery {
test.,test
}
"""
)
// Failed to generate code with error: failed at offset 41 on line 2 with code 46
// char in range } to } (code 125 to 125)
// for document:
// |
// | query MyQuery {
// | test.,test
// | >>>>>>>>>>>>>^^^^^^^ line:2, column:16, offset:41, character code code:46
// | }
// |

And also with a query validation error:

runFail(
"""
query MyQuery {
helloEnum(name: 1)
}
"""
)
// Failed to generate code with error: decoding failure for type `String` with message Got value '1' with wrong type, expecting string at root.helloEnum.name.String
// in file /tmp/3854054003651491121/query.graphql
// |
// | query MyQuery {
// | helloEnum(name: 1)
// | >>>>>>>>>>>>>>>>>>>>>>>>>^^^^^^^ line:2, column:28, offset:53, character code code:49
// | }
// |