Skip to content

Commit

Permalink
Make interpreter creation effectful (#227)
Browse files Browse the repository at this point in the history
  • Loading branch information
ghostdogpr authored Feb 22, 2020
1 parent b347121 commit ba3c60f
Show file tree
Hide file tree
Showing 20 changed files with 161 additions and 98 deletions.
2 changes: 1 addition & 1 deletion benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ class GraphQLBenchmarks {
)
)

val interpreter: GraphQLInterpreter[Any, CalibanError] = graphQL(resolver).interpreter
val interpreter: GraphQLInterpreter[Any, CalibanError] = zioRuntime.unsafeRun(graphQL(resolver).interpreter)

@Benchmark
def simpleCaliban(): Unit = {
Expand Down
12 changes: 9 additions & 3 deletions core/src/main/scala/caliban/GraphQL.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package caliban

import caliban.CalibanError.ValidationError
import caliban.Rendering.renderTypes
import caliban.execution.Executor
import caliban.introspection.Introspector
Expand Down Expand Up @@ -73,10 +74,15 @@ trait GraphQL[-R] { self =>
/**
* Creates an interpreter from your API. A GraphQLInterpreter is a wrapper around your API that allows
* adding some middleware around the query execution.
* Fails with a [[caliban.CalibanError.ValidationError]] if the schema is invalid.
*/
final def interpreter: GraphQLInterpreter[R, CalibanError] =
(query: String, operationName: Option[String], variables: Map[String, InputValue], skipValidation: Boolean) =>
self.execute(query, operationName, variables, skipValidation)
final def interpreter: IO[ValidationError, GraphQLInterpreter[R, CalibanError]] =
Validator
.validateSchema(rootType)
.as {
(query: String, operationName: Option[String], variables: Map[String, InputValue], skipValidation: Boolean) =>
self.execute(query, operationName, variables, skipValidation)
}

/**
* Attaches a function that will wrap one of the stages of query processing
Expand Down
47 changes: 35 additions & 12 deletions core/src/main/scala/caliban/validation/Validator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ object Validator {
def validate(document: Document, rootType: RootType): IO[ValidationError, Unit] =
check(document, rootType).unit

/**
* Verifies that the given schema is valid. Fails with a [[caliban.CalibanError.ValidationError]] otherwise.
*/
def validateSchema(rootType: RootType): IO[ValidationError, Unit] =
IO.foreach(rootType.types.values) { t =>
t.kind match {
case __TypeKind.ENUM => validateEnum(t)
case _ => IO.unit
}
}
.unit

/**
* Prepare the request for execution.
* Fails with a [[caliban.CalibanError.ValidationError]] otherwise.
Expand Down Expand Up @@ -272,14 +284,14 @@ object Validator {
"Defined fragments must be used within a document."
)
)
else if (detectCycles(context, f))
else
IO.fail(
ValidationError(
s"Fragment '${f.name}' forms a cycle.",
"The graph of fragment spreads must not form any cycles including spreading itself. Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data."
ValidationError(
s"Fragment '${f.name}' forms a cycle.",
"The graph of fragment spreads must not form any cycles including spreading itself. Otherwise an operation could infinitely spread or infinitely execute on cycles in the underlying data."
)
)
)
else IO.unit
.when(detectCycles(context, f))
)
.unit
}
Expand Down Expand Up @@ -379,12 +391,11 @@ object Validator {
private def validateField(context: Context, field: Field, currentType: __Type): IO[ValidationError, Unit] =
IO.when(field.name != "__typename") {
IO.fromOption(currentType.fields(__DeprecatedArgs(Some(true))).getOrElse(Nil).find(_.name == field.name))
.mapError(
_ =>
ValidationError(
s"Field '${field.name}' does not exist on type '${Rendering.renderTypeName(currentType)}'.",
"The target field of a field selection must be defined on the scoped type of the selection set. There are no limitations on alias names."
)
.asError(
ValidationError(
s"Field '${field.name}' does not exist on type '${Rendering.renderTypeName(currentType)}'.",
"The target field of a field selection must be defined on the scoped type of the selection set. There are no limitations on alias names."
)
)
.flatMap { f =>
validateFields(context, field.selectionSet, Types.innerType(f.`type`())) *>
Expand Down Expand Up @@ -550,6 +561,18 @@ object Validator {
)
}

private def validateEnum(t: __Type): IO[ValidationError, Unit] =
t.enumValues(__DeprecatedArgs(Some(true))) match {
case Some(_ :: _) => IO.unit
case _ =>
IO.fail(
ValidationError(
s"Enum ${t.name} doesn't contain any values",
"An Enum type must define one or more unique enum values."
)
)
}

case class Context(
document: Document,
rootType: RootType,
Expand Down
58 changes: 34 additions & 24 deletions core/src/test/scala/caliban/execution/ExecutionSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"amos":{"name":"Amos Burton"}}""")
)
},
Expand All @@ -42,7 +42,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo(
"""{"characters":[{"name":"James Holden"},{"name":"Naomi Nagata"},{"name":"Amos Burton"},{"name":"Alex Kamal"},{"name":"Chrisjen Avasarala"},{"name":"Josephus Miller"},{"name":"Roberta Draper"}]}"""
)
Expand All @@ -59,7 +59,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo(
"""{"characters":[{"name":"Alex Kamal","nicknames":[]},{"name":"Roberta Draper","nicknames":["Bobbie","Gunny"]}]}"""
)
Expand All @@ -75,7 +75,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"charactersIn":[{"name":"Alex Kamal"}]}""")
)
},
Expand All @@ -94,7 +94,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"amos":{"name":"Amos Burton","nicknames":[]},"naomi":{"name":"Naomi Nagata","nicknames":[]}}""")
)
},
Expand All @@ -112,7 +112,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"amos":{"name":"Amos Burton"}}""")
)
},
Expand All @@ -131,7 +131,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"amos":{"name":"Amos Burton","role":{"shipName":"Rocinante"}}}""")
)
},
Expand All @@ -145,7 +145,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo(
"""{"characters":[{"name":"James Holden"},{"name":"Naomi Nagata"},{"name":"Amos Burton"},{"name":"Alex Kamal"},{"name":"Chrisjen Avasarala"},{"name":"Josephus Miller"},{"name":"Roberta Draper"}]}"""
)
Expand All @@ -158,7 +158,7 @@ object ExecutionSpec
deleteCharacter(name: "Amos Burton")
}""")

assertM(interpreter.execute(query).map(_.data.toString), equalTo("""{"deleteCharacter":{}}"""))
assertM(interpreter.flatMap(_.execute(query)).map(_.data.toString), equalTo("""{"deleteCharacter":{}}"""))
},
testM("variable") {
val interpreter = graphQL(resolver).interpreter
Expand All @@ -170,7 +170,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query, None, Map("name" -> StringValue("Amos Burton"))).map(_.data.toString),
interpreter.flatMap(_.execute(query, None, Map("name" -> StringValue("Amos Burton")))).map(_.data.toString),
equalTo("""{"amos":{"name":"Amos Burton"}}""")
)
},
Expand All @@ -184,7 +184,10 @@ object ExecutionSpec
}
}""")

assertM(interpreter.execute(query).map(_.data.toString), equalTo("""{"amos":{"name":"Amos Burton"}}"""))
assertM(
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"amos":{"name":"Amos Burton"}}""")
)
},
testM("include directive") {
val interpreter = graphQL(resolver).interpreter
Expand All @@ -197,7 +200,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query, None, Map("included" -> BooleanValue(false))).map(_.data.toString),
interpreter.flatMap(_.execute(query, None, Map("included" -> BooleanValue(false)))).map(_.data.toString),
equalTo("""{"amos":{"name":"Amos Burton"}}""")
)
},
Expand All @@ -212,7 +215,10 @@ object ExecutionSpec
}
}""")

assertM(interpreter.execute(query).map(_.data.toString), equalTo("""{"map":[{"key":3,"value":"ok"}]}"""))
assertM(
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"map":[{"key":3,"value":"ok"}]}""")
)
},
testM("test Either") {
case class Test(either: Either[Int, String])
Expand All @@ -225,7 +231,10 @@ object ExecutionSpec
}
}""")

assertM(interpreter.execute(query).map(_.data.toString), equalTo("""{"either":{"left":null,"right":"ok"}}"""))
assertM(
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"either":{"left":null,"right":"ok"}}""")
)
},
testM("test UUID") {
case class IdArgs(id: UUID)
Expand All @@ -237,18 +246,19 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"test":"be722453-d97d-48c2-b535-9badd1b5d4c9"}""")
)
},
testM("mapError") {
import io.circe.syntax._
case class Test(either: Either[Int, String])
val interpreter = graphQL(RootResolver(Test(Right("ok")))).interpreter.mapError(_ => "my custom error")
val query = """query{}"""
val api = graphQL(RootResolver(Test(Right("ok"))))
val query = """query{}"""

for {
result <- interpreter.execute(query)
interpreter <- api.interpreter
result <- interpreter.mapError(_ => "my custom error").execute(query)
} yield assert(result.errors, equalTo(List("my custom error"))) &&
assert(result.asJson.noSpaces, equalTo("""{"data":null,"errors":[{"message":"my custom error"}]}"""))
},
Expand All @@ -264,7 +274,7 @@ object ExecutionSpec
| id
|}""".stripMargin
assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"name":"name","id":2}""")
)
},
Expand All @@ -283,7 +293,7 @@ object ExecutionSpec
}
}""")
assertM(
interpreter.execute(query).map(_.errors),
interpreter.flatMap(_.execute(query)).map(_.errors),
equalTo(
List(
ExecutionError(
Expand All @@ -305,7 +315,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"test":[1,2,3]}""")
)
},
Expand All @@ -320,7 +330,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"test":<stream>}""")
)
},
Expand All @@ -335,7 +345,7 @@ object ExecutionSpec
}""")

assertM(
interpreter.execute(query).map(_.data.toString),
interpreter.flatMap(_.execute(query)).map(_.data.toString),
equalTo("""{"test":{"a":333}}""")
)
},
Expand All @@ -349,7 +359,7 @@ object ExecutionSpec
}
}""")

assertM(interpreter.execute(query).map(_.data.toString), equalTo("""{"i":{"id":"ok"}}"""))
assertM(interpreter.flatMap(_.execute(query)).map(_.data.toString), equalTo("""{"i":{"id":"ok"}}"""))
}
)
)
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ object IntrospectionSpec
val interpreter = graphQL(resolverIO).interpreter

assertM(
interpreter.execute(fullIntrospectionQuery).map(_.data.toString),
interpreter.flatMap(_.execute(fullIntrospectionQuery)).map(_.data.toString),
equalTo(
"""{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"Use `characters`"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}]}]}}"""
)
Expand Down
7 changes: 4 additions & 3 deletions core/src/test/scala/caliban/validation/ValidationSpec.scala
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
package caliban.validation

import caliban.CalibanError
import caliban.CalibanError.ValidationError
import caliban.GraphQL._
import caliban.Macros.gqldoc
import caliban.TestUtils._
import zio.UIO
import zio.IO
import zio.test.Assertion._
import zio.test._

Expand All @@ -13,8 +14,8 @@ object ValidationSpec
val gql = graphQL(resolverWithSubscription)
val interpreter = gql.interpreter

def check(query: String, expectedMessage: String): UIO[TestResult] = {
val io = interpreter.execute(query).map(_.errors.headOption)
def check(query: String, expectedMessage: String): IO[ValidationError, TestResult] = {
val io = interpreter.flatMap(_.execute(query)).map(_.errors.headOption)
assertM(io, isSome(hasField[CalibanError, String]("msg", _.msg, equalTo(expectedMessage))))
}

Expand Down
Loading

0 comments on commit ba3c60f

Please sign in to comment.