diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6bb7a187..325d69be 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 modules/test-cats-effect/target target modules/sangria/target project/target + run: tar cf targets.tar modules/core/target modules/cats-effect-experimental/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 - name: Upload target directories uses: actions/upload-artifact@v2 diff --git a/build.sbt b/build.sbt index 691fae87..1a87d9e7 100644 --- a/build.sbt +++ b/build.sbt @@ -48,7 +48,7 @@ lazy val root = project derivation, sangriaTestMonix, sangriaTestFS2, - sangriaTestCatsEffect, + sangriaCatsEffectExperimental, sangria) .settings(inThisBuild(projectInfo)) .settings( @@ -284,16 +284,16 @@ lazy val sangriaTestFS2 = project ) .disablePlugins(MimaPlugin) -lazy val sangriaTestCatsEffect = project - .in(file("modules/test-cats-effect")) - .withId("sangria-test-cats-effect") +lazy val sangriaCatsEffectExperimental = project + .in(file("modules/cats-effect-experimental")) + .withId("sangria-cats-effect-experimental") .dependsOn(core % "compile->compile;test->test") - .settings(scalacSettings ++ shellSettings ++ noPublishSettings) + .settings(scalacSettings ++ shellSettings) .settings( - name := "sangria-test-cats-effect", - description := "Tests with Cats Effect", + name := "sangria-cats-effect-experimental", + description := "Experimental support for Cats Effect", libraryDependencies ++= List( - "org.typelevel" %% "cats-effect" % "3.4.8" % Test, + "org.typelevel" %% "cats-effect" % "3.4.8", "org.sangria-graphql" %% "sangria-circe" % "1.3.2" % Test ) ) diff --git a/modules/cats-effect-experimental/src/main/scala/sangria/catseffect/execution/IOExecutionScheme.scala b/modules/cats-effect-experimental/src/main/scala/sangria/catseffect/execution/IOExecutionScheme.scala new file mode 100644 index 00000000..5b50ef56 --- /dev/null +++ b/modules/cats-effect-experimental/src/main/scala/sangria/catseffect/execution/IOExecutionScheme.scala @@ -0,0 +1,22 @@ +package sangria.catseffect.execution + +import cats.effect.IO +import cats.effect.unsafe.implicits.global +import sangria.execution.{AsyncExecutionScheme, AsyncToFuture} + +import scala.concurrent.{ExecutionContext, Future} + +/** Prepare an [[sangria.execution.ExecutionScheme]] for [[IO]]. If you want to use another effect, + * use the same bricks to build your own. + */ +object IOExecutionScheme { + // sangria is using an implicit ExecutionContext at different places. + // For the moment, we need to expose one. + implicit val ec: ExecutionContext = global.compute + + // ideally we would need only this. + implicit val asyncExecutionScheme: AsyncExecutionScheme[IO] = + new AsyncExecutionScheme[IO](new AsyncToFuture[IO] { + override def toFuture[A](f: IO[A]): Future[A] = f.unsafeToFuture() + }) +} diff --git a/modules/cats-effect-experimental/src/main/scala/sangria/catseffect/schema/AsyncValue.scala b/modules/cats-effect-experimental/src/main/scala/sangria/catseffect/schema/AsyncValue.scala new file mode 100644 index 00000000..83466ccc --- /dev/null +++ b/modules/cats-effect-experimental/src/main/scala/sangria/catseffect/schema/AsyncValue.scala @@ -0,0 +1,18 @@ +package sangria.catseffect.schema + +import cats.effect.Async +import sangria.schema.LeafAction + +import scala.concurrent.ExecutionContext +import scala.language.implicitConversions + +case class AsyncValue[Ctx, Val, F[_]: Async](value: F[Val]) extends LeafAction[Ctx, Val] { + override def map[NewVal](fn: Val => NewVal)(implicit + ec: ExecutionContext): AsyncValue[Ctx, NewVal, F] = + new AsyncValue(Async[F].map(value)(fn)) +} + +object AsyncValue { + implicit def asyncAction[Ctx, Val, F[_]: Async](value: F[Val]): LeafAction[Ctx, Val] = + AsyncValue(value) +} diff --git a/modules/cats-effect-experimental/src/main/scala/sangria/execution/AsyncExecutionScheme.scala b/modules/cats-effect-experimental/src/main/scala/sangria/execution/AsyncExecutionScheme.scala new file mode 100644 index 00000000..c63ca1f9 --- /dev/null +++ b/modules/cats-effect-experimental/src/main/scala/sangria/execution/AsyncExecutionScheme.scala @@ -0,0 +1,31 @@ +package sangria.execution + +import cats.effect.Async + +import scala.concurrent.{ExecutionContext, Future} + +/** An [[ExecutionScheme]] that is capable of using [[sangria.catseffect.schema.AsyncValue]]. + * + * Its result is an [[Async]]. + */ +class AsyncExecutionScheme[F[_]: Async]( + val asyncToFuture: AsyncToFuture[F] +) extends ExecutionScheme { + private val asyncF: Async[F] = Async[F] + + override type Result[Ctx, Res] = F[Res] + + override def failed[Ctx, Res](error: Throwable): Result[Ctx, Res] = asyncF.raiseError(error) + + override def onComplete[Ctx, Res](result: Result[Ctx, Res])(op: => Unit)(implicit + ec: ExecutionContext): Result[Ctx, Res] = + asyncF.flatMap(result)(r => asyncF.map(asyncF.delay(op))(_ => r)) + + override def flatMapFuture[Ctx, Res, T](future: Future[T])(resultFn: T => Result[Ctx, Res])( + implicit ec: ExecutionContext): Result[Ctx, Res] = + asyncF.flatMap(asyncF.fromFuture(asyncF.delay(future)))(resultFn) + + override val resolverBuilder: ResolverBuilder = new AsyncResolverBuilder[F](asyncToFuture) + + override def extended: Boolean = false +} diff --git a/modules/cats-effect-experimental/src/main/scala/sangria/execution/AsyncResolver.scala b/modules/cats-effect-experimental/src/main/scala/sangria/execution/AsyncResolver.scala new file mode 100644 index 00000000..64171a34 --- /dev/null +++ b/modules/cats-effect-experimental/src/main/scala/sangria/execution/AsyncResolver.scala @@ -0,0 +1,169 @@ +package sangria.execution + +import cats.effect.Async +import sangria.ast +import sangria.ast.{Document, SourceMapper} +import sangria.catseffect.schema.AsyncValue +import sangria.execution.deferred.DeferredResolver +import sangria.marshalling.ResultMarshaller +import sangria.schema._ + +import scala.concurrent.{ExecutionContext, Future} + +/** The [[AsyncResolver]] is using the [[FutureResolver]] under the hood. So we need a way to + * transform our [[Async]] into a [[Future]] for now. + */ +trait AsyncToFuture[F[_]] { + def toFuture[A](f: F[A]): Future[A] +} + +private[execution] class AsyncResolverBuilder[F[_]: Async](asyncToFuture: AsyncToFuture[F]) + extends ResolverBuilder { + override def build[Ctx]( + marshaller: ResultMarshaller, + middlewareCtx: MiddlewareQueryContext[Ctx, _, _], + schema: Schema[Ctx, _], + valueCollector: ValueCollector[Ctx, _], + variables: Map[String, VariableValue], + fieldCollector: FieldCollector[Ctx, _], + userContext: Ctx, + exceptionHandler: ExceptionHandler, + deferredResolver: DeferredResolver[Ctx], + sourceMapper: Option[SourceMapper], + deprecationTracker: DeprecationTracker, + middleware: List[(Any, Middleware[Ctx])], + maxQueryDepth: Option[Int], + deferredResolverState: Any, + preserveOriginalErrors: Boolean, + validationTiming: TimeMeasurement, + queryReducerTiming: TimeMeasurement, + queryAst: Document)(implicit executionContext: ExecutionContext): Resolver[Ctx] = + new AsyncResolver[Ctx, F]( + marshaller, + middlewareCtx, + schema, + valueCollector, + variables, + fieldCollector, + userContext, + exceptionHandler, + deferredResolver, + sourceMapper, + deprecationTracker, + middleware, + maxQueryDepth, + deferredResolverState, + preserveOriginalErrors, + validationTiming, + queryReducerTiming, + queryAst, + asyncToFuture + ) +} + +/** The [[Resolver]] that is used to resolve [[AsyncValue]]. + * + * For now, it's using the [[FutureResolver]] under the hood. Later, we can update its + * implementation to avoid using any [[Future]]. + */ +private[execution] class AsyncResolver[Ctx, F[_]: Async]( + val marshaller: ResultMarshaller, + middlewareCtx: MiddlewareQueryContext[Ctx, _, _], + schema: Schema[Ctx, _], + valueCollector: ValueCollector[Ctx, _], + variables: Map[String, VariableValue], + fieldCollector: FieldCollector[Ctx, _], + userContext: Ctx, + exceptionHandler: ExceptionHandler, + deferredResolver: DeferredResolver[Ctx], + sourceMapper: Option[SourceMapper], + deprecationTracker: DeprecationTracker, + middleware: List[(Any, Middleware[Ctx])], + maxQueryDepth: Option[Int], + deferredResolverState: Any, + preserveOriginalErrors: Boolean, + validationTiming: TimeMeasurement, + queryReducerTiming: TimeMeasurement, + queryAst: ast.Document, + asyncToFuture: AsyncToFuture[F] +)(implicit executionContext: ExecutionContext) + extends Resolver[Ctx] { + + private val asyncF: Async[F] = Async[F] + + private val delegate = new FutureResolver[Ctx]( + marshaller, + middlewareCtx, + schema, + valueCollector, + variables, + fieldCollector, + userContext, + exceptionHandler, + deferredResolver, + sourceMapper, + deprecationTracker, + middleware, + maxQueryDepth, + deferredResolverState, + preserveOriginalErrors, + validationTiming, + queryReducerTiming, + queryAst + ) { del => + override protected def resolveLeafAction( + path: ExecutionPath, + tpe: ObjectType[Ctx, _], + userCtx: Ctx, + astFields: Vector[ast.Field], + field: Field[Ctx, _], + updateCtx: Option[MappedCtxUpdate[Ctx, Any, Any]])( + action: LeafAction[Ctx, Any]): (ast.Field, Resolve) = + action match { + case a: AsyncValue[Ctx, Any, F] => + val f = asyncToFuture.toFuture[Any](a.value) + super.resolveFutureValue(path, tpe, userCtx, astFields, field, updateCtx)(FutureValue(f)) + + case action: StandardLeafAction[Ctx, Any] => + super.resolveStandardLeafAction(path, tpe, userCtx, astFields, field, updateCtx)(action) + + case other => unresolvableLeafAction(path, tpe, astFields, updateCtx)(other) + } + + override protected def handleScheme( + result: Future[((Vector[RegisteredError], del.marshaller.Node), Ctx)], + scheme: ExecutionScheme): scheme.Result[Ctx, del.marshaller.Node] = + scheme match { + case s: AsyncExecutionScheme[F] => + val r: F[((Vector[RegisteredError], del.marshaller.Node), Ctx)] = + asyncF.fromFuture(asyncF.delay(result)) + handleSchemeF(r, s).asInstanceOf[scheme.Result[Ctx, del.marshaller.Node]] + + case _ => + super.handleScheme(result, scheme) + } + + private def handleSchemeF( + result: F[((Vector[RegisteredError], del.marshaller.Node), Ctx)], + scheme: AsyncExecutionScheme[F]): scheme.Result[Ctx, del.marshaller.Node] = + asyncF.map(result) { case ((_, res), _) => res } + } + + override def resolveFieldsPar(tpe: ObjectType[Ctx, _], value: Any, fields: CollectedFields)( + scheme: ExecutionScheme): scheme.Result[Ctx, marshaller.Node] = + delegate + .resolveFieldsPar(tpe, value, fields)(scheme) + .asInstanceOf[scheme.Result[Ctx, marshaller.Node]] + + override def resolveFieldsSeq(tpe: ObjectType[Ctx, _], value: Any, fields: CollectedFields)( + scheme: ExecutionScheme): scheme.Result[Ctx, marshaller.Node] = + delegate + .resolveFieldsSeq(tpe, value, fields)(scheme) + .asInstanceOf[scheme.Result[Ctx, marshaller.Node]] + + override def resolveFieldsSubs(tpe: ObjectType[Ctx, _], value: Any, fields: CollectedFields)( + scheme: ExecutionScheme): scheme.Result[Ctx, marshaller.Node] = + delegate + .resolveFieldsSubs(tpe, value, fields)(scheme) + .asInstanceOf[scheme.Result[Ctx, marshaller.Node]] +} diff --git a/modules/test-cats-effect/src/test/scala/sangria/execution/IOExecutionScheme.scala b/modules/cats-effect-experimental/src/test/scala/sangria/catseffect/IOExecutionSchemeSpec.scala similarity index 52% rename from modules/test-cats-effect/src/test/scala/sangria/execution/IOExecutionScheme.scala rename to modules/cats-effect-experimental/src/test/scala/sangria/catseffect/IOExecutionSchemeSpec.scala index 90171ff6..4024bc42 100644 --- a/modules/test-cats-effect/src/test/scala/sangria/execution/IOExecutionScheme.scala +++ b/modules/cats-effect-experimental/src/test/scala/sangria/catseffect/IOExecutionSchemeSpec.scala @@ -1,32 +1,23 @@ -package sangria.execution +package sangria.catseffect 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.execution.Executor +import sangria.catseffect.execution.IOExecutionScheme._ 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, FutureResolverBuilder) +class IOExecutionSchemeSpec extends AnyWordSpec with Matchers { - import IOExecutionScheme._ + import IOExecutionSchemeSpec._ "IOExecutionScheme" must { - "allow using IO effect" in { + "allow using IO effect with pure resolve" in { val query = gql""" query q1 { ids @@ -44,16 +35,35 @@ class IOExecutionScheme extends AnyWordSpec with Matchers { ) res.unsafeRunSync() must be(expected) } + + "allow using IO effect with IO resolve" in { + val query = + gql""" + query q1 { + parent + } + """ + val res: IO[Json] = Executor.execute(schema, query) + + val expected: Json = Json.obj( + "data" -> Json.obj( + "parent" -> Json.fromString("hello") + ) + ) + res.unsafeRunSync() must be(expected) + } } } -object IOExecutionScheme { +object IOExecutionSchemeSpec { + import sangria.catseffect.schema.AsyncValue._ private val QueryType: ObjectType[Unit, Unit] = ObjectType( "Query", () => fields[Unit, Unit]( - Field("ids", ListType(IntType), resolve = _ => List(1, 2)) + Field("ids", ListType(IntType), resolve = _ => List(1, 2)), + Field("parent", StringType, resolve = _ => IO("hello")) )) - val schema = Schema(QueryType) + private val schema = Schema(QueryType) } diff --git a/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala b/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala index 7360c05c..8340fdba 100644 --- a/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala +++ b/modules/core/src/main/scala/sangria/execution/ExecutionScheme.scala @@ -1,11 +1,10 @@ package sangria.execution -import sangria.annotations.ApiMayChange import sangria.streaming.SubscriptionStream import scala.concurrent.{ExecutionContext, Future} -sealed trait ExecutionScheme { +trait ExecutionScheme { type Result[Ctx, Res] def failed[Ctx, Res](error: Throwable): Result[Ctx, Res] @@ -40,33 +39,6 @@ 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[_]]( - val ops: EffectOps[F], - val resolverBuilder: ResolverBuilder -) 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/FutureResolver.scala b/modules/core/src/main/scala/sangria/execution/FutureResolver.scala index 33fddd5f..d1640459 100644 --- a/modules/core/src/main/scala/sangria/execution/FutureResolver.scala +++ b/modules/core/src/main/scala/sangria/execution/FutureResolver.scala @@ -168,7 +168,7 @@ private[execution] class FutureResolver[Ctx]( throw new IllegalStateException(s"Unsupported execution scheme: $s") } - private def handleScheme( + protected def handleScheme( result: Future[((Vector[RegisteredError], marshaller.Node), Ctx)], scheme: ExecutionScheme): scheme.Result[Ctx, marshaller.Node] = scheme match { case ExecutionScheme.Default => @@ -190,10 +190,6 @@ private[execution] class FutureResolver[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") } @@ -880,7 +876,7 @@ private[execution] class FutureResolver[Ctx]( e } - private def resolveLeafAction( + protected def resolveLeafAction( path: ExecutionPath, tpe: ObjectType[Ctx, _], userCtx: Ctx, @@ -888,6 +884,29 @@ private[execution] class FutureResolver[Ctx]( field: Field[Ctx, _], updateCtx: Option[MappedCtxUpdate[Ctx, Any, Any]])( action: LeafAction[Ctx, Any]): (ast.Field, Resolve) = + action match { + case a: StandardLeafAction[Ctx, Any] => + resolveStandardLeafAction(path, tpe, userCtx, astFields, field, updateCtx)(a) + case other => unresolvableLeafAction(path, tpe, astFields, updateCtx)(other) + } + + protected def unresolvableLeafAction( + path: ExecutionPath, + tpe: ObjectType[Ctx, _], + astFields: Vector[ast.Field], + updateCtx: Option[MappedCtxUpdate[Ctx, Any, Any]])( + action: LeafAction[Ctx, Any]): (ast.Field, Resolve) = + illegalActionException(path, tpe, astFields, updateCtx)( + s"Action of type '${action.getClass.toString}' is not supported by this resolver") + + protected def resolveStandardLeafAction( + path: ExecutionPath, + tpe: ObjectType[Ctx, _], + userCtx: Ctx, + astFields: Vector[ast.Field], + field: Field[Ctx, _], + updateCtx: Option[MappedCtxUpdate[Ctx, Any, Any]])( + action: StandardLeafAction[Ctx, Any]): (ast.Field, Resolve) = action match { case v: Value[Ctx, Any] => resolveValue(path, tpe, userCtx, astFields, field, updateCtx)(v) case t: TryValue[Ctx, Any] => @@ -1107,7 +1126,7 @@ private[execution] class FutureResolver[Ctx]( ) } - private def resolveFutureValue( + protected def resolveFutureValue( path: ExecutionPath, tpe: ObjectType[Ctx, _], userCtx: Ctx, @@ -1818,7 +1837,7 @@ private[execution] class FutureResolver[Ctx]( sourceMapper, position.toList) - private sealed trait Resolve { + protected sealed trait Resolve { def appendErrors( path: ExecutionPath, errors: Vector[Throwable], diff --git a/modules/core/src/main/scala/sangria/schema/Action.scala b/modules/core/src/main/scala/sangria/schema/Action.scala index e36d5aba..75f851b0 100644 --- a/modules/core/src/main/scala/sangria/schema/Action.scala +++ b/modules/core/src/main/scala/sangria/schema/Action.scala @@ -8,12 +8,13 @@ import scala.language.implicitConversions import scala.util.{Failure, Try} import scala.util.control.NonFatal -sealed trait Action[+Ctx, +Val] { +trait Action[+Ctx, +Val] { def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): Action[Ctx, NewVal] } -sealed trait LeafAction[+Ctx, +Val] extends Action[Ctx, Val] { +trait LeafAction[+Ctx, +Val] extends Action[Ctx, Val] { def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): LeafAction[Ctx, NewVal] } +sealed trait StandardLeafAction[+Ctx, +Val] extends LeafAction[Ctx, Val] sealed trait ReduceAction[+Ctx, +Val] extends Action[Ctx, Val] { def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): LeafAction[Ctx, NewVal] } @@ -53,7 +54,9 @@ object LeafAction { def apply[Ctx, Val](a: LeafAction[Ctx, Val]): LeafAction[Ctx, Val] = a } -case class Value[Ctx, Val](value: Val) extends LeafAction[Ctx, Val] with ReduceAction[Ctx, Val] { +case class Value[Ctx, Val](value: Val) + extends StandardLeafAction[Ctx, Val] + with ReduceAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): LeafAction[Ctx, NewVal] = try Value(fn(value)) @@ -63,7 +66,7 @@ case class Value[Ctx, Val](value: Val) extends LeafAction[Ctx, Val] with ReduceA } case class TryValue[Ctx, Val](value: Try[Val]) - extends LeafAction[Ctx, Val] + extends StandardLeafAction[Ctx, Val] with ReduceAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): TryValue[Ctx, NewVal] = @@ -71,7 +74,7 @@ case class TryValue[Ctx, Val](value: Try[Val]) } case class PartialValue[Ctx, Val](value: Val, errors: Vector[Throwable]) - extends LeafAction[Ctx, Val] { + extends StandardLeafAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): LeafAction[Ctx, NewVal] = try PartialValue(fn(value), errors) @@ -81,7 +84,7 @@ case class PartialValue[Ctx, Val](value: Val, errors: Vector[Throwable]) } case class FutureValue[Ctx, Val](value: Future[Val]) - extends LeafAction[Ctx, Val] + extends StandardLeafAction[Ctx, Val] with ReduceAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): FutureValue[Ctx, NewVal] = @@ -89,7 +92,7 @@ case class FutureValue[Ctx, Val](value: Future[Val]) } case class PartialFutureValue[Ctx, Val](value: Future[PartialValue[Ctx, Val]]) - extends LeafAction[Ctx, Val] { + extends StandardLeafAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): PartialFutureValue[Ctx, NewVal] = PartialFutureValue(value.map(_.map(fn) match { @@ -99,7 +102,7 @@ case class PartialFutureValue[Ctx, Val](value: Future[PartialValue[Ctx, Val]]) })) } -case class DeferredValue[Ctx, Val](value: Deferred[Val]) extends LeafAction[Ctx, Val] { +case class DeferredValue[Ctx, Val](value: Deferred[Val]) extends StandardLeafAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): DeferredValue[Ctx, NewVal] = DeferredValue(MappingDeferred(value, (v: Val) => (fn(v), Vector.empty))) @@ -109,7 +112,7 @@ case class DeferredValue[Ctx, Val](value: Deferred[Val]) extends LeafAction[Ctx, } case class DeferredFutureValue[Ctx, Val](value: Future[Deferred[Val]]) - extends LeafAction[Ctx, Val] { + extends StandardLeafAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): DeferredFutureValue[Ctx, NewVal] = DeferredFutureValue(value.map(MappingDeferred(_, (v: Val) => (fn(v), Vector.empty)))) @@ -120,7 +123,7 @@ case class DeferredFutureValue[Ctx, Val](value: Future[Deferred[Val]]) } case class SequenceLeafAction[Ctx, Val](value: Seq[LeafAction[Ctx, Val]]) - extends LeafAction[Ctx, Seq[Val]] { + extends StandardLeafAction[Ctx, Seq[Val]] { override def map[NewVal](fn: Seq[Val] => NewVal)(implicit ec: ExecutionContext): MappedSequenceLeafAction[Ctx, Val, NewVal] = new MappedSequenceLeafAction[Ctx, Val, NewVal](this, fn) @@ -129,7 +132,7 @@ case class SequenceLeafAction[Ctx, Val](value: Seq[LeafAction[Ctx, Val]]) class MappedSequenceLeafAction[Ctx, Val, NewVal]( val action: SequenceLeafAction[Ctx, Val], val mapFn: Seq[Val] => NewVal) - extends LeafAction[Ctx, NewVal] { + extends StandardLeafAction[Ctx, NewVal] { override def map[NewNewVal](fn: NewVal => NewNewVal)(implicit ec: ExecutionContext): MappedSequenceLeafAction[Ctx, Val, NewNewVal] = new MappedSequenceLeafAction[Ctx, Val, NewNewVal](action, v => fn(mapFn(v))) @@ -160,7 +163,7 @@ object UpdateCtx { private[sangria] case class SubscriptionValue[Ctx, Val, S[_]]( source: S[Any], stream: SubscriptionStream[S]) - extends LeafAction[Ctx, Val] { + extends StandardLeafAction[Ctx, Val] { override def map[NewVal](fn: Val => NewVal)(implicit ec: ExecutionContext): SubscriptionValue[Ctx, NewVal, S] = throw new IllegalStateException(