Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for field aliases and multiple root fields #39

Merged
merged 1 commit into from
Jan 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions modules/core/src/main/scala/compiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,18 @@ object QueryParser {
}

def parseSelection(sel: Selection, typeCondition: Option[String], fragments: Map[String, FragmentDefinition]): Result[Query] = sel match {
case Field(_, name, args, _, sels) =>
case Field(alias, name, args, _, sels) =>
for {
args0 <- parseArgs(args)
sels0 <- parseSelections(sels, None, fragments)
} yield {
val sel =
val sel0 =
if (sels.isEmpty) Select(name.value, args0, Empty)
else Select(name.value, args0, sels0)
val sel = alias match {
case Some(Name(nme)) => Rename(nme, sel0)
case None => sel0
}
typeCondition match {
case Some(tpnme) => UntypedNarrow(tpnme, sel)
case _ => sel
Expand Down Expand Up @@ -202,6 +206,7 @@ object QueryCompiler {

case n@Narrow(subtpe, child) => loop(child, subtpe).map(ec => n.copy(child = ec))
case w@Wrap(_, child) => loop(child, tpe).map(ec => w.copy(child = ec))
case r@Rename(_, child) => loop(child, tpe).map(ec => r.copy(child = ec))
case g@Group(queries) => queries.traverse(q => loop(q, tpe)).map(eqs => g.copy(queries = eqs))
case u@Unique(_, child) => loop(child, tpe.nonNull).map(ec => u.copy(child = ec))
case f@Filter(_, child) => loop(child, tpe.item).map(ec => f.copy(child = ec))
Expand Down Expand Up @@ -262,21 +267,22 @@ object QueryCompiler {
def apply(query: Query, schema: Schema, tpe: Type): Result[Query] = {
def loop(query: Query, tpe: Type): Result[Query] =
query match {
case Select(fieldName, args, child) =>
case PossiblyRenamedSelect(Select(fieldName, args, child), resultName) =>
val childTpe = tpe.underlyingField(fieldName)
mapping.get((tpe.underlyingObject, fieldName)) match {
case Some((cid, join)) =>
loop(child, childTpe).map { elaboratedChild =>
Wrap(fieldName, Component(cid, join, Select(fieldName, args, elaboratedChild)))
Wrap(resultName, Component(cid, join, PossiblyRenamedSelect(Select(fieldName, args, elaboratedChild), resultName)))
}
case None =>
loop(child, childTpe).map { elaboratedChild =>
Select(fieldName, args, elaboratedChild)
PossiblyRenamedSelect(Select(fieldName, args, elaboratedChild), resultName)
}
}

case n@Narrow(subtpe, child) => loop(child, subtpe).map(ec => n.copy(child = ec))
case w@Wrap(_, child) => loop(child, tpe).map(ec => w.copy(child = ec))
case r@Rename(_, child) => loop(child, tpe).map(ec => r.copy(child = ec))
case g@Group(queries) => queries.traverse(q => loop(q, tpe)).map(eqs => g.copy(queries = eqs))
case u@Unique(_, child) => loop(child, tpe.nonNull).map(ec => u.copy(child = ec))
case f@Filter(_, child) => loop(child, tpe.item).map(ec => f.copy(child = ec))
Expand Down
6 changes: 3 additions & 3 deletions modules/core/src/main/scala/datatype.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cats.Monad
import cats.implicits._
import io.circe.Json

import Query.{ Select, Wrap }
import Query.{ PossiblyRenamedSelect, Select, Wrap }
import QueryInterpreter.{ mkErrorResult, ProtoJson }
import ScalarType._

Expand Down Expand Up @@ -44,11 +44,11 @@ class DataTypeQueryInterpreter[F[_]: Monad](

def runRootValue(query: Query, rootTpe: Type): F[Result[ProtoJson]] =
query match {
case Select(fieldName, _, child) =>
case PossiblyRenamedSelect(Select(fieldName, _, child), resultName) =>
if (root.isDefinedAt(fieldName)) {
val (tpe, focus) = root(fieldName)
val cursor = DataTypeCursor(tpe, focus, fields, attrs, narrows)
runValue(Wrap(fieldName, child), rootTpe.field(fieldName), cursor).pure[F]
runValue(Wrap(resultName, child), rootTpe.field(fieldName), cursor).pure[F]
} else
mkErrorResult(s"No root field '$fieldName'").pure[F]
case _ =>
Expand Down
14 changes: 6 additions & 8 deletions modules/core/src/main/scala/introspection.scala
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,14 @@ import QueryCompiler._
object IntrospectionQueryCompiler extends QueryCompiler(SchemaSchema) {
val selectElaborator = new SelectElaborator(Map(
SchemaSchema.tpe("Query").dealias -> {
case Select("__type", List(StringBinding("name", name)), child) =>
Select("__type", Nil, Unique(FieldEquals("name", name), child)).rightIor
case sel@Select("__type", List(StringBinding("name", name)), _) =>
sel.eliminateArgs(child => Unique(FieldEquals("name", name), child)).rightIor
},
SchemaSchema.tpe("__Type").dealias -> {
case Select("fields", List(BooleanBinding("includeDeprecated", include)), child) =>
val filteredChild = if (include) child else Filter(FieldEquals("isDeprecated", false), child)
Select("fields", Nil, filteredChild).rightIor
case Select("enumValues", List(BooleanBinding("includeDeprecated", include)), child) =>
val filteredChild = if (include) child else Filter(FieldEquals("isDeprecated", false), child)
Select("enumValues", Nil, filteredChild).rightIor
case sel@Select("fields", List(BooleanBinding("includeDeprecated", include)), _) =>
sel.eliminateArgs(child => if (include) child else Filter(FieldEquals("isDeprecated", false), child)).rightIor
case sel@Select("enumValues", List(BooleanBinding("includeDeprecated", include)), _) =>
sel.eliminateArgs(child => if (include) child else Filter(FieldEquals("isDeprecated", false), child)).rightIor
}
))

Expand Down
145 changes: 127 additions & 18 deletions modules/core/src/main/scala/query.scala
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ sealed trait Query {
object Query {
/** Select field `name` given arguments `args` and continue with `child` */
case class Select(name: String, args: List[Binding], child: Query = Empty) extends Query {
def eliminateArgs(elim: Query => Query): Query = copy(args = Nil, child = elim(child))

def transformChild(f: Query => Query): Query = copy(child = f(child))

def render = {
val rargs = if(args.isEmpty) "" else s"(${args.map(_.render).mkString(", ")})"
val rchild = if(child == Empty) "" else s" { ${child.render} }"
Expand Down Expand Up @@ -83,10 +87,26 @@ object Query {
}
}

/**
* Rename the topmost field of `sel` to `name`.
*/
case class Rename(name: String, child: Query) extends Query {
def render = s"<rename: $name ${child.render}>"
}

/**
* Untyped precursor of `Narrow`.
*
* Trees of this type will be replaced by a corresponding `Narrow` by
* `SelectElaborator`.
*/
case class UntypedNarrow(tpnme: String, child: Query) extends Query {
def render = s"<narrow: $tpnme ${child.render}>"
}

/**
* The result of `child` if the focus is of type `subtpe`, `Empty` otherwise.
*/
case class Narrow(subtpe: Type, child: Query) extends Query {
def render = s"<narrow: $subtpe ${child.render}>"
}
Expand All @@ -96,6 +116,38 @@ object Query {
def render = ""
}

object PossiblyRenamedSelect {
def apply(sel: Select, resultName: String): Query = sel match {
case Select(`resultName`, _, _) => sel
case _ => Rename(resultName, sel)
}

def unapply(q: Query): Option[(Select, String)] =
q match {
case Rename(name, sel: Select) => Some((sel, name))
case sel: Select => Some((sel, sel.name))
case _ => None
}
}

def renameRoot(q: Query, rootName: String): Option[Query] = q match {
case Rename(_, sel@Select(`rootName`, _, _)) => Some(sel)
case r@Rename(`rootName`, _) => Some(r)
case Rename(_, sel: Select) => Some(Rename(rootName, sel))
case sel@Select(`rootName`, _, _) => Some(sel)
case sel: Select => Some(Rename(rootName, sel))
case w@Wrap(`rootName`, _) => Some(w)
case w: Wrap => Some(w.copy(name = rootName))
case _ => None
}

def rootName(q: Query): Option[String] = q match {
case Select(name, _, _) => Some(name)
case Wrap(name, _) => Some(name)
case Rename(name, _) => Some(name)
case _ => None
}

/** InputValue binding */
sealed trait Binding {
def name: String
Expand Down Expand Up @@ -333,8 +385,24 @@ abstract class QueryInterpreter[F[_]](implicit val F: Monad[F]) {
* Errors are accumulated on the `Left` of the result.
*/
def runRoot(query: Query, rootTpe: Type): F[Result[Json]] = {
val rootQueries =
query match {
case Group(queries) => queries
case query => List(query)
}

val rootResults = runRootValues(rootQueries.zip(Iterator.continually(rootTpe)))
val mergedResults: F[Result[ProtoJson]] =
rootResults.map {
case (errors, pvalues) =>
val merged = ProtoJson.mergeObjects(pvalues)
NonEmptyChain.fromChain(errors) match {
case Some(errs) => Ior.Both(errs, merged)
case None => Ior.Right(merged)
}
}
(for {
pvalue <- IorT(runRootValue(query, rootTpe))
pvalue <- IorT(mergedResults)
value <- IorT(complete(pvalue))
} yield value).value
}
Expand Down Expand Up @@ -395,19 +463,19 @@ abstract class QueryInterpreter[F[_]](implicit val F: Monad[F]) {
fields <- runFields(child, tp1, c)
} yield fields

case (sel@Select(fieldName, _, _), NullableType(tpe)) =>
case (PossiblyRenamedSelect(sel, resultName), NullableType(tpe)) =>
cursor.asNullable.sequence.map { rc =>
for {
c <- rc
fields <- runFields(sel, tpe, c)
} yield fields
}.getOrElse(List((fieldName, ProtoJson.fromJson(Json.Null))).rightIor)
}.getOrElse(List((resultName, ProtoJson.fromJson(Json.Null))).rightIor)

case (Select(fieldName, _, child), tpe) =>
case (PossiblyRenamedSelect(Select(fieldName, _, child), resultName), tpe) =>
for {
c <- cursor.field(fieldName)
value <- runValue(child, tpe.field(fieldName), c)
} yield List((fieldName, value))
} yield List((resultName, value))

case (Wrap(fieldName, child), tpe) =>
for {
Expand All @@ -425,28 +493,46 @@ abstract class QueryInterpreter[F[_]](implicit val F: Monad[F]) {
/**
* Interpret `query` against `cursor` with expected type `tpe`.
*
* If the query is invalide errors will be returned on teh left hand side
* If the query is invalid errors will be returned on teh left hand side
* of the result.
*/
def runValue(query: Query, tpe: Type, cursor: Cursor): Result[ProtoJson] = {
def joinType(localName: String, componentName: String, tpe: Type): Type =
ObjectType(s"Join-$localName-$componentName", None, List(Field(componentName, None, Nil, tpe, false, None)), Nil)

def mkResult[T](ot: Option[T]): Result[T] = ot match {
case Some(t) => t.rightIor
case None => mkErrorResult(s"Join continuation has unexpected shape")
}

(query, tpe.dealias) match {
case (Wrap(fieldName, child), _) =>
for {
pvalue <- runValue(child, tpe, cursor)
} yield ProtoJson.fromFields(List((fieldName, pvalue)))

case (Component(cid, join, child), _) =>
case (Component(cid, join, PossiblyRenamedSelect(child, resultName)), _) =>
for {
cont <- join(cursor, child)
} yield ProtoJson.component(cid, cont, tpe)
cont <- join(cursor, child)
componentName <- mkResult(rootName(cont))
renamedCont <- mkResult(renameRoot(cont, resultName))
} yield ProtoJson.component(cid, renamedCont, joinType(child.name, componentName, tpe.field(child.name)))

case (Defer(join, child), _) =>
for {
cont <- join(cursor, child)
} yield ProtoJson.staged(this, cont, tpe)

case (Unique(pred, child), _) if cursor.isList =>
cursor.asList.map(_.filter(pred)).flatMap(lc =>
case (Unique(pred, child), _) =>
val cursors =
if (cursor.isNullable)
cursor.asNullable.flatMap {
case None => Nil.rightIor
case Some(c) => c.asList
}
else cursor.asList

cursors.map(_.filter(pred)).flatMap(lc =>
lc match {
case List(c) => runValue(child, tpe.nonNull, c)
case Nil if tpe.isNullable => ProtoJson.fromJson(Json.Null).rightIor
Expand All @@ -455,6 +541,11 @@ abstract class QueryInterpreter[F[_]](implicit val F: Monad[F]) {
}
)

case (Filter(pred, child), ListType(tpe)) =>
cursor.asList.map(_.filter(pred)).flatMap(lc =>
lc.traverse(c => runValue(child, tpe, c)).map(ProtoJson.fromValues)
)

case (_, NullableType(tpe)) =>
cursor.asNullable.sequence.map { rc =>
for {
Expand All @@ -463,11 +554,6 @@ abstract class QueryInterpreter[F[_]](implicit val F: Monad[F]) {
} yield value
}.getOrElse(ProtoJson.fromJson(Json.Null).rightIor)

case (Filter(pred, child), ListType(tpe)) =>
cursor.asList.map(_.filter(pred)).flatMap(lc =>
lc.traverse(c => runValue(child, tpe, c)).map(ProtoJson.fromValues)
)

case (_, ListType(tpe)) =>
cursor.asList.flatMap(lc =>
lc.traverse(c => runValue(query, tpe, c)).map(ProtoJson.fromValues)
Expand Down Expand Up @@ -552,14 +638,37 @@ object QueryInterpreter {
wrap(ProtoArray(elems))

/**
* Test whether the argument contains any deferred.
* Test whether the argument contains any deferred subtrees
*
* Yields `true` if the argument contains any deferred or staged
* Yields `true` if the argument contains any component or staged
* subtrees, false otherwise.
*/
def isDeferred(p: ProtoJson): Boolean =
p.isInstanceOf[DeferredJson]

def mergeObjects(elems: List[ProtoJson]): ProtoJson = {
def loop(elems: List[ProtoJson], acc: List[(String, ProtoJson)]): List[(String, ProtoJson)] = elems match {
case Nil => acc
case (j: Json) :: tl =>
j.asObject match {
case Some(obj) => loop(tl, acc ++ obj.keys.zip(obj.values.map(fromJson)))
case None => loop(tl, acc)
}
case ProtoObject(fields) :: tl => loop(tl, acc ++ fields)
case _ :: tl => loop(tl, acc)
}

elems match {
case Nil => wrap(Json.Null)
case hd :: Nil => hd
case _ =>
loop(elems, Nil) match {
case Nil => wrap(Json.Null)
case fields => fromFields(fields)
}
}
}

private def wrap(j: AnyRef): ProtoJson = j.asInstanceOf[ProtoJson]
}

Expand Down
26 changes: 26 additions & 0 deletions modules/core/src/test/scala/compiler/CompilerSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ final class CompilerSuite extends CatsSuite {
assert(res == Ior.Right(expected))
}

test("field alias") {
val text = """
{
user(id: 4) {
id
name
smallPic: profilePic(size: 64)
bigPic: profilePic(size: 1024)
}
}
"""

val expected =
Select("user", List(IntBinding("id", 4)),
Group(List(
Select("id", Nil, Empty),
Select("name", Nil, Empty),
Rename("smallPic", Select("profilePic", List(IntBinding("size", 64)), Empty)),
Rename("bigPic", Select("profilePic", List(IntBinding("size", 1024)), Empty))
))
)

val res = QueryParser.parseText(text)
assert(res == Ior.Right(expected))
}

test("introspection query") {
val text = """
query IntrospectionQuery {
Expand Down
Loading