From 23b98716af70ac40a24890a93fa74a94ef2738d3 Mon Sep 17 00:00:00 2001 From: Pierre Ricadat Date: Sat, 22 Feb 2020 18:05:01 +0900 Subject: [PATCH] Make interpreter creation effectful --- .../scala/caliban/GraphQLBenchmarks.scala | 2 +- core/src/main/scala/caliban/GraphQL.scala | 12 +++- .../scala/caliban/validation/Validator.scala | 47 +++++++++++---- .../caliban/execution/ExecutionSpec.scala | 58 +++++++++++-------- .../introspection/IntrospectionSpec.scala | 2 +- .../caliban/validation/ValidationSpec.scala | 7 ++- .../scala/caliban/wrappers/WrappersSpec.scala | 38 ++++++------ .../scala/caliban/akkahttp/ExampleApp.scala | 5 +- .../scala/caliban/http4s/ExampleApp.scala | 2 +- .../interop/cats/ExampleCatsInterop.scala | 12 ++-- .../interop/monix/ExampleMonixInterop.scala | 8 +-- .../caliban/optimizations/NaiveTest.scala | 6 +- .../caliban/optimizations/OptimizedTest.scala | 6 +- .../caliban/interop/cats/CatsInterop.scala | 9 ++- .../interop/cats/implicits/package.scala | 5 +- .../caliban/interop/monix/MonixInterop.scala | 9 ++- .../interop/monix/implicits/package.scala | 5 +- vuepress/docs/docs/README.md | 5 +- vuepress/docs/docs/interop.md | 18 +++--- vuepress/docs/docs/middleware.md | 3 +- 20 files changed, 161 insertions(+), 98 deletions(-) diff --git a/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala b/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala index 24b50a10e..574fa5721 100644 --- a/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala +++ b/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala @@ -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 = { diff --git a/core/src/main/scala/caliban/GraphQL.scala b/core/src/main/scala/caliban/GraphQL.scala index e8db2d653..8910de938 100644 --- a/core/src/main/scala/caliban/GraphQL.scala +++ b/core/src/main/scala/caliban/GraphQL.scala @@ -1,5 +1,6 @@ package caliban +import caliban.CalibanError.ValidationError import caliban.Rendering.renderTypes import caliban.execution.Executor import caliban.introspection.Introspector @@ -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 diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index 376d16280..92e1ce85f 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -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. @@ -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 } @@ -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`())) *> @@ -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, diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index db85106b7..75f4f3af5 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -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"}}""") ) }, @@ -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"}]}""" ) @@ -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"]}]}""" ) @@ -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"}]}""") ) }, @@ -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":[]}}""") ) }, @@ -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"}}""") ) }, @@ -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"}}}""") ) }, @@ -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"}]}""" ) @@ -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 @@ -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"}}""") ) }, @@ -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 @@ -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"}}""") ) }, @@ -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]) @@ -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) @@ -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"}]}""")) }, @@ -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}""") ) }, @@ -283,7 +293,7 @@ object ExecutionSpec } }""") assertM( - interpreter.execute(query).map(_.errors), + interpreter.flatMap(_.execute(query)).map(_.errors), equalTo( List( ExecutionError( @@ -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]}""") ) }, @@ -320,7 +330,7 @@ object ExecutionSpec }""") assertM( - interpreter.execute(query).map(_.data.toString), + interpreter.flatMap(_.execute(query)).map(_.data.toString), equalTo("""{"test":}""") ) }, @@ -335,7 +345,7 @@ object ExecutionSpec }""") assertM( - interpreter.execute(query).map(_.data.toString), + interpreter.flatMap(_.execute(query)).map(_.data.toString), equalTo("""{"test":{"a":333}}""") ) }, @@ -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"}}""")) } ) ) diff --git a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala index de6aa3632..821daf138 100644 --- a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala +++ b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala @@ -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}]}]}}""" ) diff --git a/core/src/test/scala/caliban/validation/ValidationSpec.scala b/core/src/test/scala/caliban/validation/ValidationSpec.scala index 321234b96..7debb9a1a 100644 --- a/core/src/test/scala/caliban/validation/ValidationSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSpec.scala @@ -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._ @@ -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)))) } diff --git a/core/src/test/scala/caliban/wrappers/WrappersSpec.scala b/core/src/test/scala/caliban/wrappers/WrappersSpec.scala index c89399c0a..6f4b2d56b 100644 --- a/core/src/test/scala/caliban/wrappers/WrappersSpec.scala +++ b/core/src/test/scala/caliban/wrappers/WrappersSpec.scala @@ -8,7 +8,7 @@ import caliban.schema.Annotations.GQLDirective import caliban.schema.GenericSchema import caliban.wrappers.ApolloCaching.CacheControl import caliban.wrappers.Wrappers._ -import caliban.{ CalibanError, GraphQLInterpreter, RootResolver } +import caliban.{ GraphQL, RootResolver } import zio.clock.Clock import zio.duration._ import zio.test.Assertion._ @@ -16,8 +16,6 @@ import zio.test._ import zio.test.environment.TestClock import zio.{ clock, Promise, URIO, ZIO } -import scala.language.postfixOps - object WrappersSpec extends DefaultRunnableSpec( suite("WrappersSpec")( @@ -35,7 +33,7 @@ object WrappersSpec } }""") assertM( - interpreter.execute(query).map(_.errors), + interpreter.flatMap(_.execute(query)).map(_.errors), equalTo(List(ValidationError("Query has too many fields: 3. Max fields: 2.", ""))) ) }, @@ -58,7 +56,7 @@ object WrappersSpec } """) assertM( - interpreter.execute(query).map(_.errors), + interpreter.flatMap(_.execute(query)).map(_.errors), equalTo(List(ValidationError("Query has too many fields: 3. Max fields: 2.", ""))) ) }, @@ -76,7 +74,7 @@ object WrappersSpec } }""") assertM( - interpreter.execute(query).map(_.errors), + interpreter.flatMap(_.execute(query)).map(_.errors), equalTo(List(ValidationError("Query is too deep: 3. Max depth: 2.", ""))) ) }, @@ -93,7 +91,7 @@ object WrappersSpec a }""") assertM( - TestClock.adjust(1 minute) *> interpreter.execute(query).map(_.errors), + TestClock.adjust(1 minute) *> interpreter.flatMap(_.execute(query)).map(_.errors), equalTo(List(ExecutionError("""Query was interrupted after timeout of 1 m: { @@ -108,8 +106,8 @@ object WrappersSpec object schema extends GenericSchema[Clock] import schema._ - def interpreter(latch: Promise[Nothing, Unit]): GraphQLInterpreter[Clock, CalibanError] = - (graphQL( + def api(latch: Promise[Nothing, Unit]): GraphQL[Clock] = + graphQL( RootResolver( Query( Hero( @@ -122,7 +120,7 @@ object WrappersSpec ) ) ) - ) @@ ApolloTracing.apolloTracing).interpreter + ) @@ ApolloTracing.apolloTracing val query = gqldoc(""" { @@ -135,11 +133,12 @@ object WrappersSpec }""") assertM( for { - latch <- Promise.make[Nothing, Unit] - fiber <- interpreter(latch).execute(query).map(_.extensions.map(_.toString)).fork - _ <- latch.await - _ <- TestClock.adjust(1 second) - result <- fiber.join + latch <- Promise.make[Nothing, Unit] + interpreter <- api(latch).interpreter + fiber <- interpreter.execute(query).map(_.extensions.map(_.toString)).fork + _ <- latch.await + _ <- TestClock.adjust(1 second) + result <- fiber.join } yield result, isSome( equalTo( @@ -157,8 +156,8 @@ object WrappersSpec object schema extends GenericSchema[Clock] import schema._ - def interpreter: GraphQLInterpreter[Clock, CalibanError] = - (graphQL( + def api: GraphQL[Clock] = + graphQL( RootResolver( Query( Hero( @@ -171,7 +170,7 @@ object WrappersSpec ) ) ) - ) @@ ApolloCaching.apolloCaching).interpreter + ) @@ ApolloCaching.apolloCaching val query = gqldoc(""" { @@ -184,7 +183,8 @@ object WrappersSpec }""") assertM( for { - result <- interpreter.execute(query).map(_.extensions.map(_.toString)) + interpreter <- api.interpreter + result <- interpreter.execute(query).map(_.extensions.map(_.toString)) } yield result, isSome( equalTo( diff --git a/examples/src/main/scala/caliban/akkahttp/ExampleApp.scala b/examples/src/main/scala/caliban/akkahttp/ExampleApp.scala index 5855dcd62..97ef2c5d4 100644 --- a/examples/src/main/scala/caliban/akkahttp/ExampleApp.scala +++ b/examples/src/main/scala/caliban/akkahttp/ExampleApp.scala @@ -55,9 +55,8 @@ object ExampleApp extends App with GenericSchema[Console with Clock] { printSlowQueries(500 millis) @@ // wrapper that logs slow queries apolloTracing // wrapper for https://github.com/apollographql/apollo-tracing - val service = defaultRuntime.unsafeRun(ExampleService.make(sampleCharacters)) - - val interpreter = makeApi(service).interpreter + val service = defaultRuntime.unsafeRun(ExampleService.make(sampleCharacters)) + val interpreter = defaultRuntime.unsafeRun(makeApi(service).interpreter) /** * curl -X POST \ diff --git a/examples/src/main/scala/caliban/http4s/ExampleApp.scala b/examples/src/main/scala/caliban/http4s/ExampleApp.scala index 2c5a63572..1ec0cbfe5 100644 --- a/examples/src/main/scala/caliban/http4s/ExampleApp.scala +++ b/examples/src/main/scala/caliban/http4s/ExampleApp.scala @@ -64,7 +64,7 @@ object ExampleApp extends CatsApp with GenericSchema[Console with Clock] { .accessM[Blocking](_.blocking.blockingExecutor.map(_.asEC)) .map(Blocker.liftExecutionContext) service <- ExampleService.make(sampleCharacters) - interpreter = makeApi(service).interpreter + interpreter <- makeApi(service).interpreter _ <- BlazeServerBuilder[ExampleTask] .bindHttp(8088, "localhost") .withHttpApp( diff --git a/examples/src/main/scala/caliban/interop/cats/ExampleCatsInterop.scala b/examples/src/main/scala/caliban/interop/cats/ExampleCatsInterop.scala index d68f28525..2a89c6e82 100644 --- a/examples/src/main/scala/caliban/interop/cats/ExampleCatsInterop.scala +++ b/examples/src/main/scala/caliban/interop/cats/ExampleCatsInterop.scala @@ -18,9 +18,8 @@ object ExampleCatsInterop extends IOApp { val numbers = List(1, 2, 3, 4).map(Number) val randomNumber = IO(scala.util.Random.nextInt()).map(Number) - val queries = Queries(numbers, randomNumber) - val api = graphQL(RootResolver(queries)) - val interpreter = api.interpreter + val queries = Queries(numbers, randomNumber) + val api = graphQL(RootResolver(queries)) val query = """ { @@ -35,8 +34,9 @@ object ExampleCatsInterop extends IOApp { override def run(args: List[String]): IO[ExitCode] = for { - _ <- api.checkAsync[IO](query) - result <- interpreter.executeAsync[IO](query) - _ <- IO(println(result.data)) + _ <- api.checkAsync[IO](query) + interpreter <- api.interpreterAsync[IO] + result <- interpreter.executeAsync[IO](query) + _ <- IO(println(result.data)) } yield ExitCode.Success } diff --git a/examples/src/main/scala/caliban/interop/monix/ExampleMonixInterop.scala b/examples/src/main/scala/caliban/interop/monix/ExampleMonixInterop.scala index 34f758311..0528f7b24 100644 --- a/examples/src/main/scala/caliban/interop/monix/ExampleMonixInterop.scala +++ b/examples/src/main/scala/caliban/interop/monix/ExampleMonixInterop.scala @@ -25,7 +25,6 @@ object ExampleMonixInterop extends TaskApp { val subscriptions = Subscriptions(Observable.fromIterable(List(1, 2, 3))) val api = graphQL(RootResolver(queries, Option.empty[Unit], Some(subscriptions))) - val interpreter = api.interpreter val query = """ { @@ -45,9 +44,10 @@ object ExampleMonixInterop extends TaskApp { override def run(args: List[String]): Task[ExitCode] = for { - _ <- api.checkAsync(query) - result <- interpreter.executeAsync(query) - _ <- Task.eval(println(result.data)) + _ <- api.checkAsync(query) + interpreter <- api.interpreterAsync + result <- interpreter.executeAsync(query) + _ <- Task.eval(println(result.data)) _ <- api.checkAsync(subscription) result <- interpreter.executeAsync(subscription) diff --git a/examples/src/main/scala/caliban/optimizations/NaiveTest.scala b/examples/src/main/scala/caliban/optimizations/NaiveTest.scala index c1140846f..bfe507a45 100644 --- a/examples/src/main/scala/caliban/optimizations/NaiveTest.scala +++ b/examples/src/main/scala/caliban/optimizations/NaiveTest.scala @@ -81,9 +81,9 @@ object NaiveTest extends App with GenericSchema[Console] { implicit val firstArgsSchema: Schema[Any, FirstArgs] = Schema.gen[FirstArgs] implicit lazy val user: Schema[Console, User] = gen[User] - val resolver = Queries(args => getUser(args.id)) - val interpreter = GraphQL.graphQL(RootResolver(resolver)).interpreter + val resolver = Queries(args => getUser(args.id)) + val api = GraphQL.graphQL(RootResolver(resolver)) override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] = - interpreter.execute(query).map(_.errors.length) + api.interpreter.flatMap(_.execute(query).map(_.errors.length)).catchAll(err => putStrLn(err.toString).as(1)) } diff --git a/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala b/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala index 0db16146f..e6b46dcdd 100644 --- a/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala +++ b/examples/src/main/scala/caliban/optimizations/OptimizedTest.scala @@ -122,9 +122,9 @@ object OptimizedTest extends App with GenericSchema[Console] { implicit val firstArgsSchema: Schema[Any, FirstArgs] = Schema.gen[FirstArgs] implicit lazy val user: Schema[Console, User] = gen[User] - val resolver = Queries(args => getUser(args.id)) - val interpreter = GraphQL.graphQL(RootResolver(resolver)).interpreter + val resolver = Queries(args => getUser(args.id)) + val api = GraphQL.graphQL(RootResolver(resolver)) override def run(args: List[String]): ZIO[zio.ZEnv, Nothing, Int] = - interpreter.execute(query).map(_.errors.length) + api.interpreter.flatMap(_.execute(query).map(_.errors.length)).catchAll(err => putStrLn(err.toString).as(1)) } diff --git a/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala b/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala index f222bca0b..bdd17aafb 100644 --- a/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala +++ b/interop/cats/src/main/scala/caliban/interop/cats/CatsInterop.scala @@ -3,7 +3,7 @@ package caliban.interop.cats import caliban.introspection.adt.__Type import caliban.schema.Step.QueryStep import caliban.schema.{ Schema, Step } -import caliban.{ GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } +import caliban.{ CalibanError, GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } import cats.effect.implicits._ import cats.effect.{ Async, Effect } import zio.interop.catz._ @@ -30,6 +30,13 @@ object CatsInterop { runtime.unsafeRunAsync(graphQL.check(query))(exit => cb(exit.toEither)) } + def interpreterAsync[F[_]: Async, R]( + graphQL: GraphQL[R] + )(implicit runtime: Runtime[R]): F[GraphQLInterpreter[R, CalibanError]] = + Async[F].async { cb => + runtime.unsafeRunAsync(graphQL.interpreter)(exit => cb(exit.toEither)) + } + def schema[F[_]: Effect, R, A](implicit ev: Schema[R, A]): Schema[R, F[A]] = new Schema[R, F[A]] { override def toType(isInput: Boolean): __Type = diff --git a/interop/cats/src/main/scala/caliban/interop/cats/implicits/package.scala b/interop/cats/src/main/scala/caliban/interop/cats/implicits/package.scala index 3d69f1a3e..71f30fcda 100644 --- a/interop/cats/src/main/scala/caliban/interop/cats/implicits/package.scala +++ b/interop/cats/src/main/scala/caliban/interop/cats/implicits/package.scala @@ -1,7 +1,7 @@ package caliban.interop.cats import caliban.schema.Schema -import caliban.{ GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } +import caliban.{ CalibanError, GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } import cats.effect.{ Async, Effect } import zio.Runtime @@ -27,6 +27,9 @@ package object implicits { def checkAsync[F[_]: Async](query: String)(implicit runtime: Runtime[R]): F[Unit] = CatsInterop.checkAsync(underlying)(query) + + def interpreterAsync[F[_]: Async](implicit runtime: Runtime[R]): F[GraphQLInterpreter[R, CalibanError]] = + CatsInterop.interpreterAsync(underlying) } implicit def effectSchema[F[_]: Effect, R, A](implicit ev: Schema[R, A]): Schema[R, F[A]] = diff --git a/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala b/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala index c578214c0..e3c6c33fe 100644 --- a/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala +++ b/interop/monix/src/main/scala/caliban/interop/monix/MonixInterop.scala @@ -3,7 +3,7 @@ package caliban.interop.monix import caliban.introspection.adt.__Type import caliban.schema.Step.{ QueryStep, StreamStep } import caliban.schema.{ Schema, Step } -import caliban.{ GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } +import caliban.{ CalibanError, GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } import cats.effect.ConcurrentEffect import monix.eval.{ Task => MonixTask } import monix.reactive.Observable @@ -31,6 +31,13 @@ object MonixInterop { runtime.unsafeRunAsync(graphQL.check(query))(exit => cb(exit.toEither)) } + def interpreterAsync[R]( + graphQL: GraphQL[R] + )(implicit runtime: Runtime[R]): MonixTask[GraphQLInterpreter[R, CalibanError]] = + MonixTask.async { cb => + runtime.unsafeRunAsync(graphQL.interpreter)(exit => cb(exit.toEither)) + } + def taskSchema[R, A](implicit ev: Schema[R, A], ev2: ConcurrentEffect[MonixTask]): Schema[R, MonixTask[A]] = new Schema[R, MonixTask[A]] { override def toType(isInput: Boolean): __Type = ev.toType(isInput) diff --git a/interop/monix/src/main/scala/caliban/interop/monix/implicits/package.scala b/interop/monix/src/main/scala/caliban/interop/monix/implicits/package.scala index 10460def6..06669bded 100644 --- a/interop/monix/src/main/scala/caliban/interop/monix/implicits/package.scala +++ b/interop/monix/src/main/scala/caliban/interop/monix/implicits/package.scala @@ -1,7 +1,7 @@ package caliban.interop.monix import caliban.schema.{ Schema, SubscriptionSchema } -import caliban.{ GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } +import caliban.{ CalibanError, GraphQL, GraphQLInterpreter, GraphQLResponse, InputValue } import cats.effect.ConcurrentEffect import monix.eval.Task import monix.reactive.Observable @@ -29,6 +29,9 @@ package object implicits { def checkAsync(query: String)(implicit runtime: Runtime[R]): Task[Unit] = MonixInterop.checkAsync(underlying)(query) + + def interpreterAsync(implicit runtime: Runtime[R]): Task[GraphQLInterpreter[R, CalibanError]] = + MonixInterop.interpreterAsync(underlying) } implicit def effectSchema[R, A](implicit ev: Schema[R, A], ev2: ConcurrentEffect[Task]): Schema[R, Task[A]] = diff --git a/vuepress/docs/docs/README.md b/vuepress/docs/docs/README.md index c7b0ce1f0..51e40e990 100644 --- a/vuepress/docs/docs/README.md +++ b/vuepress/docs/docs/README.md @@ -79,9 +79,12 @@ type Queries { In order to process requests, you need to turn your API into an interpreter, which can be done easily by calling `.interpreter`. An interpreter is a light wrapper around the API definition that allows plugging in some middleware and possibly modifying the environment and error types (see [Middleware](middleware.md) for more info). +Creating the interpreter may fail with a `ValidationError` if some type is found invalid. ```scala -val interpreter = api.interpreter +for { + interpreter <- api.interpreter +} yield interpreter ``` Now you can call `interpreter.execute` with a given GraphQL query, and you will get an `ZIO[R, Nothing, GraphQLResponse[CalibanError]]` as a response, with `GraphQLResponse` defined as follows: diff --git a/vuepress/docs/docs/interop.md b/vuepress/docs/docs/interop.md index 7b415832e..ec2571b9e 100644 --- a/vuepress/docs/docs/interop.md +++ b/vuepress/docs/docs/interop.md @@ -5,7 +5,7 @@ If you prefer using [Cats Effect](https://github.com/typelevel/cats-effect) or [ ## Cats Effect You first need to import `caliban.interop.cats.implicits._` and have an implicit `zio.Runtime` in scope. Then a few helpers are available: -- the GraphQL object is enriched with `executeAsync` and `checkAsync`, variants of `execute` and `check` that return an `F[_]: Async` instead of a `ZIO`. +- the GraphQL object is enriched with `interpreterAsync`, `executeAsync` and `checkAsync`, variants of `interpreter`, `execute` and `check` that return an `F[_]: Async` instead of a `ZIO`. - the `Http4sAdapter` also has cats-effect variants named `makeRestServiceF` and `makeWebSocketServiceF`. In addition to that, a `Schema` for any `F[_]: Effect` is provided. That means you can include fields returning Monix Task for Cats IO in your queries, mutations or subscriptions. @@ -26,7 +26,7 @@ object ExampleCatsInterop extends IOApp { case class Queries(numbers: List[Int], randomNumber: IO[Int]) val queries = Queries(List(1, 2, 3, 4), IO(scala.util.Random.nextInt())) - val interpreter = graphQL(RootResolver(queries)).interpreter + val api = graphQL(RootResolver(queries)) val query = """ { @@ -36,8 +36,9 @@ object ExampleCatsInterop extends IOApp { override def run(args: List[String]): IO[ExitCode] = for { - result <- interpreter.executeAsync[IO](query) - _ <- IO(println(result.data)) + interpreter <- api.interpreterAsync[IO] + result <- interpreter.executeAsync[IO](query) + _ <- IO(println(result.data)) } yield ExitCode.Success } ``` @@ -47,7 +48,7 @@ You can find this example within the [examples](https://github.com/ghostdogpr/ca ## Monix You first need to import `caliban.interop.monix.implicits._` and have an implicit `zio.Runtime` in scope. Then a few helpers are available: -- the GraphQL object is enriched with `executeAsync` and `checkAsync`, variants of `execute` and `check` that return a Monix `Task` instead of a `ZIO`. +- the GraphQL object is enriched with `interpreterAsync`, `executeAsync` and `checkAsync`, variants of `interpreter`, `execute` and `check` that return a Monix `Task` instead of a `ZIO`. In addition to that, a `Schema` for any Monix `Task` as well as `Observable` is provided. @@ -70,7 +71,7 @@ object ExampleMonixInterop extends TaskApp { case class Queries(numbers: List[Int], randomNumber: Task[Int]) val queries = Queries(List(1, 2, 3, 4), Task.eval(scala.util.Random.nextInt())) - val interpreter = graphQL(RootResolver(queries)).interpreter + val api = graphQL(RootResolver(queries)) val query = """ { @@ -80,8 +81,9 @@ object ExampleMonixInterop extends TaskApp { override def run(args: List[String]): Task[ExitCode] = for { - result <- interpreter.executeAsync(query) - _ <- Task.eval(println(result.data)) + interpreter <- api.interpreterAsync + result <- interpreter.executeAsync(query) + _ <- Task.eval(println(result.data)) } yield ExitCode.Success } ``` diff --git a/vuepress/docs/docs/middleware.md b/vuepress/docs/docs/middleware.md index f7adee763..62cde6ef9 100644 --- a/vuepress/docs/docs/middleware.md +++ b/vuepress/docs/docs/middleware.md @@ -77,8 +77,7 @@ All the wrappers mentioned above require that you don't modify the environment ` It is used internally to implement `mapError` (customize errors) and `provide` (eliminate the environment), but you can use it for other purposes such as adding a general timeout, logging response times, etc. ```scala -// create an interpreter -val i: GraphQLInterpreter[MyEnv, CalibanError] = graphqQL(...).interpreter +val i: GraphQLInterpreter[MyEnv, CalibanError] = ??? // change error type to String val i2: GraphQLInterpreter[MyEnv, String] = i.mapError(_.toString)