From 2c2f4884669a5728c551b7a9ecb4c8a3315e3c8b Mon Sep 17 00:00:00 2001 From: Yann Simon Date: Mon, 20 Feb 2023 12:49:05 +0100 Subject: [PATCH] Very minimal integration with cats effect IO The integration si far away from being complete. But we will go little step by little step here, exploring how far we can go without breaking backwards compatibility. --- .github/workflows/ci.yml | 2 +- build.sbt | 30 +++++++++- .../sangria/execution/ExecutionScheme.scala | 27 +++++++++ .../scala/sangria/execution/Resolver.scala | 4 ++ .../sangria/execution/IOExecutionScheme.scala | 59 +++++++++++++++++++ 5 files changed, 118 insertions(+), 4 deletions(-) create mode 100644 modules/test-cats-effect/src/test/scala/sangria/execution/IOExecutionScheme.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fdd31099..6bb7a187 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: run: sbt ++${{ matrix.scala }} test - name: Compress target directories - run: tar cf targets.tar modules/core/target modules/parser/target modules/test-monix/target modules/benchmarks/target modules/derivation/target modules/test-fs2/target modules/ast/target target modules/sangria/target project/target + run: tar cf targets.tar modules/core/target modules/parser/target modules/test-monix/target modules/benchmarks/target modules/derivation/target modules/test-fs2/target modules/ast/target modules/test-cats-effect/target target modules/sangria/target project/target - name: Upload target directories uses: actions/upload-artifact@v2 diff --git a/build.sbt b/build.sbt index 20f20c95..3497d164 100644 --- a/build.sbt +++ b/build.sbt @@ -39,7 +39,16 @@ def emptyForScala3(isScala3: Boolean, module: ModuleID): Set[ModuleID] = lazy val root = project .in(file(".")) .withId("sangria-root") - .aggregate(ast, parser, core, benchmarks, derivation, sangriaTestMonix, sangriaTestFS2, sangria) + .aggregate( + ast, + parser, + core, + benchmarks, + derivation, + sangriaTestMonix, + sangriaTestFS2, + sangriaTestCatsEffect, + sangria) .settings(inThisBuild(projectInfo)) .settings( scalacSettings ++ shellSettings ++ noPublishSettings @@ -210,12 +219,27 @@ lazy val sangriaTestFS2 = project .dependsOn(core % "compile->compile;test->test") .settings(scalacSettings ++ shellSettings ++ noPublishSettings) .settings( - name := "sangria-test-monix", - description := "Tests with monix", + name := "sangria-test-fs2", + description := "Tests with FS2", libraryDependencies += "co.fs2" %% "fs2-core" % "3.6.0" % Test ) .disablePlugins(MimaPlugin) +lazy val sangriaTestCatsEffect = project + .in(file("modules/test-cats-effect")) + .withId("sangria-test-cats-effect") + .dependsOn(core % "compile->compile;test->test") + .settings(scalacSettings ++ shellSettings ++ noPublishSettings) + .settings( + name := "sangria-test-cats-effect", + description := "Tests with Cats Effect", + libraryDependencies ++= List( + "org.typelevel" %% "cats-effect" % "3.4.7" % Test, + "org.sangria-graphql" %% "sangria-circe" % "1.3.2" % Test + ) + ) + .disablePlugins(MimaPlugin) + lazy val benchmarks = project .in(file("modules/benchmarks")) .withId("sangria-benchmarks") diff --git a/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala b/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala index 54f876e0..93abf699 100644 --- a/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala +++ b/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala @@ -1,5 +1,6 @@ package sangria.execution +import sangria.annotations.ApiMayChange import sangria.streaming.SubscriptionStream import scala.concurrent.{ExecutionContext, Future} @@ -36,6 +37,32 @@ object ExecutionScheme extends AlternativeExecutionScheme { } } +@ApiMayChange +trait EffectOps[F[_]] { + def failed[Ctx, Res](error: Throwable): F[Res] + def flatMapFuture[Res, T](future: Future[T])(resultFn: T => F[Res]): F[Res] + def map[T, Out](in: Future[T])(f: T => Out): F[Out] +} + +@ApiMayChange +class EffectBasedExecutionScheme[F[_]]( + ops: EffectOps[F] +) extends ExecutionScheme { + override type Result[Ctx, Res] = F[Res] + override def failed[Ctx, Res](error: Throwable): Result[Ctx, Res] = + ops.failed(error) + override def onComplete[Ctx, Res](result: Result[Ctx, Res])(op: => Unit)(implicit + ec: ExecutionContext): Result[Ctx, Res] = ??? + override def flatMapFuture[Ctx, Res, T](future: Future[T])(resultFn: T => Result[Ctx, Res])( + implicit ec: ExecutionContext): Result[Ctx, Res] = + ops.flatMapFuture(future)(resultFn) + def mapEffect[Ctx, Res, T](future: Future[(Ctx, T)])(f: (Ctx, T) => Res)(implicit + ec: ExecutionContext): F[Res] = + ops.map(future) { case (ctx, in) => f(ctx, in) } + + override def extended: Boolean = false +} + trait AlternativeExecutionScheme { trait StreamBasedExecutionScheme[S[_]] { def subscriptionStream: SubscriptionStream[S] diff --git a/modules/core/src/main/scala/sangria/execution/Resolver.scala b/modules/core/src/main/scala/sangria/execution/Resolver.scala index 3766fd67..9fdb0c16 100644 --- a/modules/core/src/main/scala/sangria/execution/Resolver.scala +++ b/modules/core/src/main/scala/sangria/execution/Resolver.scala @@ -142,6 +142,10 @@ class Resolver[Ctx]( }) .asInstanceOf[scheme.Result[Ctx, marshaller.Node]] + case s: EffectBasedExecutionScheme[_] => + s.mapEffect(result.map(_.swap)) { case (_, in) => in._2 } + .asInstanceOf[scheme.Result[Ctx, marshaller.Node]] + case s => throw new IllegalStateException(s"Unsupported execution scheme: $s") } diff --git a/modules/test-cats-effect/src/test/scala/sangria/execution/IOExecutionScheme.scala b/modules/test-cats-effect/src/test/scala/sangria/execution/IOExecutionScheme.scala new file mode 100644 index 00000000..569fd92c --- /dev/null +++ b/modules/test-cats-effect/src/test/scala/sangria/execution/IOExecutionScheme.scala @@ -0,0 +1,59 @@ +package sangria.execution + +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import io.circe.Json +import org.scalatest.matchers.must.Matchers +import org.scalatest.wordspec.AnyWordSpec +import sangria.macros._ +import sangria.marshalling.circe._ +import sangria.schema._ + +import scala.concurrent.{ExecutionContext, Future} + +/** The integration with [[cats.effect.IO]] is far from being complete for now. + */ +class IOExecutionScheme extends AnyWordSpec with Matchers { + private implicit val ec: ExecutionContext = global.compute + private val ioEffectOps = new EffectOps[IO] { + override def failed[Ctx, Res](error: Throwable): IO[Res] = IO.raiseError(error) + override def flatMapFuture[Res, T](future: Future[T])(resultFn: T => IO[Res]): IO[Res] = + IO.fromFuture(IO(future)).flatMap(resultFn) + override def map[T, Out](in: Future[T])(f: T => Out): IO[Out] = IO.fromFuture(IO(in)).map(f) + } + private implicit val ioExecutionScheme: EffectBasedExecutionScheme[IO] = + new EffectBasedExecutionScheme[IO](ioEffectOps) + + import IOExecutionScheme._ + "IOExecutionScheme" must { + "allow using IO effect" in { + val query = gql""" + query q1 { + ids + } + """ + val res: IO[Json] = Executor.execute(schema, query) + + val expected: Json = Json.obj( + "data" -> Json.obj( + "ids" -> Json.arr( + Json.fromInt(1), + Json.fromInt(2) + ) + ) + ) + res.unsafeRunSync() must be(expected) + } + } +} + +object IOExecutionScheme { + private val QueryType: ObjectType[Unit, Unit] = ObjectType( + "Query", + () => + fields[Unit, Unit]( + Field("ids", ListType(IntType), resolve = _ => List(1, 2)) + )) + + val schema = Schema(QueryType) +}