diff --git a/generator/src/main/scala/io/tvc/graphql/Generator.scala b/generator/src/main/scala/io/tvc/graphql/Generator.scala index dd6a2b0..8ab9813 100644 --- a/generator/src/main/scala/io/tvc/graphql/Generator.scala +++ b/generator/src/main/scala/io/tvc/graphql/Generator.scala @@ -1,8 +1,8 @@ package io.tvc.graphql import io.tvc.graphql.parsing.{QueryParser, SchemaParser} -import io.tvc.graphql.transform.ScalaCodeGen.generate -import io.tvc.graphql.transform.TypeChecker -import io.tvc.graphql.transform.TypeDeduplicator.deduplicate +import io.tvc.graphql.generation.ScalaCodeGenerator.generate +import io.tvc.graphql.inlining.OutputInliner +import io.tvc.graphql.generation.TypeDeduplicator.deduplicate object Generator { @@ -17,6 +17,6 @@ object Generator { sch <- SchemaParser.parse(schema) opDefinition <- QueryParser.parse(query) name = opDefinition.name.fold("AnonymousQuery")(_.value.capitalize) - tree <- TypeChecker.run(sch, opDefinition).left.map(te => s"$te") + tree <- OutputInliner.run(sch, opDefinition).left.map(te => s"$te") } yield GeneratedQueryCode(namespace, name, generate(name, namespace, query, deduplicate(tree))) } diff --git a/generator/src/main/scala/io/tvc/graphql/transform/ScalaCodeGen.scala b/generator/src/main/scala/io/tvc/graphql/generation/ScalaCodeGenerator.scala similarity index 90% rename from generator/src/main/scala/io/tvc/graphql/transform/ScalaCodeGen.scala rename to generator/src/main/scala/io/tvc/graphql/generation/ScalaCodeGenerator.scala index 239528b..ddb322a 100644 --- a/generator/src/main/scala/io/tvc/graphql/transform/ScalaCodeGen.scala +++ b/generator/src/main/scala/io/tvc/graphql/generation/ScalaCodeGenerator.scala @@ -1,13 +1,15 @@ -package io.tvc.graphql.transform +package io.tvc.graphql.generation + +import TypeDeduplicator.{FlatType, TypeRef} +import io.tvc.graphql.inlining.TypeTree +import io.tvc.graphql.inlining.TypeTree.TypeModifier.{ListType, NonNullType, NullableType} +import io.tvc.graphql.inlining.TypeTree.{Scalar, TypeModifier} import cats.instances.list._ import cats.instances.option._ -import cats.instances.string._ import cats.syntax.foldable._ -import io.tvc.graphql.transform.TypeDeduplicator.{FlatType, TypeRef} -import io.tvc.graphql.transform.TypeTree.TypeModifier.{ListType, NonNullType, NullableType} -import io.tvc.graphql.transform.TypeTree.{Scalar, TypeModifier} +import cats.instances.string._ -object ScalaCodeGen { +object ScalaCodeGenerator { private val indent: String = " " diff --git a/generator/src/main/scala/io/tvc/graphql/transform/TypeDeduplicator.scala b/generator/src/main/scala/io/tvc/graphql/generation/TypeDeduplicator.scala similarity index 87% rename from generator/src/main/scala/io/tvc/graphql/transform/TypeDeduplicator.scala rename to generator/src/main/scala/io/tvc/graphql/generation/TypeDeduplicator.scala index 0c27d1c..f60cef2 100644 --- a/generator/src/main/scala/io/tvc/graphql/transform/TypeDeduplicator.scala +++ b/generator/src/main/scala/io/tvc/graphql/generation/TypeDeduplicator.scala @@ -1,8 +1,10 @@ -package io.tvc.graphql.transform +package io.tvc.graphql.generation import cats.Monad import cats.data.State -import io.tvc.graphql.transform.TypeTree.{Enum, Metadata, Object, RecTypeTree, Scalar, Union} +import io.tvc.graphql.inlining.TypeTree.{Enum, Metadata, Object, RecTypeTree, Scalar, Union} +import io.tvc.graphql.inlining.TypeTree +import io.tvc.graphql.recursion.Fix object TypeDeduplicator { diff --git a/generator/src/main/scala/io/tvc/graphql/inlining/Inliner.scala b/generator/src/main/scala/io/tvc/graphql/inlining/Inliner.scala new file mode 100644 index 0000000..c4cc94f --- /dev/null +++ b/generator/src/main/scala/io/tvc/graphql/inlining/Inliner.scala @@ -0,0 +1,29 @@ +package io.tvc.graphql.inlining + +import io.tvc.graphql.parsing.QueryModel.OperationDefinition +import io.tvc.graphql.parsing.SchemaModel.Schema +import io.tvc.graphql.inlining.Utilities.TypeError.OrMissing +import io.tvc.graphql.inlining.InputInliner.RecInputTypeTree +import io.tvc.graphql.inlining.TypeTree.RecTypeTree +import cats.instances.either._ +import cats.syntax.apply._ + +/** + * Given a query and schema def, zip the two together and inline all the types + * to produce a series of recursive ASTs for both the query inputs and the outputs + * This is so later steps can deal with duplicate names etc later + */ +object Inliner { + + case class InlinedQuery( + inputs: List[RecInputTypeTree], + output: RecTypeTree + ) + + def apply(schema: Schema, operationDefinition: OperationDefinition): OrMissing[InlinedQuery] = + ( + InputInliner.run(schema, operationDefinition), + OutputInliner.run(schema, operationDefinition) + ).mapN(InlinedQuery.apply) + +} diff --git a/generator/src/main/scala/io/tvc/graphql/inlining/InputInliner.scala b/generator/src/main/scala/io/tvc/graphql/inlining/InputInliner.scala new file mode 100644 index 0000000..d9f9c00 --- /dev/null +++ b/generator/src/main/scala/io/tvc/graphql/inlining/InputInliner.scala @@ -0,0 +1,99 @@ +package io.tvc.graphql.inlining + +import cats.instances.either._ +import cats.instances.list._ +import cats.syntax.either._ +import cats.syntax.functor._ +import cats.syntax.traverse._ +import cats.{Applicative, Eval, Traverse} +import io.tvc.graphql.parsing.QueryModel.OperationDefinition +import io.tvc.graphql.parsing.SchemaModel.TypeDefinition.InputObjectTypeDefinition +import io.tvc.graphql.parsing.SchemaModel.{InputValueDefinition, Schema, TypeDefinition} +import io.tvc.graphql.recursion.Fix +import io.tvc.graphql.recursion.Fix.unfoldF +import io.tvc.graphql.inlining.Utilities._ +import io.tvc.graphql.inlining.Utilities.TypeError.OrMissing +import io.tvc.graphql.inlining.TypeTree.{Field, FieldName, Metadata, Object} + +import scala.language.higherKinds + +/** + * Here we take a query and a schema and produce a list InputTypeTree ASTs. + * This is a recursive structure of complex types required by the query + */ +object InputInliner { + + /** + * An InputValue is the same structurally as an output value + * but it also can have a default value so we need to wrap it + */ + case class InputValue[+A](default: Option[String], value: A) + + object InputValue { + + implicit val traverse: Traverse[InputValue] = + new Traverse[InputValue] { + def traverse[G[_]: Applicative, A, B](fa: InputValue[A])(f: A => G[B]): G[InputValue[B]] = + f(fa.value).map(b => fa.copy(value = b)) + def foldLeft[A, B](fa: InputValue[A], b: B)(f: (B, A) => B): B = + f(b, fa.value) + def foldRight[A, B](fa: InputValue[A], lb: Eval[B])(f: (A, Eval[B]) => Eval[B]): Eval[B] = + f(fa.value, lb) + } + } + + type InputField = Field[InputValue[TypeDefinition]] + type InputTypeTree[+A] = TypeTree[InputValue[A]] + type RecInputTypeTree = Fix[InputTypeTree] + + implicit val traverse: Traverse[InputTypeTree] = + TypeTree.ttTraverse.compose(InputValue.traverse) + + /** + * Given an InputValueDefinition create an input field which + * contains a raw TypeDefinition that will be expanded by unfoldF + */ + def createField(schema: Schema)(v: InputValueDefinition): OrMissing[InputField] = + findTypeDefinition(schema, v.`type`).map { defn => + Field( + name = FieldName(alias = None, value = v.name.value), + `type` = InputValue(v.defaultValue.map(_.toString), defn), + modifiers = modifiers(v.`type`).toList + ) + } + + /** + * Create metadata for an input object, + * preserving the description + name + */ + def metadata(i: InputObjectTypeDefinition): Metadata = + Metadata(i.description.map(_.value), i.name.value) + + /** + * Try to create a complex type from this type definition, + * will fail if the type isn't an input object + */ + def complex(schema: Schema, td: TypeDefinition): OrMissing[InputTypeTree[TypeDefinition]] = + td match { + case i: InputObjectTypeDefinition => i.values.traverse(createField(schema)).map(Object(metadata(i), _)) + case e => Left(TypeError.Todo(s"Expected input object, got $e")) + } + + /** + * Try to create a simple type from this type definition, + * will fail if the type isn't a scalar (Enum / etc) + */ + def simple(td: TypeDefinition): OrMissing[InputTypeTree[TypeDefinition]] = + OutputInliner.createScalar(td) + + /** + * Run the inlining process on the given query, taking all of it's arguments + * and thus producing a recursive list of input types the user must submit to the server + */ + def run(schema: Schema, query: OperationDefinition): OrMissing[List[RecInputTypeTree]] = + query.variableDefinitions.traverse { v => + findTypeDefinition(schema, v.`type`).flatMap { td => + unfoldF(td)(t => simple(t).orElse(complex(schema, t))) + } + } +} diff --git a/generator/src/main/scala/io/tvc/graphql/transform/TypeChecker.scala b/generator/src/main/scala/io/tvc/graphql/inlining/OutputInliner.scala similarity index 57% rename from generator/src/main/scala/io/tvc/graphql/transform/TypeChecker.scala rename to generator/src/main/scala/io/tvc/graphql/inlining/OutputInliner.scala index 348cf05..c83255a 100644 --- a/generator/src/main/scala/io/tvc/graphql/transform/TypeChecker.scala +++ b/generator/src/main/scala/io/tvc/graphql/inlining/OutputInliner.scala @@ -1,31 +1,24 @@ -package io.tvc.graphql.transform +package io.tvc.graphql.inlining import cats.instances.either._ import cats.instances.list._ import cats.instances.string._ import cats.syntax.traverse._ -import io.tvc.graphql.parsing.CommonModel.Type.{ListType, NamedType, NonNullType} -import io.tvc.graphql.parsing.CommonModel.{Name, Type} +import io.tvc.graphql.inlining.TypeTree.{FieldName, Metadata, RecTypeTree} +import io.tvc.graphql.inlining.Utilities.TypeError._ +import io.tvc.graphql.inlining.Utilities._ +import io.tvc.graphql.parsing.CommonModel.Name +import io.tvc.graphql.parsing.CommonModel.Type.NamedType import io.tvc.graphql.parsing.QueryModel.{Field, OperationDefinition, SelectionSet} import io.tvc.graphql.parsing.SchemaModel.TypeDefinition._ import io.tvc.graphql.parsing.SchemaModel.{FieldDefinition, Schema, TypeDefinition} -import io.tvc.graphql.transform.TypeChecker.TypeError.{ExpectedFields, MissingType, OrMissing} -import io.tvc.graphql.transform.TypeTree.TypeModifier.NullableType -import io.tvc.graphql.transform.TypeTree.{FieldName, Metadata, RecTypeTree, TypeModifier} - -import scala.annotation.tailrec +import io.tvc.graphql.recursion.Fix /** - * Here we take a query and a schema and produce a QueryResult AST. - * This is a flat structure of root level complex types used by the query + * Here we take a query and a schema and produce a TypeTree AST. + * This is a recursive structure of complex types returned by the query */ -object TypeChecker { - - /** - * https://graphql.github.io/graphql-spec/June2018/#sec-Scalars - */ - val predefinedTypes: List[TypeDefinition] = - List("Int", "Float", "Boolean", "String", "ID").map(t => ScalarTypeDefinition(None, Name(t), List.empty)) +object OutputInliner { /** * A cursor field gathers together all the possible information about a field in a SelectionSet - @@ -39,45 +32,32 @@ object TypeChecker { * where the previous items are fields with complex types we've delved down into, * this is kind of inspired by Circe's cursor */ - private case class Cursor( + private case class FieldCursor( schema: Schema, prev: Vector[CursorField[ComplexType]], focus: CursorField[TypeDefinition]) { - def down(field: Field): OrMissing[Cursor] = + def down(field: Field): OrMissing[FieldCursor] = down(FieldName(field.alias.map(_.value), field.name.value)) - def down(field: FieldName): OrMissing[Cursor] = + def down(field: FieldName): OrMissing[FieldCursor] = for { complexType <- ComplexType.fromDefinition(focus.tpe) fieldDef <- findFieldDefinition(complexType, field) tpe <- findTypeDefinition(schema, fieldDef.`type`) - } yield Cursor(schema, prev :+ focus.copy(tpe = complexType), CursorField(field, fieldDef, tpe)) + } yield FieldCursor(schema, prev :+ focus.copy(tpe = complexType), CursorField(field, fieldDef, tpe)) def parent: Option[CursorField[ComplexType]] = prev.lastOption } - /** - * Given a type, dig through it to find out what the modifiers are - * They should then be applied left-to-right to the eventual type name - */ - private def modifiers(tpe: Type, mods: Vector[TypeModifier] = Vector.empty): Vector[TypeModifier] = - tpe match { - case Type.NonNullType(t) => modifiers(t, mods :+ TypeModifier.NonNullType) - case a if !mods.lastOption.contains(TypeModifier.NonNullType) && - !mods.lastOption.contains(TypeModifier.NullableType) => modifiers(a, mods :+ NullableType) - case Type.ListType(a) => modifiers(a, mods :+ TypeModifier.ListType) - case Type.NamedType(_) => mods - } - /** * Given a cursor try to turn it into a Scalar type tree by matching on it's current definition * Will fail if the definition isn't an enum or a scalar type */ - def createScalar(c: Cursor): OrMissing[TypeTree[Nothing]] = - c.focus.tpe match { + def createScalar(tpe: TypeDefinition): OrMissing[TypeTree[Nothing]] = + tpe match { case e: EnumTypeDefinition => Right( TypeTree.Enum( @@ -88,18 +68,9 @@ object TypeChecker { case s: ScalarTypeDefinition => Right(TypeTree.Scalar(Metadata(s.description.map(_.value), s.name.value))) case _ => - Left(ExpectedFields(c.focus.tpe.name.value)) + Left(ExpectedFields(tpe.name.value)) } - sealed trait TypeError - - object TypeError { - type OrMissing[A] = Either[TypeError, A] - case class MissingType(name: String) extends TypeError - case class MissingField(name: String) extends TypeError - case class ExpectedFields(field: String) extends TypeError - case class Todo(name: String) extends TypeError - } /** * Field types in GraphQL can be both concrete objects and also interfaces @@ -117,26 +88,6 @@ object TypeChecker { } } - /** - * Given a type, which may be non nullable or a list - * or some nested variant thereof, extract its name - */ - @tailrec - private def typeName(typ: Type): Name = - typ match { - case NamedType(value) => value - case ListType(value) => typeName(value) - case NonNullType(value) => typeName(value) - } - - /** - * Try to dig the given type out of the schema, - * we ignore any type modifiers and just search for a type with the same name - * but we include the input type including modifiers in the response - */ - private def findTypeDefinition(schema: Schema, tpe: Type): OrMissing[TypeDefinition] = - (predefinedTypes ++ schema).find(_.name == typeName(tpe)) - .toRight(MissingType(typeName(tpe).value)) /** * Given a complex type and a field name, find the definition of the field. @@ -157,7 +108,7 @@ object TypeChecker { * Given a cursor and the type for a field * then create a TypeTree field with the given inlined type */ - private def createField(c: Cursor)(tpe: RecTypeTree): TypeTree.Field[RecTypeTree] = + private def createField(c: FieldCursor)(tpe: RecTypeTree): TypeTree.Field[RecTypeTree] = TypeTree.Field( `type` = tpe, name = c.focus.name, @@ -168,14 +119,14 @@ object TypeChecker { * Create a field from a cursor, assuming the cursor is pointing * at a scalar type definition. Will fail with a TypeError if it isn't */ - private def createScalarField(cursor: Cursor): Either[TypeError, TypeTree.Field[RecTypeTree]] = - createScalar(cursor).map(f => createField(cursor)(Fix[TypeTree](f))) + private def createScalarField(cursor: FieldCursor): Either[TypeError, TypeTree.Field[RecTypeTree]] = + createScalar(cursor.focus.tpe).map(f => createField(cursor)(Fix[TypeTree](f))) /** * Given a recursive SelectionSet, go down through it * and create a TypeTree by inlining all the type definitions of it's fields */ - private def createTree(cursor: Cursor, selectionSet: SelectionSet): Either[TypeError, RecTypeTree] = + private def createTree(cursor: FieldCursor, selectionSet: SelectionSet): Either[TypeError, RecTypeTree] = selectionSet.fields.traverse { case f @ Node(other) => cursor.down(f).flatMap(c => createTree(c, other).map(createField(c))) case f => cursor.down(f).flatMap(c => createScalarField(c)) @@ -194,7 +145,7 @@ object TypeChecker { for { root <- findTypeDefinition(schema, NamedType(Name("Query"))) queryDef = FieldDefinition(None, Name("Query"), List.empty, NamedType(Name("Query")), List.empty) - cursor = Cursor(schema, Vector.empty, CursorField(FieldName(None, "Query"), queryDef, root)) + cursor = FieldCursor(schema, Vector.empty, CursorField(FieldName(None, "Query"), queryDef, root)) result <- createTree(cursor, operationDefinition.selectionSet) } yield result } diff --git a/generator/src/main/scala/io/tvc/graphql/transform/TypeTree.scala b/generator/src/main/scala/io/tvc/graphql/inlining/TypeTree.scala similarity index 96% rename from generator/src/main/scala/io/tvc/graphql/transform/TypeTree.scala rename to generator/src/main/scala/io/tvc/graphql/inlining/TypeTree.scala index 31ec987..b76b936 100644 --- a/generator/src/main/scala/io/tvc/graphql/transform/TypeTree.scala +++ b/generator/src/main/scala/io/tvc/graphql/inlining/TypeTree.scala @@ -1,11 +1,12 @@ -package io.tvc.graphql.transform +package io.tvc.graphql.inlining import cats.instances.list._ import cats.syntax.foldable._ import cats.syntax.functor._ import cats.syntax.traverse._ import cats.{Applicative, Eval, Traverse} -import io.tvc.graphql.transform.TypeTree.Metadata +import io.tvc.graphql.recursion.Fix +import io.tvc.graphql.inlining.TypeTree.Metadata import scala.language.higherKinds diff --git a/generator/src/main/scala/io/tvc/graphql/inlining/Utilities.scala b/generator/src/main/scala/io/tvc/graphql/inlining/Utilities.scala new file mode 100644 index 0000000..1315deb --- /dev/null +++ b/generator/src/main/scala/io/tvc/graphql/inlining/Utilities.scala @@ -0,0 +1,64 @@ +package io.tvc.graphql.inlining + +import io.tvc.graphql.parsing.CommonModel.{Name, Type} +import io.tvc.graphql.parsing.CommonModel.Type.{ListType, NamedType, NonNullType} +import io.tvc.graphql.parsing.SchemaModel.{Schema, TypeDefinition} +import io.tvc.graphql.parsing.SchemaModel.TypeDefinition.ScalarTypeDefinition +import io.tvc.graphql.inlining.Utilities.TypeError.{MissingType, OrMissing} +import io.tvc.graphql.inlining.TypeTree.TypeModifier +import io.tvc.graphql.inlining.TypeTree.TypeModifier.NullableType + +import scala.annotation.tailrec + +object Utilities { + + sealed trait TypeError + + object TypeError { + type OrMissing[A] = Either[TypeError, A] + case class MissingType(name: String) extends TypeError + case class MissingField(name: String) extends TypeError + case class ExpectedFields(field: String) extends TypeError + case class Todo(name: String) extends TypeError + } + + /** + * https://graphql.github.io/graphql-spec/June2018/#sec-Scalars + */ + val predefinedTypes: List[TypeDefinition] = + List("Int", "Float", "Boolean", "String", "ID").map(t => ScalarTypeDefinition(None, Name(t), List.empty)) + + /** + * Given a type, which may be non nullable or a list + * or some nested variant thereof, extract its name + */ + @tailrec + private def typeName(typ: Type): Name = + typ match { + case NamedType(value) => value + case ListType(value) => typeName(value) + case NonNullType(value) => typeName(value) + } + + /** + * Try to dig the given type out of the schema, + * we ignore any type modifiers and just search for a type with the same name + * but we include the input type including modifiers in the response + */ + def findTypeDefinition(schema: Schema, tpe: Type): OrMissing[TypeDefinition] = + (predefinedTypes ++ schema).find(_.name == typeName(tpe)) + .toRight(MissingType(typeName(tpe).value)) + + /** + * Given a type, dig through it to find out what the modifiers are + * They should then be applied left-to-right to the eventual type name + */ + def modifiers(tpe: Type, mods: Vector[TypeModifier] = Vector.empty): Vector[TypeModifier] = + tpe match { + case Type.NonNullType(t) => modifiers(t, mods :+ TypeModifier.NonNullType) + case a if !mods.lastOption.contains(TypeModifier.NonNullType) && + !mods.lastOption.contains(TypeModifier.NullableType) => modifiers(a, mods :+ NullableType) + case Type.ListType(a) => modifiers(a, mods :+ TypeModifier.ListType) + case Type.NamedType(_) => mods + } +} diff --git a/generator/src/main/scala/io/tvc/graphql/transform/Fix.scala b/generator/src/main/scala/io/tvc/graphql/recursion/Fix.scala similarity index 78% rename from generator/src/main/scala/io/tvc/graphql/transform/Fix.scala rename to generator/src/main/scala/io/tvc/graphql/recursion/Fix.scala index 969c4f6..a608836 100644 --- a/generator/src/main/scala/io/tvc/graphql/transform/Fix.scala +++ b/generator/src/main/scala/io/tvc/graphql/recursion/Fix.scala @@ -1,10 +1,10 @@ -package io.tvc.graphql.transform +package io.tvc.graphql.recursion -import cats.{Id, Monad, Traverse} import cats.instances.either._ +import cats.syntax.flatMap._ import cats.syntax.functor._ import cats.syntax.traverse._ -import cats.syntax.flatMap._ +import cats.{Id, Monad, Traverse} import scala.language.higherKinds @@ -16,6 +16,13 @@ case class Fix[F[+_]](unfix: F[Fix[F]]) object Fix { + /** + * Given a seed value perform an effect to construct a tree + * The opposite of the folds below but just as thrillingly un stack safe + */ + def unfoldF[F[+_]: Traverse, G[_]: Monad, A](a: A)(f: A => G[F[A]]): G[Fix[F]] = + f(a).flatMap(_.traverse(unfoldF(_)(f)).map(Fix(_))) + /** * Find out if we're at the end of a tree by using the traverse instance of the fixed type * If we're at the end of the tree we'll return the leaf type (i.e. an F[Nothing]) diff --git a/generator/src/test/resources/queries/query.graphql b/generator/src/test/resources/queries/query.graphql index 6f70a25..09912ff 100644 --- a/generator/src/test/resources/queries/query.graphql +++ b/generator/src/test/resources/queries/query.graphql @@ -1,4 +1,4 @@ -query foo { +query foo($blah:CloneTemplateRepositoryInput) { organization(login: "blah") { repo1: repository(name: "repo-one") { pullRequests(states: OPEN, first: 100) { diff --git a/generator/src/test/scala/io/tvc/graphql/inlining/InputInlinerTest.scala b/generator/src/test/scala/io/tvc/graphql/inlining/InputInlinerTest.scala new file mode 100644 index 0000000..885b843 --- /dev/null +++ b/generator/src/test/scala/io/tvc/graphql/inlining/InputInlinerTest.scala @@ -0,0 +1,26 @@ +package io.tvc.graphql.inlining + +import io.tvc.graphql.parsing.{Loader, QueryParser, SchemaParser} +import org.scalatest.{Matchers, WordSpec} + +class InputInlinerTest extends WordSpec with Matchers { + + "Output type checker" should { + + "Work" in { + + + val q = QueryParser.parse(Loader.load("/queries/query.graphql")).right.get + val s = SchemaParser.parse(Loader.load("/schemas/github.idl")).right.get + + + println(InputInliner.run(s, q)) + pending + + } + + + } + + +}