diff --git a/modules/core/src/main/scala/gql/Validation.scala b/modules/core/src/main/scala/gql/Validation.scala index 118024164..323b31ff2 100644 --- a/modules/core/src/main/scala/gql/Validation.scala +++ b/modules/core/src/main/scala/gql/Validation.scala @@ -22,8 +22,7 @@ import cats.mtl._ import gql.ast._ import gql.parser.GraphqlParser import gql.preparation.PositionalError -import gql.preparation.ArgParsing -import gql.preparation.RootPreparation +import gql.preparation._ import gql.preparation.FieldMerging object Validation { @@ -318,10 +317,9 @@ object Validation { arg.entries.toChain .mapFilter(x => x.defaultValue.tupleLeft(x)) .traverse_[G, Unit] { case (a: ArgValue[a], pv) => - RootPreparation.Stack.runK[Unit] { - ArgParsing[RootPreparation.Stack[Unit, *], Unit](Map.empty) - .decodeIn[a](a.input.value, pv.map(List(_)), ambigiousEnum = false) - } match { + (new ArgParsing[Unit](Map.empty)) + .decodeIn[a](a.input.value, pv.map(List(_)), ambigiousEnum = false) + .run match { case Left(errs) => errs.traverse_ { err => val suf = err.position @@ -433,10 +431,9 @@ object Validation { case (None, Some(_)) => raise[F, G](Error.InterfaceImplementationMissingDefaultArg(ol.name, i.value.name, k, argName)) case (Some(ld), Some(rd)) => - RootPreparation.Stack - .runK { - FieldMerging[RootPreparation.Stack[Unit, *], Unit].compareValues(ld, rd, None) - } + (new FieldMerging[Unit]) + .compareValues(ld, rd, None) + .run .swap .toOption .traverse_(_.traverse_ { pe => diff --git a/modules/core/src/main/scala/gql/preparation/Alg.scala b/modules/core/src/main/scala/gql/preparation/Alg.scala new file mode 100644 index 000000000..25320cf39 --- /dev/null +++ b/modules/core/src/main/scala/gql/preparation/Alg.scala @@ -0,0 +1,245 @@ +/* + * Copyright 2023 Valdemar Grange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package gql.preparation + +import gql._ +import cats.implicits._ +import cats._ +import cats.arrow.FunctionK +import cats.data._ +import org.typelevel.scalaccompat.annotation._ + +sealed trait Alg[+C, +A] { + def run[C2 >: C]: EitherNec[PositionalError[C2], A] = Alg.run(this) +} +object Alg { + case object NextId extends Alg[Nothing, Int] + + final case class UseVariable(name: String) extends Alg[Nothing, Unit] + final case class UsedVariables() extends Alg[Nothing, Set[String]] + + case object CycleAsk extends Alg[Nothing, Set[String]] + final case class CycleOver[C, A](name: String, fa: Alg[C, A]) extends Alg[C, A] + + case object CursorAsk extends Alg[Nothing, Cursor] + final case class CursorOver[C, A](cursor: Cursor, fa: Alg[C, A]) extends Alg[C, A] + + final case class RaiseError[C](pe: NonEmptyChain[PositionalError[C]]) extends Alg[C, Nothing] + + final case class Pure[A](a: A) extends Alg[Nothing, A] + final case class FlatMap[C, A, B]( + fa: Alg[C, A], + f: A => Alg[C, B] + ) extends Alg[C, B] + final case class ParAp[C, A, B]( + fa: Alg[C, A], + fab: Alg[C, A => B] + ) extends Alg[C, B] + + final case class Attempt[C, A]( + fa: Alg[C, A] + ) extends Alg[C, EitherNec[PositionalError[C], A]] + + implicit def monadErrorForPreparationAlg[C]: MonadError[Alg[C, *], NonEmptyChain[PositionalError[C]]] = + new MonadError[Alg[C, *], NonEmptyChain[PositionalError[C]]] { + override def pure[A](x: A): Alg[C, A] = Ops[C].pure(x) + + override def raiseError[A](e: NonEmptyChain[PositionalError[C]]): Alg[C, A] = + Alg.RaiseError(e) + + override def handleErrorWith[A](fa: Alg[C, A])( + f: NonEmptyChain[PositionalError[C]] => Alg[C, A] + ): Alg[C, A] = + Ops[C].flatMap(Ops[C].attempt(fa)) { + case Left(pe) => f(pe) + case Right(a) => Ops[C].pure(a) + } + + override def flatMap[A, B](fa: Alg[C, A])(f: A => Alg[C, B]): Alg[C, B] = + Ops[C].flatMap(fa)(f) + + override def tailRecM[A, B](a: A)(f: A => Alg[C, Either[A, B]]): Alg[C, B] = + Ops[C].flatMap(f(a)) { + case Left(a) => tailRecM(a)(f) + case Right(b) => Ops[C].pure(b) + } + } + + implicit def parallelForPreparationAlg[C]: Parallel[Alg[C, *]] = + new Parallel[Alg[C, *]] { + type F[A] = Alg[C, A] + + override def sequential: F ~> F = FunctionK.id[F] + + override def parallel: F ~> F = FunctionK.id[F] + + override def applicative: Applicative[F] = + new Applicative[F] { + override def pure[A](x: A): F[A] = Ops[C].pure(x) + + override def ap[A, B](ff: F[A => B])(fa: F[A]): F[B] = Ops[C].parAp(fa)(ff) + } + + override def monad: Monad[Alg[C, *]] = monadErrorForPreparationAlg[C] + } + + def run[C, A](fa: Alg[C, A]): EitherNec[PositionalError[C], A] = { + final case class State( + nextId: Int, + usedVariables: Set[String], + cycleSet: Set[String], + cursor: Cursor + ) + sealed trait Outcome[+B] { + def modifyState(f: State => State): Outcome[B] + } + object Outcome { + final case class Result[B](value: B, state: State) extends Outcome[B] { + def modifyState(f: State => State): Outcome[B] = Result(value, f(state)) + } + final case class Errors(pe: NonEmptyChain[PositionalError[C]]) extends Outcome[Nothing] { + def modifyState(f: State => State): Outcome[Nothing] = this + } + } + @nowarn3("msg=.*cannot be checked at runtime because its type arguments can't be determined.*") + def go[B]( + fa: Alg[C, B], + state: State + ): Eval[Outcome[B]] = Eval.defer { + fa match { + case NextId => + val s = state.copy(nextId = state.nextId + 1) + Eval.now(Outcome.Result(s.nextId, s)) + case Pure(a) => Eval.now(Outcome.Result(a, state)) + case bind: FlatMap[C, a, B] => + go[a](bind.fa, state).flatMap { + case Outcome.Errors(pes) => Eval.now(Outcome.Errors(pes)) + case Outcome.Result(a, state) => go(bind.f(a), state) + } + case parAp: ParAp[C, a, B] => + go(parAp.fa, state).flatMap { + case Outcome.Errors(pes1) => + go(parAp.fab, state).flatMap { + case Outcome.Errors(pes2) => Eval.now(Outcome.Errors(pes1 ++ pes2)) + case Outcome.Result(_, _) => Eval.now(Outcome.Errors(pes1)) + } + case Outcome.Result(a, state) => + go(parAp.fab, state).flatMap { + case Outcome.Errors(pes2) => Eval.now(Outcome.Errors(pes2)) + case Outcome.Result(f, state) => Eval.now(Outcome.Result(f(a), state)) + } + } + case UseVariable(name) => + Eval.now(Outcome.Result((), state.copy(usedVariables = state.usedVariables + name))) + case UsedVariables() => + Eval.now(Outcome.Result(state.usedVariables, state)) + case CycleAsk => + Eval.now(Outcome.Result(state.cycleSet, state)) + case CycleOver(name, fa) => + go(fa, state.copy(cycleSet = state.cycleSet + name)) + .map(_.modifyState(s => s.copy(cycleSet = s.cycleSet - name))) + case CursorAsk => + Eval.now(Outcome.Result(state.cursor, state)) + case CursorOver(cursor, fa) => + go(fa, state.copy(cursor = cursor)) + .map(_.modifyState(s => s.copy(cursor = s.cursor))) + case re: RaiseError[C] => + Eval.now(Outcome.Errors(re.pe)) + case alg: Attempt[C, a] => + go(alg.fa, state).flatMap { + case Outcome.Errors(pes) => Eval.now(Outcome.Result(Left(pes), state)) + case Outcome.Result(a, state) => Eval.now(Outcome.Result(Right(a), state)) + } + } + } + + go(fa, State(0, Set.empty, Set.empty, Cursor.empty)).value match { + case Outcome.Errors(pes) => Left(pes) + case Outcome.Result(a, _) => Right(a) + } + } + + trait Ops[C] { + def nextId: Alg[C, Int] = Alg.NextId + + def useVariable(name: String): Alg[C, Unit] = + Alg.UseVariable(name) + + def usedVariables: Alg[C, Set[String]] = + Alg.UsedVariables() + + def cycleAsk: Alg[C, Set[String]] = Alg.CycleAsk + + def cycleOver[A](name: String, fa: Alg[C, A]): Alg[C, A] = + Alg.CycleOver(name, fa) + + def cursorAsk: Alg[C, Cursor] = Alg.CursorAsk + + def cursorOver[A](cursor: Cursor, fa: Alg[C, A]): Alg[C, A] = + Alg.CursorOver(cursor, fa) + + def raiseError(pe: PositionalError[C]): Alg[C, Nothing] = + Alg.RaiseError(NonEmptyChain.one(pe)) + + def raise[A](message: String, carets: List[C]): Alg[C, A] = + cursorAsk.flatMap(c => raiseError(PositionalError(c, carets, message))) + + def raiseEither[A](e: Either[String, A], carets: List[C]): Alg[C, A] = + e match { + case Left(value) => raise(value, carets) + case Right(value) => pure(value) + } + + def raiseOpt[A](oa: Option[A], message: String, carets: List[C]): Alg[C, A] = + raiseEither(oa.toRight(message), carets) + + def modifyError[A](f: PositionalError[C] => PositionalError[C])(fa: Alg[C, A]): Alg[C, A] = + attempt(fa).flatMap { + case Right(a) => pure(a) + case Left(pes) => Alg.RaiseError(pes.map(f)) + } + + def appendMessage[A](message: String)(fa: Alg[C, A]): Alg[C, A] = + modifyError[A](d => d.copy(message = d.message + "\n" + message))(fa) + + def pure[A](a: A): Alg[Nothing, A] = Alg.Pure(a) + + def flatMap[A, B](fa: Alg[C, A])(f: A => Alg[C, B]): Alg[C, B] = + Alg.FlatMap(fa, f) + + def parAp[A, B](fa: Alg[C, A])(fab: Alg[C, A => B]): Alg[C, B] = + Alg.ParAp(fa, fab) + + def attempt[A](fa: Alg[C, A]): Alg[C, EitherNec[PositionalError[C], A]] = + Alg.Attempt(fa) + + def unit: Alg[C, Unit] = pure(()) + + def ambientEdge[A](edge: GraphArc)(fa: Alg[C, A]): Alg[C, A] = + cursorAsk.flatMap { cursor => + cursorOver(cursor.add(edge), fa) + } + + def ambientField[A](name: String)(fa: Alg[C, A]): Alg[C, A] = + ambientEdge(GraphArc.Field(name))(fa) + + def ambientIndex[A](index: Int)(fa: Alg[C, A]): Alg[C, A] = + ambientEdge(GraphArc.Index(index))(fa) + } + object Ops { + def apply[C] = new Ops[C] {} + } +} diff --git a/modules/core/src/main/scala/gql/preparation/ArgParsing.scala b/modules/core/src/main/scala/gql/preparation/ArgParsing.scala index 6880c76f8..43bf9d5d2 100644 --- a/modules/core/src/main/scala/gql/preparation/ArgParsing.scala +++ b/modules/core/src/main/scala/gql/preparation/ArgParsing.scala @@ -18,223 +18,203 @@ package gql.preparation import cats._ import cats.data._ import cats.implicits._ -import cats.mtl._ import gql._ import gql.ast._ import gql.parser.AnyValue import gql.parser.NonVar import gql.parser.{Value => V} -trait ArgParsing[F[_], C] { +class ArgParsing[C](variables: VariableMap[C]) { + type G[A] = Alg[C, A] + val G = Alg.Ops[C] + def decodeIn[A]( a: In[A], value: V[AnyValue, List[C]], ambigiousEnum: Boolean - ): F[A] - - def decodeArg[A]( - arg: Arg[A], - values: Map[String, V[AnyValue, List[C]]], - ambigiousEnum: Boolean, - context: List[C] - ): F[A] -} - -object ArgParsing { - type UsedVariables = Set[String] - - def apply[F[_]: Parallel, C]( - variables: VariableMap[C] - )(implicit - F: Monad[F], - L: Local[F, Cursor], - H: Handle[F, NonEmptyChain[PositionalError[C]]], - T: Tell[F, UsedVariables] - ): ArgParsing[F, C] = new ArgParsing[F, C] { - val E = ErrorAlg.errorAlgForHandle[F, NonEmptyChain, C] - val P = PathAlg[F] - import E._ - import P._ - - override def decodeIn[A](a: In[A], value: V[AnyValue, List[C]], ambigiousEnum: Boolean): F[A] = { - (a, value) match { - case (_, V.VariableValue(vn, cs)) => - T.tell(Set(vn)) *> { - variables.get(vn) match { - case None => - raise( - s"Variable '$$$vn' was not declared and provided as a possible variable for this operation. Hint add the variable to the variables list of the operation '(..., $$$vn: ${ModifierStack - .fromIn(a) - .show(_.name)})' and provide a value in the variables parameter.", - cs - ) - case Some(v) => - val parseInnerF: F[A] = v.value match { - case Right(pval) => decodeIn(a, pval.map(c2 => c2 :: cs), ambigiousEnum = false) - case Left(j) => decodeIn(a, V.fromJson(j).as(cs), ambigiousEnum = true) - } + ): G[A] = { + (a, value) match { + case (_, V.VariableValue(vn, cs)) => + G.useVariable(vn) *> { + variables.get(vn) match { + case None => + G.raise( + s"Variable '$$$vn' was not declared and provided as a possible variable for this operation. Hint add the variable to the variables list of the operation '(..., $$$vn: ${ModifierStack + .fromIn(a) + .show(_.name)})' and provide a value in the variables parameter.", + cs + ) + case Some(v) => + val parseInnerF: G[A] = v.value match { + case Right(pval) => decodeIn(a, pval.map(c2 => c2 :: cs), ambigiousEnum = false) + case Left(j) => decodeIn(a, V.fromJson(j).as(cs), ambigiousEnum = true) + } - val vt: ModifierStack[String] = ModifierStack.fromType(v.tpe) - val at = ModifierStack.fromIn(a) + val vt: ModifierStack[String] = ModifierStack.fromType(v.tpe) + val at = ModifierStack.fromIn(a) - def showType(xs: List[Modifier], name: String): String = - ModifierStack(xs, name).show(identity) + def showType(xs: List[Modifier], name: String): String = + ModifierStack(xs, name).show(identity) - def showVarType(xs: List[Modifier]): String = - showType(xs, vt.inner) + def showVarType(xs: List[Modifier]): String = + showType(xs, vt.inner) - def showArgType(xs: List[Modifier]): String = - showType(xs, at.inner.name) + def showArgType(xs: List[Modifier]): String = + showType(xs, at.inner.name) - lazy val prefix = - s"Variable '$$${vn}' of type `${vt.show(identity)}` was not compatible with expected argument type `${at.map(_.name).show(identity)}`" + lazy val prefix = + s"Variable '$$${vn}' of type `${vt.show(identity)}` was not compatible with expected argument type `${at.map(_.name).show(identity)}`" - def remaining(vs: List[Modifier], as: List[Modifier]): String = - if (vs.size == vt.modifiers.size && as.size == at.modifiers.size) "." - else - s". The remaining type for the variable `${showVarType(vs)}` is not compatible with the remaining type for the argument `${showArgType(as)}`" + def remaining(vs: List[Modifier], as: List[Modifier]): String = + if (vs.size == vt.modifiers.size && as.size == at.modifiers.size) "." + else + s". The remaining type for the variable `${showVarType(vs)}` is not compatible with the remaining type for the argument `${showArgType(as)}`" - def showModifier(m: Option[Modifier]): String = m match { - case None => "no modifiers" - case Some(Modifier.NonNull) => "a non-null modifier" - case Some(Modifier.List) => "a list modifier" - } + def showModifier(m: Option[Modifier]): String = m match { + case None => "no modifiers" + case Some(Modifier.NonNull) => "a non-null modifier" + case Some(Modifier.List) => "a list modifier" + } - /* - * We must verify if the variable may occur here by comparing the type of the variable with the type of the arg - * If we don't do this, variables will be structurally typed - * Var should be more constrained than the arg - * a ::= [a] | a! | A - * v ::= [v] | v! | V - * a compat v ::= ok | fail - * - * a compat v -> outcome - * -------------------------- - * A compat V -> ok - * a! compat v! -> a compat v - * a! compat [v] -> fail - * a! compat V -> fail - * [a] compat v! -> [a] compat v - * A compat v! -> A compat v - * [a] compat [v] -> [a] compat [v] - * [a] compat V -> fail - * A compat [v] -> fail - */ - def verifyTypeShape(argShape: List[Modifier], varShape: List[Modifier]): F[Unit] = - (argShape, varShape) match { - // A compat V - case (Nil, Nil) => F.unit - // a! compat v! -> ok - case (Modifier.NonNull :: xs, Modifier.NonNull :: ys) => verifyTypeShape(xs, ys) - // a! compat ([v] | V) -> fail - case (Modifier.NonNull :: _, (Modifier.List :: _) | Nil) => - raise( - s"${prefix}, because the argument expected a not-null (!) modifier, but was given ${showModifier( - varShape.headOption - )}${remaining(varShape, argShape)}", - cs - ) - // ([a] | A) compat v! -> ok - case (xs, Modifier.NonNull :: ys) => verifyTypeShape(xs, ys) - // [a] compat [v] -> ok - case (Modifier.List :: xs, Modifier.List :: ys) => verifyTypeShape(xs, ys) - // [a] compat V -> fail - case (Modifier.List :: _, Nil) => - raise( - s"${prefix}, because the argumented expected a list modifier ([A]) but no more modifiers were provided${remaining(varShape, argShape)}", - cs - ) - // A compat [v] -> fail - case (Nil, Modifier.List :: _) => - raise( - s"${prefix}, because the argumented expected no more modifiers but was given a list modifier ([A])${remaining(varShape, argShape)}", - cs - ) - } - - val verifiedF: F[Unit] = verifyTypeShape(at.modifiers, vt.modifiers) - - val verifiedTypenameF: F[Unit] = - if (vt.inner === at.inner.name) F.unit - else - raise( - s"${prefix}, typename of the variable '${vt.inner}' was not the same as the argument typename '${at.inner.name}'", + /* + * We must verify if the variable may occur here by comparing the type of the variable with the type of the arg + * If we don't do this, variables will be structurally typed + * Var should be more constrained than the arg + * a ::= [a] | a! | A + * v ::= [v] | v! | V + * a compat v ::= ok | fail + * + * a compat v -> outcome + * -------------------------- + * A compat V -> ok + * a! compat v! -> a compat v + * a! compat [v] -> fail + * a! compat V -> fail + * [a] compat v! -> [a] compat v + * A compat v! -> A compat v + * [a] compat [v] -> [a] compat [v] + * [a] compat V -> fail + * A compat [v] -> fail + */ + def verifyTypeShape(argShape: List[Modifier], varShape: List[Modifier]): G[Unit] = + (argShape, varShape) match { + // A compat V + case (Nil, Nil) => G.unit + // a! compat v! -> ok + case (Modifier.NonNull :: xs, Modifier.NonNull :: ys) => verifyTypeShape(xs, ys) + // a! compat ([v] | V) -> fail + case (Modifier.NonNull :: _, (Modifier.List :: _) | Nil) => + G.raise( + s"${prefix}, because the argument expected a not-null (!) modifier, but was given ${showModifier( + varShape.headOption + )}${remaining(varShape, argShape)}", + cs + ) + // ([a] | A) compat v! -> ok + case (xs, Modifier.NonNull :: ys) => verifyTypeShape(xs, ys) + // [a] compat [v] -> ok + case (Modifier.List :: xs, Modifier.List :: ys) => verifyTypeShape(xs, ys) + // [a] compat V -> fail + case (Modifier.List :: _, Nil) => + G.raise( + s"${prefix}, because the argumented expected a list modifier ([A]) but no more modifiers were provided${remaining(varShape, argShape)}", + cs + ) + // A compat [v] -> fail + case (Nil, Modifier.List :: _) => + G.raise( + s"${prefix}, because the argumented expected no more modifiers but was given a list modifier ([A])${remaining(varShape, argShape)}", cs ) + } - verifiedF *> verifiedTypenameF *> parseInnerF - } - } - case (e @ Enum(name, _, _), v) => - val fa: F[(String, List[C])] = v match { - case V.EnumValue(s, cs) => F.pure((s, cs)) - case V.StringValue(s, cs) if ambigiousEnum => F.pure((s, cs)) - case _ => raise(s"Enum value expected for `$name`, but got ${pValueName(v)}.", v.c) - } + val verifiedF: G[Unit] = verifyTypeShape(at.modifiers, vt.modifiers) - fa.flatMap[A] { case (s, cs) => - e.m.lookup(s) match { - case Some(x) => F.pure(x) - case None => - val names = e.m.keys.toList - raise( - s"Enum value `$s` does not occur in enum type `$name`, possible enum values are ${names.map(s => s"`$s`").mkString_(", ")}.", - cs - ) - } + val verifiedTypenameF: G[Unit] = + if (vt.inner === at.inner.name) G.unit + else + G.raise( + s"${prefix}, typename of the variable '${vt.inner}' was not the same as the argument typename '${at.inner.name}'", + cs + ) + + verifiedF *> verifiedTypenameF *> parseInnerF } - case (Scalar(name, _, decoder, _), x: NonVar[List[C]]) => - ambientField(name) { - raiseEither(decoder(x.map(_ => ())), x.c) + } + case (e @ Enum(name, _, _), v) => + val fa: G[(String, List[C])] = v match { + case V.EnumValue(s, cs) => G.pure((s, cs)) + case V.StringValue(s, cs) if ambigiousEnum => G.pure((s, cs)) + case _ => G.raise(s"Enum value expected for `$name`, but got ${pValueName(v)}.", v.c) + } + + fa.flatMap[A] { case (s, cs) => + e.m.lookup(s) match { + case Some(x) => G.pure(x) + case None => + val names = e.m.keys.toList + G.raise( + s"Enum value `$s` does not occur in enum type `$name`, possible enum values are ${names.map(s => s"`$s`").mkString_(", ")}.", + cs + ) } - case (Input(_, fields, _), V.ObjectValue(xs, cs)) => decodeArg(fields, xs.toMap, ambigiousEnum, cs) - case (a: InArr[a, c], V.ListValue(vs, cs)) => - vs.zipWithIndex - .parTraverse { case (v, i) => - ambientIndex(i) { - decodeIn(a.of, v, ambigiousEnum) - } + } + case (Scalar(name, _, decoder, _), x: NonVar[List[C]]) => + G.ambientField(name) { + G.raiseEither(decoder(x.map(_ => ())), x.c) + } + case (Input(_, fields, _), V.ObjectValue(xs, cs)) => decodeArg(fields, xs.toMap, ambigiousEnum, cs) + case (a: InArr[a, c], V.ListValue(vs, cs)) => + vs.zipWithIndex + .parTraverse { case (v, i) => + G.ambientIndex(i) { + decodeIn(a.of, v, ambigiousEnum) } - .flatMap[c](a.fromSeq(_).fold(raise(_, cs), F.pure(_))) - case (_: InOpt[a], V.NullValue(_)) => F.pure(Option.empty[a]) - case (opt: InOpt[a], v) => decodeIn(opt.of, v, ambigiousEnum).map(Option(_)) - case (i, v) => raise(s"Expected type `${ModifierStack.fromIn(i).show(_.name)}`, but got value ${pValueName(value)}.", v.c) - } + } + .flatMap[c](a.fromSeq(_).fold(G.raise(_, cs), G.pure(_))) + case (_: InOpt[a], V.NullValue(_)) => G.pure(Option.empty[a]) + case (opt: InOpt[a], v) => decodeIn(opt.of, v, ambigiousEnum).map(Option(_)) + case (i, v) => G.raise(s"Expected type `${ModifierStack.fromIn(i).show(_.name)}`, but got value ${pValueName(value)}.", v.c) } + } - override def decodeArg[A](arg: Arg[A], values: Map[String, V[AnyValue, List[C]]], ambigiousEnum: Boolean, context: List[C]): F[A] = { - val expected = arg.entries.toList.map(_.name).toSet - val provided = values.keySet - - val tooMuch = provided -- expected - val tooMuchF: F[Unit] = - if (tooMuch.isEmpty) F.unit - else raise(s"Too many fields provided, unknown fields are ${tooMuch.toList.map(x => s"'$x'").mkString_(", ")}.", context) - - val fv = arg.impl.foldMap[F, ValidatedNec[String, A]](new (Arg.Impl ~> F) { - def apply[B](fa: Arg.Impl[B]): F[B] = fa match { - case fa: ArgDecoder[a, B] => - ambientField(fa.av.name) { - def compileWith(x: V[AnyValue, List[C]], default: Boolean) = - decodeIn[a](fa.av.input.value, x, ambigiousEnum) - .flatMap(a => raiseEither(fa.decode(ArgParam(default, a)), x.c)) - - values - .get(fa.av.name) - .map(compileWith(_, false)) - .orElse(fa.av.defaultValue.map(dv => compileWith(dv.as(Nil), true))) - .getOrElse { - fa.av.input.value match { - case _: gql.ast.InOpt[a] => raiseEither(fa.decode(ArgParam(true, None)), context) - case _ => - raise(s"Missing argument for '${fa.av.name}' and no default value was found.", context) - } + def decodeArg[A]( + arg: Arg[A], + values: Map[String, V[AnyValue, List[C]]], + ambigiousEnum: Boolean, + context: List[C] + ): G[A] = { + val expected = arg.entries.toList.map(_.name).toSet + val provided = values.keySet + + val tooMuch = provided -- expected + val tooMuchF: G[Unit] = + if (tooMuch.isEmpty) G.unit + else G.raise(s"Too many fields provided, unknown fields are ${tooMuch.toList.map(x => s"'$x'").mkString_(", ")}.", context) + + val fv = arg.impl.foldMap[G, ValidatedNec[String, A]](new (Arg.Impl ~> G) { + def apply[B](fa: Arg.Impl[B]): G[B] = fa match { + case fa: ArgDecoder[a, B] => + G.ambientField(fa.av.name) { + def compileWith(x: V[AnyValue, List[C]], default: Boolean) = + decodeIn[a](fa.av.input.value, x, ambigiousEnum) + .flatMap(a => G.raiseEither(fa.decode(ArgParam(default, a)), x.c)) + + values + .get(fa.av.name) + .map(compileWith(_, false)) + .orElse(fa.av.defaultValue.map(dv => compileWith(dv.as(Nil), true))) + .getOrElse { + fa.av.input.value match { + case _: gql.ast.InOpt[a] => G.raiseEither(fa.decode(ArgParam(true, None)), context) + case _ => + G.raise(s"Missing argument for '${fa.av.name}' and no default value was found.", context) } - } - } - }) + } + } + } + }) - tooMuchF &> fv.flatMap(v => raiseEither(v.toEither.leftMap(_.mkString_(", ")), context)) - } + tooMuchF &> fv.flatMap(v => G.raiseEither(v.toEither.leftMap(_.mkString_(", ")), context)) } } diff --git a/modules/core/src/main/scala/gql/preparation/DirectiveAlg.scala b/modules/core/src/main/scala/gql/preparation/DirectiveAlg.scala index a8048b11f..315681f55 100644 --- a/modules/core/src/main/scala/gql/preparation/DirectiveAlg.scala +++ b/modules/core/src/main/scala/gql/preparation/DirectiveAlg.scala @@ -20,64 +20,57 @@ import cats._ import cats.implicits._ import gql._ -trait DirectiveAlg[F[_], G[_], C] { - def parseArg[P[x] <: Position[G, x], A](p: P[A], args: Option[QueryAst.Arguments[C, AnyValue]], context: List[C]): F[A] +class DirectiveAlg[F[_], C]( + positions: Map[String, List[Position[F, ?]]], + ap: ArgParsing[C] +) { + type G[A] = Alg[C, A] + val G = Alg.Ops[C] + + def parseArg[P[x] <: Position[F, x], A](p: P[A], args: Option[QueryAst.Arguments[C, AnyValue]], context: List[C]): G[A] = { + p.directive.arg match { + case EmptyableArg.Empty => + args match { + case Some(_) => G.raise(s"Directive '${p.directive.name}' does not expect arguments", context) + case None => G.unit + } + case EmptyableArg.Lift(a) => + val argFields = args.toList.flatMap(_.nel.toList).map(a => a.name -> a.value.map(List(_))).toMap + ap.decodeArg(a, argFields, ambigiousEnum = false, context) + } + } + + def foldDirectives[P[x] <: Position[F, x]]: DirectiveAlg.PartiallyAppliedFold[F, C, P] = + new DirectiveAlg.PartiallyAppliedFold[F, C, P] { + override def apply[H[_]: Traverse, A](directives: Option[QueryAst.Directives[C, AnyValue]], context: List[C])(base: A)( + f: PartialFunction[(A, P[Any], QueryAst.Directive[C, AnyValue]), Alg[C, H[A]]] + )(implicit H: Monad[H]): Alg[C, H[A]] = { + def foldNext(rest: List[QueryAst.Directive[C, AnyValue]], accum: A): Alg[C, H[A]] = + rest match { + case Nil => G.pure(H.pure(accum)) + case x :: xs => + val name = x.name + positions.get(name) match { + case None => G.raise(s"Couldn't find directive '$name'", context) + case Some(d) => + val faOpt = d.map(p => (accum, p, x)).collectFirst { case f(fa) => fa } + G.raiseOpt(faOpt, s"Directive '$name' cannot appear here", context) + .flatten + .flatMap(_.flatTraverse(a => foldNext(xs, a))) + } + } + + foldNext(directives.map(_.nel.toList).getOrElse(Nil), base) + } + } - def foldDirectives[P[x] <: Position[G, x]]: DirectiveAlg.PartiallyAppliedFold[F, G, C, P] } object DirectiveAlg { - trait PartiallyAppliedFold[F[_], G[_], C, P[x] <: Position[G, x]] { + trait PartiallyAppliedFold[F[_], C, P[x] <: Position[F, x]] { def apply[H[_]: Traverse, A]( directives: Option[QueryAst.Directives[C, AnyValue]], context: List[C] - )(base: A)(f: PartialFunction[(A, P[Any], QueryAst.Directive[C, AnyValue]), F[H[A]]])(implicit H: Monad[H]): F[H[A]] - } - - def forPositions[F[_], G[_], C]( - positions: Map[String, List[Position[G, ?]]] - )(implicit - EA: ErrorAlg[F, C], - AP: ArgParsing[F, C], - F: Monad[F] - ): DirectiveAlg[F, G, C] = { - import EA._ - new DirectiveAlg[F, G, C] { - override def parseArg[P[x] <: Position[G, x], A](p: P[A], args: Option[QueryAst.Arguments[C, AnyValue]], context: List[C]): F[A] = { - p.directive.arg match { - case EmptyableArg.Empty => - args match { - case Some(_) => raise(s"Directive '${p.directive.name}' does not expect arguments", context) - case None => F.unit - } - case EmptyableArg.Lift(a) => - val argFields = args.toList.flatMap(_.nel.toList).map(a => a.name -> a.value.map(List(_))).toMap - AP.decodeArg(a, argFields, ambigiousEnum = false, context) - } - } - - override def foldDirectives[P[x] <: Position[G, x]]: PartiallyAppliedFold[F, G, C, P] = - new PartiallyAppliedFold[F, G, C, P] { - override def apply[H[_]: Traverse, A](directives: Option[QueryAst.Directives[C, AnyValue]], context: List[C])(base: A)( - f: PartialFunction[(A, P[Any], QueryAst.Directive[C, AnyValue]), F[H[A]]] - )(implicit H: Monad[H]): F[H[A]] = { - def foldNext(rest: List[QueryAst.Directive[C, AnyValue]], accum: A): F[H[A]] = - rest match { - case Nil => F.pure(H.pure(accum)) - case x :: xs => - val name = x.name - positions.get(name) match { - case None => raise(s"Couldn't find directive '$name'", context) - case Some(d) => - val faOpt = d.map(p => (accum, p, x)).collectFirst { case f(fa) => fa } - raiseOpt(faOpt, s"Directive '$name' cannot appear here", context).flatten - .flatMap(_.flatTraverse(a => foldNext(xs, a))) - } - } - - foldNext(directives.map(_.nel.toList).getOrElse(Nil), base) - } - } - } + )(base: A)(f: PartialFunction[(A, P[Any], QueryAst.Directive[C, AnyValue]), Alg[C, H[A]]])(implicit H: Monad[H]): Alg[C, H[A]] } } diff --git a/modules/core/src/main/scala/gql/preparation/ErrorAlg.scala b/modules/core/src/main/scala/gql/preparation/ErrorAlg.scala deleted file mode 100644 index 24ecca5cd..000000000 --- a/modules/core/src/main/scala/gql/preparation/ErrorAlg.scala +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright 2023 Valdemar Grange - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package gql.preparation - -import cats._ -import cats.implicits._ -import cats.mtl._ -import gql.Cursor - -trait ErrorAlg[F[_], C] { - def raise[A](message: String, carets: List[C]): F[A] - - def raiseEither[A](e: Either[String, A], carets: List[C]): F[A] - - def raiseOpt[A](oa: Option[A], message: String, carets: List[C]): F[A] = - raiseEither(oa.toRight(message), carets) - - def modifyError[A](f: PositionalError[C] => PositionalError[C])(fa: F[A]): F[A] - - def appendMessage[A](message: String)(fa: F[A]): F[A] = - modifyError[A](d => d.copy(message = d.message + "\n" + message))(fa) -} - -object ErrorAlg { - def apply[F[_], C](implicit ev: ErrorAlg[F, C]): ErrorAlg[F, C] = ev - - def errorAlgForHandle[F[_]: Monad, G[_]: Applicative, C](implicit - H: Handle[F, G[PositionalError[C]]], - L: Local[F, Cursor] - ): ErrorAlg[F, C] = new ErrorAlg[F, C] { - override def raise[A](message: String, carets: List[C]): F[A] = - L.ask.flatMap(c => H.raise(Applicative[G].pure(PositionalError(c, carets, message)))) - - override def raiseEither[A](e: Either[String, A], carets: List[C]): F[A] = - e match { - case Left(value) => raise(value, carets) - case Right(value) => Monad[F].pure(value) - } - - override def modifyError[A](f: PositionalError[C] => PositionalError[C])(fa: F[A]): F[A] = - H.handleWith(fa)(xs => H.raise(xs.map(f))) - } -} diff --git a/modules/core/src/main/scala/gql/preparation/FieldCollection.scala b/modules/core/src/main/scala/gql/preparation/FieldCollection.scala index ee39dad01..ea1698217 100644 --- a/modules/core/src/main/scala/gql/preparation/FieldCollection.scala +++ b/modules/core/src/main/scala/gql/preparation/FieldCollection.scala @@ -18,198 +18,177 @@ package gql.preparation import cats._ import cats.data._ import cats.implicits._ -import cats.mtl._ import gql.Arg import gql.Cursor import gql.InverseModifierStack import gql.SchemaShape import gql.ast._ -import gql.parser.QueryAst import gql.parser.{QueryAst => QA} import gql.parser.AnyValue import gql.Position -trait FieldCollection[F[_], G[_], C] { +class FieldCollection[F[_], C]( + implementations: SchemaShape.Implementations[F], + fragments: Map[String, QA.FragmentDefinition[C]], + ap: ArgParsing[C], + da: DirectiveAlg[F, C] +) { + type G[A] = Alg[C, A] + val G = Alg.Ops[C] + + def inFragment[A]( + fragmentName: String, + carets: List[C] + )(faf: QA.FragmentDefinition[C] => G[A]): G[A] = + G.cycleAsk + .map(_.contains(fragmentName)) + .ifM( + G.raise(s"Fragment by '$fragmentName' is cyclic. Hint: graphql queries must be finite.", carets), + fragments.get(fragmentName) match { + case None => G.raise(s"Unknown fragment name '$fragmentName'.", carets) + case Some(f) => G.cycleOver(fragmentName, faf(f)) + } + ) + def matchType( name: String, - sel: Selectable[G, ?], + sel: Selectable[F, ?], caret: C - ): F[Selectable[G, ?]] + ): Alg[C, Selectable[F, ?]] = { + if (sel.name == name) G.pure(sel) + else { + sel match { + case t: Type[F, ?] => + // Check downcast + t.implementsMap.get(name) match { + case None => + G.raise(s"Tried to match with type `$name` on type object type `${sel.name}`.", List(caret)) + case Some(i) => G.pure(i.value) + } + case i: Interface[F, ?] => + // What types implement this interface? + // We can both downcast and up-match + i.implementsMap.get(name) match { + case Some(i) => G.pure(i.value) + case None => + G.raiseOpt( + implementations.get(i.name), + s"The interface `${i.name}` is not implemented by any type.", + List(caret) + ).flatMap { m => + G.raiseOpt( + m.get(name).map { + case t: SchemaShape.InterfaceImpl.TypeImpl[F @unchecked, ?, ?] => t.t + case i: SchemaShape.InterfaceImpl.OtherInterface[F @unchecked, ?] => i.i + }, + s"`$name` does not implement interface `${i.name}`, possible implementations are ${m.keySet.mkString(", ")}.", + List(caret) + ) + } + } + case u: Union[F, ?] => + // Can match to any type or any of it's types' interfacees + u.instanceMap.get(name) match { + case Some(i) => G.pure(i.tpe.value) + case None => + G.raiseOpt( + u.types.toList.map(_.tpe.value).collectFirstSome(_.implementsMap.get(name)), + s"`$name` is not a member of the union `${u.name}` (or any of the union's types' implemented interfaces), possible members are ${u.instanceMap.keySet + .mkString(", ")}.", + List(caret) + ).map(_.value) + } + } + } + } def collectSelectionInfo( - sel: Selectable[G, ?], + sel: Selectable[F, ?], ss: QA.SelectionSet[C] - ): F[List[SelectionInfo[G, C]]] - - def collectFieldInfo( - qf: AbstractField[G, ?], - f: QA.Field[C], - caret: C - ): F[FieldInfo[G, C]] -} - -object FieldCollection { - type CycleSet = Set[String] - - def apply[F[_]: Parallel, G[_], C]( - implementations: SchemaShape.Implementations[G], - fragments: Map[String, QA.FragmentDefinition[C]] - )(implicit - F: Monad[F], - E: ErrorAlg[F, C], - L: Local[F, CycleSet], - C: Local[F, Cursor], - A: ArgParsing[F, C], - DA: DirectiveAlg[F, G, C] - ) = { - implicit val PA: PathAlg[F] = PathAlg.pathAlgForLocal[F] - import E._ - import PA._ - - new FieldCollection[F, G, C] { - def inFragment[A]( - fragmentName: String, - carets: List[C] - )(faf: QA.FragmentDefinition[C] => F[A]): F[A] = - L.ask[CycleSet] - .map(_.contains(fragmentName)) - .ifM( - raise(s"Fragment by '$fragmentName' is cyclic. Hint: graphql queries must be finite.", carets), - fragments.get(fragmentName) match { - case None => raise(s"Unknown fragment name '$fragmentName'.", carets) - case Some(f) => L.local(faf(f))(_ + fragmentName) - } - ) - - override def matchType(name: String, sel: Selectable[G, ?], caret: C): F[Selectable[G, ?]] = { - if (sel.name == name) F.pure(sel) - else { - sel match { - case t: Type[G, ?] => - // Check downcast - t.implementsMap.get(name) match { - case None => - raise(s"Tried to match with type `$name` on type object type `${sel.name}`.", List(caret)) - case Some(i) => F.pure(i.value) - } - case i: Interface[G, ?] => - // What types implement this interface? - // We can both downcast and up-match - i.implementsMap.get(name) match { - case Some(i) => F.pure(i.value) - case None => - raiseOpt( - implementations.get(i.name), - s"The interface `${i.name}` is not implemented by any type.", - List(caret) - ).flatMap { m => - raiseOpt( - m.get(name).map { - case t: SchemaShape.InterfaceImpl.TypeImpl[G @unchecked, ?, ?] => t.t - case i: SchemaShape.InterfaceImpl.OtherInterface[G @unchecked, ?] => i.i - }, - s"`$name` does not implement interface `${i.name}`, possible implementations are ${m.keySet.mkString(", ")}.", - List(caret) - ) - } - } - case u: Union[G, ?] => - // Can match to any type or any of it's types' interfacees - u.instanceMap.get(name) match { - case Some(i) => F.pure(i.tpe.value) - case None => - raiseOpt( - u.types.toList.map(_.tpe.value).collectFirstSome(_.implementsMap.get(name)), - s"`$name` is not a member of the union `${u.name}` (or any of the union's types' implemented interfaces), possible members are ${u.instanceMap.keySet - .mkString(", ")}.", - List(caret) - ).map(_.value) - } - } + ): Alg[C, List[SelectionInfo[F, C]]] = { + val all = ss.selections + val fields = all.collect { case QA.Selection.FieldSelection(field, c) => (c, field) } + + val actualFields = + sel.abstractFieldMap + ("__typename" -> AbstractField(None, Eval.now(stringScalar), None)) + + val validateFieldsF = fields + .parTraverse { case (caret, field) => + actualFields.get(field.name) match { + case None => G.raise[FieldInfo[F, C]](s"Field '${field.name}' is not a member of `${sel.name}`.", List(caret)) + case Some(f) => G.ambientField(field.name)(collectFieldInfo(f, field, caret)) } } - - override def collectSelectionInfo(sel: Selectable[G, ?], ss: QueryAst.SelectionSet[C]): F[List[SelectionInfo[G, C]]] = { - val all = ss.selections - val fields = all.collect { case QA.Selection.FieldSelection(field, c) => (c, field) } - - val actualFields = - sel.abstractFieldMap + ("__typename" -> AbstractField(None, Eval.now(stringScalar), None)) - - val validateFieldsF = fields - .parTraverse { case (caret, field) => - actualFields.get(field.name) match { - case None => raise[FieldInfo[G, C]](s"Field '${field.name}' is not a member of `${sel.name}`.", List(caret)) - case Some(f) => ambientField(field.name)(collectFieldInfo(f, field, caret)) - } + .map(_.toNel.toList.map(SelectionInfo(sel, _, None))) + + val realInlines = + all + .collect { case QA.Selection.InlineFragmentSelection(f, c) => (c, f) } + .parFlatTraverse { case (caret, f) => + da.foldDirectives[Position.InlineFragmentSpread](f.directives, List(caret))(f) { + case (f, p: Position.InlineFragmentSpread[a], d) => + da.parseArg(p, d.arguments, List(caret)).map(p.handler(_, f)).flatMap(G.raiseEither(_, List(caret))) + }.map(_ tupleLeft caret) + } + .flatMap(_.parFlatTraverse { case (caret, f) => + f.typeCondition.traverse(matchType(_, sel, caret)).map(_.getOrElse(sel)).flatMap { t => + collectSelectionInfo(t, f.selectionSet).map(_.toList) } - .map(_.toNel.toList.map(SelectionInfo(sel, _, None))) - - val realInlines = - all - .collect { case QA.Selection.InlineFragmentSelection(f, c) => (c, f) } - .parFlatTraverse { case (caret, f) => - DA.foldDirectives[Position.InlineFragmentSpread](f.directives, List(caret))(f) { - case (f, p: Position.InlineFragmentSpread[a], d) => - DA.parseArg(p, d.arguments, List(caret)).map(p.handler(_, f)).flatMap(raiseEither(_, List(caret))) - }.map(_ tupleLeft caret) - } - .flatMap(_.parFlatTraverse { case (caret, f) => - f.typeCondition.traverse(matchType(_, sel, caret)).map(_.getOrElse(sel)).flatMap { t => - collectSelectionInfo(t, f.selectionSet).map(_.toList) - } - }) - - val realFragments = all - .collect { case QA.Selection.FragmentSpreadSelection(f, c) => (c, f) } - .parFlatTraverse { case (caret, f) => - DA.foldDirectives[Position.FragmentSpread](f.directives, List(caret))(f) { case (f, p: Position.FragmentSpread[a], d) => - DA.parseArg(p, d.arguments, List(caret)).map(p.handler(_, f)).flatMap(raiseEither(_, List(caret))) - }.map(_ tupleLeft caret) + }) + + val realFragments = all + .collect { case QA.Selection.FragmentSpreadSelection(f, c) => (c, f) } + .parFlatTraverse { case (caret, f) => + da.foldDirectives[Position.FragmentSpread](f.directives, List(caret))(f) { case (f, p: Position.FragmentSpread[a], d) => + da.parseArg(p, d.arguments, List(caret)).map(p.handler(_, f)).flatMap(G.raiseEither(_, List(caret))) + }.map(_ tupleLeft caret) + } + .flatMap(_.parFlatTraverse { case (caret, f) => + val fn = f.fragmentName + inFragment(fn, List(caret)) { f => + matchType(f.typeCnd, sel, f.caret).flatMap { t => + collectSelectionInfo(t, f.selectionSet) + .map(_.toList.map(_.copy(fragmentName = Some(fn)))) } - .flatMap(_.parFlatTraverse { case (caret, f) => - val fn = f.fragmentName - inFragment(fn, List(caret)) { f => - matchType(f.typeCnd, sel, f.caret).flatMap { t => - collectSelectionInfo(t, f.selectionSet) - .map(_.toList.map(_.copy(fragmentName = Some(fn)))) - } - } - }) + } + }) - (validateFieldsF :: realInlines :: realFragments :: Nil).parFlatSequence - } + (validateFieldsF :: realInlines :: realFragments :: Nil).parFlatSequence + } - override def collectFieldInfo(qf: AbstractField[G, _], f: QueryAst.Field[C], caret: C): F[FieldInfo[G, C]] = { - val fields = f.arguments.toList.flatMap(_.nel.toList).map(x => x.name -> x.value).toMap - val verifyArgsF = qf.arg.parTraverse_ { case a: Arg[a] => - A.decodeArg[a](a, fields.fmap(_.map(List(_))), ambigiousEnum = false, context = List(caret)).void - } + def collectFieldInfo( + qf: AbstractField[F, ?], + f: QA.Field[C], + caret: C + ): Alg[C, FieldInfo[F, C]] = { + val fields = f.arguments.toList.flatMap(_.nel.toList).map(x => x.name -> x.value).toMap + val verifyArgsF = qf.arg.parTraverse_ { case a: Arg[a] => + ap.decodeArg[a](a, fields.fmap(_.map(List(_))), ambigiousEnum = false, context = List(caret)).void + } - val c = f.caret - val x = f.selectionSet - val ims = InverseModifierStack.fromOut(qf.output.value) - val tl = ims.inner - val i: F[TypeInfo[G, C]] = tl match { - case s: Selectable[G, ?] => - raiseOpt( - x, - s"Field `${f.name}` of type `${tl.name}` must have a selection set.", - List(c) - ).flatMap(ss => collectSelectionInfo(s, ss)).map(TypeInfo.Selectable(tl.name, _)) - case _: Enum[?] => - if (x.isEmpty) F.pure(TypeInfo.Enum(tl.name)) - else raise(s"Field `${f.name}` of enum type `${tl.name}` must not have a selection set.", List(c)) - case _: Scalar[?] => - if (x.isEmpty) - F.pure(TypeInfo.Scalar(tl.name)) - else raise(s"Field `${f.name}` of scalar type `${tl.name}` must not have a selection set.", List(c)) - } + val c = f.caret + val x = f.selectionSet + val ims = InverseModifierStack.fromOut(qf.output.value) + val tl = ims.inner + val i: G[TypeInfo[F, C]] = tl match { + case s: Selectable[F, ?] => + G.raiseOpt( + x, + s"Field `${f.name}` of type `${tl.name}` must have a selection set.", + List(c) + ).flatMap(ss => collectSelectionInfo(s, ss)) + .map(TypeInfo.Selectable(tl.name, _)) + case _: Enum[?] => + if (x.isEmpty) G.pure(TypeInfo.Enum(tl.name)) + else G.raise(s"Field `${f.name}` of enum type `${tl.name}` must not have a selection set.", List(c)) + case _: Scalar[?] => + if (x.isEmpty) + G.pure(TypeInfo.Scalar(tl.name)) + else G.raise(s"Field `${f.name}` of scalar type `${tl.name}` must not have a selection set.", List(c)) + } - verifyArgsF &> i.flatMap { fi => - C.ask.map(c => FieldInfo[G, C](f.name, f.alias, f.arguments, ims.copy(inner = fi), f.directives, caret, c)) - } - } + verifyArgsF &> i.flatMap { fi => + G.cursorAsk.map(c => FieldInfo[F, C](f.name, f.alias, f.arguments, ims.copy(inner = fi), f.directives, caret, c)) } } } diff --git a/modules/core/src/main/scala/gql/preparation/FieldMerging.scala b/modules/core/src/main/scala/gql/preparation/FieldMerging.scala index 008436eb4..5ee9b7989 100644 --- a/modules/core/src/main/scala/gql/preparation/FieldMerging.scala +++ b/modules/core/src/main/scala/gql/preparation/FieldMerging.scala @@ -15,249 +15,215 @@ */ package gql.preparation -import cats._ import cats.data._ import cats.implicits._ -import cats.mtl._ -import gql.Cursor import gql.InverseModifierStack import gql.ast._ import gql.parser.AnyValue -import gql.parser.QueryAst import gql.parser.{QueryAst => QA} import gql.parser.{Value => V} -trait FieldMerging[F[_], C] { - def checkSelectionsMerge[G[_]](xs: NonEmptyList[SelectionInfo[G, C]]): F[Unit] - - def checkFieldsMerge[G[_]]( - a: FieldInfo[G, C], - asi: SelectionInfo[G, C], - b: FieldInfo[G, C], - bsi: SelectionInfo[G, C] - ): F[Unit] +class FieldMerging[C] { + type G[A] = Alg[C, A] + val G = Alg.Ops[C] + + def checkSelectionsMerge[F[_]](xs: NonEmptyList[SelectionInfo[F, C]]): G[Unit] = { + val ys: NonEmptyList[NonEmptyList[(SelectionInfo[F, C], FieldInfo[F, C])]] = + xs.flatMap(si => si.fields tupleLeft si) + .groupByNem { case (_, f) => f.outputName } + .toNel + .map { case (_, v) => v } + + ys.parTraverse_ { zs => + // TODO partition into what should be fullchecked and what should be structural + val mergeFieldsF = { + val (siHead, fiHead) = zs.head + zs.tail.parTraverse_ { case (si, fi) => checkFieldsMerge(fiHead, siHead, fi, si) } + } - // These technically don't need to be in the trait, but it's convenient because of error handling - // If needed, they can always be moved - def compareArguments(name: String, aa: QA.Arguments[C, AnyValue], ba: QA.Arguments[C, AnyValue], caret: Option[C]): F[Unit] + mergeFieldsF >> + zs.toList + .map { case (_, fi) => fi.tpe.inner } + .collect { case s: TypeInfo.Selectable[F, C] => s.selection.toList } + .flatten + .toNel + .traverse_(checkSelectionsMerge) + } + } - def compareValues(av: V[AnyValue, C], bv: V[AnyValue, C], caret: Option[C]): F[Unit] -} + // Optimization: we don't check selections recursively since checkSelectionsMerge traverses the whole tree + // We only need to check the immidiate children and will eventually have checked the whole tree + def checkSimplifiedTypeShape[F[_]]( + a: InverseModifierStack[TypeInfo[F, C]], + b: InverseModifierStack[TypeInfo[F, C]], + caret: C + ): G[Unit] = { + (a.inner, b.inner) match { + // It turns out we don't care if more fields are selected in one object than the other + case (TypeInfo.Selectable(_, _), TypeInfo.Selectable(_, _)) => G.unit + // case (SimplifiedType.Selectable(_, l), SimplifiedType.Selectable(_, r)) => F.unit + // val lComb = l.flatMap(x => x.fields tupleLeft x).groupByNem { case (_, f) => f.outputName } + // val rComb = r.flatMap(x => x.fields tupleLeft x).groupByNem { case (_, f) => f.outputName } + // (lComb align rComb).toNel.parTraverse_ { + // case (_, Ior.Both(_, _)) => F.unit + // case (k, Ior.Left(_)) => raise(s"Field '$k' was missing when verifying shape equivalence.", Some(caret)) + // case (k, Ior.Right(_)) => raise(s"Field '$k' was missing when verifying shape equivalence.", Some(caret)) + // } + case (TypeInfo.Enum(l), TypeInfo.Enum(r)) => + if (l === r) G.unit + else G.raise(s"Enums are not the same, got '$l' and '$r'.", List(caret)) + case (TypeInfo.Scalar(l), TypeInfo.Scalar(r)) => + if (l === r) G.unit + else G.raise(s"Scalars are not the same, got '$l' and '$r'.", List(caret)) + case _ => + G.raise(s"Types are not the same, got `${a.invert.show(_.name)}` and `${b.invert.show(_.name)}`.", List(caret)) + } + } -object FieldMerging { - def apply[F[_]: Parallel, C](implicit - F: Monad[F], - L: Local[F, Cursor], - H: Handle[F, NonEmptyChain[PositionalError[C]]] - ): FieldMerging[F, C] = { - val E = ErrorAlg.errorAlgForHandle[F, NonEmptyChain, C] - val P = PathAlg[F] - import E._ - import P._ + def checkFieldsMerge[F[_]]( + a: FieldInfo[F, C], + asi: SelectionInfo[F, C], + b: FieldInfo[F, C], + bsi: SelectionInfo[F, C] + ): G[Unit] = { + sealed trait EitherObject + object EitherObject { + case object FirstIsObject extends EitherObject + case object SecondIsObject extends EitherObject + case object NeitherIsObject extends EitherObject + case object BothAreObjects extends EitherObject + } + lazy val objectPair = (asi.s, bsi.s) match { + case (_: Type[F, ?], _: Type[F, ?]) => EitherObject.BothAreObjects + case (_: Type[F, ?], _) => EitherObject.FirstIsObject + case (_, _: Type[F, ?]) => EitherObject.SecondIsObject + case _ => EitherObject.NeitherIsObject + } - new FieldMerging[F, C] { - override def checkSelectionsMerge[G[_]](xs: NonEmptyList[SelectionInfo[G, C]]): F[Unit] = { - val ys: NonEmptyList[NonEmptyList[(SelectionInfo[G, C], FieldInfo[G, C])]] = - xs.flatMap(si => si.fields tupleLeft si) - .groupByNem { case (_, f) => f.outputName } - .toNel - .map { case (_, v) => v } + val parentNameSame = asi.s.name === bsi.s.name - ys.parTraverse_ { zs => - // TODO partition into what should be fullchecked and what should be structural - val mergeFieldsF = { - val (siHead, fiHead) = zs.head - zs.tail.parTraverse_ { case (si, fi) => checkFieldsMerge(fiHead, siHead, fi, si) } - } + lazy val aIn = s"${fieldName(a)} in type `${asi.s.name}`" + lazy val bIn = s"${fieldName(b)} in type `${bsi.s.name}`" - mergeFieldsF >> - zs.toList - .map { case (_, fi) => fi.tpe.inner } - .collect { case s: TypeInfo.Selectable[G, C] => s.selection.toList } - .flatten - .toNel - .traverse_(checkSelectionsMerge) - } + lazy val whyMerge = { + val why1 = if (parentNameSame) Some("they have the same parent type") else None + val why2 = objectPair match { + case EitherObject.FirstIsObject => Some(s"the second field ${fieldName(a)} is not an object but the first was") + case EitherObject.SecondIsObject => Some(s"the first field ${fieldName(b)} is not an object but the second was") + case EitherObject.NeitherIsObject => Some(s"neither field ${fieldName(a)} nor ${fieldName(b)} are objects") + case EitherObject.BothAreObjects => None } + List(why1, why2).collect { case Some(err) => err }.mkString(" and ") + "." + } - // Optimization: we don't check selections recursively since checkSelectionsMerge traverses the whole tree - // We only need to check the immidiate children and will eventually have checked the whole tree - def checkSimplifiedTypeShape[G[_]]( - a: InverseModifierStack[TypeInfo[G, C]], - b: InverseModifierStack[TypeInfo[G, C]], - caret: C - ): F[Unit] = { - (a.inner, b.inner) match { - // It turns out we don't care if more fields are selected in one object than the other - case (TypeInfo.Selectable(_, _), TypeInfo.Selectable(_, _)) => F.unit - // case (SimplifiedType.Selectable(_, l), SimplifiedType.Selectable(_, r)) => F.unit - // val lComb = l.flatMap(x => x.fields tupleLeft x).groupByNem { case (_, f) => f.outputName } - // val rComb = r.flatMap(x => x.fields tupleLeft x).groupByNem { case (_, f) => f.outputName } - // (lComb align rComb).toNel.parTraverse_ { - // case (_, Ior.Both(_, _)) => F.unit - // case (k, Ior.Left(_)) => raise(s"Field '$k' was missing when verifying shape equivalence.", Some(caret)) - // case (k, Ior.Right(_)) => raise(s"Field '$k' was missing when verifying shape equivalence.", Some(caret)) - // } - case (TypeInfo.Enum(l), TypeInfo.Enum(r)) => - if (l === r) F.unit - else raise(s"Enums are not the same, got '$l' and '$r'.", List(caret)) - case (TypeInfo.Scalar(l), TypeInfo.Scalar(r)) => - if (l === r) F.unit - else raise(s"Scalars are not the same, got '$l' and '$r'.", List(caret)) - case _ => - raise(s"Types are not the same, got `${a.invert.show(_.name)}` and `${b.invert.show(_.name)}`.", List(caret)) - } + // 2. in FieldsInSetCanMerge + val thoroughCheckF = if (parentNameSame || objectPair != EitherObject.BothAreObjects) { + val argsF = (a.args, b.args) match { + case (None, None) => G.unit + case (Some(_), None) => G.raise(s"A selection of field ${fieldName(a)} has arguments, while another doesn't.", List(b.caret)) + case (None, Some(_)) => G.raise(s"A selection of field ${fieldName(a)} has arguments, while another doesn't.", List(b.caret)) + case (Some(aa), Some(ba)) => compareArguments(fieldName(a), aa, ba, Some(b.caret)) } - override def checkFieldsMerge[G[_]]( - a: FieldInfo[G, C], - asi: SelectionInfo[G, C], - b: FieldInfo[G, C], - bsi: SelectionInfo[G, C] - ): F[Unit] = { - sealed trait EitherObject - object EitherObject { - case object FirstIsObject extends EitherObject - case object SecondIsObject extends EitherObject - case object NeitherIsObject extends EitherObject - case object BothAreObjects extends EitherObject + val nameSameF = + if (a.name === b.name) G.unit + else { + G.raise( + s"Field $aIn and $bIn must have the same name (not alias) when they are merged.", + List(a.caret) + ) } - lazy val objectPair = (asi.s, bsi.s) match { - case (_: Type[G, ?], _: Type[G, ?]) => EitherObject.BothAreObjects - case (_: Type[G, ?], _) => EitherObject.FirstIsObject - case (_, _: Type[G, ?]) => EitherObject.SecondIsObject - case _ => EitherObject.NeitherIsObject - } - - val parentNameSame = asi.s.name === bsi.s.name - - lazy val aIn = s"${fieldName(a)} in type `${asi.s.name}`" - lazy val bIn = s"${fieldName(b)} in type `${bsi.s.name}`" - lazy val whyMerge = { - val why1 = if (parentNameSame) Some("they have the same parent type") else None - val why2 = objectPair match { - case EitherObject.FirstIsObject => Some(s"the second field ${fieldName(a)} is not an object but the first was") - case EitherObject.SecondIsObject => Some(s"the first field ${fieldName(b)} is not an object but the second was") - case EitherObject.NeitherIsObject => Some(s"neither field ${fieldName(a)} nor ${fieldName(b)} are objects") - case EitherObject.BothAreObjects => None - } - List(why1, why2).collect { case Some(err) => err }.mkString(" and ") + "." - } - - // 2. in FieldsInSetCanMerge - val thoroughCheckF = if (parentNameSame || objectPair != EitherObject.BothAreObjects) { - val argsF = (a.args, b.args) match { - case (None, None) => F.unit - case (Some(_), None) => raise(s"A selection of field ${fieldName(a)} has arguments, while another doesn't.", List(b.caret)) - case (None, Some(_)) => raise(s"A selection of field ${fieldName(a)} has arguments, while another doesn't.", List(b.caret)) - case (Some(aa), Some(ba)) => compareArguments(fieldName(a), aa, ba, Some(b.caret)) - } - - val nameSameF = - if (a.name === b.name) F.unit - else { - raise( - s"Field $aIn and $bIn must have the same name (not alias) when they are merged.", - List(a.caret) - ) - } - - appendMessage(s"They were merged since $whyMerge") { - argsF &> nameSameF - } - } else F.unit + G.appendMessage(s"They were merged since $whyMerge") { + argsF &> nameSameF + } + } else G.unit - // 1. in FieldsInSetCanMerge - val shapeCheckF = checkSimplifiedTypeShape(a.tpe, b.tpe, a.caret) + // 1. in FieldsInSetCanMerge + val shapeCheckF = checkSimplifiedTypeShape(a.tpe, b.tpe, a.caret) - thoroughCheckF &> shapeCheckF + thoroughCheckF &> shapeCheckF - } - - override def compareArguments( - name: String, - aa: QueryAst.Arguments[C, AnyValue], - ba: QueryAst.Arguments[C, AnyValue], - caret: Option[C] - ): F[Unit] = { - def checkUniqueness(x: QA.Arguments[C, AnyValue]): F[Map[String, QA.Argument[C, AnyValue]]] = - x.nel.toList - .groupBy(_.name) - .toList - .parTraverse { - case (k, v :: Nil) => F.pure(k -> v) - case (k, _) => - raise[(String, QA.Argument[C, AnyValue])](s"Argument '$k' of field $name was not unique.", caret.toList) - } - .map(_.toMap) + } - (checkUniqueness(aa), checkUniqueness(ba)).parTupled.flatMap { case (amap, bmap) => - (amap align bmap).toList.parTraverse_[F, Unit] { - case (k, Ior.Left(_)) => - raise(s"Field $name is already selected with argument '$k', but no argument was given here.", caret.toList) - case (k, Ior.Right(_)) => - raise(s"Field $name is already selected without argument '$k', but an argument was given here.", caret.toList) - case (k, Ior.Both(l, r)) => ambientField(k)(compareValues(l.value, r.value, caret)) - } + // These technically don't need to be in the trait, but it's convenient because of error handling + // If needed, they can always be moved + def compareArguments(name: String, aa: QA.Arguments[C, AnyValue], ba: QA.Arguments[C, AnyValue], caret: Option[C]): G[Unit] = { + def checkUniqueness(x: QA.Arguments[C, AnyValue]): G[Map[String, QA.Argument[C, AnyValue]]] = + x.nel.toList + .groupBy(_.name) + .toList + .parTraverse { + case (k, v :: Nil) => G.pure(k -> v) + case (k, _) => + G.raise[(String, QA.Argument[C, AnyValue])](s"Argument '$k' of field $name was not unique.", caret.toList) } + .map(_.toMap) + + (checkUniqueness(aa), checkUniqueness(ba)).parTupled.flatMap { case (amap, bmap) => + (amap align bmap).toList.parTraverse_[G, Unit] { + case (k, Ior.Left(_)) => + G.raise(s"Field $name is already selected with argument '$k', but no argument was given here.", caret.toList) + case (k, Ior.Right(_)) => + G.raise(s"Field $name is already selected without argument '$k', but an argument was given here.", caret.toList) + case (k, Ior.Both(l, r)) => G.ambientField(k)(compareValues(l.value, r.value, caret)) } + } + } - override def compareValues(av: V[AnyValue, C], bv: V[AnyValue, C], caret: Option[C]): F[Unit] = { - val cs = av.c :: bv.c :: caret.toList - (av, bv) match { - case (V.VariableValue(avv, _), V.VariableValue(bvv, _)) => - if (avv === bvv) F.unit - else raise(s"Variable '$avv' and '$bvv' are not equal.", cs) - case (V.IntValue(ai, _), V.IntValue(bi, _)) => - if (ai === bi) F.unit - else raise(s"Int '$ai' and '$bi' are not equal.", cs) - case (V.FloatValue(af, _), V.FloatValue(bf, _)) => - if (af === bf) F.unit - else raise(s"Float '$af' and '$bf' are not equal.", cs) - case (V.StringValue(as, _), V.StringValue(bs, _)) => - if (as === bs) F.unit - else raise(s"String '$as' and '$bs' are not equal.", cs) - case (V.BooleanValue(ab, _), V.BooleanValue(bb, _)) => - if (ab === bb) F.unit - else raise(s"Boolean '$ab' and '$bb' are not equal.", cs) - case (V.EnumValue(ae, _), V.EnumValue(be, _)) => - if (ae === be) F.unit - else raise(s"Enum '$ae' and '$be' are not equal.", cs) - case (V.NullValue(_), V.NullValue(_)) => F.unit - case (V.ListValue(al, _), V.ListValue(bl, _)) => - if (al.length === bl.length) { - al.zip(bl).zipWithIndex.parTraverse_ { case ((a, b), i) => ambientIndex(i)(compareValues(a, b, caret)) } - } else - raise(s"Lists are not af same size. Found list of length ${al.length} versus list of length ${bl.length}.", cs) - case (V.ObjectValue(ao, _), V.ObjectValue(bo, _)) => - if (ao.size =!= bo.size) - raise( - s"Objects are not af same size. Found object of length ${ao.size} versus object of length ${bo.size}.", - cs - ) - else { - def checkUniqueness(xs: List[(String, V[AnyValue, C])]) = - xs.groupMap { case (k, _) => k } { case (_, v) => v } - .toList - .parTraverse { - case (k, v :: Nil) => F.pure(k -> v) - case (k, _) => raise[(String, V[AnyValue, C])](s"Key '$k' is not unique in object.", cs) - } - .map(_.toMap) - - (checkUniqueness(ao), checkUniqueness(bo)).parTupled.flatMap { case (amap, bmap) => - // TODO test that verifies that order does not matter - (amap align bmap).toList.parTraverse_[F, Unit] { - case (k, Ior.Left(_)) => raise(s"Key '$k' is missing in object.", cs) - case (k, Ior.Right(_)) => raise(s"Key '$k' is missing in object.", cs) - case (k, Ior.Both(l, r)) => ambientField(k)(compareValues(l, r, caret)) - } + def compareValues(av: V[AnyValue, C], bv: V[AnyValue, C], caret: Option[C]): G[Unit] = { + val cs = av.c :: bv.c :: caret.toList + (av, bv) match { + case (V.VariableValue(avv, _), V.VariableValue(bvv, _)) => + if (avv === bvv) G.unit + else G.raise(s"Variable '$avv' and '$bvv' are not equal.", cs) + case (V.IntValue(ai, _), V.IntValue(bi, _)) => + if (ai === bi) G.unit + else G.raise(s"Int '$ai' and '$bi' are not equal.", cs) + case (V.FloatValue(af, _), V.FloatValue(bf, _)) => + if (af === bf) G.unit + else G.raise(s"Float '$af' and '$bf' are not equal.", cs) + case (V.StringValue(as, _), V.StringValue(bs, _)) => + if (as === bs) G.unit + else G.raise(s"String '$as' and '$bs' are not equal.", cs) + case (V.BooleanValue(ab, _), V.BooleanValue(bb, _)) => + if (ab === bb) G.unit + else G.raise(s"Boolean '$ab' and '$bb' are not equal.", cs) + case (V.EnumValue(ae, _), V.EnumValue(be, _)) => + if (ae === be) G.unit + else G.raise(s"Enum '$ae' and '$be' are not equal.", cs) + case (V.NullValue(_), V.NullValue(_)) => G.unit + case (V.ListValue(al, _), V.ListValue(bl, _)) => + if (al.length === bl.length) { + al.zip(bl).zipWithIndex.parTraverse_ { case ((a, b), i) => G.ambientIndex(i)(compareValues(a, b, caret)) } + } else + G.raise(s"Lists are not af same size. Found list of length ${al.length} versus list of length ${bl.length}.", cs) + case (V.ObjectValue(ao, _), V.ObjectValue(bo, _)) => + if (ao.size =!= bo.size) + G.raise( + s"Objects are not af same size. Found object of length ${ao.size} versus object of length ${bo.size}.", + cs + ) + else { + def checkUniqueness(xs: List[(String, V[AnyValue, C])]) = + xs.groupMap { case (k, _) => k } { case (_, v) => v } + .toList + .parTraverse { + case (k, v :: Nil) => G.pure(k -> v) + case (k, _) => G.raise[(String, V[AnyValue, C])](s"Key '$k' is not unique in object.", cs) } + .map(_.toMap) + + (checkUniqueness(ao), checkUniqueness(bo)).parTupled.flatMap { case (amap, bmap) => + // TODO test that verifies that order does not matter + (amap align bmap).toList.parTraverse_[G, Unit] { + case (k, Ior.Left(_)) => G.raise(s"Key '$k' is missing in object.", cs) + case (k, Ior.Right(_)) => G.raise(s"Key '$k' is missing in object.", cs) + case (k, Ior.Both(l, r)) => G.ambientField(k)(compareValues(l, r, caret)) } - case _ => raise(s"Values are not the same type, got ${pValueName(av)} and ${pValueName(bv)}.", cs) + } } - } + case _ => G.raise(s"Values are not the same type, got ${pValueName(av)} and ${pValueName(bv)}.", cs) } } } diff --git a/modules/core/src/main/scala/gql/preparation/PathAlg.scala b/modules/core/src/main/scala/gql/preparation/PathAlg.scala deleted file mode 100644 index 3338a04c4..000000000 --- a/modules/core/src/main/scala/gql/preparation/PathAlg.scala +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2023 Valdemar Grange - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package gql.preparation - -import gql.GraphArc -import cats.mtl.Local -import gql.Cursor - -trait PathAlg[F[_]] { - def ambientEdge[A](edge: GraphArc)(fa: F[A]): F[A] - - def ambientField[A](name: String)(fa: F[A]): F[A] = - ambientEdge(GraphArc.Field(name))(fa) - - def ambientIndex[A](i: Int)(fa: F[A]): F[A] = - ambientEdge(GraphArc.Index(i))(fa) -} - -object PathAlg { - def apply[F[_]](implicit ev: PathAlg[F]): PathAlg[F] = ev - - implicit def pathAlgForLocal[F[_]](implicit L: Local[F, Cursor]): PathAlg[F] = new PathAlg[F] { - override def ambientEdge[A](edge: GraphArc)(fa: F[A]): F[A] = L.local(fa)(_.add(edge)) - } -} diff --git a/modules/core/src/main/scala/gql/preparation/QueryPreparation.scala b/modules/core/src/main/scala/gql/preparation/QueryPreparation.scala index 416c1a52f..6b836bcc0 100644 --- a/modules/core/src/main/scala/gql/preparation/QueryPreparation.scala +++ b/modules/core/src/main/scala/gql/preparation/QueryPreparation.scala @@ -20,424 +20,319 @@ import gql.ast._ import gql.resolver._ import gql.parser.{QueryAst => QA, AnyValue} import cats._ -import cats.mtl._ import cats.data._ import cats.implicits._ import io.circe._ import gql.std.LazyT +import org.typelevel.scalaccompat.annotation._ -trait QueryPreparation[F[_], G[_], C] { - import QueryPreparation._ +class QueryPreparation[F[_], C]( + ap: ArgParsing[C], + da: DirectiveAlg[F, C], + variables: VariableMap[C], + implementations: SchemaShape.Implementations[F] +) { + type G[A] = Alg[C, A] + val G = Alg.Ops[C] + type Analyze[A] = LazyT[WriterT[G, Chain[(Arg[?], Any)], *], PreparedMeta[F], A] + implicit val L: Applicative[Analyze] = LazyT.applicativeForParallelLazyT + + def liftK[A](fa: G[A]): Analyze[A] = LazyT.liftF(WriterT.liftF(fa)) + + def findImplementations[A]( + s: Selectable[F, A] + ): List[Specialization[F, A, ?]] = s match { + case t: Type[F, ?] => List(Specialization.Type(t)) + case u: Union[F, ?] => + u.types.toList.map { case x: gql.ast.Variant[F, A, b] => + Specialization.Union(u, x) + } + case it @ Interface(_, _, _, _) => + val m: Map[String, SchemaShape.InterfaceImpl[F, A]] = + implementations + .get(it.name) + .getOrElse(Map.empty) + .collect { case (k, v: SchemaShape.InterfaceImpl[F, A] @unchecked) => (k, v) } + + m.values.toList + .collect { case ti: SchemaShape.InterfaceImpl.TypeImpl[F, A, b] => + Specialization.Interface(ti.t, ti.impl) + } + } + @nowarn3("msg=.*cannot be checked at runtime because its type arguments can't be determined.*") def prepareStep[I, O]( - step: Step[G, I, O], - fieldMeta: PartialFieldMeta[C] - ): Effect[F, G, PreparedStep[G, I, O]] - /* - def prepareStep2[I, O]( - step: Step[G, I, O], - fieldMeta: PartialFieldMeta[C] - ): H[F, PreparedStep[G, I, O]]*/ + step: Step[F, I, O], + fieldMeta: PartialFieldMeta[C], + uec: UniqueEdgeCursor + ): Analyze[PreparedStep[F, I, O]] = { + val nextId = liftK(G.nextId).map(i => StepEffectId(NodeId(i), uec)) + + def rec[I2, O2]( + step: Step[F, I2, O2], + edge: String + ): Analyze[PreparedStep[F, I2, O2]] = + prepareStep[I2, O2](step, fieldMeta, uec append edge) + + step match { + case Step.Alg.Lift(f) => L.pure(PreparedStep.Lift(f)) + case Step.Alg.EmbedError() => L.pure(PreparedStep.EmbedError[F, O]()) + case alg: Step.Alg.Compose[F, i, a, o] => + val left = rec[i, a](alg.left, "compose-left") + val right = rec[a, o](alg.right, "compose-right") + (left, right).mapN(PreparedStep.Compose[F, i, a, o](_, _)) + case _: Step.Alg.EmbedEffect[F, i] => + nextId.map(PreparedStep.EmbedEffect[F, i](_)) + case alg: Step.Alg.EmbedStream[F, i] => + nextId.map(PreparedStep.EmbedStream[F, i](alg.signal, _)) + case alg: Step.Alg.Choose[F, a, b, c, d] => + val left = rec[a, c](alg.fac, "choice-left") + val right = rec[b, d](alg.fab, "choice-right") + (left, right).mapN(PreparedStep.Choose[F, a, b, c, d](_, _)) + case _: Step.Alg.GetMeta[?, i] => + LazyT.lift((pm: Eval[PreparedMeta[F]]) => PreparedStep.GetMeta[F, I](pm)) + case alg: Step.Alg.Batch[F, k, v] => + liftK(G.nextId.map(i => PreparedStep.Batch[F, k, v](alg.id, UniqueBatchInstance(NodeId(i))))) + case alg: Step.Alg.InlineBatch[F, k, v] => + nextId.map(PreparedStep.InlineBatch[F, k, v](alg.run, _)) + case alg: Step.Alg.First[F, i, o, c] => + rec[i, o](alg.step, "first").map(PreparedStep.First[F, i, o, c](_)) + case alg: Step.Alg.Argument[?, a] => + val expected = alg.arg.entries.toList.map(_.name).toSet + val fields = fieldMeta.fields.filter { case (k, _) => expected.contains(k) } + LazyT.liftF { + WriterT { + ap + .decodeArg(alg.arg, fields.fmap(_.map(List(_))), ambigiousEnum = false, context = Nil) + .map(a => (Chain(alg.arg -> a), PreparedStep.Lift[F, I, O](_ => a))) + } + } + } + } + @nowarn3("msg=.*cannot be checked at runtime because its type arguments can't be determined.*") def prepare[A]( - fi: MergedFieldInfo[G, C], - t: Out[G, A], - fieldMeta: PartialFieldMeta[C] - ): Effect[F, G, Prepared[G, A]] + fi: MergedFieldInfo[F, C], + t: Out[F, A], + fieldMeta: PartialFieldMeta[C], + uec: UniqueEdgeCursor + ): Analyze[Prepared[F, A]] = + (t, fi.selections.toNel) match { + case (out: gql.ast.OutArr[F, a, c, b], _) => + val innerStep: Step[F, a, b] = out.resolver.underlying + val compiledStep = prepareStep[a, b](innerStep, fieldMeta, uec) + val compiledCont = prepare[b](fi, out.of, fieldMeta, uec append "in-arr") + (compiledStep, compiledCont).mapN((s, c) => PreparedList(PreparedCont(s, c), out.toSeq)) + case (out: gql.ast.OutOpt[F, a, b], _) => + val innerStep: Step[F, a, b] = out.resolver.underlying + val compiledStep = prepareStep[a, b](innerStep, fieldMeta, uec) + val compiledCont = prepare[b](fi, out.of, fieldMeta, uec append "in-opt") + (compiledStep, compiledCont).mapN((s, c) => PreparedOption(PreparedCont(s, c))) + case (s: Selectable[F, a], Some(ss)) => + liftK(prepareSelectable[A](s, ss).widen[Prepared[F, A]]) + case (e: Enum[a], None) => + L.pure(PreparedLeaf(e.name, x => Json.fromString(e.revm(x)))) + case (s: Scalar[a], None) => + import io.circe.syntax._ + L.pure(PreparedLeaf(s.name, x => s.encoder(x).asJson)) + case (o, Some(_)) => + liftK(G.raise(s"Type `${ModifierStack.fromOut(o).show(_.name)}` cannot have selections.", List(fi.caret))) + case (o, None) => + liftK(G.raise(s"Object like type `${ModifierStack.fromOut(o).show(_.name)}` must have a selection.", List(fi.caret))) + } def prepareField[I, O]( - fi: MergedFieldInfo[G, C], - field: Field[G, I, O], + fi: MergedFieldInfo[F, C], + field: Field[F, I, O], currentTypename: String - ): F[List[PreparedDataField[G, I, ?]]] - - def mergeImplementations[A]( - base: Selectable[G, A], - sels: NonEmptyList[SelectionInfo[G, C]] - ): F[NonEmptyList[MergedSpecialization[G, A, ?, C]]] + ): G[List[PreparedDataField[F, I, ?]]] = { + da + .foldDirectives[Position.Field[F, *]][List, (Field[F, I, ?], MergedFieldInfo[F, C])](fi.directives, List(fi.caret))( + (field, fi) + ) { case ((f: Field[F, I, ?], fi), p: Position.Field[F, a], d) => + da.parseArg(p, d.arguments, List(fi.caret)) + .map(p.handler(_, f, fi)) + .flatMap(G.raiseEither(_, List(fi.caret))) + } + .flatMap(_.parTraverse { case (field: Field[F, I, o2], fi) => + val rootUniqueName = UniqueEdgeCursor(s"${currentTypename}_${fi.name}") - def prepareSelectable[A]( - s: Selectable[G, A], - sis: NonEmptyList[SelectionInfo[G, C]] - ): F[Selection[G, A]] -} + val meta: PartialFieldMeta[C] = PartialFieldMeta(fi.alias, fi.args) -object QueryPreparation { - type H[F[_], A] = Kleisli[WriterT[F, List[(Arg[?], Any)], *], UniqueEdgeCursor, A] - type Effect[F[_], G[_], A] = LazyT[H[F, *], PreparedMeta[G], A] - - def apply[F[_]: Parallel, G[_], C]( - variables: VariableMap[C], - implementations: SchemaShape.Implementations[G] - )(implicit - F: Monad[F], - AP: ArgParsing[F, C], - S: Stateful[F, Int], - EA: ErrorAlg[F, C], - DA: DirectiveAlg[F, G, C] - ) = { - import EA._ - - def findImplementations2[A]( - s: Selectable[G, A] - ): List[Specialization[G, A, ?]] = s match { - case t: Type[G, ?] => List(Specialization.Type(t)) - case u: Union[G, ?] => - u.types.toList.map { case x: gql.ast.Variant[G, A, b] => - Specialization.Union(u, x) + def findArgs(o: Out[F, ?]): Chain[Arg[?]] = o match { + case x: OutArr[F, a, c, b] => collectArgs(x.resolver.underlying) ++ findArgs(x.of) + case x: OutOpt[F, a, b] => collectArgs(x.resolver.underlying) ++ findArgs(x.of) + case _ => Chain.empty } - case it @ Interface(_, _, _, _) => - val m: Map[String, SchemaShape.InterfaceImpl[G, A]] = - implementations - .get(it.name) - .getOrElse(Map.empty) - .collect { case (k, v: SchemaShape.InterfaceImpl[G, A] @unchecked) => (k, v) } - - m.values.toList - .collect { case ti: SchemaShape.InterfaceImpl.TypeImpl[G, A, b] => - Specialization.Interface(ti.t, ti.impl) - } - } - type L[A] = Effect[F, G, A] - implicit val applicativeForL: Applicative[L] = - LazyT.applicativeForParallelLazyT[H[F, *], PreparedMeta[G]] + val providedArgNames = meta.fields.keySet - def lift[A](fa: F[A]): H[F, A] = Kleisli.liftF(WriterT.liftF(fa)) - def liftK[A](fa: F[A]): L[A] = LazyT.liftF(lift(fa)) + val declaredArgs: Chain[Arg[?]] = collectArgs(field.resolve.underlying) ++ findArgs(field.output.value) + val declaredArgNames = declaredArgs.toList.flatMap(_.entries.toList.map(_.name)).toSet - def pure[A](a: A): H[F, A] = lift(F.pure(a)) - def pureK[A](a: A): L[A] = LazyT.liftF(pure(a)) + val tooMany = providedArgNames -- declaredArgNames - val L = Local[H[F, *], UniqueEdgeCursor] - - val W = Tell[H[F, *], List[(Arg[?], Any)]] - - def inK[A](p: String)(fa: L[A]): L[A] = LazyT(L.local(fa.fb)(_ append p)) - - // UniqueEdgeCursor => (List[(Arg[?], Any)], Eval[PreparedMeta[G]] => A) - - def askK: L[UniqueEdgeCursor] = LazyT.liftF(L.ask[UniqueEdgeCursor]) - - def nextId = S.get.map(i => NodeId(NonEmptyList.one(i))) <* S.modify(_ + 1) - - def nextSei = (liftK(nextId), askK).mapN(StepEffectId(_, _)) - - new QueryPreparation[F, G, C] { /* - def prepareStep2[I, O]( - step: Step[G, I, O], - fieldMeta: PartialFieldMeta[C] - ): H[F, PreparedStep[G, I, O]] = { - def rec[I2, O2]( - step: Step[G, I2, O2], - edge: String - ): L[PreparedStep[G, I2, O2]] = inK(edge) { - prepareStep[I2, O2](step, fieldMeta) - } + val verifyTooManyF: G[Unit] = + if (tooMany.isEmpty) G.unit + else + G.raise( + s"Too many arguments provided for field `${fi.name}`. Provided: ${providedArgNames.toList + .map(x => s"'$x'") + .mkString(", ")}. Declared: ${declaredArgNames.toList.map(x => s"'$x'").mkString(", ")}", + List(fi.caret) + ) - step match { - case Step.Alg.Lift(f) => pureK(PreparedStep.Lift(f)) - case Step.Alg.EmbedError() => pureK(PreparedStep.EmbedError[G, O]()) - case alg: Step.Alg.Compose[?, i, a, o] => - val left = rec[i, a](alg.left, "compose-left") - val right = rec[a, o](alg.right, "compose-right") - (left, right).mapN(PreparedStep.Compose[G, i, a, o](_, _)) - case _: Step.Alg.EmbedEffect[?, i] => nextSei.map(PreparedStep.EmbedEffect[G, i](_)) - case alg: Step.Alg.EmbedStream[?, i] => - nextSei.map(PreparedStep.EmbedStream[G, i](alg.signal, _)) - case alg: Step.Alg.Choose[?, a, b, c, d] => - val left = rec[a, c](alg.fac, "choice-left") - val right = rec[b, d](alg.fab, "choice-right") - (left, right).mapN(PreparedStep.Choose[G, a, b, c, d](_, _)) - case _: Step.Alg.GetMeta[?, i] => - K(pure((pm: Eval[PreparedMeta[G]]) => PreparedStep.GetMeta[G, I](pm))) - case alg: Step.Alg.Batch[?, k, v] => - liftK(nextId.map(i => PreparedStep.Batch[G, k, v](alg.id, UniqueBatchInstance(i)))) - case alg: Step.Alg.InlineBatch[?, k, v] => - nextSei.map(PreparedStep.InlineBatch[G, k, v](alg.run, _)) - case alg: Step.Alg.First[?, i, o, c] => - rec[i, o](alg.step, "first").map(PreparedStep.First[G, i, o, c](_)) - case alg: Step.Alg.Argument[?, a] => - val expected = alg.arg.entries.toList.map(_.name).toSet - val fields = fieldMeta.fields.filter { case (k, _) => expected.contains(k) } - K { - lift(AP.decodeArg(alg.arg, fields.fmap(_.map(List(_))), ambigiousEnum = false, context = Nil)) - .flatTap(a => W.tell(List(alg.arg -> a))) - .map(o => _ => PreparedStep.Lift[G, I, O](_ => o)) - } - } - }*/ - - override def prepareStep[I, O]( - step: Step[G, I, O], - fieldMeta: PartialFieldMeta[C] - ): L[PreparedStep[G, I, O]] = { - - def rec[I2, O2]( - step: Step[G, I2, O2], - edge: String - ): L[PreparedStep[G, I2, O2]] = inK(edge) { - prepareStep[I2, O2](step, fieldMeta) - } + val preparedF = ( + prepareStep(field.resolve.underlying, meta, rootUniqueName), + prepare(fi, field.output.value, meta, rootUniqueName append "in-root") + ).tupled - step match { - case Step.Alg.Lift(f) => pureK(PreparedStep.Lift(f)) - case Step.Alg.EmbedError() => pureK(PreparedStep.EmbedError[G, O]()) - case alg: Step.Alg.Compose[?, i, a, o] => - val left = rec[i, a](alg.left, "compose-left") - val right = rec[a, o](alg.right, "compose-right") - (left, right).mapN(PreparedStep.Compose[G, i, a, o](_, _)) - case _: Step.Alg.EmbedEffect[?, i] => nextSei.map(PreparedStep.EmbedEffect[G, i](_)) - case alg: Step.Alg.EmbedStream[?, i] => - nextSei.map(PreparedStep.EmbedStream[G, i](alg.signal, _)) - case alg: Step.Alg.Choose[?, a, b, c, d] => - val left = rec[a, c](alg.fac, "choice-left") - val right = rec[b, d](alg.fab, "choice-right") - (left, right).mapN(PreparedStep.Choose[G, a, b, c, d](_, _)) - case _: Step.Alg.GetMeta[?, i] => - LazyT.lift((pm: Eval[PreparedMeta[G]]) => PreparedStep.GetMeta[G, I](pm)) - case alg: Step.Alg.Batch[?, k, v] => - liftK(nextId.map(i => PreparedStep.Batch[G, k, v](alg.id, UniqueBatchInstance(i)))) - case alg: Step.Alg.InlineBatch[?, k, v] => - nextSei.map(PreparedStep.InlineBatch[G, k, v](alg.run, _)) - case alg: Step.Alg.First[?, i, o, c] => - rec[i, o](alg.step, "first").map(PreparedStep.First[G, i, o, c](_)) - case alg: Step.Alg.Argument[?, a] => - val expected = alg.arg.entries.toList.map(_.name).toSet - val fields = fieldMeta.fields.filter { case (k, _) => expected.contains(k) } - LazyT.liftF { - lift(AP.decodeArg(alg.arg, fields.fmap(_.map(List(_))), ambigiousEnum = false, context = Nil)) - .flatTap(a => W.tell(List(alg.arg -> a))) - .map(o => PreparedStep.Lift[G, I, O](_ => o)) + val pdfF: LazyT[G, PreparedMeta[F], PreparedDataField[F, I, ?]] = + preparedF.mapF(_.run.map { case (w, f) => + f.andThen { case (x, y) => + PreparedDataField(fi.name, fi.alias, PreparedCont(x, y), field, w.toList.toMap) } - } - } + }) - override def prepare[A]( - fi: MergedFieldInfo[G, C], - t: Out[G, A], - fieldMeta: PartialFieldMeta[C] - ): L[Prepared[G, A]] = - (t, fi.selections.toNel) match { - case (out: gql.ast.OutArr[g, a, c, b], _) => - val innerStep: Step[G, a, b] = out.resolver.underlying - val compiledStep = prepareStep[a, b](innerStep, fieldMeta) - val compiledCont = prepare[b](fi, out.of, fieldMeta) - (compiledStep, compiledCont).mapN((s, c) => PreparedList(PreparedCont(s, c), out.toSeq)) - case (out: gql.ast.OutOpt[g, a, b], _) => - val innerStep: Step[G, a, b] = out.resolver.underlying - val compiledStep = prepareStep[a, b](innerStep, fieldMeta) - val compiledCont = prepare[b](fi, out.of, fieldMeta) - (compiledStep, compiledCont).mapN((s, c) => PreparedOption(PreparedCont(s, c))) - case (s: Selectable[G, a], Some(ss)) => - liftK(prepareSelectable[A](s, ss).widen[Prepared[G, A]]) - case (e: Enum[a], None) => - pureK(PreparedLeaf(e.name, x => Json.fromString(e.revm(x)))) - case (s: Scalar[a], None) => - import io.circe.syntax._ - pureK(PreparedLeaf(s.name, x => s.encoder(x).asJson)) - case (o, Some(_)) => - liftK(raise(s"Type `${ModifierStack.fromOut(o).show(_.name)}` cannot have selections.", List(fi.caret))) - case (o, None) => - liftK(raise(s"Object like type `${ModifierStack.fromOut(o).show(_.name)}` must have a selection.", List(fi.caret))) + val out = pdfF.runWithValue { pdf => + PreparedMeta( + variables.map { case (k, v) => k -> v.copy(value = v.value.map(_.void)) }, + meta.args.map(_.map(_ => ())), + pdf + ) } - override def prepareField[I, O]( - fi: MergedFieldInfo[G, C], - field: Field[G, I, O], - currentTypename: String - ): F[List[PreparedDataField[G, I, ?]]] = { - DA - .foldDirectives[Position.Field[G, *]][List, (Field[G, I, ?], MergedFieldInfo[G, C])](fi.directives, List(fi.caret))( - (field, fi) - ) { case ((f: Field[G, I, ?], fi), p: Position.Field[G, a], d) => - DA.parseArg(p, d.arguments, List(fi.caret)) - .map(p.handler(_, f, fi)) - .flatMap(raiseEither(_, List(fi.caret))) - } - .flatMap(_.parTraverse { case (field: Field[G, I, o2], fi) => - val rootUniqueName = UniqueEdgeCursor(s"${currentTypename}_${fi.name}") - - val meta: PartialFieldMeta[C] = PartialFieldMeta(fi.alias, fi.args) + verifyTooManyF &> out + }) + } - def findArgs(o: Out[G, ?]): Chain[Arg[?]] = o match { - case x: OutArr[g, a, c, b] => collectArgs(x.resolver.underlying) ++ findArgs(x.of) - case x: OutOpt[g, a, b] => collectArgs(x.resolver.underlying) ++ findArgs(x.of) - case _ => Chain.empty - } + def mergeImplementations[A]( + base: Selectable[F, A], + sels: NonEmptyList[SelectionInfo[F, C]] + ): G[NonEmptyList[MergedSpecialization[F, A, ?, C]]] = { + // We need to find all implementations of the base type + val concreteBaseMap = findImplementations[A](base).map(x => x.target.name -> x).toMap + + val concreteBase: List[(String, Specialization[F, A, ?])] = + concreteBaseMap.toList + + type Typename = String + // Concrete type of caller match type -> fields for that type + // If A <: I and B <: I (subtype <: supertype) and we select name on I, + // then we have List("A" -> NonEmptyList.of(nameField), "B" -> NonEmptyList.of(nameField)) + val nestedSelections: List[(Typename, NonEmptyList[FieldInfo[F, C]])] = sels.toList.flatMap { sel => + /* The set of typenames that implement whatever we're selecting on + * ```graphql + * interface A { + * name: String + * } + * + * { + * ... + * ... on A { + * name + * } + * } + * ``` + * In this case, we have a selection on `A`, so we must figure out what types implement `A` + * and then for every type `T` that implements `A`, we must find the field `name` and select it on `T`. + */ + + // What typenames implement whatever the caller matched on + val concreteIntersections = findImplementations(sel.s).map(_.target.name) + + concreteIntersections tupleRight sel.fields + } - val providedArgNames = meta.fields.keySet - - val declaredArgs: Chain[Arg[?]] = collectArgs(field.resolve.underlying) ++ findArgs(field.output.value) - val declaredArgNames = declaredArgs.toList.flatMap(_.entries.toList.map(_.name)).toSet - - val tooMany = providedArgNames -- declaredArgNames - - val verifyTooManyF: F[Unit] = - if (tooMany.isEmpty) F.unit - else - raise( - s"Too many arguments provided for field `${fi.name}`. Provided: ${providedArgNames.toList - .map(x => s"'$x'") - .mkString(", ")}. Declared: ${declaredArgNames.toList.map(x => s"'$x'").mkString(", ")}", - List(fi.caret) - ) - - val preparedF = ( - prepareStep(field.resolve.underlying, meta), - prepare(fi, field.output.value, meta) - ).tupled - - val pdfF: LazyT[F, PreparedMeta[G], PreparedDataField[G, I, ?]] = - preparedF.mapF(_.run(rootUniqueName).run.map { case (w, f) => - f.andThen { case (x, y) => - PreparedDataField(fi.name, fi.alias, PreparedCont(x, y), field, w.toMap) - } - }) - - val out = pdfF.runWithValue { pdf => - PreparedMeta( - variables.map { case (k, v) => k -> v.copy(value = v.value.map(_.void)) }, - meta.args.map(_.map(_ => ())), - pdf - ) + // TODO field merging can be optimized significantly by deduplicating fragment spreads + // (if two fields are in the same fragment (maybe also the same position)?) + /* + * Now we must merge all fields that are selected on the same type. + * + * Merge fields at this level only. + * We cannot merge fields globally, because we need to know the base type + * And even if we looked for the base type, we might as well do resolver/step preparation and argument parsing + * since that would require us to walk the tree again. + */ + + type FieldName = String + // There may be more than one field with the same name + // This is fine, but we need to merge their implementations + val grouped: Map[Typename, NonEmptyMap[FieldName, NonEmptyList[FieldInfo[F, C]]]] = nestedSelections + .groupMap { case (k, _) => k } { case (_, vs) => vs } + .collect { case (k, x :: xs) => k -> NonEmptyList(x, xs).flatten.groupByNem(_.outputName) } + + // Since we have multiple fields implementations for each fieldname + // we pick one of them as the correct one + // if the schema is valid, any one field on same the typename should be equivalent + val merged: Map[Typename, NonEmptyMap[FieldName, MergedFieldInfo[F, C]]] = + grouped.fmap(_.fmap { fields => + // TODO at a glance, there might be some field duplication here + val sels = fields.toList + .map(_.tpe.inner) + .collect { case s: TypeInfo.Selectable[F, C] => s.selection.toList } + .flatten + MergedFieldInfo( + fields.head.name, + fields.head.alias, + fields.head.args, + sels, + fields.head.directives, + fields.head.caret, + fields.head.path + ) + }) + + // For every concrete implementation of the ast type (possible type) + // We find the selection for that type (and omit it if the type was not selected) + val collected: G[List[MergedSpecialization[F, A, ?, C]]] = concreteBase.parFlatTraverse { case (k, (sp: Specialization[F, A, b])) => + val t = sp.target + merged.get(k).toList.traverse { fields => + fields.toNonEmptyList + .parTraverse { f => + if (f.name === "__typename") + G.pure(PairedFieldSelection[F, b, C](f, gql.dsl.field.lift[b](_ => t.name))) + else { + t.fieldMap.get(f.name) match { + case None => + G.raise[PairedFieldSelection[F, b, C]](s"Could not find field '${f.name}' on type `${t.name}`.", Nil) + case Some(field) => G.pure(PairedFieldSelection[F, b, C](f, field)) + } } - - verifyTooManyF &> out - }) + } + .map(fields => MergedSpecialization[F, A, b, C](sp, fields)) } + } - override def mergeImplementations[A]( - // Whatever is defined in the ast at this position - base: Selectable[G, A], - // What the caller has matched on at this position in the query - sels: NonEmptyList[SelectionInfo[G, C]] - ): F[NonEmptyList[MergedSpecialization[G, A, ?, C]]] = { - // We need to find all implementations of the base type - val concreteBaseMap = findImplementations2[A](base).map(x => x.target.name -> x).toMap - - val concreteBase: List[(String, Specialization[G, A, ?])] = - concreteBaseMap.toList - - type Typename = String - // Concrete type of caller match type -> fields for that type - // If A <: I and B <: I (subtype <: supertype) and we select name on I, - // then we have List("A" -> NonEmptyList.of(nameField), "B" -> NonEmptyList.of(nameField)) - val nestedSelections: List[(Typename, NonEmptyList[FieldInfo[G, C]])] = sels.toList.flatMap { sel => - /* The set of typenames that implement whatever we're selecting on - * ```graphql - * interface A { - * name: String - * } - * - * { - * ... - * ... on A { - * name - * } - * } - * ``` - * In this case, we have a selection on `A`, so we must figure out what types implement `A` - * and then for every type `T` that implements `A`, we must find the field `name` and select it on `T`. - */ - - // What typenames implement whatever the caller matched on - val concreteIntersections = findImplementations2(sel.s).map(_.target.name) - - concreteIntersections tupleRight sel.fields - } - - // TODO field merging can be optimized significantly by deduplicating fragment spreads - // (if two fields are in the same fragment (maybe also the same position)?) - /* - * Now we must merge all fields that are selected on the same type. - * - * Merge fields at this level only. - * We cannot merge fields globally, because we need to know the base type - * And even if we looked for the base type, we might as well do resolver/step preparation and argument parsing - * since that would require us to walk the tree again. - */ - - type FieldName = String - // There may be more than one field with the same name - // This is fine, but we need to merge their implementations - val grouped: Map[Typename, NonEmptyMap[FieldName, NonEmptyList[FieldInfo[G, C]]]] = nestedSelections - .groupMap { case (k, _) => k } { case (_, vs) => vs } - .collect { case (k, x :: xs) => k -> NonEmptyList(x, xs).flatten.groupByNem(_.outputName) } - - // Since we have multiple fields implementations for each fieldname - // we pick one of them as the correct one - // if the schema is valid, any one field on same the typename should be equivalent - val merged: Map[Typename, NonEmptyMap[FieldName, MergedFieldInfo[G, C]]] = - grouped.fmap(_.fmap { fields => - // TODO at a glance, there might be some field duplication here - val sels = fields.toList - .map(_.tpe.inner) - .collect { case s: TypeInfo.Selectable[G, C] => s.selection.toList } - .flatten - MergedFieldInfo( - fields.head.name, - fields.head.alias, - fields.head.args, - sels, - fields.head.directives, - fields.head.caret, - fields.head.path - ) - }) + collected.flatMap { xs => + xs.toNel match { + case Some(x) => G.pure(x) + case None => + G.raise[NonEmptyList[MergedSpecialization[F, A, ?, C]]]( + s"Could not find any implementations of `${base.name}` in the selection set.", + Nil + ) + } + } + } - // For every concrete implementation of the ast type (possible type) - // We find the selection for that type (and omit it if the type was not selected) - val collected: F[List[MergedSpecialization[G, A, ?, C]]] = concreteBase.parFlatTraverse { case (k, (sp: Specialization[G, A, b])) => - val t = sp.target - merged.get(k).toList.traverse { fields => - fields.toNonEmptyList - .parTraverse { f => - if (f.name === "__typename") - F.pure(PairedFieldSelection[G, b, C](f, gql.dsl.field.lift[b](_ => t.name))) - else { - t.fieldMap.get(f.name) match { - case None => - raise[PairedFieldSelection[G, b, C]](s"Could not find field '${f.name}' on type `${t.name}`.", Nil) - case Some(field) => F.pure(PairedFieldSelection[G, b, C](f, field)) - } - } - } - .map(fields => MergedSpecialization[G, A, b, C](sp, fields)) + def prepareSelectable[A]( + s: Selectable[F, A], + sis: NonEmptyList[SelectionInfo[F, C]] + ): G[Selection[F, A]] = + mergeImplementations[A](s, sis) + .flatMap { impls => + impls.parTraverse[G, PreparedSpecification[F, A, ?]] { case impl: MergedSpecialization[F, A, b, C] => + val fa = impl.selections.toList.parFlatTraverse { sel => + sel.field match { + case field: Field[F, b2, t] => prepareField[b, t](sel.info, field, impl.spec.typename) + } } - } - collected.flatMap { xs => - xs.toNel match { - case Some(x) => F.pure(x) - case None => - raise[NonEmptyList[MergedSpecialization[G, A, ?, C]]]( - s"Could not find any implementations of `${base.name}` in the selection set.", - Nil - ) - } + fa.map(xs => PreparedSpecification[F, A, b](impl.spec, xs)) } } - - override def prepareSelectable[A]( - s: Selectable[G, A], - sis: NonEmptyList[SelectionInfo[G, C]] - ): F[Selection[G, A]] = - mergeImplementations[A](s, sis) - .flatMap { impls => - impls.parTraverse[F, PreparedSpecification[G, A, ?]] { case impl: MergedSpecialization[G, A, b, C] => - val fa = impl.selections.toList.parFlatTraverse { sel => - sel.field match { - case field: Field[G, b2, t] => prepareField[b, t](sel.info, field, impl.spec.typename) - } - } - - fa.map(xs => PreparedSpecification[G, A, b](impl.spec, xs)) - } - } - .map(xs => Selection(xs.toList, s)) - } - } + .map(xs => Selection(xs.toList, s)) } final case class MergedFieldInfo[G[_], C]( diff --git a/modules/core/src/main/scala/gql/preparation/RootPreparation.scala b/modules/core/src/main/scala/gql/preparation/RootPreparation.scala index f21dab5f3..41bfe0239 100644 --- a/modules/core/src/main/scala/gql/preparation/RootPreparation.scala +++ b/modules/core/src/main/scala/gql/preparation/RootPreparation.scala @@ -15,13 +15,8 @@ */ package gql.preparation -import cats._ import cats.data._ import cats.implicits._ -import cats.mtl.Listen -import cats.mtl.Local -import cats.mtl.Stateful -import gql.Cursor import gql.InverseModifier import gql.InverseModifierStack import gql.ModifierStack @@ -31,211 +26,180 @@ import gql.parser.{QueryAst => QA} import gql.parser.{Value => V} import io.circe._ -trait RootPreparation[F[_], G[_], C] { +class RootPreparation[F[_], C] { + type G[A] = Alg[C, A] + val G = Alg.Ops[C] + def pickRootOperation( ops: List[(QA.OperationDefinition[C], C)], operationName: Option[String] - ): F[QA.OperationDefinition[C]] + ): G[QA.OperationDefinition[C]] = { + lazy val applied = ops.map { case (x, _) => x } + + lazy val positions = ops.map { case (_, x) => x } + + lazy val possible = applied + .collect { case d: QA.OperationDefinition.Detailed[C] => d.name } + .collect { case Some(x) => s"'$x'" } + .mkString(", ") + + (applied, operationName) match { + case (Nil, _) => G.raise(s"No operations provided.", Nil) + case (x :: Nil, _) => G.pure(x) + case (_, _) if applied.exists { + case _: QA.OperationDefinition.Simple[C] => true + case x: QA.OperationDefinition.Detailed[C] if x.name.isEmpty => true + case _ => false + } => + G.raise(s"Exactly one operation must be suplied if the operations include at least one unnamed operation.", positions) + case (_, None) => + G.raise(s"Operation name must be supplied when supplying multiple operations, provided operations are $possible.", positions) + case (_, Some(name)) => + val o = applied.collectFirst { case d: QA.OperationDefinition.Detailed[C] if d.name.contains(name) => d } + G.raiseOpt(o, s"Unable to find operation '$name', provided possible operations are $possible.", positions) + } + } def variables( op: QA.OperationDefinition[C], variableMap: Map[String, Json], - schema: SchemaShape[G, ?, ?, ?] - ): F[VariableMap[C]] - - def prepareRoot[Q, M, S]( - executabels: NonEmptyList[QA.ExecutableDefinition[C]], - schema: SchemaShape[G, Q, M, S], - variableMap: Map[String, Json], - operationName: Option[String] - ): F[PreparedRoot[G, Q, M, S]] -} + schema: SchemaShape[F, ?, ?, ?] + ): Alg[C, VariableMap[C]] = { + val ap = new ArgParsing[C](Map.empty) + /* + * Convert the variable signature into a gql arg and parse both the default value and the provided value + * Then save the provided getOrElse default into a map along with the type + */ + op match { + case QA.OperationDefinition.Simple(_) => G.pure(Map.empty) + case QA.OperationDefinition.Detailed(_, _, variableDefinitions, _, _) => + variableDefinitions.toList + .flatMap(_.nel.toList) + .parTraverse[G, (String, Variable[C])] { pvd => + val pos = pvd.c + val vd = pvd + + val ms = ModifierStack.fromType(vd.tpe) + + val oe: Option[Either[Json, V[Const, C]]] = (variableMap.get(vd.name).map(_.asLeft) orElse vd.defaultValue.map(_.asRight)) + + val fo: G[Either[Json, V[Const, C]]] = oe match { + case None => + if (ms.invert.modifiers.headOption.contains(InverseModifier.Optional)) G.pure(Right(V.NullValue(pos))) + else G.raise(s"Variable '$$${vd.name}' is required but was not provided.", List(pos)) + case Some(x) => + schema.stubInputs.get(ms.inner) match { + case None => + G.raise( + s"Variable '$$${vd.name}' referenced type `${ms.inner}`, but `${ms.inner}` does not exist in the schema.", + List(pos) + ) + case Some(stubTLArg) => + val t = InverseModifierStack.toIn(ms.copy(inner = stubTLArg).invert) + + G.ambientField(vd.name) { + t match { + case in: gql.ast.In[a] => + val (v, amb) = x match { + case Left(j) => (V.fromJson(j).as(pos), true) + case Right(v) => (v, false) + } + ap.decodeIn[a](in, v.map(List(_)), ambigiousEnum = amb).void + } + } as x + } + } -object RootPreparation { - type IdGen[F[_], A] = StateT[F, Int, A] - type UsedVars[F[_], A] = WriterT[F, ArgParsing.UsedVariables, A] - type CycleF[F[_], A] = Kleisli[F, FieldCollection.CycleSet, A] - type CursorF[F[_], A] = Kleisli[F, Cursor, A] - type ErrF[F[_], C, A] = EitherT[F, NonEmptyChain[PositionalError[C]], A] - - type Stack[C, A] = ErrF[CycleF[CursorF[UsedVars[IdGen[Eval, *], *], *], *], C, A] - - object Stack { - def runK[C] = new (Stack[C, *] ~> EitherNec[PositionalError[C], *]) { - override def apply[A](fa: Stack[C, A]): EitherNec[PositionalError[C], A] = - fa.value.run(Set.empty).run(Cursor.empty).value.runA(0).value + fo.map(e => vd.name -> Variable(vd.tpe, e)) + } + .map(_.toMap) } } - def prepareRun[G[_], C, Q, M, S]( + def prepareRoot[Q, M, S]( executabels: NonEmptyList[QA.ExecutableDefinition[C]], - schema: SchemaShape[G, Q, M, S], + schema: SchemaShape[F, Q, M, S], variableMap: Map[String, Json], operationName: Option[String] - ): EitherNec[PositionalError[C], PreparedRoot[G, Q, M, S]] = Stack.runK[C] { - apply[Stack[C, *], G, C].prepareRoot(executabels, schema, variableMap, operationName) - } - - def apply[F[_]: Parallel, G[_], C](implicit - F: MonadError[F, NonEmptyChain[PositionalError[C]]], - C: Local[F, Cursor], - L: Local[F, FieldCollection.CycleSet], - S: Stateful[F, Int], - LI: Listen[F, ArgParsing.UsedVariables] - ) = { - implicit val EA: ErrorAlg[F, C] = ErrorAlg.errorAlgForHandle[F, NonEmptyChain, C] - implicit val PA: PathAlg[F] = PathAlg.pathAlgForLocal[F] - import EA._ - import PA._ - - new RootPreparation[F, G, C] { - override def pickRootOperation( - ops: List[(QA.OperationDefinition[C], C)], - operationName: Option[String] - ): F[QA.OperationDefinition[C]] = { - lazy val applied = ops.map { case (x, _) => x } - - lazy val positions = ops.map { case (_, x) => x } - - lazy val possible = applied - .collect { case d: QA.OperationDefinition.Detailed[C] => d.name } - .collect { case Some(x) => s"'$x'" } - .mkString(", ") - (applied, operationName) match { - case (Nil, _) => raise(s"No operations provided.", Nil) - case (x :: Nil, _) => F.pure(x) - case (_, _) if applied.exists { - case _: QA.OperationDefinition.Simple[C] => true - case x: QA.OperationDefinition.Detailed[C] if x.name.isEmpty => true - case _ => false - } => - raise(s"Exactly one operation must be suplied if the operations include at least one unnamed operation.", positions) - case (_, None) => - raise(s"Operation name must be supplied when supplying multiple operations, provided operations are $possible.", positions) - case (_, Some(name)) => - val o = applied.collectFirst { case d: QA.OperationDefinition.Detailed[C] if d.name.contains(name) => d } - raiseOpt(o, s"Unable to find operation '$name', provided possible operations are $possible.", positions) - } - } - - override def variables( - op: QA.OperationDefinition[C], - variableMap: Map[String, Json], - schema: SchemaShape[G, ?, ?, ?] - ): F[VariableMap[C]] = { - val AP = ArgParsing[F, C](Map.empty) - /* - * Convert the variable signature into a gql arg and parse both the default value and the provided value - * Then save the provided getOrElse default into a map along with the type - */ - op match { - case QA.OperationDefinition.Simple(_) => F.pure(Map.empty) - case QA.OperationDefinition.Detailed(_, _, variableDefinitions, _, _) => - variableDefinitions.toList - .flatMap(_.nel.toList) - .parTraverse[F, (String, Variable[C])] { pvd => - val pos = pvd.c - val vd = pvd - - val ms = ModifierStack.fromType(vd.tpe) - - val oe: Option[Either[Json, V[Const, C]]] = (variableMap.get(vd.name).map(_.asLeft) orElse vd.defaultValue.map(_.asRight)) - - val fo: F[Either[Json, V[Const, C]]] = oe match { - case None => - if (ms.invert.modifiers.headOption.contains(InverseModifier.Optional)) F.pure(Right(V.NullValue(pos))) - else raise(s"Variable '$$${vd.name}' is required but was not provided.", List(pos)) - case Some(x) => - schema.stubInputs.get(ms.inner) match { - case None => - raise( - s"Variable '$$${vd.name}' referenced type `${ms.inner}`, but `${ms.inner}` does not exist in the schema.", - List(pos) - ) - case Some(stubTLArg) => - val t = InverseModifierStack.toIn(ms.copy(inner = stubTLArg).invert) - - ambientField(vd.name) { - t match { - case in: gql.ast.In[a] => - val (v, amb) = x match { - case Left(j) => (V.fromJson(j).as(pos), true) - case Right(v) => (v, false) - } - AP.decodeIn[a](in, v.map(List(_)), ambigiousEnum = amb).void - } - } as x - } - } + ): G[PreparedRoot[F, Q, M, S]] = { + val (ops, frags) = executabels.toList.partitionEither { + case QA.ExecutableDefinition.Operation(op, c) => Left((op, c)) + case QA.ExecutableDefinition.Fragment(frag, _) => Right(frag) + } - fo.map(e => vd.name -> Variable(vd.tpe, e)) - } - .map(_.toMap) - } + pickRootOperation(ops, operationName).flatMap { od => + val (ot, ss) = od match { + case QA.OperationDefinition.Simple(ss) => (QA.OperationType.Query, ss) + case QA.OperationDefinition.Detailed(ot, _, _, _, ss) => (ot, ss) } - override def prepareRoot[Q, M, S]( - executabels: NonEmptyList[QA.ExecutableDefinition[C]], - schema: SchemaShape[G, Q, M, S], - variableMap: Map[String, Json], - operationName: Option[String] - ): F[PreparedRoot[G, Q, M, S]] = { - val (ops, frags) = executabels.toList.partitionEither { - case QA.ExecutableDefinition.Operation(op, c) => Left((op, c)) - case QA.ExecutableDefinition.Fragment(frag, _) => Right(frag) - } - - pickRootOperation(ops, operationName).flatMap { od => - val (ot, ss) = od match { - case QA.OperationDefinition.Simple(ss) => (QA.OperationType.Query, ss) - case QA.OperationDefinition.Detailed(ot, _, _, _, ss) => (ot, ss) + def runWith[A](o: gql.ast.Type[F, A]): G[Selection[F, A]] = + variables(od, variableMap, schema).flatMap { vm => + val ap = new ArgParsing[C](vm) + val da = new DirectiveAlg[F, C](schema.discover.positions, ap) + + val fragMap = frags.map(x => x.name -> x).toMap + val fc = new FieldCollection[F, C]( + schema.discover.implementations, + fragMap, + ap, + da + ) + val fm = new FieldMerging[C] + val qp = new QueryPreparation[F, C](ap, da, vm, schema.discover.implementations) + + // implicit val AP: ArgParsing[F, C] = ArgParsing[F, C](vm) + // implicit val DA: DirectiveAlg[F, G, C] = DirectiveAlg.forPositions[F, G, C](schema.discover.positions) + // val FC: FieldCollection[F, G, C] = FieldCollection[F, G, C]( + // schema.discover.implementations, + // fragMap + // ) + // val FM = FieldMerging[F, C] + // val QP = QueryPreparation[F, G, C](vm, schema.discover.implementations) + val prog: G[Selection[F, A]] = fc.collectSelectionInfo(o, ss).flatMap { + case x :: xs => + val r = NonEmptyList(x, xs) + fm.checkSelectionsMerge(r) >> qp.prepareSelectable(o, r) + case _ => G.pure(Selection(Nil, o)) } - - def runWith[A](o: gql.ast.Type[G, A]): F[Selection[G, A]] = - variables(od, variableMap, schema).flatMap { vm => - implicit val AP: ArgParsing[F, C] = ArgParsing[F, C](vm) - implicit val DA: DirectiveAlg[F, G, C] = DirectiveAlg.forPositions[F, G, C](schema.discover.positions) - val fragMap = frags.map(x => x.name -> x).toMap - val FC: FieldCollection[F, G, C] = FieldCollection[F, G, C]( - schema.discover.implementations, - fragMap - ) - val FM = FieldMerging[F, C] - val QP = QueryPreparation[F, G, C](vm, schema.discover.implementations) - val prog: F[Selection[G, A]] = FC.collectSelectionInfo(o, ss).flatMap { - case x :: xs => - val r = NonEmptyList(x, xs) - FM.checkSelectionsMerge(r) >> QP.prepareSelectable(o, r) - case _ => F.pure(Selection(Nil, o)) - } - LI.listen(prog).flatMap { case (res, used) => - val unused = vm.keySet -- used - if (unused.nonEmpty) raise(s"Unused variables: ${unused.map(str => s"'$str'").mkString(", ")}", Nil) - else F.pure(res) - } - } - - ot match { - case QA.OperationType.Query => - val i: NonEmptyList[(String, gql.ast.Field[G, Unit, ?])] = schema.introspection - val q = schema.query - val full = q.copy(fields = i.map { case (k, v) => k -> v.contramap[G, Q](_ => ()) } concatNel q.fields) - runWith[Q](full).map(PreparedRoot.Query(_)) - case QA.OperationType.Mutation => - raiseOpt(schema.mutation, "No `Mutation` type defined in this schema.", Nil) - .flatMap(runWith[M]) - .map(PreparedRoot.Mutation(_)) - case QA.OperationType.Subscription => - raiseOpt(schema.subscription, "No `Subscription` type defined in this schema.", Nil) - .flatMap(runWith[S]) - .map(PreparedRoot.Subscription(_)) + (prog, G.usedVariables).tupled.flatMap { case (res, used) => + val unused = vm.keySet -- used + if (unused.nonEmpty) G.raise(s"Unused variables: ${unused.map(str => s"'$str'").mkString(", ")}", Nil) + else G.pure(res) } } + + ot match { + case QA.OperationType.Query => + val i: NonEmptyList[(String, gql.ast.Field[F, Unit, ?])] = schema.introspection + val q = schema.query + val full = q.copy(fields = i.map { case (k, v) => k -> v.contramap[F, Q](_ => ()) } concatNel q.fields) + runWith[Q](full).map(PreparedRoot.Query(_)) + case QA.OperationType.Mutation => + G.raiseOpt(schema.mutation, "No `Mutation` type defined in this schema.", Nil) + .flatMap(runWith[M]) + .map(PreparedRoot.Mutation(_)) + case QA.OperationType.Subscription => + G.raiseOpt(schema.subscription, "No `Subscription` type defined in this schema.", Nil) + .flatMap(runWith[S]) + .map(PreparedRoot.Subscription(_)) } } } } +object RootPreparation { + def prepareRun[F[_], C, Q, M, S]( + executabels: NonEmptyList[QA.ExecutableDefinition[C]], + schema: SchemaShape[F, Q, M, S], + variableMap: Map[String, Json], + operationName: Option[String] + ): EitherNec[PositionalError[C], PreparedRoot[F, Q, M, S]] = { + val rp = new RootPreparation[F, C] + rp.prepareRoot(executabels, schema, variableMap, operationName).run + } +} + sealed trait PreparedRoot[G[_], Q, M, S] object PreparedRoot { final case class Query[G[_], Q, M, S](query: Selection[G, Q]) extends PreparedRoot[G, Q, M, S] diff --git a/modules/server/src/test/scala/gql/StarWarsTest.scala b/modules/server/src/test/scala/gql/StarWarsTest.scala index e0c455172..eb085dc7c 100644 --- a/modules/server/src/test/scala/gql/StarWarsTest.scala +++ b/modules/server/src/test/scala/gql/StarWarsTest.scala @@ -607,12 +607,15 @@ class StarWarsTest extends CatsEffectSuite { { "errors": [ { - "message" : "Field 'cors' is not a member of `Query`.", + "message" : "Field 'dorse' is not a member of `Character`.", "locations" : [ { - "line" : 2, - "column" : 8 + "line" : 7, + "column" : 10 } + ], + "path" : [ + "hero" ] }, { @@ -628,15 +631,12 @@ class StarWarsTest extends CatsEffectSuite { ] }, { - "message" : "Field 'dorse' is not a member of `Character`.", + "message" : "Field 'cors' is not a member of `Query`.", "locations" : [ { - "line" : 7, - "column" : 10 + "line" : 2, + "column" : 8 } - ], - "path" : [ - "hero" ] } ]