From 6203fa73f4d70a00c4857bdebc959c89a3da0492 Mon Sep 17 00:00:00 2001 From: Nepomuk Seiler Date: Fri, 10 Aug 2018 11:55:42 +0200 Subject: [PATCH] FIX #44 Generate code and decoders for `enum` type (#45) * 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 --- README.md | 82 +++++++++++++++++-- .../codegen/ApolloSourceGenerator.scala | 66 ++++++++++++--- .../muki/graphql/codegen/CodeGenStyles.scala | 32 +++++++- .../muki/graphql/codegen/JsonCodeGen.scala | 28 +++++++ .../muki/graphql/codegen/TypedDocument.scala | 3 +- .../graphql/codegen/TypedDocumentParser.scala | 3 +- src/sbt-test/codegen/apollo-circe/build.sbt | 2 +- .../apollo-duplicate-fragments/build.sbt | 16 ++++ .../project/plugins.sbt | 1 + .../graphql/HeroNestedFragmentQuery.graphql | 20 +++++ .../graphql/HeroNestedFragmentQuery2.graphql | 20 +++++ .../src/main/resources/schema.graphql | 74 +++++++++++++++++ .../codegen/apollo-duplicate-fragments/test | 3 + src/sbt-test/codegen/apollo/build.sbt | 4 + src/sbt-test/codegen/apollo/test | 1 + .../resources/apollo/blog/AddArticle.scala | 4 +- .../apollo/blog/AddArticleTypes.scala | 5 ++ .../apollo/blog/BlogArticleQuery.scala | 8 +- .../apollo/blog/BlogArticleQueryTypes.scala | 9 ++ src/test/resources/apollo/blog/BlogByID.scala | 2 +- .../apollo/starwars-circe/EpisodeEnum.graphql | 6 ++ .../apollo/starwars-circe/EpisodeEnum.scala | 19 +++++ .../starwars-circe/EpisodeEnumTypes.scala | 20 +++++ .../starwars-circe/HeroAndFriends.scala | 1 + .../starwars-circe/HeroFragmentQuery.scala | 6 +- .../apollo/starwars-circe/HeroNameQuery.scala | 1 + .../starwars-circe/InputVariables.scala | 1 + .../apollo/starwars-circe/SearchQuery.scala | 1 + .../apollo/starwars/EpisodeEnum.graphql | 6 ++ .../apollo/starwars/EpisodeEnum.scala | 15 ++++ .../apollo/starwars/EpisodeEnumTypes.scala | 8 ++ .../apollo/starwars/HeroAndFriends.scala | 1 + .../apollo/starwars/HeroFragmentQuery.scala | 6 +- .../HeroFragmentQueryInterfaces.scala | 1 + .../apollo/starwars/HeroNameQuery.scala | 1 + .../starwars/HeroNestedFragmentQuery.graphql | 19 +++++ .../starwars/HeroNestedFragmentQuery.scala | 30 +++++++ .../HeroNestedFragmentQueryInterfaces.scala | 5 ++ .../apollo/starwars/InputVariables.scala | 1 + .../apollo/starwars/SearchQuery.scala | 1 + .../style/apollo/ApolloCodegenBaseSpec.scala | 69 +++++++++++++++- .../src/main/graphql/EpisodeEnum.graphql | 6 ++ .../main/graphql/HeroFragmentQuery.graphql | 12 +++ .../graphql/HeroNestedFragmentQuery.graphql | 19 +++++ 44 files changed, 598 insertions(+), 40 deletions(-) create mode 100644 src/sbt-test/codegen/apollo-duplicate-fragments/build.sbt create mode 100644 src/sbt-test/codegen/apollo-duplicate-fragments/project/plugins.sbt create mode 100644 src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery.graphql create mode 100644 src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery2.graphql create mode 100644 src/sbt-test/codegen/apollo-duplicate-fragments/src/main/resources/schema.graphql create mode 100644 src/sbt-test/codegen/apollo-duplicate-fragments/test create mode 100644 src/test/resources/apollo/blog/AddArticleTypes.scala create mode 100644 src/test/resources/apollo/blog/BlogArticleQueryTypes.scala create mode 100644 src/test/resources/apollo/starwars-circe/EpisodeEnum.graphql create mode 100644 src/test/resources/apollo/starwars-circe/EpisodeEnum.scala create mode 100644 src/test/resources/apollo/starwars-circe/EpisodeEnumTypes.scala create mode 100644 src/test/resources/apollo/starwars/EpisodeEnum.graphql create mode 100644 src/test/resources/apollo/starwars/EpisodeEnum.scala create mode 100644 src/test/resources/apollo/starwars/EpisodeEnumTypes.scala create mode 100644 src/test/resources/apollo/starwars/HeroFragmentQueryInterfaces.scala create mode 100644 src/test/resources/apollo/starwars/HeroNestedFragmentQuery.graphql create mode 100644 src/test/resources/apollo/starwars/HeroNestedFragmentQuery.scala create mode 100644 src/test/resources/apollo/starwars/HeroNestedFragmentQueryInterfaces.scala create mode 100644 test-project/client/src/main/graphql/EpisodeEnum.graphql create mode 100644 test-project/client/src/main/graphql/HeroFragmentQuery.graphql create mode 100644 test-project/client/src/main/graphql/HeroNestedFragmentQuery.graphql diff --git a/README.md b/README.md index 3dfc4b6..ed1f808 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 @@ -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. @@ -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 } @@ -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. diff --git a/src/main/scala/rocks/muki/graphql/codegen/ApolloSourceGenerator.scala b/src/main/scala/rocks/muki/graphql/codegen/ApolloSourceGenerator.scala index 999f06e..c680a10 100644 --- a/src/main/scala/rocks/muki/graphql/codegen/ApolloSourceGenerator.scala +++ b/src/main/scala/rocks/muki/graphql/codegen/ApolloSourceGenerator.scala @@ -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( @@ -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"))) @@ -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$", "") @@ -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 } """ )) @@ -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) => diff --git a/src/main/scala/rocks/muki/graphql/codegen/CodeGenStyles.scala b/src/main/scala/rocks/muki/graphql/codegen/CodeGenStyles.scala index da097f4..ed951dc 100644 --- a/src/main/scala/rocks/muki/graphql/codegen/CodeGenStyles.scala +++ b/src/main/scala/rocks/muki/graphql/codegen/CodeGenStyles.scala @@ -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 == @@ -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 } diff --git a/src/main/scala/rocks/muki/graphql/codegen/JsonCodeGen.scala b/src/main/scala/rocks/muki/graphql/codegen/JsonCodeGen.scala index e42c2fc..d95cb51 100644 --- a/src/main/scala/rocks/muki/graphql/codegen/JsonCodeGen.scala +++ b/src/main/scala/rocks/muki/graphql/codegen/JsonCodeGen.scala @@ -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 { @@ -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 { @@ -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 + } """) } } } diff --git a/src/main/scala/rocks/muki/graphql/codegen/TypedDocument.scala b/src/main/scala/rocks/muki/graphql/codegen/TypedDocument.scala index 3c95ad4..102c2be 100644 --- a/src/main/scala/rocks/muki/graphql/codegen/TypedDocument.scala +++ b/src/main/scala/rocks/muki/graphql/codegen/TypedDocument.scala @@ -98,5 +98,6 @@ object TypedDocument { */ case class Api(operations: List[Operation], interfaces: List[Interface], - types: List[Type]) + types: List[Type], + original: ast.Document) } diff --git a/src/main/scala/rocks/muki/graphql/codegen/TypedDocumentParser.scala b/src/main/scala/rocks/muki/graphql/codegen/TypedDocumentParser.scala index 4494471..ee20d95 100644 --- a/src/main/scala/rocks/muki/graphql/codegen/TypedDocumentParser.scala +++ b/src/main/scala/rocks/muki/graphql/codegen/TypedDocumentParser.scala @@ -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 )) /** diff --git a/src/sbt-test/codegen/apollo-circe/build.sbt b/src/sbt-test/codegen/apollo-circe/build.sbt index e6643f0..8a0cdb0 100644 --- a/src/sbt-test/codegen/apollo-circe/build.sbt +++ b/src/sbt-test/codegen/apollo-circe/build.sbt @@ -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")}") } diff --git a/src/sbt-test/codegen/apollo-duplicate-fragments/build.sbt b/src/sbt-test/codegen/apollo-duplicate-fragments/build.sbt new file mode 100644 index 0000000..6342085 --- /dev/null +++ b/src/sbt-test/codegen/apollo-duplicate-fragments/build.sbt @@ -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 ")}") +} diff --git a/src/sbt-test/codegen/apollo-duplicate-fragments/project/plugins.sbt b/src/sbt-test/codegen/apollo-duplicate-fragments/project/plugins.sbt new file mode 100644 index 0000000..231e0dd --- /dev/null +++ b/src/sbt-test/codegen/apollo-duplicate-fragments/project/plugins.sbt @@ -0,0 +1 @@ +addSbtPlugin("rocks.muki" % "sbt-graphql" % sys.props("project.version")) diff --git a/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery.graphql b/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery.graphql new file mode 100644 index 0000000..f602c3f --- /dev/null +++ b/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery.graphql @@ -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 + } +} diff --git a/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery2.graphql b/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery2.graphql new file mode 100644 index 0000000..67e071a --- /dev/null +++ b/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/graphql/HeroNestedFragmentQuery2.graphql @@ -0,0 +1,20 @@ +query HeroNestedFragmentQuery2 { + droid(id: "23") { + name + appearsIn + } + hero { + ...CharacterInfo + } +} + +fragment CharacterFriends on Character { + name +} + +fragment CharacterInfo on Character { + name + friends { + ...CharacterFriends + } +} diff --git a/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/resources/schema.graphql b/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/resources/schema.graphql new file mode 100644 index 0000000..3fa59fe --- /dev/null +++ b/src/sbt-test/codegen/apollo-duplicate-fragments/src/main/resources/schema.graphql @@ -0,0 +1,74 @@ +# A character in the Star Wars Trilogy +interface Character { + # The id of the character. + id: String! + + # The name of the character. + name: String + + # The friends of the character, or an empty list if they have none. + friends: [Character!]! + + # Which movies they appear in. + appearsIn: [Episode] +} + +# A mechanical creature in the Star Wars universe. +type Droid implements Character { + # The id of the droid. + id: String! + + # The name of the droid. + name: String + + # The friends of the droid, or an empty list if they have none. + friends: [Character!]! + + # Which movies they appear in. + appearsIn: [Episode] + + # The primary function of the droid. + primaryFunction: String +} + +# One of the films in the Star Wars Trilogy +enum Episode { + # Released in 1977. + NEWHOPE + + # Released in 1980. + EMPIRE + + # Released in 1983. + JEDI +} + +# A humanoid creature in the Star Wars universe. +type Human implements Character { + # The id of the human. + id: String! + + # The name of the human. + name: String + + # The friends of the human, or an empty list if they have none. + friends: [Character!]! + + # Which movies they appear in. + appearsIn: [Episode] + + # The home planet of the human, or null if unknown. + homePlanet: String +} + +type Query { + hero( + # If omitted, returns the hero of the whole saga. If provided, returns the hero of that particular episode. + episode: Episode): Character! @deprecated(reason: "Use `human` or `droid` fields instead") + human( + # id of the character + id: String!): Human + droid( + # id of the character + id: String!): Droid! +} diff --git a/src/sbt-test/codegen/apollo-duplicate-fragments/test b/src/sbt-test/codegen/apollo-duplicate-fragments/test new file mode 100644 index 0000000..eb02e42 --- /dev/null +++ b/src/sbt-test/codegen/apollo-duplicate-fragments/test @@ -0,0 +1,3 @@ +> update +> compile +> check diff --git a/src/sbt-test/codegen/apollo/build.sbt b/src/sbt-test/codegen/apollo/build.sbt index de856fb..dfdad52 100644 --- a/src/sbt-test/codegen/apollo/build.sbt +++ b/src/sbt-test/codegen/apollo/build.sbt @@ -2,6 +2,10 @@ name := "test" enablePlugins(GraphQLCodegenPlugin) scalaVersion := "2.12.4" +libraryDependencies ++= Seq( + "org.sangria-graphql" %% "sangria" % "1.3.0" +) + graphqlCodegenStyle := Apollo TaskKey[Unit]("check") := { diff --git a/src/sbt-test/codegen/apollo/test b/src/sbt-test/codegen/apollo/test index 762c625..eb02e42 100644 --- a/src/sbt-test/codegen/apollo/test +++ b/src/sbt-test/codegen/apollo/test @@ -1,2 +1,3 @@ > update +> compile > check diff --git a/src/test/resources/apollo/blog/AddArticle.scala b/src/test/resources/apollo/blog/AddArticle.scala index 93a2f06..8309ace 100644 --- a/src/test/resources/apollo/blog/AddArticle.scala +++ b/src/test/resources/apollo/blog/AddArticle.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object AddArticle { object addArticle extends GraphQLQuery { val document: sangria.ast.Document = graphql"""mutation addArticle($$content: ArticleContent!) { @@ -11,7 +12,4 @@ object AddArticle { case class Data(addArticle: AddArticle) case class AddArticle(id: ID, title: String) } - case class ArticleAuthor(id: ID) - case class ArticleContent(title: String, body: String, tags: Option[List[String]], author: ArticleAuthor) - type ID = String } diff --git a/src/test/resources/apollo/blog/AddArticleTypes.scala b/src/test/resources/apollo/blog/AddArticleTypes.scala new file mode 100644 index 0000000..7391296 --- /dev/null +++ b/src/test/resources/apollo/blog/AddArticleTypes.scala @@ -0,0 +1,5 @@ +object types { + case class ArticleAuthor(id: ID) + case class ArticleContent(title: String, body: String, tags: Option[List[String]], author: ArticleAuthor) + type ID = String +} \ No newline at end of file diff --git a/src/test/resources/apollo/blog/BlogArticleQuery.scala b/src/test/resources/apollo/blog/BlogArticleQuery.scala index 191ab3f..3990014 100644 --- a/src/test/resources/apollo/blog/BlogArticleQuery.scala +++ b/src/test/resources/apollo/blog/BlogArticleQuery.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object BlogArticleQuery { object BlogArticleQuery extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query BlogArticleQuery($$query: ArticleQuery!) { @@ -11,11 +12,4 @@ object BlogArticleQuery { case class Data(articles: List[Articles]) case class Articles(id: ID, status: ArticleStatus) } - case class ArticleQuery(ids: Option[List[ID]], statuses: Option[List[ArticleStatus]]) - sealed trait ArticleStatus - object ArticleStatus { - case object DRAFT extends ArticleStatus - case object PUBLISHED extends ArticleStatus - } - type ID = String } diff --git a/src/test/resources/apollo/blog/BlogArticleQueryTypes.scala b/src/test/resources/apollo/blog/BlogArticleQueryTypes.scala new file mode 100644 index 0000000..d407388 --- /dev/null +++ b/src/test/resources/apollo/blog/BlogArticleQueryTypes.scala @@ -0,0 +1,9 @@ +object types { + case class ArticleQuery(ids: Option[List[ID]], statuses: Option[List[ArticleStatus]]) + sealed trait ArticleStatus + object ArticleStatus { + case object DRAFT extends ArticleStatus + case object PUBLISHED extends ArticleStatus + } + type ID = String +} \ No newline at end of file diff --git a/src/test/resources/apollo/blog/BlogByID.scala b/src/test/resources/apollo/blog/BlogByID.scala index fba6223..103fa66 100644 --- a/src/test/resources/apollo/blog/BlogByID.scala +++ b/src/test/resources/apollo/blog/BlogByID.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object BlogByID { object Blog extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query Blog($$blogId: ID!) { @@ -10,5 +11,4 @@ object BlogByID { case class Data(blog: Blog) case class Blog(title: String) } - type ID = String } diff --git a/src/test/resources/apollo/starwars-circe/EpisodeEnum.graphql b/src/test/resources/apollo/starwars-circe/EpisodeEnum.graphql new file mode 100644 index 0000000..3192ead --- /dev/null +++ b/src/test/resources/apollo/starwars-circe/EpisodeEnum.graphql @@ -0,0 +1,6 @@ +query EpisodeEnum { + hero { + name + appearsIn + } +} diff --git a/src/test/resources/apollo/starwars-circe/EpisodeEnum.scala b/src/test/resources/apollo/starwars-circe/EpisodeEnum.scala new file mode 100644 index 0000000..b5d5099 --- /dev/null +++ b/src/test/resources/apollo/starwars-circe/EpisodeEnum.scala @@ -0,0 +1,19 @@ +import io.circe.Decoder +import io.circe.generic.semiauto.deriveDecoder +import sangria.macros._ +import types._ +object EpisodeEnum { + object EpisodeEnum extends GraphQLQuery { + val document: sangria.ast.Document = graphql"""query EpisodeEnum { + hero { + name + appearsIn + } +}""" + case class Variables() + case class Data(hero: Hero) + object Data { implicit val jsonDecoder: Decoder[Data] = deriveDecoder[Data] } + case class Hero(name: Option[String], appearsIn: Option[List[Option[Episode]]]) + object Hero { implicit val jsonDecoder: Decoder[Hero] = deriveDecoder[Hero] } + } +} diff --git a/src/test/resources/apollo/starwars-circe/EpisodeEnumTypes.scala b/src/test/resources/apollo/starwars-circe/EpisodeEnumTypes.scala new file mode 100644 index 0000000..41e491f --- /dev/null +++ b/src/test/resources/apollo/starwars-circe/EpisodeEnumTypes.scala @@ -0,0 +1,20 @@ +import io.circe.Decoder +import io.circe.generic.semiauto.deriveDecoder +object types { + sealed trait Episode + object Episode { + case object NEWHOPE extends Episode + case object EMPIRE extends Episode + case object JEDI extends Episode + implicit val jsonDecoder: Decoder[Episode] = Decoder.decodeString.emap({ + case "NEWHOPE" => + Right(NEWHOPE) + case "EMPIRE" => + Right(EMPIRE) + case "JEDI" => + Right(JEDI) + case other => + Left("invalid enum value: " + other) + }) + } +} diff --git a/src/test/resources/apollo/starwars-circe/HeroAndFriends.scala b/src/test/resources/apollo/starwars-circe/HeroAndFriends.scala index 737ad1f..16e944c 100644 --- a/src/test/resources/apollo/starwars-circe/HeroAndFriends.scala +++ b/src/test/resources/apollo/starwars-circe/HeroAndFriends.scala @@ -1,6 +1,7 @@ import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder import sangria.macros._ +import types._ object HeroAndFriends { object HeroAndFriends extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query HeroAndFriends { diff --git a/src/test/resources/apollo/starwars-circe/HeroFragmentQuery.scala b/src/test/resources/apollo/starwars-circe/HeroFragmentQuery.scala index 500af18..9c5254c 100644 --- a/src/test/resources/apollo/starwars-circe/HeroFragmentQuery.scala +++ b/src/test/resources/apollo/starwars-circe/HeroFragmentQuery.scala @@ -1,6 +1,7 @@ import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder import sangria.macros._ +import types._ object HeroFragmentQuery { object HeroFragmentQuery extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query HeroFragmentQuery { @@ -10,6 +11,10 @@ object HeroFragmentQuery { human(id: "Lea") { ...CharacterInfo } +} + +fragment CharacterInfo on Character { + name }""" case class Variables() case class Data(hero: Hero, human: Option[Human]) @@ -19,5 +24,4 @@ object HeroFragmentQuery { case class Human(name: Option[String]) extends CharacterInfo object Human { implicit val jsonDecoder: Decoder[Human] = deriveDecoder[Human] } } - trait CharacterInfo { def name: Option[String] } } diff --git a/src/test/resources/apollo/starwars-circe/HeroNameQuery.scala b/src/test/resources/apollo/starwars-circe/HeroNameQuery.scala index d99d4ff..34d4472 100644 --- a/src/test/resources/apollo/starwars-circe/HeroNameQuery.scala +++ b/src/test/resources/apollo/starwars-circe/HeroNameQuery.scala @@ -1,6 +1,7 @@ import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder import sangria.macros._ +import types._ object HeroNameQuery { object HeroNameQuery extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query HeroNameQuery { diff --git a/src/test/resources/apollo/starwars-circe/InputVariables.scala b/src/test/resources/apollo/starwars-circe/InputVariables.scala index 48f2000..5807521 100644 --- a/src/test/resources/apollo/starwars-circe/InputVariables.scala +++ b/src/test/resources/apollo/starwars-circe/InputVariables.scala @@ -1,6 +1,7 @@ import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder import sangria.macros._ +import types._ object InputVariables { object InputVariables extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query InputVariables($$humanId: String!) { diff --git a/src/test/resources/apollo/starwars-circe/SearchQuery.scala b/src/test/resources/apollo/starwars-circe/SearchQuery.scala index 0f3d149..d741c1e 100644 --- a/src/test/resources/apollo/starwars-circe/SearchQuery.scala +++ b/src/test/resources/apollo/starwars-circe/SearchQuery.scala @@ -1,6 +1,7 @@ import io.circe.Decoder import io.circe.generic.semiauto.deriveDecoder import sangria.macros._ +import types._ object SearchQuery { object SearchQuery extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query SearchQuery($$text: String!) { diff --git a/src/test/resources/apollo/starwars/EpisodeEnum.graphql b/src/test/resources/apollo/starwars/EpisodeEnum.graphql new file mode 100644 index 0000000..3192ead --- /dev/null +++ b/src/test/resources/apollo/starwars/EpisodeEnum.graphql @@ -0,0 +1,6 @@ +query EpisodeEnum { + hero { + name + appearsIn + } +} diff --git a/src/test/resources/apollo/starwars/EpisodeEnum.scala b/src/test/resources/apollo/starwars/EpisodeEnum.scala new file mode 100644 index 0000000..e438632 --- /dev/null +++ b/src/test/resources/apollo/starwars/EpisodeEnum.scala @@ -0,0 +1,15 @@ +import sangria.macros._ +import types._ +object EpisodeEnum { + object EpisodeEnum extends GraphQLQuery { + val document: sangria.ast.Document = graphql"""query EpisodeEnum { + hero { + name + appearsIn + } +}""" + case class Variables() + case class Data(hero: Hero) + case class Hero(name: Option[String], appearsIn: Option[List[Option[Episode]]]) + } +} diff --git a/src/test/resources/apollo/starwars/EpisodeEnumTypes.scala b/src/test/resources/apollo/starwars/EpisodeEnumTypes.scala new file mode 100644 index 0000000..794fc9f --- /dev/null +++ b/src/test/resources/apollo/starwars/EpisodeEnumTypes.scala @@ -0,0 +1,8 @@ +object types { + sealed trait Episode + object Episode { + case object NEWHOPE extends Episode + case object EMPIRE extends Episode + case object JEDI extends Episode + } +} \ No newline at end of file diff --git a/src/test/resources/apollo/starwars/HeroAndFriends.scala b/src/test/resources/apollo/starwars/HeroAndFriends.scala index ad879c4..ace0998 100644 --- a/src/test/resources/apollo/starwars/HeroAndFriends.scala +++ b/src/test/resources/apollo/starwars/HeroAndFriends.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object HeroAndFriends { object HeroAndFriends extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query HeroAndFriends { diff --git a/src/test/resources/apollo/starwars/HeroFragmentQuery.scala b/src/test/resources/apollo/starwars/HeroFragmentQuery.scala index 0839c2b..234d5b7 100644 --- a/src/test/resources/apollo/starwars/HeroFragmentQuery.scala +++ b/src/test/resources/apollo/starwars/HeroFragmentQuery.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object HeroFragmentQuery { object HeroFragmentQuery extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query HeroFragmentQuery { @@ -8,11 +9,14 @@ object HeroFragmentQuery { human(id: "Lea") { ...CharacterInfo } +} + +fragment CharacterInfo on Character { + name }""" case class Variables() case class Data(hero: Hero, human: Option[Human]) case class Hero(name: Option[String]) extends CharacterInfo case class Human(name: Option[String]) extends CharacterInfo } - trait CharacterInfo { def name: Option[String] } } diff --git a/src/test/resources/apollo/starwars/HeroFragmentQueryInterfaces.scala b/src/test/resources/apollo/starwars/HeroFragmentQueryInterfaces.scala new file mode 100644 index 0000000..7aa0eef --- /dev/null +++ b/src/test/resources/apollo/starwars/HeroFragmentQueryInterfaces.scala @@ -0,0 +1 @@ +trait CharacterInfo { def name: Option[String] } diff --git a/src/test/resources/apollo/starwars/HeroNameQuery.scala b/src/test/resources/apollo/starwars/HeroNameQuery.scala index a793fb7..886af40 100644 --- a/src/test/resources/apollo/starwars/HeroNameQuery.scala +++ b/src/test/resources/apollo/starwars/HeroNameQuery.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object HeroNameQuery { object HeroNameQuery extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query HeroNameQuery { diff --git a/src/test/resources/apollo/starwars/HeroNestedFragmentQuery.graphql b/src/test/resources/apollo/starwars/HeroNestedFragmentQuery.graphql new file mode 100644 index 0000000..680816a --- /dev/null +++ b/src/test/resources/apollo/starwars/HeroNestedFragmentQuery.graphql @@ -0,0 +1,19 @@ +query HeroNestedFragmentQuery { + hero { + ...CharacterInfo + } + human(id: "Lea") { + ...CharacterInfo + } +} + +fragment CharacterFriends on Character { + name +} + +fragment CharacterInfo on Character { + name + friends { + ...CharacterFriends + } +} diff --git a/src/test/resources/apollo/starwars/HeroNestedFragmentQuery.scala b/src/test/resources/apollo/starwars/HeroNestedFragmentQuery.scala new file mode 100644 index 0000000..2b18a20 --- /dev/null +++ b/src/test/resources/apollo/starwars/HeroNestedFragmentQuery.scala @@ -0,0 +1,30 @@ +import sangria.macros._ +import types._ +object HeroNestedFragmentQuery { + object HeroNestedFragmentQuery extends GraphQLQuery { + val document: sangria.ast.Document = graphql"""query HeroNestedFragmentQuery { + hero { + ...CharacterInfo + } + human(id: "Lea") { + ...CharacterInfo + } +} + +fragment CharacterFriends on Character { + name +} +fragment CharacterInfo on Character { + name + friends { + ...CharacterFriends + } +}""" + case class Variables() + case class Data(hero: Hero, human: Option[Human]) + case class Hero(name: Option[String], friends: Option[List[Option[Hero.Friends]]]) extends CharacterInfo + object Hero { case class Friends(name: Option[String]) extends CharacterFriends } + case class Human(name: Option[String], friends: Option[List[Option[Human.Friends]]]) extends CharacterInfo + object Human { case class Friends(name: Option[String]) extends CharacterFriends } + } +} \ No newline at end of file diff --git a/src/test/resources/apollo/starwars/HeroNestedFragmentQueryInterfaces.scala b/src/test/resources/apollo/starwars/HeroNestedFragmentQueryInterfaces.scala new file mode 100644 index 0000000..c539c16 --- /dev/null +++ b/src/test/resources/apollo/starwars/HeroNestedFragmentQueryInterfaces.scala @@ -0,0 +1,5 @@ +trait CharacterFriends { def name: Option[String] } +trait CharacterInfo { + def name: Option[String] + def friends: Option[List[Option[CharacterFriends]]] +} \ No newline at end of file diff --git a/src/test/resources/apollo/starwars/InputVariables.scala b/src/test/resources/apollo/starwars/InputVariables.scala index 373860d..15b0dbc 100644 --- a/src/test/resources/apollo/starwars/InputVariables.scala +++ b/src/test/resources/apollo/starwars/InputVariables.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object InputVariables { object InputVariables extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query InputVariables($$humanId: String!) { diff --git a/src/test/resources/apollo/starwars/SearchQuery.scala b/src/test/resources/apollo/starwars/SearchQuery.scala index 6c7162e..44b7c28 100644 --- a/src/test/resources/apollo/starwars/SearchQuery.scala +++ b/src/test/resources/apollo/starwars/SearchQuery.scala @@ -1,4 +1,5 @@ import sangria.macros._ +import types._ object SearchQuery { object SearchQuery extends GraphQLQuery { val document: sangria.ast.Document = graphql"""query SearchQuery($$text: String!) { diff --git a/src/test/scala/rocks/muki/graphql/codegen/style/apollo/ApolloCodegenBaseSpec.scala b/src/test/scala/rocks/muki/graphql/codegen/style/apollo/ApolloCodegenBaseSpec.scala index 070f655..335c8a1 100644 --- a/src/test/scala/rocks/muki/graphql/codegen/style/apollo/ApolloCodegenBaseSpec.scala +++ b/src/test/scala/rocks/muki/graphql/codegen/style/apollo/ApolloCodegenBaseSpec.scala @@ -19,19 +19,19 @@ import org.scalatest.{EitherValues, TryValues, WordSpec} import java.io.File import rocks.muki.graphql.codegen.{ + ApolloSourceGenerator, DocumentLoader, - Generator, TypedDocumentParser } import rocks.muki.graphql.schema.SchemaLoader -import scala.io.{Source => IOSource, Codec} +import scala.io.{Codec, Source => IOSource} import scala.meta._ import sbt._ abstract class ApolloCodegenBaseSpec( name: String, - generator: (String => Generator[List[Stat]])) + generator: String => ApolloSourceGenerator) extends WordSpec with EitherValues with TryValues { @@ -63,8 +63,69 @@ abstract class ApolloCodegenBaseSpec( val actual = stats.map(_.show[Syntax]).mkString("\n") val expectedSource = contentOf(expected).parse[Source].get - assert(actual === expectedSource.show[Syntax].trim, actual) + assert(actual === expectedSource.show[Syntax].trim, + s"------\n$actual\n------") } } + + for { + input <- inputDir.listFiles() + if input.getName.endsWith(".graphql") + name = input.getName.replace(".graphql", "") + expected = new File(inputDir, s"${name}Interfaces.scala") + if expected.exists + } { + s"generate interface code for ${input.getName}" in { + + val schema = + SchemaLoader + .fromFile(inputDir / "schema.graphql") + .loadSchema() + val document = DocumentLoader.single(schema, input).right.value + val typedDocument = + TypedDocumentParser(schema, document).parse().right.value + val stats = generator(input.getName) + .generateInterfaces(typedDocument) + .right + .value + + val actual = stats.map(_.show[Syntax]).mkString("\n") + val expectedSource = contentOf(expected).parse[Source].get + + assert(actual === expectedSource.show[Syntax].trim, + s"------\n$actual\n------") + } + } + + for { + input <- inputDir.listFiles() + if input.getName.endsWith(".graphql") + name = input.getName.replace(".graphql", "") + expected = new File(inputDir, s"${name}Types.scala") + if expected.exists + } { + s"generate types code for ${input.getName}" in { + + val schema = + SchemaLoader + .fromFile(inputDir / "schema.graphql") + .loadSchema() + val document = DocumentLoader.single(schema, input).right.value + val typedDocument = + TypedDocumentParser(schema, document).parse().right.value + val stats = generator(input.getName) + .generateTypes(typedDocument) + .right + .value + + val actual = stats.map(_.show[Syntax]).mkString("\n") + val expectedSource = contentOf(expected).parse[Source].get + + assert(actual === expectedSource.show[Syntax].trim, + s"------\n$actual\n------") + } + } + } + } diff --git a/test-project/client/src/main/graphql/EpisodeEnum.graphql b/test-project/client/src/main/graphql/EpisodeEnum.graphql new file mode 100644 index 0000000..3192ead --- /dev/null +++ b/test-project/client/src/main/graphql/EpisodeEnum.graphql @@ -0,0 +1,6 @@ +query EpisodeEnum { + hero { + name + appearsIn + } +} diff --git a/test-project/client/src/main/graphql/HeroFragmentQuery.graphql b/test-project/client/src/main/graphql/HeroFragmentQuery.graphql new file mode 100644 index 0000000..55c39c6 --- /dev/null +++ b/test-project/client/src/main/graphql/HeroFragmentQuery.graphql @@ -0,0 +1,12 @@ +query HeroFragmentQuery { + hero { + ...CharacterInfo + } + human(id: "Lea") { + ...CharacterInfo + } +} + +fragment CharacterInfo on Character { + name +} diff --git a/test-project/client/src/main/graphql/HeroNestedFragmentQuery.graphql b/test-project/client/src/main/graphql/HeroNestedFragmentQuery.graphql new file mode 100644 index 0000000..387a34b --- /dev/null +++ b/test-project/client/src/main/graphql/HeroNestedFragmentQuery.graphql @@ -0,0 +1,19 @@ +query HeroNestedFragmentQuery { + hero { + ...CharacterInfoWithFriends + } + human(id: "Lea") { + ...CharacterInfoWithFriends + } +} + +fragment CharacterInfo on Character { + name +} + +fragment CharacterInfoWithFriends on Character { + name + friends { + ...CharacterInfo + } +}