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) +}