diff --git a/build.sbt b/build.sbt index a431d80559..2ecfbe82b7 100644 --- a/build.sbt +++ b/build.sbt @@ -85,8 +85,8 @@ lazy val codegen = project sbtPlugin := true, testFrameworks := Seq(new TestFramework("zio.test.sbt.ZTestFramework")), libraryDependencies ++= Seq( - "org.scalameta" %%% "scalafmt-dynamic" % "2.3.3-RC2", - "com.geirsson" %%% "scalafmt-core" % "1.5.1", + "org.scalameta" %%% "scalafmt-dynamic" % "2.3.2", + "org.scalameta" %%% "scalafmt-core" % "2.3.2", "dev.zio" %%% "zio-test" % "1.0.0-RC17" % "test", "dev.zio" %%% "zio-test-sbt" % "1.0.0-RC17" % "test" ) diff --git a/codegen/src/main/scala/caliban/codegen/CodegenPlugin.scala b/codegen/src/main/scala/caliban/codegen/CodegenPlugin.scala index e87bd6287e..6eeb06b280 100644 --- a/codegen/src/main/scala/caliban/codegen/CodegenPlugin.scala +++ b/codegen/src/main/scala/caliban/codegen/CodegenPlugin.scala @@ -10,9 +10,9 @@ import zio.{ DefaultRuntime, Task, UIO, ZIO } object CodegenPlugin extends AutoPlugin { import Console.Live.console._ - override lazy val projectSettings = Seq(commands += codegenCommand) - lazy val codegenCommand = - Command.args("codegen", helpMsg) { (state: State, args: Seq[String]) => + override lazy val projectSettings = Seq(commands += genSchemaCommand) + lazy val genSchemaCommand = + Command.args("calibanGenSchema", helpMsg) { (state: State, args: Seq[String]) => val runtime = new DefaultRuntime {} runtime.unsafeRun( @@ -28,12 +28,11 @@ object CodegenPlugin extends AutoPlugin { } private val helpMsg = """ - |codegen schemaPath outPath ?scalafmtPath + |calibanGenSchema schemaPath outputPath ?scalafmtPath | - |Command will write a scala file (outPath) containing GraphQL types, - |queries and subscriptions for provided json schema (schemaPath) and will - |format generated code with scalafmt with config in (scalafmtPath) or - |default config provided along with caliban-codegen. + |This command will create a Scala file in `outputPath` containing all the types + |defined in the provided GraphQL schema defined at `schemaPath`. The generated + |code will be formatted with Scalafmt using the configuration defined by `scalafmtPath`. | |""".stripMargin @@ -47,16 +46,14 @@ object CodegenPlugin extends AutoPlugin { def doSchemaGenerate(schemaPath: String, toPath: String, fmtPath: Option[String]): ZIO[Console, Throwable, Unit] = for { - schema_string <- Task(scala.io.Source.fromFile(schemaPath)) - .bracket(f => UIO(f.close()), f => Task(f.mkString)) - schema <- Parser.parseQuery(schema_string) - code <- Task(Generator.generate(schema)(ScalaWriter.DefaultGQLWriter)) - formatted <- fmtPath - .map(Generator.format(code, _)) - .getOrElse(Generator.formatStr(code, ScalaWriter.scalafmtConfig)) - _ <- Task(new PrintWriter(new File(toPath))) - .bracket(q => UIO(q.close()), { pw => - Task(pw.println(formatted)) - }) + _ <- putStrLn(s"Generating schema for $schemaPath") + schema_string <- Task(scala.io.Source.fromFile(schemaPath)).bracket(f => UIO(f.close()), f => Task(f.mkString)) + schema <- Parser.parseQuery(schema_string) + code <- Task(Generator.generate(schema)(ScalaWriter.DefaultGQLWriter)) + formatted <- fmtPath.fold(Generator.formatStr(code, ScalaWriter.scalafmtConfig))(Generator.format(code, _)) + _ <- Task(new PrintWriter(new File(toPath))).bracket(q => UIO(q.close()), { pw => + Task(pw.println(formatted)) + }) + _ <- putStrLn(s"Schema generation done") } yield () } diff --git a/codegen/src/main/scala/caliban/codegen/Generator.scala b/codegen/src/main/scala/caliban/codegen/Generator.scala index 3a225a93ab..c3a7a8ce5b 100644 --- a/codegen/src/main/scala/caliban/codegen/Generator.scala +++ b/codegen/src/main/scala/caliban/codegen/Generator.scala @@ -2,9 +2,7 @@ package caliban.codegen import java.net.{ URL, URLClassLoader } import java.nio.file.Paths - -import caliban.parsing.adt.Definition.TypeSystemDefinition.{ EnumTypeDefinition, ObjectTypeDefinition } -import caliban.parsing.adt.Type.FieldDefinition +import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition._ import caliban.parsing.adt.{ Document, Type } import org.scalafmt.dynamic.ScalafmtReflect import org.scalafmt.dynamic.utils.ReentrantCache @@ -26,7 +24,7 @@ object Generator { val scalafmtReflect = ScalafmtReflect( new URLClassLoader(new Array[URL](0), this.getClass.getClassLoader), - "2.2.1", + "2.3.2", respectVersion = false ) val config = scalafmtReflect.parseConfigFromString(fmt) @@ -40,9 +38,12 @@ object Generator { trait GQLWriterContext { implicit val fieldWriter: GQLWriter[FieldDefinition, ObjectTypeDefinition] + implicit val inputValueWriter: GQLWriter[InputValueDefinition, InputObjectTypeDefinition] implicit val typeWriter: GQLWriter[Type, Any] implicit val objectWriter: GQLWriter[ObjectTypeDefinition, Document] + implicit val inputObjectWriter: GQLWriter[InputObjectTypeDefinition, Document] implicit val enumWriter: GQLWriter[EnumTypeDefinition, Document] + implicit val unionWriter: GQLWriter[Union, Document] implicit val docWriter: GQLWriter[Document, Any] implicit val rootQueryWriter: GQLWriter[RootQueryDef, Document] implicit val queryWriter: GQLWriter[QueryDef, Document] @@ -64,6 +65,8 @@ object Generator { case class Args(field: FieldDefinition) + case class Union(typedef: UnionTypeDefinition, objects: List[ObjectTypeDefinition]) + object GQLWriter { def apply[A, D](implicit instance: GQLWriter[A, D]): GQLWriter[A, D] = instance diff --git a/codegen/src/main/scala/caliban/codegen/ScalaWriter.scala b/codegen/src/main/scala/caliban/codegen/ScalaWriter.scala index f843603260..9cb552ced5 100644 --- a/codegen/src/main/scala/caliban/codegen/ScalaWriter.scala +++ b/codegen/src/main/scala/caliban/codegen/ScalaWriter.scala @@ -1,38 +1,41 @@ package caliban.codegen import caliban.codegen.Generator._ -import caliban.parsing.adt.Definition.TypeSystemDefinition.{ EnumTypeDefinition, ObjectTypeDefinition } -import caliban.parsing.adt.Type.{ FieldDefinition, ListType, NamedType } +import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition._ +import caliban.parsing.adt.Type.{ ListType, NamedType } import caliban.parsing.adt.{ Document, Type } object ScalaWriter { - val scalafmtConfig = """ - |version = "2.2.1" - | - |maxColumn = 120 - |align = most - |continuationIndent.defnSite = 2 - |assumeStandardLibraryStripMargin = true - |docstrings = JavaDoc - |lineEndings = preserve - |includeCurlyBraceInSelectChains = false - |danglingParentheses = true - |spaces { - | inImportCurlyBraces = true - |} - |optIn.annotationNewlines = true - | - |rewrite.rules = [SortImports, RedundantBraces] - |""".stripMargin + val scalafmtConfig: String = """ + |version = "2.3.2" + | + |maxColumn = 120 + |align = most + |continuationIndent.defnSite = 2 + |assumeStandardLibraryStripMargin = true + |docstrings = JavaDoc + |lineEndings = preserve + |includeCurlyBraceInSelectChains = false + |danglingParentheses = true + |spaces { + | inImportCurlyBraces = true + |} + |optIn.annotationNewlines = true + | + |rewrite.rules = [SortImports, RedundantBraces] + |""".stripMargin def reservedType(typeDefinition: ObjectTypeDefinition): Boolean = typeDefinition.name == "Query" || typeDefinition.name == "Mutation" || typeDefinition.name == "Subscription" trait ScalaGQLWriter extends GQLWriterContext { override implicit val fieldWriter = FieldWriter + override implicit val inputValueWriter = InputValueWriter override implicit val typeWriter = TypeWriter override implicit val objectWriter = ObjectWriter + override implicit val inputObjectWriter = InputObjectWriter override implicit val enumWriter = EnumWriter + override implicit val unionWriter = UnionWriter override implicit val docWriter = DocumentWriter override implicit val rootQueryWriter = RootQueryDefWriter override implicit val queryWriter = QueryDefWriter @@ -49,49 +52,70 @@ object ScalaWriter { override def write(schema: Document)(nothing: Any)(implicit context: GQLWriterContext): String = { import context._ + val schemaDef = Document.schemaDefinitions(schema).headOption + val argsTypes = Document .objectTypeDefinitions(schema) - .filterNot(reservedType) .flatMap(_.fields.filter(_.args.nonEmpty).map(c => GQLWriter[Args, String].write(Args(c))(""))) .mkString("\n") + val unionTypes = Document + .unionTypeDefinitions(schema) + .map(union => Union(union, union.memberTypes.flatMap(Document.objectTypeDefinition(schema, _)))) + + val unions = unionTypes + .map(GQLWriter[Union, Document].write(_)(schema)) + .mkString("\n") + val objects = Document .objectTypeDefinitions(schema) - .filterNot(reservedType) + .filterNot( + obj => + reservedType(obj) || + schemaDef.exists(_.query.contains(obj.name)) || + schemaDef.exists(_.mutation.contains(obj.name)) || + schemaDef.exists(_.subscription.contains(obj.name)) || + unionTypes.exists(_.objects.exists(_.name == obj.name)) + ) .map(GQLWriter[ObjectTypeDefinition, Document].write(_)(schema)) .mkString("\n") + val inputs = Document + .inputObjectTypeDefinitions(schema) + .map(GQLWriter[InputObjectTypeDefinition, Document].write(_)(schema)) + .mkString("\n") + val enums = Document .enumTypeDefinitions(schema) .map(GQLWriter[EnumTypeDefinition, Document].write(_)(schema)) .mkString("\n") val queries = Document - .objectTypeDefinition(schema, "Query") + .objectTypeDefinition(schema, schemaDef.flatMap(_.query).getOrElse("Query")) .map(t => GQLWriter[RootQueryDef, Document].write(RootQueryDef(t))(schema)) .getOrElse("") val mutations = Document - .objectTypeDefinition(schema, "Mutation") + .objectTypeDefinition(schema, schemaDef.flatMap(_.mutation).getOrElse("Mutation")) .map(t => GQLWriter[RootMutationDef, Document].write(RootMutationDef(t))(schema)) .getOrElse("") val subscriptions = Document - .objectTypeDefinition(schema, "Subscription") + .objectTypeDefinition(schema, schemaDef.flatMap(_.subscription).getOrElse("Subscription")) .map(t => GQLWriter[RootSubscriptionDef, Document].write(RootSubscriptionDef(t))(schema)) .getOrElse("") - val hasSubscriptions = Document.objectTypeDefinition(schema, "Subscription").nonEmpty - val hasTypes = argsTypes.length + objects.length + enums.length > 0 + val hasSubscriptions = subscriptions.nonEmpty + val hasTypes = argsTypes.length + objects.length + enums.length + unions.length + inputs.length > 0 val hasOperations = queries.length + mutations.length + subscriptions.length > 0 - s"""${if (hasTypes && hasOperations) "import Types._\n" else ""} - ${if (hasSubscriptions) "import zio.stream.ZStream" else ""} - + val typesAndOperations = s""" ${if (hasTypes) "object Types {\n" + argsTypes + "\n" + objects + "\n" + + inputs + "\n" + + unions + "\n" + enums + "\n" + "\n}\n" else ""} @@ -104,7 +128,13 @@ object ScalaWriter { "\n}" else ""} """ - // TODO add import GQLDescription + + s"""${if (hasTypes && hasOperations) "import Types._\n" else ""} + ${if (typesAndOperations.contains("@GQL")) "import caliban.schema.Annotations._\n" else ""} + ${if (hasSubscriptions) "import zio.stream.ZStream" else ""} + + $typesAndOperations + """ } } @@ -124,11 +154,7 @@ object ScalaWriter { import context._ s""" - |${queryDef.op.fields - .filter(_.args.nonEmpty) - .map(c => GQLWriter[Args, String].write(Args(c))("")) - .mkString(",\n")} - |case class Query( + |${writeDescription(queryDef.op.description)}case class ${queryDef.op.name}( |${queryDef.op.fields.map(c => GQLWriter[QueryDef, Document].write(QueryDef(c))(schema)).mkString(",\n")} |)""".stripMargin } @@ -149,11 +175,7 @@ object ScalaWriter { import context._ s""" - |${mutationDef.op.fields - .filter(_.args.nonEmpty) - .map(c => GQLWriter[Args, String].write(Args(c))("")) - .mkString(",\n")} - |case class Mutation( + |${writeDescription(mutationDef.op.description)}case class ${mutationDef.op.name}( |${mutationDef.op.fields.map(c => GQLWriter[QueryDef, Document].write(QueryDef(c))(schema)).mkString(",\n")} |)""".stripMargin } @@ -181,11 +203,7 @@ object ScalaWriter { import context._ s""" - |${subscriptionDef.op.fields - .filter(_.args.nonEmpty) - .map(c => GQLWriter[Args, String].write(Args(c))("")) - .mkString(",\n")} - |case class Subscription( + |${writeDescription(subscriptionDef.op.description)}case class ${subscriptionDef.op.name}( |${subscriptionDef.op.fields .map(c => GQLWriter[SubscriptionDef, Document].write(SubscriptionDef(c))(schema)) .mkString(",\n")} @@ -197,22 +215,49 @@ object ScalaWriter { override def write(typedef: ObjectTypeDefinition)(schema: Document)(implicit context: GQLWriterContext): String = { import context._ - s"""case class ${typedef.name}(${typedef.fields + s"""${writeDescription(typedef.description)}case class ${typedef.name}(${typedef.fields .map(GQLWriter[FieldDefinition, ObjectTypeDefinition].write(_)(typedef)) .mkString(", ")})""" } } + object InputObjectWriter extends GQLWriter[InputObjectTypeDefinition, Document] { + override def write( + typedef: InputObjectTypeDefinition + )(schema: Document)(implicit context: GQLWriterContext): String = { + import context._ + + s"""${writeDescription(typedef.description)}case class ${typedef.name}(${typedef.fields + .map(GQLWriter[InputValueDefinition, InputObjectTypeDefinition].write(_)(typedef)) + .mkString(", ")})""" + } + } + object EnumWriter extends GQLWriter[EnumTypeDefinition, Document] { override def write(typedef: EnumTypeDefinition)(schema: Document)(implicit context: GQLWriterContext): String = - s"""sealed trait ${typedef.name} extends Product with Serializable + s"""${writeDescription(typedef.description)}sealed trait ${typedef.name} extends Product with Serializable object ${typedef.name} { ${typedef.enumValuesDefinition - .map(v => s"case object ${v.enumValue} extends ${typedef.name}") + .map(v => s"${writeDescription(v.description)}case object ${v.enumValue} extends ${typedef.name}") + .mkString("\n")} + } + """ + } + + object UnionWriter extends GQLWriter[Union, Document] { + override def write(typedef: Union)(schema: Document)(implicit context: GQLWriterContext): String = { + import context._ + + s"""${writeDescription(typedef.typedef.description)}sealed trait ${typedef.typedef.name} extends Product with Serializable + + object ${typedef.typedef.name} { + ${typedef.objects + .map(o => s"${GQLWriter[ObjectTypeDefinition, Document].write(o)(schema)} extends ${typedef.typedef.name}") .mkString("\n")} } """ + } } object FieldWriter extends GQLWriter[FieldDefinition, ObjectTypeDefinition] { @@ -221,28 +266,42 @@ object ScalaWriter { if (field.args.nonEmpty) { //case when field is parametrized - s"${field.name}: ${of.name.capitalize}${field.name.capitalize}Args => ${GQLWriter[Type, Any].write(field.ofType)(Nil)}" + s"${writeDescription(field.description)}${field.name}: ${of.name.capitalize}${field.name.capitalize}Args => ${GQLWriter[Type, Any] + .write(field.ofType)(Nil)}" } else { - s"""${field.name}: ${GQLWriter[Type, Any].write(field.ofType)(field)}""" + s"""${writeDescription(field.description)}${field.name}: ${GQLWriter[Type, Any].write(field.ofType)(field)}""" } } } + object InputValueWriter extends GQLWriter[InputValueDefinition, InputObjectTypeDefinition] { + override def write( + value: InputValueDefinition + )(of: InputObjectTypeDefinition)(implicit context: GQLWriterContext): String = { + import context._ + + s"""${writeDescription(value.description)}${value.name}: ${GQLWriter[Type, Any].write(value.ofType)(value)}""" + } + } + object ArgsWriter extends GQLWriter[Args, String] { override def write(arg: Args)(prefix: String)(implicit context: GQLWriterContext): String = if (arg.field.args.nonEmpty) { - //case when field is parametrized s"case class ${prefix.capitalize}${arg.field.name.capitalize}Args(${fields(arg.field.args)})" } else { "" } - def fields(args: List[(String, Type)])(implicit context: GQLWriterContext): String = { + def fields(args: List[InputValueDefinition])(implicit context: GQLWriterContext): String = { import context._ - s"${args.map(arg => s"${arg._1}: ${GQLWriter[Type, Any].write(arg._2)(Nil)}").mkString(", ")}" + s"${args.map(arg => s"${arg.name}: ${GQLWriter[Type, Any].write(arg.ofType)(Nil)}").mkString(", ")}" } } + def writeDescription(description: Option[String]): String = + description.fold("")(d => s"""@GQLDescription("$d") + |""".stripMargin) + object TypeWriter extends GQLWriter[Type, Any] { def convertType(name: String): String = name match { case _ => name diff --git a/codegen/src/test/scala/GeneratorSpec.scala b/codegen/src/test/scala/caliban/codegen/GeneratorSpec.scala similarity index 73% rename from codegen/src/test/scala/GeneratorSpec.scala rename to codegen/src/test/scala/caliban/codegen/GeneratorSpec.scala index 3b4bf3b333..cad2a5738e 100644 --- a/codegen/src/test/scala/GeneratorSpec.scala +++ b/codegen/src/test/scala/caliban/codegen/GeneratorSpec.scala @@ -1,11 +1,12 @@ -package codegen -import caliban.codegen.{ Generator, ScalaWriter } +package caliban.codegen + import caliban.codegen.Generator.{ Args, RootMutationDef, RootQueryDef, RootSubscriptionDef } import caliban.parsing.Parser import caliban.parsing.adt.Document import zio.ZIO -import zio.test.Assertion._ -import zio.test._ +import zio.test.Assertion.equalTo +import zio.test.{ assertM, suite, testM, DefaultRunnableSpec } + object GeneratorSpec extends DefaultRunnableSpec( suite("Generator single values")( @@ -77,7 +78,6 @@ object GeneratorSpec caseclassstrdef, equalTo( """ -case class UserArgs(id: Option[Int]) case class Query( user: UserArgs => Option[User], userList: () => List[Option[User]] @@ -107,7 +107,6 @@ userList: () => List[Option[User]] caseclassstrdef, equalTo( """ - |case class SetMessageArgs(message: Option[String]) |case class Mutation( |setMessage: SetMessageArgs => Option[String] |)""".stripMargin @@ -136,7 +135,6 @@ userList: () => List[Option[User]] caseclassstrdef, equalTo( """ - |case class UserWatchArgs(id: Int) |case class Subscription( |UserWatch: UserWatchArgs => ZStream[Any, Nothing, String] |)""".stripMargin @@ -175,7 +173,7 @@ userList: () => List[Option[User]] |import zio.stream.ZStream | |object Types { - | + | case class AddPostArgs(author: Option[String], comment: Option[String]) | case class Post(author: Option[String], comment: Option[String]) | |} @@ -186,7 +184,6 @@ userList: () => List[Option[User]] | posts: () => Option[List[Option[Post]]] | ) | - | case class AddPostArgs(author: Option[String], comment: Option[String]) | case class Mutation( | addPost: AddPostArgs => Option[Post] | ) @@ -241,6 +238,113 @@ userList: () => List[Option[User]] case object BELT extends Origin } +} +""" + ) + ) + }, + testM("union type") { + val gqltype = + """ + "role" + union Role = Captain | Pilot + + type Captain { + "ship" shipName: String! + } + + type Pilot { + shipName: String! + } + """.stripMargin + + implicit val writer = ScalaWriter.DefaultGQLWriter + + val generated: ZIO[Any, Throwable, String] = Parser + .parseQuery(gqltype) + .flatMap(s => Generator.formatStr(Generator.generate(s), ScalaWriter.scalafmtConfig)) + + assertM( + generated, + equalTo( + """import caliban.schema.Annotations._ + +object Types { + + @GQLDescription("role") + sealed trait Role extends Product with Serializable + + object Role { + case class Captain( + @GQLDescription("ship") + shipName: String + ) extends Role + case class Pilot(shipName: String) extends Role + } + +} +""" + ) + ) + }, + testM("schema") { + val gqltype = + """ + schema { + query: Queries + } + + type Queries { + characters: Int! + } + """.stripMargin + + implicit val writer = ScalaWriter.DefaultGQLWriter + + val generated: ZIO[Any, Throwable, String] = Parser + .parseQuery(gqltype) + .flatMap(s => Generator.formatStr(Generator.generate(s), ScalaWriter.scalafmtConfig)) + + assertM( + generated, + equalTo( + """object Operations { + + case class Queries( + characters: () => Int + ) + +} +""" + ) + ) + }, + testM("input type") { + val gqltype = + """ + type Character { + name: String! + } + + input CharacterArgs { + name: String! + } + """.stripMargin + + implicit val writer = ScalaWriter.DefaultGQLWriter + + val generated: ZIO[Any, Throwable, String] = Parser + .parseQuery(gqltype) + .flatMap(s => Generator.formatStr(Generator.generate(s), ScalaWriter.scalafmtConfig)) + + assertM( + generated, + equalTo( + """object Types { + + case class Character(name: String) + case class CharacterArgs(name: String) + } """ ) diff --git a/core/src/main/scala/caliban/parsing/Parser.scala b/core/src/main/scala/caliban/parsing/Parser.scala index 3f0342e25a..4fa05645be 100644 --- a/core/src/main/scala/caliban/parsing/Parser.scala +++ b/core/src/main/scala/caliban/parsing/Parser.scala @@ -6,7 +6,8 @@ import caliban.InputValue._ import caliban.Value._ import caliban.parsing.adt.Definition._ import caliban.parsing.adt.Definition.ExecutableDefinition._ -import caliban.parsing.adt.Definition.TypeSystemDefinition._ +import caliban.parsing.adt.Definition.TypeSystemDefinition.{ SchemaDefinition, TypeDefinition } +import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition._ import caliban.parsing.adt.Selection._ import caliban.parsing.adt.Type._ import caliban.parsing.adt._ @@ -127,13 +128,19 @@ object Parser { private def namedType[_: P]: P[NamedType] = P(name.filter(_ != "null")).map(NamedType(_, nonNull = false)) private def listType[_: P]: P[ListType] = P("[" ~ type_ ~ "]").map(t => ListType(t, nonNull = false)) - private def argumentDefinition[_: P]: P[(String, Type)] = P(name ~ ":" ~ type_) - private def argumentDefinitions[_: P]: P[List[(String, Type)]] = - P("(" ~/ argumentDefinition.rep ~ ")").map(t => t.toList) + private def argumentDefinition[_: P]: P[InputValueDefinition] = + P(stringValue.? ~ name ~ ":" ~ type_ ~ defaultValue.? ~ directives.?).map { + case (description, name, type_, defaultValue, directives) => + InputValueDefinition(description.map(_.value), name, type_, defaultValue, directives.getOrElse(Nil)) + } + private def argumentDefinitions[_: P]: P[List[InputValueDefinition]] = + P("(" ~/ argumentDefinition.rep ~ ")").map(_.toList) private def fieldDefinition[_: P]: P[FieldDefinition] = - P(stringValue.? ~ name ~ argumentDefinitions.? ~ ":" ~ type_ ~ directives.?) - .map(t => FieldDefinition(t._1.map(_.value), t._2, t._3.getOrElse(List()), t._4, t._5.getOrElse(List()))) + P(stringValue.? ~ name ~ argumentDefinitions.? ~ ":" ~ type_ ~ directives.?).map { + case (description, name, args, type_, directives) => + FieldDefinition(description.map(_.value), name, args.getOrElse(Nil), type_, directives.getOrElse(Nil)) + } private def nonNullType[_: P]: P[Type] = P((namedType | listType) ~ "!").map { case t: NamedType => t.copy(nonNull = true) @@ -191,10 +198,17 @@ object Parser { case (name, typeCondition, dirs, sel) => FragmentDefinition(name, typeCondition, dirs, sel) } - private def typeSystemDefinition[_: P]: P[TypeSystemDefinition] = objectTypeDefinition | enumTypeDefinition - private def objectTypeDefinition[_: P]: P[ObjectTypeDefinition] = - P("type" ~/ name ~ "{" ~ fieldDefinition.rep ~ "}").map(t => ObjectTypeDefinition(t._1, t._2.toList)) + P(stringValue.? ~ "type" ~/ name ~ directives.? ~ "{" ~ fieldDefinition.rep ~ "}").map { + case (description, name, directives, fields) => + ObjectTypeDefinition(description.map(_.value), name, directives.getOrElse(Nil), fields.toList) + } + + private def inputObjectTypeDefinition[_: P]: P[InputObjectTypeDefinition] = + P(stringValue.? ~ "input" ~/ name ~ directives.? ~ "{" ~ argumentDefinition.rep ~ "}").map { + case (description, name, directives, fields) => + InputObjectTypeDefinition(description.map(_.value), name, directives.getOrElse(Nil), fields.toList) + } private def enumValueDefinition[_: P]: P[EnumValueDefinition] = P(stringValue.? ~ name ~ directives.?).map { @@ -211,6 +225,37 @@ object Parser { EnumTypeDefinition(description.map(_.value), name, directives.getOrElse(Nil), enumValuesDefinition.toList) } + private def unionTypeDefinition[_: P]: P[UnionTypeDefinition] = + P(stringValue.? ~ "union" ~/ name ~ directives.? ~ "=" ~ ("|".? ~ namedType) ~ ("|" ~ namedType).rep).map { + case (description, name, directives, m, ms) => + UnionTypeDefinition(description.map(_.value), name, directives.getOrElse(Nil), (m :: ms.toList).map(_.name)) + } + + private def scalarTypeDefinition[_: P]: P[ScalarTypeDefinition] = + P(stringValue.? ~ "scalar" ~/ name ~ directives.?).map { + case (description, name, directives) => + ScalarTypeDefinition(description.map(_.value), name, directives.getOrElse(Nil)) + } + + private def rootOperationTypeDefinition[_: P]: P[(OperationType, NamedType)] = P(operationType ~ ":" ~ namedType) + + private def schemaDefinition[_: P]: P[SchemaDefinition] = + P("schema" ~/ directives.? ~ "{" ~ rootOperationTypeDefinition.rep ~ "}").map { + case (directives, ops) => + val opsMap = ops.toMap + SchemaDefinition( + directives.getOrElse(Nil), + opsMap.get(OperationType.Query).map(_.name), + opsMap.get(OperationType.Mutation).map(_.name), + opsMap.get(OperationType.Subscription).map(_.name) + ) + } + + private def typeDefinition[_: P]: P[TypeDefinition] = + objectTypeDefinition | inputObjectTypeDefinition | enumTypeDefinition | unionTypeDefinition | scalarTypeDefinition + + private def typeSystemDefinition[_: P]: P[TypeSystemDefinition] = typeDefinition | schemaDefinition + private def executableDefinition[_: P]: P[ExecutableDefinition] = P(operationDefinition | fragmentDefinition) diff --git a/core/src/main/scala/caliban/parsing/adt/Definition.scala b/core/src/main/scala/caliban/parsing/adt/Definition.scala index 069f5bc3fd..6887c7b2bc 100644 --- a/core/src/main/scala/caliban/parsing/adt/Definition.scala +++ b/core/src/main/scala/caliban/parsing/adt/Definition.scala @@ -1,6 +1,7 @@ package caliban.parsing.adt -import caliban.parsing.adt.Type.{ FieldDefinition, NamedType } +import caliban.InputValue +import caliban.parsing.adt.Type.NamedType sealed trait Definition @@ -25,15 +26,67 @@ object Definition { sealed trait TypeSystemDefinition extends Definition object TypeSystemDefinition { - case class ObjectTypeDefinition(name: String, fields: List[FieldDefinition]) extends TypeSystemDefinition - case class EnumTypeDefinition( - description: Option[String], - name: String, + + case class SchemaDefinition( directives: List[Directive], - enumValuesDefinition: List[EnumValueDefinition] + query: Option[String], + mutation: Option[String], + subscription: Option[String] ) extends TypeSystemDefinition - case class EnumValueDefinition(description: Option[String], enumValue: String, directives: List[Directive]) - } + sealed trait TypeDefinition extends TypeSystemDefinition + object TypeDefinition { + + case class ObjectTypeDefinition( + description: Option[String], + name: String, + directives: List[Directive], + fields: List[FieldDefinition] + ) extends TypeDefinition + + case class InputObjectTypeDefinition( + description: Option[String], + name: String, + directives: List[Directive], + fields: List[InputValueDefinition] + ) extends TypeDefinition + + case class EnumTypeDefinition( + description: Option[String], + name: String, + directives: List[Directive], + enumValuesDefinition: List[EnumValueDefinition] + ) extends TypeDefinition + + case class UnionTypeDefinition( + description: Option[String], + name: String, + directives: List[Directive], + memberTypes: List[String] + ) extends TypeDefinition + case class ScalarTypeDefinition(description: Option[String], name: String, directives: List[Directive]) + extends TypeDefinition + + case class InputValueDefinition( + description: Option[String], + name: String, + ofType: Type, + defaultValue: Option[InputValue], + directives: List[Directive] + ) + + case class FieldDefinition( + description: Option[String], + name: String, + args: List[InputValueDefinition], + ofType: Type, + directives: List[Directive] + ) + + case class EnumValueDefinition(description: Option[String], enumValue: String, directives: List[Directive]) + + } + + } } diff --git a/core/src/main/scala/caliban/parsing/adt/Document.scala b/core/src/main/scala/caliban/parsing/adt/Document.scala index 3ef743c94c..2fded08153 100644 --- a/core/src/main/scala/caliban/parsing/adt/Document.scala +++ b/core/src/main/scala/caliban/parsing/adt/Document.scala @@ -2,19 +2,29 @@ package caliban.parsing.adt import caliban.parsing.SourceMapper import caliban.parsing.adt.Definition.ExecutableDefinition.{ FragmentDefinition, OperationDefinition } -import caliban.parsing.adt.Definition.TypeSystemDefinition.{ EnumTypeDefinition, ObjectTypeDefinition } +import caliban.parsing.adt.Definition.TypeSystemDefinition.SchemaDefinition +import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition._ import caliban.parsing.adt.OperationType.{ Mutation, Query, Subscription } object Document { def objectTypeDefinitions(doc: Document): List[ObjectTypeDefinition] = doc.definitions.collect { case td: ObjectTypeDefinition => td } + def inputObjectTypeDefinitions(doc: Document): List[InputObjectTypeDefinition] = doc.definitions.collect { + case td: InputObjectTypeDefinition => td + } def enumTypeDefinitions(doc: Document): List[EnumTypeDefinition] = doc.definitions.collect { case td: EnumTypeDefinition => td } + def unionTypeDefinitions(doc: Document): List[UnionTypeDefinition] = doc.definitions.collect { + case td: UnionTypeDefinition => td + } def fragmentDefinitions(doc: Document): List[FragmentDefinition] = doc.definitions.collect { case fd: FragmentDefinition => fd } + def schemaDefinitions(doc: Document): List[SchemaDefinition] = doc.definitions.collect { + case sd: SchemaDefinition => sd + } def operationDefinitions(doc: Document): List[OperationDefinition] = doc.definitions.collect { case od: OperationDefinition => od } diff --git a/core/src/main/scala/caliban/parsing/adt/Type.scala b/core/src/main/scala/caliban/parsing/adt/Type.scala index f9031f64a3..369ea7a769 100644 --- a/core/src/main/scala/caliban/parsing/adt/Type.scala +++ b/core/src/main/scala/caliban/parsing/adt/Type.scala @@ -9,14 +9,6 @@ object Type { case class NamedType(name: String, nonNull: Boolean) extends Type case class ListType(ofType: Type, nonNull: Boolean) extends Type - case class FieldDefinition( - description: Option[String], - name: String, - args: List[(String, Type)], - ofType: Type, - directives: List[Directive] - ) - @tailrec def innerType(t: Type): String = t match { case NamedType(name, _) => name diff --git a/core/src/test/scala/caliban/parsing/ParserSpec.scala b/core/src/test/scala/caliban/parsing/ParserSpec.scala index 8b3581b0f3..0334519c89 100644 --- a/core/src/test/scala/caliban/parsing/ParserSpec.scala +++ b/core/src/test/scala/caliban/parsing/ParserSpec.scala @@ -6,10 +6,10 @@ import caliban.InputValue._ import caliban.Value._ import caliban.parsing.ParserSpecUtils._ import caliban.parsing.adt.Definition.ExecutableDefinition.{ FragmentDefinition, OperationDefinition } -import caliban.parsing.adt.Definition.TypeSystemDefinition.ObjectTypeDefinition +import caliban.parsing.adt.Definition.TypeSystemDefinition.TypeDefinition._ import caliban.parsing.adt.OperationType.{ Mutation, Query } import caliban.parsing.adt.Selection.{ Field, FragmentSpread, InlineFragment } -import caliban.parsing.adt.Type.{ FieldDefinition, ListType, NamedType } +import caliban.parsing.adt.Type.{ ListType, NamedType } import caliban.parsing.adt._ import zio.test.Assertion._ import zio.test._ @@ -457,12 +457,14 @@ object ParserSpec Document( List( ObjectTypeDefinition( + None, "Hero", + Nil, List( FieldDefinition( Some("name desc"), "name", - List("pad" -> NamedType("Int", true)), + List(InputValueDefinition(None, "pad", NamedType("Int", true), None, Nil)), NamedType("String", true), List(Directive("skip", Map("if" -> VariableValue("someTestM")), index = 49)) ), diff --git a/vuepress/docs/docs/schema.md b/vuepress/docs/docs/schema.md index c34a467900..b75a8afcd8 100644 --- a/vuepress/docs/docs/schema.md +++ b/vuepress/docs/docs/schema.md @@ -174,9 +174,9 @@ implicit val unitSchema: Schema[Any, Unit] = scalarSchema("Unit", None, _ => Obj ## Schema generation -Caliban can automatically generate Scala code out of a GraphQL schema provided as a json file for easier and faster development. +Caliban can automatically generate Scala code from a GraphQL schema. -In order to use this feature add the caliban sbt plugin to your project and enable it. +In order to use this feature, add the `caliban-codegen` sbt plugin to your project and enable it. ```scala addSbtPlugin("com.github.ghostdogpr" % "caliban-codegen" % "0.5.0") @@ -184,13 +184,14 @@ enablePlugins(CodegenPlugin) ``` Then call the `codegen` sbt command. ```scala -codegen schemaPath outPath ?scalafmtPath +calibanGenSchema schemaPath outPath ?scalafmtPath -codegen project/schema.json src/main/GQLSchema.scala +calibanGenSchema project/schema.json src/main/GQLSchema.scala ``` -Command will write a scala file (outPath) containing GraphQL types, -queries, subscriptions and a few utility classes for provided json schema (schemaPath) and will -format generated code with scalafmt with config in (scalafmtPath) or -default config provided along with caliban-codegen. +This command will create a Scala file in `outputPath` containing all the types defined in the provided GraphQL schema defined at `schemaPath`. The generated code will be formatted with Scalafmt using the configuration defined by `scalafmtPath`. -Expressions that can not be generated in the current release are following: enum, schema, interfaces, union types, input. \ No newline at end of file +::: warning Unsupported features +Some features are not supported by Caliban and will cause an error during code generation: +- interfaces +- extensions +::: \ No newline at end of file