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

Make interpreter creation effectful #227

Merged
merged 1 commit into from
Feb 22, 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
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