Skip to content

Commit

Permalink
Giant refactor to begin to support input types
Browse files Browse the repository at this point in the history
  • Loading branch information
tomverran committed Sep 1, 2019
1 parent bf26c16 commit 7230f95
Show file tree
Hide file tree
Showing 11 changed files with 270 additions and 89 deletions.
8 changes: 4 additions & 4 deletions 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 {

Expand All @@ -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)))
}
@@ -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 = " "

Expand Down
@@ -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 {

Expand Down
29 changes: 29 additions & 0 deletions 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)

}
@@ -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)))
}
}
}
@@ -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 -
Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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,
Expand All @@ -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))
Expand All @@ -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
}
@@ -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

Expand Down

0 comments on commit 7230f95

Please sign in to comment.