diff --git a/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala b/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala index 48719e429..15c0c61a5 100644 --- a/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala +++ b/codegen-sbt/src/main/scala/caliban/codegen/CalibanSettings.scala @@ -13,6 +13,8 @@ sealed trait CalibanSettings { def genView: Option[Boolean] def scalarMappings: Seq[(String, String)] def imports: Seq[String] + def splitFiles: Option[Boolean] + def enableFmt: Option[Boolean] def append(other: Type): Type } @@ -24,7 +26,9 @@ case class CalibanFileSettings( packageName: Option[String], genView: Option[Boolean], scalarMappings: Seq[(String, String)], - imports: Seq[String] + imports: Seq[String], + splitFiles: Option[Boolean], + enableFmt: Option[Boolean] ) extends CalibanSettings { type Type = CalibanFileSettings val headers = Seq.empty // Not applicable for file generator @@ -37,7 +41,9 @@ case class CalibanFileSettings( packageName = other.packageName.orElse(packageName), genView = other.genView.orElse(genView), scalarMappings = scalarMappings ++ other.scalarMappings, - imports = imports ++ other.imports + imports = imports ++ other.imports, + splitFiles = other.splitFiles.orElse(splitFiles), + enableFmt = other.enableFmt.orElse(enableFmt) ) def clientName(value: String): CalibanFileSettings = this.copy(clientName = Some(value)) @@ -47,6 +53,8 @@ case class CalibanFileSettings( def scalarMapping(mapping: (String, String)*): CalibanFileSettings = this.copy(scalarMappings = this.scalarMappings ++ mapping) def imports(values: String*): CalibanFileSettings = this.copy(imports = this.imports ++ values) + def splitFiles(value: Boolean): CalibanFileSettings = this.copy(splitFiles = Some(value)) + def enableFmt(value: Boolean): CalibanFileSettings = this.copy(enableFmt = Some(value)) } case class CalibanUrlSettings( @@ -57,7 +65,9 @@ case class CalibanUrlSettings( packageName: Option[String], genView: Option[Boolean], scalarMappings: Seq[(String, String)], - imports: Seq[String] + imports: Seq[String], + splitFiles: Option[Boolean], + enableFmt: Option[Boolean] ) extends CalibanSettings { type Type = CalibanUrlSettings def append(other: CalibanUrlSettings): CalibanUrlSettings = @@ -69,7 +79,9 @@ case class CalibanUrlSettings( packageName = other.packageName.orElse(packageName), genView = other.genView.orElse(genView), scalarMappings = scalarMappings ++ other.scalarMappings, - imports = imports ++ other.imports + imports = imports ++ other.imports, + splitFiles = other.splitFiles.orElse(splitFiles), + enableFmt = other.enableFmt.orElse(enableFmt) ) def clientName(value: String): CalibanUrlSettings = this.copy(clientName = Some(value)) @@ -81,6 +93,8 @@ case class CalibanUrlSettings( def scalarMapping(mapping: (String, String)*): CalibanUrlSettings = this.copy(scalarMappings = this.scalarMappings ++ mapping) def imports(values: String*): CalibanUrlSettings = this.copy(imports = this.imports ++ values) + def splitFiles(value: Boolean): CalibanUrlSettings = this.copy(splitFiles = Some(value)) + def enableFmt(value: Boolean): CalibanUrlSettings = this.copy(enableFmt = Some(value)) } object CalibanSettings { @@ -92,7 +106,9 @@ object CalibanSettings { packageName = Option.empty[String], genView = Option.empty[Boolean], scalarMappings = Seq.empty[(String, String)], - imports = Seq.empty[String] + imports = Seq.empty[String], + splitFiles = Option.empty[Boolean], + enableFmt = Option.empty[Boolean] ) def emptyUrl(url: URL): CalibanUrlSettings = CalibanUrlSettings( @@ -103,6 +119,8 @@ object CalibanSettings { packageName = Option.empty[String], genView = Option.empty[Boolean], scalarMappings = Seq.empty[(String, String)], - imports = Seq.empty[String] + imports = Seq.empty[String], + splitFiles = Option.empty[Boolean], + enableFmt = Option.empty[Boolean] ) } diff --git a/codegen-sbt/src/main/scala/caliban/codegen/CalibanSourceGenerator.scala b/codegen-sbt/src/main/scala/caliban/codegen/CalibanSourceGenerator.scala index 4fda59dcb..94b5d58b5 100644 --- a/codegen-sbt/src/main/scala/caliban/codegen/CalibanSourceGenerator.scala +++ b/codegen-sbt/src/main/scala/caliban/codegen/CalibanSourceGenerator.scala @@ -84,8 +84,15 @@ object CalibanSourceGenerator { ) // NB: Presuming zio-config can read toString'd booleans val scalarMappings = pairList("--scalarMappings", settings.scalarMappings) val imports = list("--imports", settings.imports) - - scalafmtPath ++ headers ++ packageName ++ genView ++ scalarMappings ++ imports + val splitFiles = singleOpt( + "--splitFiles", + settings.splitFiles.map(_.toString()) + ) // NB: Presuming zio-config can read toString'd booleans + val enableFmt = singleOpt( + "--enableFmt", + settings.enableFmt.map(_.toString()) + ) // NB: Presuming zio-config can read toString'd booleans + scalafmtPath ++ headers ++ packageName ++ genView ++ scalarMappings ++ imports ++ splitFiles ++ enableFmt } def apply( diff --git a/codegen-sbt/src/sbt-test/codegen/test-compile/test b/codegen-sbt/src/sbt-test/codegen/test-compile/test index ab7c44bcb..98fd901c6 100644 --- a/codegen-sbt/src/sbt-test/codegen/test-compile/test +++ b/codegen-sbt/src/sbt-test/codegen/test-compile/test @@ -25,9 +25,11 @@ $ mkdir src/main/scala/genview/client > calibanGenClient project/schema-to-check-name-uniqueness.graphql src/main/scala/genview/client/ClientNameUniqueness.scala --packageName genview.client --genView true $ exists src/main/scala/genview/client/ClientNameUniqueness.scala $ exec sh verify.sh StarshipView ./src/main/scala/genview/client/ClientNameUniqueness.scala -> calibanGenClient project/gitlab-schema.graphql src/main/scala/genview/client/ClientGitLab.scala --packageName genview.client --genView true -$ exists src/main/scala/genview/client/ClientGitLab.scala -$ exec sh verify.sh VulnerableProjectsByGradeView ./src/main/scala/genview/client/ClientGitLab.scala +> calibanGenClient project/gitlab-schema.graphql src/main/scala/genview/client/ClientGitLab.scala --packageName genview.client --genView true --splitFiles true --enableFmt false +$ exists src/main/scala/genview/client/package.scala +$ exec sh verify.sh VulnerableProjectsByGrade ./src/main/scala/genview/client/package.scala +$ exists src/main/scala/genview/client/VulnerableProjectsByGrade.scala +$ exec sh verify.sh VulnerableProjectsByGradeView ./src/main/scala/genview/client/VulnerableProjectsByGrade.scala > compile $ exists target/scala-2.12/src_managed/main/caliban-codegen-sbt/caliban/Client.scala diff --git a/docs/docs/client.html b/docs/docs/client.html index 5660e07fe..b45d5700c 100644 --- a/docs/docs/client.html +++ b/docs/docs/client.html @@ -64,10 +64,12 @@ def genView(value: Boolean): CalibanSettings // Provide a case class and helper method to select all fields on an object (default: false) def scalarMapping(mapping: (String,String)*): CalibanSettings // A mapping from GraphQL scalar types to JVM types, as unknown scalar types are represented as String by default. def imports(values: String*): CalibanSettings // A list of imports to be added to the top of a generated client + def splitFiles(value: Boolean): CalibanSettings // Split single client object into multiple files (default: false) + def enableFmt(value: Boolean): CalibanSettings // Enable code formatting with scalafmt (default: true) // Only defined for `url` settings, for use in supplying extra headers when fetching the schema itself def headers(pairs: (String,String)*): CalibanSettings -</code></pre></div><h3 id="calibangenclient"><a href="#calibangenclient" class="header-anchor">#</a> <code>calibanGenClient</code></h3> <p>If you prefer to generate the client explicitly rather than automatically, you can use <code>calibanGenClient</code> on the SBT CLI as follows:</p> <div class="language-scala extra-class"><pre class="language-scala"><code>calibanGenClient schemaPath outputPath <span class="token punctuation">[</span><span class="token operator">--</span>scalafmtPath path<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>headers name<span class="token operator">:</span>value<span class="token punctuation">,</span>name2<span class="token operator">:</span>value2<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>genView <span class="token boolean">true</span><span class="token operator">|</span><span class="token boolean">false</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>scalarMappings gqlType<span class="token operator">:</span>f<span class="token punctuation">.</span>q<span class="token punctuation">.</span>d<span class="token punctuation">.</span>n<span class="token punctuation">.</span>Type<span class="token punctuation">,</span>gqlType2<span class="token operator">:</span>f<span class="token punctuation">.</span>q<span class="token punctuation">.</span>d<span class="token punctuation">.</span>n<span class="token punctuation">.</span>Type2<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>imports a<span class="token punctuation">.</span>b<span class="token punctuation">.</span>c<span class="token punctuation">.</span>_<span class="token punctuation">,</span>c<span class="token punctuation">.</span>d<span class="token punctuation">.</span>E<span class="token punctuation">]</span> +</code></pre></div><h3 id="calibangenclient"><a href="#calibangenclient" class="header-anchor">#</a> <code>calibanGenClient</code></h3> <p>If you prefer to generate the client explicitly rather than automatically, you can use <code>calibanGenClient</code> on the SBT CLI as follows:</p> <div class="language-scala extra-class"><pre class="language-scala"><code>calibanGenClient schemaPath outputPath <span class="token punctuation">[</span><span class="token operator">--</span>scalafmtPath path<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>headers name<span class="token operator">:</span>value<span class="token punctuation">,</span>name2<span class="token operator">:</span>value2<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>genView <span class="token boolean">true</span><span class="token operator">|</span><span class="token boolean">false</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>scalarMappings gqlType<span class="token operator">:</span>f<span class="token punctuation">.</span>q<span class="token punctuation">.</span>d<span class="token punctuation">.</span>n<span class="token punctuation">.</span>Type<span class="token punctuation">,</span>gqlType2<span class="token operator">:</span>f<span class="token punctuation">.</span>q<span class="token punctuation">.</span>d<span class="token punctuation">.</span>n<span class="token punctuation">.</span>Type2<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>imports a<span class="token punctuation">.</span>b<span class="token punctuation">.</span>c<span class="token punctuation">.</span>_<span class="token punctuation">,</span>c<span class="token punctuation">.</span>d<span class="token punctuation">.</span>E<span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>splitFiles <span class="token boolean">true</span><span class="token operator">|</span><span class="token boolean">false</span><span class="token punctuation">]</span> <span class="token punctuation">[</span><span class="token operator">--</span>enableFmt <span class="token boolean">true</span><span class="token operator">|</span><span class="token boolean">false</span><span class="token punctuation">]</span> calibanGenClient project<span class="token operator">/</span>schema<span class="token punctuation">.</span>graphql src<span class="token operator">/</span>main<span class="token operator">/</span>client<span class="token operator">/</span>Client<span class="token punctuation">.</span>scala <span class="token operator">--</span>genView <span class="token boolean">true</span> </code></pre></div><p>This command will generate a Scala file in <code>outputPath</code> containing helper functions for all the types defined in the provided GraphQL schema defined at <code>schemaPath</code>. @@ -80,7 +82,11 @@ option. Provide <code>--genView true</code> option if you want to generate a view for the GraphQL types. If you want to force a mapping between a GraphQL type and a Scala class (such as scalars), you can use the -<code>--scalarMappings</code> option. Also you can add imports for example for your ArgEncoder implicits by providing <code>--imports</code> option.</p> <h2 id="query-building"><a href="#query-building" class="header-anchor">#</a> Query building</h2> <p>Once the boilerplate code is generated, you can start building queries. For each <em>type</em> in your schema, a corresponding Scala object has been created. For each <em>field</em> in your schema, a corresponding Scala function has been created.</p> <p>For example, given the following schema:</p> <div class="language-graphql extra-class"><pre class="language-graphql"><code><span class="token keyword">type</span> <span class="token class-name">Character</span> <span class="token punctuation">{</span> +<code>--scalarMappings</code> option. Also you can add imports for example for your ArgEncoder implicits by providing <code>--imports</code> option. +Use the <code>--splitFiles true</code> option if you want to generate multiple files within the same package instead of a single file. +In this case the filename part of the <code>outputPath</code> will be ignored, but the value will still be used to determine the mandatory package name and destination directory. +This can be helpful with large schemas and incremental compilation. +Provide <code>--enableFmt</code> option if you don't need to format generated files.</p> <h2 id="query-building"><a href="#query-building" class="header-anchor">#</a> Query building</h2> <p>Once the boilerplate code is generated, you can start building queries. For each <em>type</em> in your schema, a corresponding Scala object has been created. For each <em>field</em> in your schema, a corresponding Scala function has been created.</p> <p>For example, given the following schema:</p> <div class="language-graphql extra-class"><pre class="language-graphql"><code><span class="token keyword">type</span> <span class="token class-name">Character</span> <span class="token punctuation">{</span> <span class="token attr-name">name</span><span class="token punctuation">:</span> <span class="token scalar">String</span><span class="token operator">!</span> <span class="token attr-name">nicknames</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token scalar">String</span><span class="token operator">!</span><span class="token punctuation">]</span><span class="token operator">!</span> <span class="token attr-name">origin</span><span class="token punctuation">:</span> <span class="token class-name">Origin</span><span class="token operator">!</span> diff --git a/tools/src/main/scala/caliban/tools/ClientWriter.scala b/tools/src/main/scala/caliban/tools/ClientWriter.scala index f19774617..621518240 100644 --- a/tools/src/main/scala/caliban/tools/ClientWriter.scala +++ b/tools/src/main/scala/caliban/tools/ClientWriter.scala @@ -19,8 +19,11 @@ object ClientWriter { objectName: String = "Client", packageName: Option[String] = None, genView: Boolean = false, - additionalImports: Option[List[String]] = None - )(implicit scalarMappings: ScalarMappings): String = { + additionalImports: Option[List[String]] = None, + splitFiles: Boolean = false + )(implicit scalarMappings: ScalarMappings): List[(String, String)] = { + require(packageName.isDefined || !splitFiles, "splitFiles option requires a package name") + val schemaDef = schema.schemaDefinition implicit val mappingClashedTypeNames: MappingClashedTypeNames = MappingClashedTypeNames( @@ -32,7 +35,8 @@ object ClientWriter { case UnionTypeDefinition(_, name, _, _) => name case ScalarTypeDefinition(_, name, _) => name case InterfaceTypeDefinition(_, name, _, _) => name - } + }, + if (splitFiles) List("package") else Nil ) ) @@ -47,6 +51,18 @@ object ClientWriter { safeTypeName(name) -> op }.toMap) + val objectTypes = + if (splitFiles) + schema.objectTypeDefinitions + .filterNot(obj => + reservedType(obj) || + schemaDef.exists(_.query.getOrElse("Query") == obj.name) || + schemaDef.exists(_.mutation.getOrElse("Mutation") == obj.name) || + schemaDef.exists(_.subscription.getOrElse("Subscription") == obj.name) + ) + .map(writeObjectType) + else Nil + val objects = schema.objectTypeDefinitions .filterNot(obj => reservedType(obj) || @@ -54,74 +70,192 @@ object ClientWriter { schemaDef.exists(_.mutation.getOrElse("Mutation") == obj.name) || schemaDef.exists(_.subscription.getOrElse("Subscription") == obj.name) ) - .map(writeObject(_, genView)) - .mkString("\n") + .map { typedef => + val content = writeObject(typedef, genView) + val fullContent = + if (splitFiles) + s"""import caliban.client.FieldBuilder._ + |import caliban.client._ + | + |$content + |""".stripMargin + else + s"""${writeObjectType(typedef)} + |$content + |""".stripMargin + safeTypeName(typedef.name) -> fullContent + } - val inputs = schema.inputObjectTypeDefinitions.map(writeInputObject).mkString("\n") + val inputs = schema.inputObjectTypeDefinitions.map { typedef => + val content = writeInputObject(typedef) + val fullContent = + if (splitFiles) + s"""import caliban.client._ + |import caliban.client.__Value._ + | + |$content + |""".stripMargin + else + content + safeTypeName(typedef.name) -> fullContent + } val enums = schema.enumTypeDefinitions .filter(e => !scalarMappings.scalarMap.exists(_.contains(e.name))) - .map(writeEnum) - .mkString("\n") + .map { typedef => + val content = writeEnum(typedef) + val fullContent = + if (splitFiles) + s"""import caliban.client.CalibanClientError.DecodingError + |import caliban.client._ + |import caliban.client.__Value._ + | + |$content + |""".stripMargin + else + content + safeTypeName(typedef.name) -> fullContent + } val scalars = schema.scalarTypeDefinitions .filterNot(s => isScalarSupported(s.name)) .map(writeScalar) - .mkString("\n") + + val queryTypes = + if (splitFiles) + schema + .objectTypeDefinition(schemaDef.flatMap(_.query).getOrElse("Query")) + .map(writeRootQueryType) + .toList + else Nil val queries = schema .objectTypeDefinition(schemaDef.flatMap(_.query).getOrElse("Query")) - .map(writeRootQuery) - .getOrElse("") + .map { typedef => + val content = writeRootQuery(typedef) + val fullContent = + if (splitFiles) + s"""import caliban.client.FieldBuilder._ + |import caliban.client._ + | + |$content + |""".stripMargin + else + s"""${writeRootQueryType(typedef)} + |$content + |""".stripMargin + safeTypeName(typedef.name) -> fullContent + } + + val mutationTypes = + if (splitFiles) + schema + .objectTypeDefinition(schemaDef.flatMap(_.mutation).getOrElse("Mutation")) + .map(writeRootMutationType) + .toList + else Nil val mutations = schema .objectTypeDefinition(schemaDef.flatMap(_.mutation).getOrElse("Mutation")) - .map(writeRootMutation) - .getOrElse("") + .map { typedef => + val content = writeRootMutation(typedef) + val fullContent = + if (splitFiles) + s"""import caliban.client.FieldBuilder._ + |import caliban.client._ + | + |$content + |""".stripMargin + else + s"""${writeRootMutationType(typedef)} + |$content + |""".stripMargin + safeTypeName(typedef.name) -> fullContent + } + + val subscriptionTypes = + if (splitFiles) + schema + .objectTypeDefinition(schemaDef.flatMap(_.subscription).getOrElse("Subscription")) + .map(writeRootSubscriptionType) + .toList + else Nil val subscriptions = schema .objectTypeDefinition(schemaDef.flatMap(_.subscription).getOrElse("Subscription")) - .map(writeRootSubscription) - .getOrElse("") - - val additionalImportsString = additionalImports.fold("")(_.map(i => s"import $i").mkString("\n")) - - val imports = - s"""${if (enums.nonEmpty) - """import caliban.client.CalibanClientError.DecodingError - |""".stripMargin - else ""}${if (objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty) - """import caliban.client.FieldBuilder._ - |""".stripMargin - else - ""}${if ( - enums.nonEmpty || objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty || inputs.nonEmpty - ) - """import caliban.client._ - |""".stripMargin - else ""}${if (enums.nonEmpty || inputs.nonEmpty) - """import caliban.client.__Value._ - |""".stripMargin - else ""}""" - - s"""${packageName.fold("")(p => s"package $p\n\n")}$imports\n - |$additionalImportsString - | - |object $objectName { - | - | $scalars - | $enums - | $objects - | $inputs - | $queries - | $mutations - | $subscriptions - | - |}""".stripMargin + .map { typedef => + val content = writeRootSubscription(typedef) + val fullContent = + if (splitFiles) + s"""import caliban.client.FieldBuilder._ + |import caliban.client._ + | + |$content + |""".stripMargin + else + s"""${writeRootSubscriptionType(typedef)} + |$content + |""".stripMargin + safeTypeName(typedef.name) -> fullContent + } + + if (splitFiles) { + val parentPackageName = packageName.filter(_.contains(".")).map(_.reverse.dropWhile(_ != '.').drop(1).reverse) + val packageObject = "package" -> + s"""${parentPackageName.fold("")(p => s"package $p\n")} + |package object ${packageName.get.reverse.takeWhile(_ != '.').reverse} { + | ${(scalars ::: objectTypes ::: queryTypes ::: mutationTypes ::: subscriptionTypes).mkString("\n")} + |} + |""".stripMargin + val classFiles = + (enums ::: objects ::: inputs ::: queries.toList ::: mutations.toList ::: subscriptions.toList).map { + case (name, content) => + val fullContent = + s"""${packageName.fold("")(p => s"package $p\n\n")}$content\n + |""".stripMargin + name -> fullContent + } + packageObject :: classFiles + } else { + val additionalImportsString = additionalImports.fold("")(_.map(i => s"import $i").mkString("\n")) + + val imports = + s"""${if (enums.nonEmpty) + """import caliban.client.CalibanClientError.DecodingError + |""".stripMargin + else ""}${if (objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty) + """import caliban.client.FieldBuilder._ + |""".stripMargin + else + ""}${if ( + enums.nonEmpty || objects.nonEmpty || queries.nonEmpty || mutations.nonEmpty || subscriptions.nonEmpty || inputs.nonEmpty + ) + """import caliban.client._ + |""".stripMargin + else ""}${if (enums.nonEmpty || inputs.nonEmpty) + """import caliban.client.__Value._ + |""".stripMargin + else ""}""" + + List(objectName -> s"""${packageName.fold("")(p => s"package $p\n\n")}$imports\n + |$additionalImportsString + | + |object $objectName { + | + | ${scalars.mkString("\n")} + | ${enums.map(_._2).mkString("\n")} + | ${objects.map(_._2).mkString("\n")} + | ${inputs.map(_._2).mkString("\n")} + | ${queries.map(_._2).mkString("\n")} + | ${mutations.map(_._2).mkString("\n")} + | ${subscriptions.map(_._2).mkString("\n")} + | + |}""".stripMargin) + } } - private def getMappingsClashedNames(typeNames: List[String]): Map[String, String] = - typeNames + private def getMappingsClashedNames(typeNames: List[String], reservedNames: List[String] = Nil): Map[String, String] = + (reservedNames ::: typeNames) .map(name => name.toLowerCase -> name) .groupBy(_._1) .collect { @@ -145,6 +279,11 @@ object ClientWriter { def reservedType(typeDefinition: ObjectTypeDefinition): Boolean = typeDefinition.name == "Query" || typeDefinition.name == "Mutation" || typeDefinition.name == "Subscription" + def writeRootQueryType( + typedef: ObjectTypeDefinition + ): String = + s"type ${typedef.name} = _root_.caliban.client.Operations.RootQuery" + def writeRootQuery( typedef: ObjectTypeDefinition )(implicit @@ -152,12 +291,16 @@ object ClientWriter { mappingClashedTypeNames: MappingClashedTypeNames, scalarMappings: ScalarMappings ): String = - s"""type ${typedef.name} = _root_.caliban.client.Operations.RootQuery - |object ${typedef.name} { + s"""object ${typedef.name} { | ${typedef.fields.map(writeField(_, "_root_.caliban.client.Operations.RootQuery")).mkString("\n ")} |} |""".stripMargin + def writeRootMutationType( + typedef: ObjectTypeDefinition + ): String = + s"type ${typedef.name} = _root_.caliban.client.Operations.RootMutation" + def writeRootMutation( typedef: ObjectTypeDefinition )(implicit @@ -165,12 +308,16 @@ object ClientWriter { mappingClashedTypeNames: MappingClashedTypeNames, scalarMappings: ScalarMappings ): String = - s"""type ${typedef.name} = _root_.caliban.client.Operations.RootMutation - |object ${typedef.name} { + s"""object ${typedef.name} { | ${typedef.fields.map(writeField(_, "_root_.caliban.client.Operations.RootMutation")).mkString("\n ")} |} |""".stripMargin + def writeRootSubscriptionType( + typedef: ObjectTypeDefinition + ): String = + s"type ${typedef.name} = _root_.caliban.client.Operations.RootSubscription" + def writeRootSubscription( typedef: ObjectTypeDefinition )(implicit @@ -178,12 +325,23 @@ object ClientWriter { mappingClashedTypeNames: MappingClashedTypeNames, scalarMappings: ScalarMappings ): String = - s"""type ${typedef.name} = _root_.caliban.client.Operations.RootSubscription - |object ${typedef.name} { + s"""object ${typedef.name} { | ${typedef.fields.map(writeField(_, "_root_.caliban.client.Operations.RootSubscription")).mkString("\n ")} |} |""".stripMargin + def writeObjectType( + typedef: ObjectTypeDefinition + )(implicit + mappingClashedTypeNames: MappingClashedTypeNames, + scalarMappings: ScalarMappings + ): String = { + + val objectName: String = safeTypeName(typedef.name) + + s"type $objectName" + } + def writeObject( typedef: ObjectTypeDefinition, genView: Boolean @@ -200,8 +358,7 @@ object ClientWriter { "\n " + writeView(typedef.name, fields.map(_.typeInfo)) else "" - s"""type $objectName - |object $objectName {$view + s"""object $objectName {$view | ${fields.map(writeFieldInfo).mkString("\n ")} |} |""".stripMargin diff --git a/tools/src/main/scala/caliban/tools/Codegen.scala b/tools/src/main/scala/caliban/tools/Codegen.scala index b05fb0609..b38734e47 100644 --- a/tools/src/main/scala/caliban/tools/Codegen.scala +++ b/tools/src/main/scala/caliban/tools/Codegen.scala @@ -28,22 +28,31 @@ object Codegen { } val genView = arguments.genView.getOrElse(false) val scalarMappings = arguments.scalarMappings + val splitFiles = arguments.splitFiles.getOrElse(false) + val enableFmt = arguments.enableFmt.getOrElse(true) val loader = getSchemaLoader(arguments.schemaPath, arguments.headers) for { schema <- loader.load code = genType match { case GenType.Schema => - SchemaWriter.write(schema, packageName, effect, arguments.imports, abstractEffectType)( - ScalarMappings(scalarMappings) + List( + objectName -> SchemaWriter.write(schema, packageName, effect, arguments.imports, abstractEffectType)( + ScalarMappings(scalarMappings) + ) ) case GenType.Client => - ClientWriter.write(schema, objectName, packageName, genView, arguments.imports)( + ClientWriter.write(schema, objectName, packageName, genView, arguments.imports, splitFiles)( ScalarMappings(scalarMappings) ) } - formatted <- Formatter.format(code, arguments.fmtPath) - _ <- Task(new PrintWriter(new File(arguments.toPath))) - .bracket(q => UIO(q.close()), pw => Task(pw.println(formatted))) + formatted <- if (enableFmt) Formatter.format(code, arguments.fmtPath) else Task.succeed(code) + _ <- Task.collectAll(formatted.map { case (objectName, objectCode) => + val path = + if (splitFiles) s"${arguments.toPath.reverse.dropWhile(_ != '/').reverse}$objectName.scala" + else arguments.toPath + Task(new PrintWriter(new File(path))) + .bracket(q => UIO(q.close()), pw => Task(pw.println(objectCode))) + }) } yield () } diff --git a/tools/src/main/scala/caliban/tools/Formatter.scala b/tools/src/main/scala/caliban/tools/Formatter.scala index 50f4c0d5c..b8bbecc20 100644 --- a/tools/src/main/scala/caliban/tools/Formatter.scala +++ b/tools/src/main/scala/caliban/tools/Formatter.scala @@ -7,15 +7,20 @@ import zio.Task object Formatter { - def format(str: String, fmtPath: Option[String]): Task[String] = Task { + def format(str: String, fmtPath: Option[String]): Task[String] = + format(List("Nil.scala" -> str), fmtPath).map(_.head._2) + + def format(strs: List[(String, String)], fmtPath: Option[String]): Task[List[(String, String)]] = Task { val scalafmt = Scalafmt.create(this.getClass.getClassLoader) val defaultConfigPath = Paths.get(".scalafmt.conf") val defaultConfig = if (Files.exists(defaultConfigPath)) defaultConfigPath else Paths.get("") val config = fmtPath.fold(defaultConfig)(Paths.get(_)) - val result = scalafmt - .withRespectVersion(false) - .format(config, Paths.get("Nil.scala"), str) + val result = strs.map { case (name, code) => + name -> scalafmt + .withRespectVersion(false) + .format(config, Paths.get(s"$name.scala"), code) + } scalafmt.clear() result } diff --git a/tools/src/main/scala/caliban/tools/Options.scala b/tools/src/main/scala/caliban/tools/Options.scala index 5d6aca1b3..27fb19eae 100644 --- a/tools/src/main/scala/caliban/tools/Options.scala +++ b/tools/src/main/scala/caliban/tools/Options.scala @@ -13,7 +13,9 @@ final case class Options( effect: Option[String], scalarMappings: Option[Map[String, String]], imports: Option[List[String]], - abstractEffectType: Option[Boolean] + abstractEffectType: Option[Boolean], + splitFiles: Option[Boolean], + enableFmt: Option[Boolean] ) object Options { @@ -26,7 +28,9 @@ object Options { effect: Option[String], scalarMappings: Option[List[String]], imports: Option[List[String]], - abstractEffectType: Option[Boolean] + abstractEffectType: Option[Boolean], + splitFiles: Option[Boolean], + enableFmt: Option[Boolean] ) def fromArgs(args: List[String]): Option[Options] = @@ -65,7 +69,9 @@ object Options { }.toMap }, rawOpts.imports, - rawOpts.abstractEffectType + rawOpts.abstractEffectType, + rawOpts.splitFiles, + rawOpts.enableFmt ) } case _ => None diff --git a/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala b/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala index 8641ce0f4..3528b7ce6 100644 --- a/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala +++ b/tools/src/test/scala/caliban/tools/ClientWriterSpec.scala @@ -17,7 +17,24 @@ object ClientWriterSpec extends DefaultRunnableSpec { .parseQuery(schema) .flatMap(doc => Formatter.format( - ClientWriter.write(doc, additionalImports = Some(additionalImports))( + ClientWriter + .write(doc, additionalImports = Some(additionalImports))( + ScalarMappings(Some(scalarMappings)) + ) + .head + ._2, + None + ) + ) + + def genSplit( + schema: String, + scalarMappings: Map[String, String] = Map.empty + ): Task[List[(String, String)]] = Parser + .parseQuery(schema) + .flatMap(doc => + Formatter.format( + ClientWriter.write(doc, packageName = Some("test"), splitFiles = true)( ScalarMappings(Some(scalarMappings)) ), None @@ -711,6 +728,58 @@ object Client { """ ) } + }, + testM("schema with splitFiles") { + val schema = + """ + schema { + query: Q + } + + type Q { + characters: [Character!]! + } + + type Character { + name: String! + nicknames: [String!]! + } + """.stripMargin + + assertM(genSplit(schema))( + equalTo( + List( + "package" -> """package object test { + | type Character + | type Q = _root_.caliban.client.Operations.RootQuery + |} + |""".stripMargin, + "Character" -> """package test + | + |import caliban.client.FieldBuilder._ + |import caliban.client._ + | + |object Character { + | def name: SelectionBuilder[Character, String] = _root_.caliban.client.SelectionBuilder.Field("name", Scalar()) + | def nicknames: SelectionBuilder[Character, List[String]] = + | _root_.caliban.client.SelectionBuilder.Field("nicknames", ListOf(Scalar())) + |} + |""".stripMargin, + "Q" -> """package test + | + |import caliban.client.FieldBuilder._ + |import caliban.client._ + | + |object Q { + | def characters[A]( + | innerSelection: SelectionBuilder[Character, A] + | ): SelectionBuilder[_root_.caliban.client.Operations.RootQuery, List[A]] = + | _root_.caliban.client.SelectionBuilder.Field("characters", ListOf(Obj(innerSelection))) + |} + |""".stripMargin + ) + ) + ) } ) @@ TestAspect.sequential } diff --git a/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala b/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala index 5c44e3636..9d809c466 100644 --- a/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala +++ b/tools/src/test/scala/caliban/tools/ClientWriterViewSpec.scala @@ -12,7 +12,7 @@ object ClientWriterViewSpec extends DefaultRunnableSpec { val gen: String => Task[String] = (schema: String) => Parser .parseQuery(schema) - .flatMap(doc => Formatter.format(ClientWriter.write(doc, genView = true)(ScalarMappings(None)), None)) + .flatMap(doc => Formatter.format(ClientWriter.write(doc, genView = true)(ScalarMappings(None)).head._2, None)) override def spec: ZSpec[TestEnvironment, Any] = suite("ClientWriterViewSpec")( diff --git a/tools/src/test/scala/caliban/tools/OptionsSpec.scala b/tools/src/test/scala/caliban/tools/OptionsSpec.scala index 5dbbf5305..7e1211801 100644 --- a/tools/src/test/scala/caliban/tools/OptionsSpec.scala +++ b/tools/src/test/scala/caliban/tools/OptionsSpec.scala @@ -24,6 +24,8 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, + None, None ) ) @@ -46,6 +48,8 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, + None, None ) ) @@ -58,7 +62,7 @@ object OptionsSpec extends DefaultRunnableSpec { assert(result)( equalTo( Some( - Options("schema", "output", None, None, None, None, None, None, None, None) + Options("schema", "output", None, None, None, None, None, None, None, None, None, None) ) ) ) @@ -93,6 +97,8 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, + None, None ) ) @@ -115,6 +121,8 @@ object OptionsSpec extends DefaultRunnableSpec { Some("cats.effect.IO"), None, None, + None, + None, None ) ) @@ -137,6 +145,8 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, + None, None ) ) @@ -159,6 +169,8 @@ object OptionsSpec extends DefaultRunnableSpec { None, Some(Map("Long" -> "scala.Long")), None, + None, + None, None ) ) @@ -181,6 +193,8 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, Some(List("a.b.Clazz", "b.c._")), + None, + None, None ) ) @@ -203,7 +217,9 @@ object OptionsSpec extends DefaultRunnableSpec { Some("F"), None, None, - Some(true) + Some(true), + None, + None ) ) ) @@ -225,6 +241,8 @@ object OptionsSpec extends DefaultRunnableSpec { None, None, None, + None, + None, None ) )