From ed2cfa5e34b2e43997c8eda76d89aaab5568f4c2 Mon Sep 17 00:00:00 2001 From: lWarne Date: Mon, 5 Jul 2021 18:20:57 +0100 Subject: [PATCH 1/3] Allow for abstracted effect type in schema gen Allow for the use of an abstracted effect type in schema generation and add tests for usage. --- .../scala/caliban/tools/SchemaWriter.scala | 17 +++--- .../caliban/tools/SchemaWriterSpec.scala | 59 ++++++++++++++++++- 2 files changed, 67 insertions(+), 9 deletions(-) diff --git a/tools/src/main/scala/caliban/tools/SchemaWriter.scala b/tools/src/main/scala/caliban/tools/SchemaWriter.scala index 5965bcd2c..55459a43f 100644 --- a/tools/src/main/scala/caliban/tools/SchemaWriter.scala +++ b/tools/src/main/scala/caliban/tools/SchemaWriter.scala @@ -12,7 +12,8 @@ object SchemaWriter { schema: Document, packageName: Option[String] = None, effect: String = "zio.UIO", - imports: Option[List[String]] = None + imports: Option[List[String]] = None, + isEffectTypeAbstract: Boolean = false )(implicit scalarMappings: ScalarMappings): String = { val schemaDef = schema.schemaDefinition @@ -43,12 +44,12 @@ object SchemaWriter { val queries = schema .objectTypeDefinition(schemaDef.flatMap(_.query).getOrElse("Query")) - .map(t => writeRootQueryOrMutationDef(t, effect)) + .map(t => writeRootQueryOrMutationDef(t, effect, isEffectTypeAbstract)) .getOrElse("") val mutations = schema .objectTypeDefinition(schemaDef.flatMap(_.mutation).getOrElse("Mutation")) - .map(t => writeRootQueryOrMutationDef(t, effect)) + .map(t => writeRootQueryOrMutationDef(t, effect, isEffectTypeAbstract)) .getOrElse("") val subscriptions = schema @@ -103,17 +104,19 @@ object SchemaWriter { s"${safeName(field.name)} :$argsTypeName $effect[${writeType(field.ofType)}]" } - def writeRootQueryOrMutationDef(op: ObjectTypeDefinition, effect: String)(implicit + def writeRootQueryOrMutationDef(op: ObjectTypeDefinition, effect: String, isEffectTypeAbstract: Boolean)(implicit scalarMappings: ScalarMappings - ): String = + ): String = { + val typeParamOrEmpty = if (isEffectTypeAbstract) s"[$effect[_]]" else "" s""" - |${writeDescription(op.description)}case class ${op.name}( + |${writeDescription(op.description)}case class ${op.name}$typeParamOrEmpty( |${op.fields.map(c => writeRootField(c, op, effect)).mkString(",\n")} |)""".stripMargin + } def writeSubscriptionField(field: FieldDefinition, od: ObjectTypeDefinition)(implicit scalarMappings: ScalarMappings - ): String = + ): String = "%s:%s ZStream[Any, Nothing, %s]".format( safeName(field.name), if (field.args.nonEmpty) s" ${argsName(field, od)} =>" else "", diff --git a/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala b/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala index 72e7c6a48..26abe2212 100644 --- a/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala +++ b/tools/src/test/scala/caliban/tools/SchemaWriterSpec.scala @@ -76,7 +76,9 @@ object SchemaWriterSpec extends DefaultRunnableSpec { val result = Parser .parseQuery(schema) .map( - _.objectTypeDefinition("Query").map(SchemaWriter.writeRootQueryOrMutationDef(_, "zio.UIO")).mkString("\n") + _.objectTypeDefinition("Query") + .map(SchemaWriter.writeRootQueryOrMutationDef(_, "zio.UIO", false)) + .mkString("\n") ) .flatMap(Formatter.format(_, None).map(_.trim)) @@ -100,7 +102,7 @@ object SchemaWriterSpec extends DefaultRunnableSpec { .parseQuery(schema) .map( _.objectTypeDefinition("Mutation") - .map(SchemaWriter.writeRootQueryOrMutationDef(_, "zio.UIO")) + .map(SchemaWriter.writeRootQueryOrMutationDef(_, "zio.UIO", false)) .mkString("\n") ) .flatMap(Formatter.format(_, None).map(_.trim)) @@ -134,6 +136,59 @@ object SchemaWriterSpec extends DefaultRunnableSpec { ) ) }, + testM("simple queries with abstracted effect type") { + val schema = + """ + type Query { + user(id: Int): User + userList: [User]! + } + type User { + id: Int + name: String + profilePic: String + }""" + + val result = Parser + .parseQuery(schema) + .map( + _.objectTypeDefinition("Query").map(SchemaWriter.writeRootQueryOrMutationDef(_, "F", true)).mkString("\n") + ) + .flatMap(Formatter.format(_, None).map(_.trim)) + + assertM(result)( + equalTo( + """case class Query[F[_]]( + user: QueryUserArgs => F[Option[User]], + userList: F[List[Option[User]]] +)""".stripMargin + ) + ) + }, + testM("simple mutation with abstracted effect type") { + val schema = + """ + type Mutation { + setMessage(message: String): String + } + """ + val result = Parser + .parseQuery(schema) + .map( + _.objectTypeDefinition("Mutation") + .map(SchemaWriter.writeRootQueryOrMutationDef(_, "F", true)) + .mkString("\n") + ) + .flatMap(Formatter.format(_, None).map(_.trim)) + + assertM(result)( + equalTo( + """case class Mutation[F[_]]( + | setMessage: MutationSetMessageArgs => F[Option[String]] + |)""".stripMargin + ) + ) + }, testM("schema test") { val schema = """ From 94514c367cbd7a8069ea6249848792ea584267ca Mon Sep 17 00:00:00 2001 From: lWarne Date: Mon, 5 Jul 2021 19:16:14 +0100 Subject: [PATCH 2/3] Add abstractEffectType as a cmdline option Add --abstractEffectType as a command line option and add tests to check it's parsed correctly. --- .../scala/caliban/codegen/CalibanCli.scala | 5 ++- .../main/scala/caliban/tools/Codegen.scala | 21 +++++++----- .../main/scala/caliban/tools/Options.scala | 9 +++-- .../scala/caliban/tools/OptionsSpec.scala | 34 +++++++++++++++++-- 4 files changed, 55 insertions(+), 14 deletions(-) diff --git a/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala b/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala index dfda5eef3..5d2829f05 100644 --- a/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala +++ b/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala @@ -51,7 +51,7 @@ object CalibanCli { private val genSchemaHelpMsg = s""" - |calibanGenSchema schemaPath outputPath [--scalafmtPath path] [--headers name:value,name2:value2] [--packageName name] [--effect fqdn.Effect] + |calibanGenSchema schemaPath outputPath [--scalafmtPath path] [--headers name:value,name2:value2] [--packageName name] [--effect fqdn.Effect] [--abstractEffectType] | |This command will create a Scala file in `outputPath` containing all the types |defined in the provided GraphQL schema defined at `schemaPath`. Instead of a path, @@ -61,6 +61,9 @@ object CalibanCli { | |By default, each Query and Mutation will be wrapped into a `zio.UIO` effect. |This can be overridden by providing an alternative effect with the `--effect` option. + |The --abstractEffectType flag can also be used to indicate that the effect + |type is abstract, so that it will be added as a type parameter to the generated + |Query and Mutation classes (if applicable). |""".stripMargin private val genClientHelpMsg = diff --git a/tools/src/main/scala/caliban/tools/Codegen.scala b/tools/src/main/scala/caliban/tools/Codegen.scala index 89a353b83..8b40d98b7 100644 --- a/tools/src/main/scala/caliban/tools/Codegen.scala +++ b/tools/src/main/scala/caliban/tools/Codegen.scala @@ -19,18 +19,23 @@ object Codegen { arguments: Options, genType: GenType ): Task[Unit] = { - val s = ".*/scala[^/]*/(.*)/(.*).scala".r.findFirstMatchIn(arguments.toPath) - val packageName = arguments.packageName.orElse(s.map(_.group(1).split("/").mkString("."))) - val objectName = s.map(_.group(2)).getOrElse("Client") - val effect = arguments.effect.getOrElse("zio.UIO") - val genView = arguments.genView.getOrElse(false) - val scalarMappings = arguments.scalarMappings - val loader = getSchemaLoader(arguments.schemaPath, arguments.headers) + val s = ".*/scala[^/]*/(.*)/(.*).scala".r.findFirstMatchIn(arguments.toPath) + val packageName = arguments.packageName.orElse(s.map(_.group(1).split("/").mkString("."))) + val objectName = s.map(_.group(2)).getOrElse("Client") + val abstractEffectType = arguments.abstractEffectType.getOrElse(false) + val effect = arguments.effect.getOrElse { + if (abstractEffectType) "F" else "zio.UIO" + } + val genView = arguments.genView.getOrElse(false) + val scalarMappings = arguments.scalarMappings + val loader = getSchemaLoader(arguments.schemaPath, arguments.headers) for { schema <- loader.load code = genType match { case GenType.Schema => - SchemaWriter.write(schema, packageName, effect, arguments.imports)(ScalarMappings(scalarMappings)) + SchemaWriter.write(schema, packageName, effect, arguments.imports, abstractEffectType)( + ScalarMappings(scalarMappings) + ) case GenType.Client => ClientWriter.write(schema, objectName, packageName, genView, arguments.imports)( ScalarMappings(scalarMappings) diff --git a/tools/src/main/scala/caliban/tools/Options.scala b/tools/src/main/scala/caliban/tools/Options.scala index 8df0876a0..5d6aca1b3 100644 --- a/tools/src/main/scala/caliban/tools/Options.scala +++ b/tools/src/main/scala/caliban/tools/Options.scala @@ -12,7 +12,8 @@ final case class Options( genView: Option[Boolean], effect: Option[String], scalarMappings: Option[Map[String, String]], - imports: Option[List[String]] + imports: Option[List[String]], + abstractEffectType: Option[Boolean] ) object Options { @@ -24,7 +25,8 @@ object Options { genView: Option[Boolean], effect: Option[String], scalarMappings: Option[List[String]], - imports: Option[List[String]] + imports: Option[List[String]], + abstractEffectType: Option[Boolean] ) def fromArgs(args: List[String]): Option[Options] = @@ -62,7 +64,8 @@ object Options { } }.toMap }, - rawOpts.imports + rawOpts.imports, + rawOpts.abstractEffectType ) } case _ => None diff --git a/tools/src/test/scala/caliban/tools/OptionsSpec.scala b/tools/src/test/scala/caliban/tools/OptionsSpec.scala index 2b9d6e359..5dbbf5305 100644 --- a/tools/src/test/scala/caliban/tools/OptionsSpec.scala +++ b/tools/src/test/scala/caliban/tools/OptionsSpec.scala @@ -23,6 +23,7 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, None ) ) @@ -44,6 +45,7 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, None ) ) @@ -56,7 +58,7 @@ object OptionsSpec extends DefaultRunnableSpec { assert(result)( equalTo( Some( - Options("schema", "output", None, None, None, None, None, None, None) + Options("schema", "output", None, None, None, None, None, None, None, None) ) ) ) @@ -90,6 +92,7 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, None ) ) @@ -111,6 +114,7 @@ object OptionsSpec extends DefaultRunnableSpec { None, Some("cats.effect.IO"), None, + None, None ) ) @@ -132,6 +136,7 @@ object OptionsSpec extends DefaultRunnableSpec { Some(true), None, None, + None, None ) ) @@ -153,6 +158,7 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, Some(Map("Long" -> "scala.Long")), + None, None ) ) @@ -174,7 +180,30 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, - Some(List("a.b.Clazz", "b.c._")) + Some(List("a.b.Clazz", "b.c._")), + None + ) + ) + ) + ) + }, + test("provide abstractEffectType") { + val input = List("schema", "output", "--effect", "F", "--abstractEffectType", "true") + val result = Options.fromArgs(input) + assert(result)( + equalTo( + Some( + Options( + "schema", + "output", + None, + None, + None, + None, + Some("F"), + None, + None, + Some(true) ) ) ) @@ -195,6 +224,7 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, None ) ) From 5a0e2aaeacb0b422744f891469b9eb2af05864c9 Mon Sep 17 00:00:00 2001 From: lWarne Date: Mon, 5 Jul 2021 19:31:39 +0100 Subject: [PATCH 3/3] Add documentation around abstractEffectType option Add to documentation in schema.md around usage of the --abstractEffectType option. --- codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala | 5 +++-- vuepress/docs/docs/schema.md | 4 +++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala b/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala index 5d2829f05..5f4eb7853 100644 --- a/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala +++ b/codegen-sbt/src/main/scala/caliban/codegen/CalibanCli.scala @@ -51,7 +51,7 @@ object CalibanCli { private val genSchemaHelpMsg = s""" - |calibanGenSchema schemaPath outputPath [--scalafmtPath path] [--headers name:value,name2:value2] [--packageName name] [--effect fqdn.Effect] [--abstractEffectType] + |calibanGenSchema schemaPath outputPath [--scalafmtPath path] [--headers name:value,name2:value2] [--packageName name] [--effect fqdn.Effect] [--abstractEffectType true|false] | |This command will create a Scala file in `outputPath` containing all the types |defined in the provided GraphQL schema defined at `schemaPath`. Instead of a path, @@ -63,7 +63,8 @@ object CalibanCli { |This can be overridden by providing an alternative effect with the `--effect` option. |The --abstractEffectType flag can also be used to indicate that the effect |type is abstract, so that it will be added as a type parameter to the generated - |Query and Mutation classes (if applicable). + |Query and Mutation classes (if applicable). In such cases `F` will be used by + |as the type parameter unless the `--effect` option is explicitly given. |""".stripMargin private val genClientHelpMsg = diff --git a/vuepress/docs/docs/schema.md b/vuepress/docs/docs/schema.md index 5ed89d8ca..1cf5a49ac 100644 --- a/vuepress/docs/docs/schema.md +++ b/vuepress/docs/docs/schema.md @@ -234,7 +234,7 @@ enablePlugins(CalibanPlugin) Then call the `calibanGenSchema` sbt command. ```scala -calibanGenSchema schemaPath outputPath [--scalafmtPath path] [--headers name:value,name2:value2] [--packageName name] [--effect fqdn.Effect] [--scalarMappings gqlType:f.q.d.n.Type,gqlType2:f.q.d.n.Type2] [--imports a.b.c._,c.d.E] +calibanGenSchema schemaPath outputPath [--scalafmtPath path] [--headers name:value,name2:value2] [--packageName name] [--effect fqdn.Effect] [--scalarMappings gqlType:f.q.d.n.Type,gqlType2:f.q.d.n.Type2] [--imports a.b.c._,c.d.E] [--abstractEffectType true|false] calibanGenSchema project/schema.graphql src/main/MyAPI.scala ``` @@ -246,6 +246,8 @@ The package of the generated code is derived from the folder of `outputPath`. Th By default, each Query and Mutation will be wrapped into a `zio.UIO` effect. This can be overridden by providing an alternative effect with the `--effect` option. +You can also indicate that the effect type is abstract via `--abstractEffectType true`, in which case `Query` will be replaced by `Query[F[_]]` and so on (note `F` will be used unless `--effect ` is explicitly given in which case `` would be used in place of `F`). + If you want to force a mapping between a GraphQL type and a Scala class (such as scalars), you can use the `--scalarMappings` option. Also you can add additional imports by providing `--imports` option.