Skip to content

Commit

Permalink
FIX #44 Generate code and decoders for enum type (#45)
Browse files Browse the repository at this point in the history
* FIX #44 Generate code and decoders for `enum` type

This adds the general capability for generating interfaces and types in the ApolloSource generator

* Add fragments to generated document

* Initial tests for fragment code generation

* Additional test for nested fragments

* Remove interfaces from concrete queries

* scalafmt

* Remove types from concrete queries and add test for types generation

* Fix circe generation test

* Adding json generation for enum types

* Scalafmt

* Wrap types in object types and import in generated code

* Add nested fragment to test project

* Add documentation

* Add scripted test for duplicated fragment names
  • Loading branch information
muuki88 authored Aug 10, 2018
1 parent 10d2e5d commit 6203fa7
Show file tree
Hide file tree
Showing 44 changed files with 598 additions and 40 deletions.
82 changes: 77 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ You can configure the output in various ways
* `graphqlCodegenImports: Seq[String]` - A list of additional that are included in every generated file


#### JSON support
### JSON support

The common serialization format for graphql results and input variables is JSON.
sbt-graphql supports JSON decoder/encoder code generation.
Expand All @@ -264,7 +264,7 @@ In your `build.sbt` you can configure the JSON library with
graphqlCodegenJson := JsonCodec.Circe
```

#### Scalar types
### Scalar types

The code generation doesn't know about your additional scalar types.
sbt-graphql provides a setting `graphqlCodegenImports` to add an import to every
Expand All @@ -283,7 +283,7 @@ which is represented as `java.time.ZoneDateTime`. Add this as an import
graphqlCodegenImports += "java.time.ZoneDateTime"
```

#### Codegen style Apollo
### Codegen style Apollo

As the name suggests the output is similar to the one in apollo codegen.

Expand Down Expand Up @@ -321,7 +321,7 @@ import graphql.codegen.GraphQLQuery
import sangria.macros._
object HeroNameQuery {
object HeroNameQuery extends GraphQLQuery {
val Document = graphql"""query HeroNameQuery {
val document: sangria.ast.Document = graphql"""query HeroNameQuery {
hero {
name
}
Expand All @@ -333,7 +333,79 @@ object HeroNameQuery {
}
```

#### Codegen Style Sangria
#### Interfaces, types and aliases

The `ApolloSourceGenerator` generates an additional file `Interfaces.scala` with the following shape:

```scala
object types {
// contains all defined types like enums and aliases
}
// all used fragments and interfaces are generated as traits here
```

##### Use case

> Share common business logic around a fragment that shouldn't be a directive
You can now do this by defining a `fragment` and include it in every query that
requires to apply this logic. `sbt-graphql` will generate the common `trait?`,
all generated case classes will extend this fragment `trait`.

##### Limitations

You need to **copy the fragments into every `graphql` query** that should use it.
If you have a lot of queries that reuse the fragment and you want to apply changes,
this is cumbersome.

You **cannot nest fragments**. The code generation isn't capable of naming the nested data structure. This means that you need create fragments for every nesting.

**Invalid**
```graphql
query HeroNestedFragmentQuery {
hero {
...CharacterInfo
}
human(id: "Lea") {
...CharacterInfo
}
}

# This will generate code that may compile, but is not usable
fragment CharacterInfo on Character {
name
friends {
name
}
}
```

**correct**

```graphql
query HeroNestedFragmentQuery {
hero {
...CharacterInfo
}
human(id: "Lea") {
...CharacterInfo
}
}

# create a fragment for the nested query
fragment CharacterFriends on Character {
name
}

fragment CharacterInfo on Character {
name
friends {
...CharacterFriends
}
}
```

### Codegen Style Sangria

This style generates one object with a specified `moduleName` and puts everything in there.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,41 @@ case class ApolloSourceGenerator(fileName: String,
jsonCodeGen: JsonCodeGen)
extends Generator[List[Stat]] {

override def apply(document: TypedDocument.Api): Result[List[Stat]] = {
/**
* Generates only the interfaces (fragments) that appear in the given
* document.
*
* This method works great with DocumentLoader.merge, merging all
* fragments together and generating a single interface definition object.
*
* @param document schema + query
* @return interfaces
*/
def generateInterfaces(document: TypedDocument.Api): Result[List[Stat]] = {
Right(document.interfaces.map(generateInterface(_, isSealed = false)))
}

// TODO refactor Generator trait into something more flexible
/**
* Generates only the types that appear in the given
* document.
*
* This method works great with DocumentLoader.merge, merging all
* fragments together and generating a single type definition object.
*
*
* @param document schema + query
* @return types
*/
def generateTypes(document: TypedDocument.Api): Result[List[Stat]] = {
val typeStats = document.types.flatMap(generateType)
Right(
jsonCodeGen.imports ++ List(q"""object types {
..$typeStats
}""")
)
}

override def apply(document: TypedDocument.Api): Result[List[Stat]] = {

val operations = document.operations.map { operation =>
val typeName = Term.Name(
Expand All @@ -50,9 +82,21 @@ case class ApolloSourceGenerator(fileName: String,
// replacing single $ with $$ for escaping
val escapedDocumentString =
operation.original.renderPretty.replaceAll("\\$", "\\$\\$")
val document = Term.Interpolate(Term.Name("graphql"),
Lit.String(escapedDocumentString) :: Nil,
Nil)

// add the fragments to the query as well
val escapedFragmentString = Option(document.original.fragments)
.filter(_.nonEmpty)
.map { fragments =>
fragments.values
.map(_.renderPretty.replaceAll("\\$", "\\$\\$"))
.mkString("\n\n", "\n", "")
}
.getOrElse("")

val documentString = escapedDocumentString + escapedFragmentString
val graphqlDocument = Term.Interpolate(Term.Name("graphql"),
Lit.String(documentString) :: Nil,
Nil)

val dataJsonDecoder =
Option(jsonCodeGen.generateFieldDecoder(Type.Name("Data")))
Expand All @@ -64,15 +108,13 @@ case class ApolloSourceGenerator(fileName: String,

q"""
object $typeName extends ..$additionalInits {
val document: sangria.ast.Document = $document
val document: sangria.ast.Document = $graphqlDocument
case class Variables(..$inputParams)
case class Data(..$dataParams)
..$dataJsonDecoder
..$data
}"""
}
val interfaces =
document.interfaces.map(generateInterface(_, isSealed = false))
val types = document.types.flatMap(generateType)
val objectName = fileName.replaceAll("\\.graphql$|\\.gql$", "")

Expand All @@ -81,11 +123,10 @@ case class ApolloSourceGenerator(fileName: String,
jsonCodeGen.imports ++
List(
q"import sangria.macros._",
q"import types._",
q"""
object ${Term.Name(objectName)} {
..$operations
..$interfaces
..$types
}
"""
))
Expand Down Expand Up @@ -315,9 +356,12 @@ case class ApolloSourceGenerator(fileName: String,

val enumName = Type.Name(name)
val objectName = Term.Name(name)
val jsonDecoder = jsonCodeGen.generateEnumFieldDecoder(enumName, values)
val enumStats: List[Stat] = enumValues ++ jsonDecoder

List[Stat](
q"sealed trait $enumName",
q"object $objectName { ..$enumValues }"
q"object $objectName { ..$enumStats }"
)

case TypedDocument.TypeAlias(from, to) =>
Expand Down
32 changes: 28 additions & 4 deletions src/main/scala/rocks/muki/graphql/codegen/CodeGenStyles.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ package rocks.muki.graphql.codegen

import java.io.File

import sangria.schema.Schema
import sbt._

import scala.meta._
import scala.util.Success
import sangria.ast

/**
* == CodeGen Styles ==
Expand Down Expand Up @@ -77,12 +76,37 @@ object CodeGenStyles {
}
}

val interfaceFile = for {
// use all queries to determine the interfaces & types we need
allQueries <- DocumentLoader.merged(schema, inputFiles.toList)
typedDocument <- TypedDocumentParser(schema, allQueries)
.parse()
codeGenerator = ApolloSourceGenerator("Interfaces.scala",
additionalImports,
additionalInits,
context.jsonCodeGen)
interfaces <- codeGenerator.generateInterfaces(typedDocument)
types <- codeGenerator.generateTypes(typedDocument)
} yield {
val stats = q"""package $packageName {
..$interfaces
..$types
}
"""
val outputFile = context.targetDirectory / "Interfaces.scala"
SourceCodeWriter.write(outputFile, stats)
context.log.info(s"Generated source $outputFile")
outputFile
}

val allFiles = files :+ interfaceFile

// split errors and success
val success = files.collect {
val success = allFiles.collect {
case Right(file) => file
}

val errors = files.collect {
val errors = allFiles.collect {
case Left(error) => error
}

Expand Down
28 changes: 28 additions & 0 deletions src/main/scala/rocks/muki/graphql/codegen/JsonCodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,15 @@ trait JsonCodeGen {
unionNames: List[String],
typeDiscriminatorField: String): List[Stat]

/**
*
* @param enumTrait the enum trait
* @param enumValues all enum field names
* @return a json decoder instance for enum types
*/
def generateEnumFieldDecoder(enumTrait: Type.Name,
enumValues: List[String]): List[Stat]

}

object JsonCodeGens {
Expand All @@ -42,6 +51,9 @@ object JsonCodeGens {
unionTrait: Type.Name,
unionNames: List[String],
typeDiscriminatorField: String): List[Stat] = Nil

def generateEnumFieldDecoder(enumTrait: Type.Name,
enumValues: List[String]): List[Stat] = Nil
}

object Circe extends JsonCodeGen {
Expand Down Expand Up @@ -73,7 +85,23 @@ object JsonCodeGens {
value <- typeDiscriminator match { ..case $patterns }
} yield value
""")
}

override def generateEnumFieldDecoder(
enumTrait: Type.Name,
enumValues: List[String]): List[Stat] = {
val patterns = enumValues.map { name =>
val nameLiteral = Lit.String(name)
val enumTerm = Term.Name(name)
p"case $nameLiteral => Right($enumTerm)"
} ++ List(
p"""case other => Left("invalid enum value: " + other)"""
)

List(q"""
implicit val jsonDecoder: Decoder[$enumTrait] = Decoder.decodeString.emap {
..case $patterns
} """)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -98,5 +98,6 @@ object TypedDocument {
*/
case class Api(operations: List[Operation],
interfaces: List[Interface],
types: List[Type])
types: List[Type],
original: ast.Document)
}
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ case class TypedDocumentParser(schema: Schema[_, _], document: ast.Document) {
document.operations.values.map(generateOperation).toList,
document.fragments.values.toList.map(generateFragment),
// Include only types that have been used in the document
schema.typeList.filter(types).collect(generateType).toList
schema.typeList.filter(types).collect(generateType).toList,
document
))

/**
Expand Down
2 changes: 1 addition & 1 deletion src/sbt-test/codegen/apollo-circe/build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@ libraryDependencies ++= Seq(

TaskKey[Unit]("check") := {
val generatedFiles = (graphqlCodegen in Compile).value
assert(generatedFiles.length == 5, s"Expected 5 files to be generated, but got\n${generatedFiles.mkString("\n")}")
assert(generatedFiles.length == 6, s"Expected 6 files to be generated, but got\n${generatedFiles.mkString("\n")}")
}
16 changes: 16 additions & 0 deletions src/sbt-test/codegen/apollo-duplicate-fragments/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name := "test"
enablePlugins(GraphQLCodegenPlugin)
scalaVersion := "2.12.4"

libraryDependencies ++= Seq(
"org.sangria-graphql" %% "sangria" % "1.3.0"
)

graphqlCodegenStyle := Apollo

TaskKey[Unit]("check") := {
val generatedFiles = (graphqlCodegen in Compile).value
val interfacesFile = generatedFiles.find(_.getName == "Interfaces.scala")

assert(interfacesFile.isDefined, s"Could not find generated scala class. Available files\n ${generatedFiles.mkString("\n ")}")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("rocks.muki" % "sbt-graphql" % sys.props("project.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
query HeroNestedFragmentQuery {
hero {
...CharacterInfo
}
human(id: "Lea") {
homePlanet
...CharacterInfo
}
}

fragment CharacterFriends on Character {
name
}

fragment CharacterInfo on Character {
name
friends {
...CharacterFriends
}
}
Loading

0 comments on commit 6203fa7

Please sign in to comment.