diff --git a/src/main/scala/sangria/execution/DeprecationTracker.scala b/src/main/scala/sangria/execution/DeprecationTracker.scala index fafc7d7d..2df9fca6 100644 --- a/src/main/scala/sangria/execution/DeprecationTracker.scala +++ b/src/main/scala/sangria/execution/DeprecationTracker.scala @@ -19,7 +19,7 @@ object NilDeprecationTracker extends DeprecationTracker { object PrintingDeprecationTracker extends DeprecationTracker { def deprecatedFieldUsed[Ctx](ctx: Context[Ctx, _]) = - println(s"Deprecated field '${ctx.parentType.name}.${ctx.field.name}' used at path '${ctx.path mkString "."}'.") + println(s"Deprecated field '${ctx.parentType.name}.${ctx.field.name}' used at path '${ctx.path}'.") def deprecatedEnumValueUsed[T, Ctx](enum: EnumType[T], value: T, userContext: Ctx) = println(s"Deprecated enum value '$value' used of enum '${enum.name}'.") diff --git a/src/main/scala/sangria/execution/ExecutionPath.scala b/src/main/scala/sangria/execution/ExecutionPath.scala new file mode 100644 index 00000000..91fa9970 --- /dev/null +++ b/src/main/scala/sangria/execution/ExecutionPath.scala @@ -0,0 +1,41 @@ +package sangria.execution + +import sangria.marshalling.ResultMarshaller +import sangria.ast + +case class ExecutionPath private (path: Vector[Any], cacheKeyPath: ExecutionPath.PathCacheKey) { + def +(fieldName: String) = new ExecutionPath(path :+ fieldName, cacheKey :+ fieldName) + def +(field: ast.Field) = new ExecutionPath(path :+ field.outputName, cacheKey :+ field.outputName) + + def isEmpty = path.isEmpty + def nonEmpty = path.nonEmpty + + /** + * @return last index in the path, if available + */ + def lastIndex: Option[Int] = path.lastOption.collect {case i: Int ⇒ i} + + /** + * @return the size of the path excluding the indexes + */ + def size = cacheKeyPath.size + + def marshal(m: ResultMarshaller): m.Node = m.arrayNode(path.map { + case s: String ⇒ m.scalarNode(s, "String", Set.empty) + case i: Int ⇒ m.scalarNode(i, "Int", Set.empty) + }) + + def cacheKey: ExecutionPath.PathCacheKey = cacheKeyPath + + override def toString = path.foldLeft("") { + case ("", str: String) ⇒ str + case (acc, str: String) ⇒ acc + "." + str + case (acc, idx: Int) ⇒ acc + "[" + idx + "]" + } +} + +object ExecutionPath { + type PathCacheKey = Vector[String] + + val empty = new ExecutionPath(Vector.empty, Vector.empty) +} \ No newline at end of file diff --git a/src/main/scala/sangria/execution/Executor.scala b/src/main/scala/sangria/execution/Executor.scala index e5ca9090..68c7e249 100644 --- a/src/main/scala/sangria/execution/Executor.scala +++ b/src/main/scala/sangria/execution/Executor.scala @@ -39,13 +39,13 @@ case class Executor[Ctx, Root]( unmarshalledVariables ← valueCollector.getVariableValues(operation.variables) fieldCollector = new FieldCollector[Ctx, Root](schema, queryAst, unmarshalledVariables, queryAst.sourceMapper, valueCollector, exceptionHandler) tpe ← getOperationRootType(operation, queryAst.sourceMapper) - fields ← fieldCollector.collectFields(Vector.empty, tpe, Vector(operation)) + fields ← fieldCollector.collectFields(ExecutionPath.empty, tpe, Vector(operation)) } yield { val preparedFields = fields.fields.flatMap { case CollectedField(_, astField, Success(_)) ⇒ val allFields = tpe.getField(schema, astField.name).asInstanceOf[Vector[Field[Ctx, Root]]] val field = allFields.head - val args = valueCollector.getFieldArgumentValues(Vector(astField.name), field.arguments, astField.arguments, unmarshalledVariables) + val args = valueCollector.getFieldArgumentValues(ExecutionPath.empty + astField, field.arguments, astField.arguments, unmarshalledVariables) args.toOption.map(PreparedField(field, _)) case _ ⇒ None @@ -91,7 +91,7 @@ case class Executor[Ctx, Root]( unmarshalledVariables ← valueCollector.getVariableValues(operation.variables) fieldCollector = new FieldCollector[Ctx, Root](schema, queryAst, unmarshalledVariables, queryAst.sourceMapper, valueCollector, exceptionHandler) tpe ← getOperationRootType(operation, queryAst.sourceMapper) - fields ← fieldCollector.collectFields(Vector.empty, tpe, Vector(operation)) + fields ← fieldCollector.collectFields(ExecutionPath.empty, tpe, Vector(operation)) } yield reduceQuerySafe(fieldCollector, valueCollector, unmarshalledVariables, tpe, fields, userContext) match { case fut: Future[Ctx] ⇒ fut.flatMap(executeOperation(queryAst, operationName, variables, um, operation, queryAst.sourceMapper, valueCollector, @@ -224,12 +224,12 @@ case class Executor[Ctx, Root]( // Using mutability here locally in order to reduce footprint import scala.collection.mutable.ListBuffer - val argumentValuesFn = (path: Vector[String], argumentDefs: List[Argument[_]], argumentAsts: List[ast.Argument]) ⇒ + val argumentValuesFn = (path: ExecutionPath, argumentDefs: List[Argument[_]], argumentAsts: List[ast.Argument]) ⇒ valueCollector.getFieldArgumentValues(path, argumentDefs, argumentAsts, variables) val initialValues: Vector[Any] = reducers map (_.initial) - def loop(path: Vector[String], tpe: OutputType[_], astFields: Vector[ast.Field]): Seq[Any] = + def loop(path: ExecutionPath, tpe: OutputType[_], astFields: Vector[ast.Field]): Seq[Any] = tpe match { case OptionType(ofType) ⇒ loop(path, ofType, astFields) case ListType(ofType) ⇒ loop(path, ofType, astFields) @@ -240,7 +240,7 @@ case class Executor[Ctx, Root]( case (acc, CollectedField(_, _, Success(fields))) if objTpe.getField(schema, fields.head.name).nonEmpty ⇒ val astField = fields.head val field = objTpe.getField(schema, astField.name).head - val newPath = path :+ astField.outputName + val newPath = path + astField val childReduced = loop(newPath, field.fieldType, fields) for (i ← reducers.indices) { @@ -278,7 +278,7 @@ case class Executor[Ctx, Root]( case (acc, CollectedField(_, _, Success(astFields))) if rootTpe.getField(schema, astFields.head.name).nonEmpty => val astField = astFields.head val field = rootTpe.getField(schema, astField.name).head - val path = Vector(astField.outputName) + val path = ExecutionPath.empty + astField val childReduced = loop(path, field.fieldType, astFields) for (i ← reducers.indices) { diff --git a/src/main/scala/sangria/execution/FieldCollector.scala b/src/main/scala/sangria/execution/FieldCollector.scala index aa966c1d..a87acf31 100644 --- a/src/main/scala/sangria/execution/FieldCollector.scala +++ b/src/main/scala/sangria/execution/FieldCollector.scala @@ -18,10 +18,10 @@ class FieldCollector[Ctx, Val]( valueCollector: ValueCollector[Ctx, _], exceptionHandler: Executor.ExceptionHandler) { - private val resultCache = TrieMap[(Vector[String], String), Try[CollectedFields]]() + private val resultCache = TrieMap[(ExecutionPath.PathCacheKey, String), Try[CollectedFields]]() - def collectFields(path: Vector[String], tpe: ObjectType[Ctx, _], selections: Vector[ast.SelectionContainer]): Try[CollectedFields] = - resultCache.getOrElseUpdate(path → tpe.name, { + def collectFields(path: ExecutionPath, tpe: ObjectType[Ctx, _], selections: Vector[ast.SelectionContainer]): Try[CollectedFields] = + resultCache.getOrElseUpdate(path.cacheKey → tpe.name, { val builder: Try[CollectedFieldsBuilder] = Success(new CollectedFieldsBuilder) selections.foldLeft(builder) { diff --git a/src/main/scala/sangria/execution/QueryReducer.scala b/src/main/scala/sangria/execution/QueryReducer.scala index 1cd78316..9c143b46 100644 --- a/src/main/scala/sangria/execution/QueryReducer.scala +++ b/src/main/scala/sangria/execution/QueryReducer.scala @@ -16,20 +16,20 @@ trait QueryReducer[-Ctx, +Out] { def reduceField[Val]( fieldAcc: Acc, childrenAcc: Acc, - path: Vector[String], + path: ExecutionPath, ctx: Ctx, astFields: Vector[ast.Field], parentType: ObjectType[Out, Val] @uncheckedVariance, field: Field[Ctx, Val] @uncheckedVariance, - argumentValuesFn: (Vector[String], List[Argument[_]], List[ast.Argument]) ⇒ Try[Args]): Acc + argumentValuesFn: (ExecutionPath, List[Argument[_]], List[ast.Argument]) ⇒ Try[Args]): Acc def reduceScalar[T]( - path: Vector[String], + path: ExecutionPath, ctx: Ctx, tpe: ScalarType[T]): Acc def reduceEnum[T]( - path: Vector[String], + path: ExecutionPath, ctx: Ctx, tpe: EnumType[T]): Acc @@ -60,12 +60,12 @@ class MeasureComplexity[Ctx](action: (Double, Ctx) ⇒ ReduceAction[Ctx, Ctx]) e def reduceField[Val]( fieldAcc: Acc, childrenAcc: Acc, - path: Vector[String], + path: ExecutionPath, ctx: Ctx, astFields: Vector[ast.Field], parentType: ObjectType[Ctx, Val], field: Field[Ctx, Val], - argumentValuesFn: (Vector[String], List[Argument[_]], List[ast.Argument]) ⇒ Try[Args]): Acc = { + argumentValuesFn: (ExecutionPath, List[Argument[_]], List[ast.Argument]) ⇒ Try[Args]): Acc = { val estimate = field.complexity match { case Some(fn) ⇒ argumentValuesFn(path, field.arguments, astFields.head.arguments) match { @@ -79,12 +79,12 @@ class MeasureComplexity[Ctx](action: (Double, Ctx) ⇒ ReduceAction[Ctx, Ctx]) e } def reduceScalar[T]( - path: Vector[String], + path: ExecutionPath, ctx: Ctx, tpe: ScalarType[T]): Acc = tpe.complexity def reduceEnum[T]( - path: Vector[String], + path: ExecutionPath, ctx: Ctx, tpe: EnumType[T]): Acc = initial @@ -106,21 +106,21 @@ class TagCollector[Ctx, T](tagMatcher: PartialFunction[FieldTag, T], action: (Se def reduceField[Val]( fieldAcc: Acc, childrenAcc: Acc, - path: Vector[String], + path: ExecutionPath, ctx: Ctx, astFields: Vector[ast.Field], parentType: ObjectType[Ctx, Val], field: Field[Ctx, Val], - argumentValuesFn: (Vector[String], List[Argument[_]], List[ast.Argument]) ⇒ Try[Args]): Acc = + argumentValuesFn: (ExecutionPath, List[Argument[_]], List[ast.Argument]) ⇒ Try[Args]): Acc = fieldAcc ++ childrenAcc ++ field.tags.collect {case t if tagMatcher.isDefinedAt(t) ⇒ tagMatcher(t)} def reduceScalar[ST]( - path: Vector[String], + path: ExecutionPath, ctx: Ctx, tpe: ScalarType[ST]): Acc = initial def reduceEnum[ET]( - path: Vector[String], + path: ExecutionPath, ctx: Ctx, tpe: EnumType[ET]): Acc = initial diff --git a/src/main/scala/sangria/execution/Resolver.scala b/src/main/scala/sangria/execution/Resolver.scala index cf6db2c8..40250abd 100644 --- a/src/main/scala/sangria/execution/Resolver.scala +++ b/src/main/scala/sangria/execution/Resolver.scala @@ -32,13 +32,13 @@ class Resolver[Ctx]( import Resolver._ def resolveFieldsPar(tpe: ObjectType[Ctx, _], value: Any, fields: CollectedFields): Future[marshaller.Node] = { - val actions = collectActionsPar(Vector.empty, tpe, value, fields, ErrorRegistry.empty, userContext) + val actions = collectActionsPar(ExecutionPath.empty, tpe, value, fields, ErrorRegistry.empty, userContext) - processFinalResolve(resolveActionsPar(Vector.empty, tpe, actions, userContext, fields.namesOrdered)) + processFinalResolve(resolveActionsPar(ExecutionPath.empty, tpe, actions, userContext, fields.namesOrdered)) } def resolveFieldsSeq(tpe: ObjectType[Ctx, _], value: Any, fields: CollectedFields): Future[marshaller.Node] = { - val actions = resolveSeq(Vector.empty, tpe, value, fields, ErrorRegistry.empty) + val actions = resolveSeq(ExecutionPath.empty, tpe, value, fields, ErrorRegistry.empty) actions flatMap processFinalResolve } @@ -58,7 +58,7 @@ class Resolver[Ctx]( private type Actions = (ErrorRegistry, Option[Vector[(Vector[ast.Field], Option[(Field[Ctx, _], Option[MappedCtxUpdate[Ctx, Any, Any]], LeafAction[Ctx, _])])]]) def resolveSeq( - path: Vector[String], + path: ExecutionPath, tpe: ObjectType[Ctx, _], value: Any, fields: CollectedFields, @@ -69,11 +69,11 @@ class Resolver[Ctx]( case (acc @ (Result(_, None), _), _) ⇒ Future.successful(acc) case (acc, CollectedField(name, origField, _)) if tpe.getField(schema, origField.name).isEmpty ⇒ Future.successful(acc) case ((Result(errors, s @ Some(acc)), uc), CollectedField(name, origField, Failure(error))) ⇒ - Future.successful(Result(errors.add(path :+ name, error), + Future.successful(Result(errors.add(path + name, error), if (isOptional(tpe, origField.name)) Some(marshaller.addMapNodeElem(acc.asInstanceOf[marshaller.MapBuilder], origField.outputName, marshaller.nullNode, optional = true)) else None) → uc) case ((accRes @ Result(errors, s @ Some(acc)), uc), CollectedField(name, origField, Success(fields))) ⇒ - resolveField(uc, tpe, path :+ name, value, errors, name, fields) match { + resolveField(uc, tpe, path + name, value, errors, name, fields) match { case (updatedErrors, None, _) if isOptional(tpe, origField.name) ⇒ Future.successful(Result(updatedErrors, Some(marshaller.addMapNodeElem(acc.asInstanceOf[marshaller.MapBuilder], fields.head.outputName, marshaller.nullNode, optional = isOptional(tpe, origField.name)))) → uc) case (updatedErrors, None, _) ⇒ Future.successful(Result(updatedErrors, None) → uc) @@ -101,57 +101,57 @@ class Resolver[Ctx]( try { result match { case Value(v) ⇒ - Future.successful(resolveValue(path :+ fields.head.outputName, fields, sfield.fieldType, sfield, resolveVal(v), uc) → resolveUc(v)) + Future.successful(resolveValue(path + fields.head, fields, sfield.fieldType, sfield, resolveVal(v), uc) → resolveUc(v)) case PartialValue(v, es) ⇒ Future.successful( - resolveValue(path :+ fields.head.outputName, fields, sfield.fieldType, sfield, resolveVal(v), uc) - .appendErrors(path :+ fields.head.outputName, es, fields.head.position) → resolveUc(v)) + resolveValue(path + fields.head, fields, sfield.fieldType, sfield, resolveVal(v), uc) + .appendErrors(path + fields.head, es, fields.head.position) → resolveUc(v)) case TryValue(v) ⇒ Future.successful(v match { case Success(success) ⇒ - resolveValue(path :+ fields.head.outputName, fields, sfield.fieldType, sfield, resolveVal(success), uc) → resolveUc(v) - case Failure(e) ⇒ Result(ErrorRegistry(path :+ fields.head.outputName, resolveError(e), fields.head.position), None) → uc + resolveValue(path + fields.head, fields, sfield.fieldType, sfield, resolveVal(success), uc) → resolveUc(v) + case Failure(e) ⇒ Result(ErrorRegistry(path + fields.head, resolveError(e), fields.head.position), None) → uc }) case DeferredValue(d) ⇒ val p = Promise[Any]() resolveDeferred(uc, DeferredResult(Vector(Future.successful(Vector(Defer(p, d)))), p.future.flatMap { v ⇒ - resolveValue(path :+ fields.head.outputName, fields, sfield.fieldType, sfield, resolveVal(v), uc) match { + resolveValue(path + fields.head, fields, sfield.fieldType, sfield, resolveVal(v), uc) match { case r: Result ⇒ Future.successful(r) case er: DeferredResult ⇒ resolveDeferred(uc, er) } }.recover { - case e ⇒ Result(ErrorRegistry(path :+ fields.head.outputName, resolveError(e), fields.head.position), None) + case e ⇒ Result(ErrorRegistry(path + fields.head, resolveError(e), fields.head.position), None) })).flatMap(r ⇒ p.future map (r → resolveUc(_)) recover { case _ ⇒ r → uc}) case FutureValue(f) ⇒ - f.map(v ⇒ resolveValue(path :+ fields.head.outputName, fields, sfield.fieldType, sfield, resolveVal(v), uc) → resolveUc(v)) - .recover { case e ⇒ Result(errors.add(path :+ name, resolveError(e), fields.head.position), None) → uc} + f.map(v ⇒ resolveValue(path + fields.head, fields, sfield.fieldType, sfield, resolveVal(v), uc) → resolveUc(v)) + .recover { case e ⇒ Result(errors.add(path + name, resolveError(e), fields.head.position), None) → uc} case PartialFutureValue(f) ⇒ - f.map{case PartialValue(v, es) ⇒ resolveValue(path :+ fields.head.outputName, fields, sfield.fieldType, sfield, resolveVal(v), uc) - .appendErrors(path :+ fields.head.outputName, es, fields.head.position) → resolveUc(v)} - .recover { case e ⇒ Result(errors.add(path :+ name, resolveError(e), fields.head.position), None) → uc} + f.map{case PartialValue(v, es) ⇒ resolveValue(path + fields.head, fields, sfield.fieldType, sfield, resolveVal(v), uc) + .appendErrors(path + fields.head, es, fields.head.position) → resolveUc(v)} + .recover { case e ⇒ Result(errors.add(path + name, resolveError(e), fields.head.position), None) → uc} case DeferredFutureValue(df) ⇒ val p = Promise[Any]() resolveDeferred(uc, DeferredResult(Vector(df.map(d ⇒ Vector(Defer(p, d)))), p.future.flatMap { v ⇒ - resolveValue(path :+ fields.head.outputName, fields, sfield.fieldType, sfield, resolveVal(v), uc) match { + resolveValue(path + fields.head, fields, sfield.fieldType, sfield, resolveVal(v), uc) match { case r: Result ⇒ Future.successful(r) case er: DeferredResult ⇒ resolveDeferred(uc, er) } }.recover { - case e ⇒ Result(ErrorRegistry(path :+ fields.head.outputName, resolveError(e), fields.head.position), None) + case e ⇒ Result(ErrorRegistry(path + fields.head, resolveError(e), fields.head.position), None) })).flatMap(r ⇒ p.future map (r → resolveUc(_)) recover { case _ ⇒ r → uc}) } } catch { case NonFatal(e) ⇒ - Future.successful(Result(ErrorRegistry(path :+ fields.head.outputName, resolveError(e), fields.head.position), None) → uc) + Future.successful(Result(ErrorRegistry(path + fields.head, resolveError(e), fields.head.position), None) → uc) } resolve.flatMap { case (r : Result, newUc) ⇒ - Future.successful(accRes.addToMap(r, fields.head.outputName, isOptional(tpe, fields.head.name), path :+ fields.head.outputName, fields.head.position) → newUc) + Future.successful(accRes.addToMap(r, fields.head.outputName, isOptional(tpe, fields.head.name), path + fields.head, fields.head.position) → newUc) case (dr : DeferredResult, newUc) ⇒ - resolveDeferred(newUc, dr) map (accRes.addToMap(_, fields.head.outputName, isOptional(tpe, fields.head.name), path :+ fields.head.outputName, fields.head.position) → newUc) + resolveDeferred(newUc, dr) map (accRes.addToMap(_, fields.head.outputName, isOptional(tpe, fields.head.name), path + fields.head, fields.head.position) → newUc) } } } @@ -162,7 +162,7 @@ class Resolver[Ctx]( } def collectActionsPar( - path: Vector[String], + path: ExecutionPath, tpe: ObjectType[Ctx, _], value: Any, fields: CollectedFields, @@ -172,16 +172,16 @@ class Resolver[Ctx]( case (acc @ (_, None), _) ⇒ acc case (acc, CollectedField(name, origField, _)) if tpe.getField(schema, origField.name).isEmpty ⇒ acc case ((errors, s @ Some(acc)), CollectedField(name, origField, Failure(error))) ⇒ - errors.add(path :+ name, error) → (if (isOptional(tpe, origField.name)) Some(acc :+ (Vector(origField) → None)) else None) + errors.add(path + name, error) → (if (isOptional(tpe, origField.name)) Some(acc :+ (Vector(origField) → None)) else None) case ((errors, s @ Some(acc)), CollectedField(name, origField, Success(fields))) ⇒ - resolveField(userCtx, tpe, path :+ name, value, errors, name, fields) match { + resolveField(userCtx, tpe, path + name, value, errors, name, fields) match { case (updatedErrors, Some(result), updateCtx) ⇒ updatedErrors → Some(acc :+ (fields → Some((tpe.getField(schema, origField.name).head, updateCtx, result)))) case (updatedErrors, None, _) if isOptional(tpe, origField.name) ⇒ updatedErrors → Some(acc :+ (Vector(origField) → None)) case (updatedErrors, None, _) ⇒ updatedErrors → None } } - def resolveActionsPar(path: Vector[String], tpe: ObjectType[Ctx, _], actions: Actions, userCtx: Ctx, fieldsNamesOrdered: Vector[String]): Resolve = { + def resolveActionsPar(path: ExecutionPath, tpe: ObjectType[Ctx, _], actions: Actions, userCtx: Ctx, fieldsNamesOrdered: Vector[String]): Resolve = { val (errors, res) = actions def resolveUc(newUc: Option[MappedCtxUpdate[Ctx, Any, Any]], v: Any) = newUc map (_.ctxFn(v)) getOrElse userCtx @@ -208,31 +208,31 @@ class Resolver[Ctx]( case (astFields, None) ⇒ astFields.head → Result(ErrorRegistry.empty, None) case (astFields, Some((field, updateCtx, Value(v)))) ⇒ try { - astFields.head → resolveValue(path :+ astFields.head.outputName, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v)) + astFields.head → resolveValue(path + astFields.head, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v)) } catch { case NonFatal(e) ⇒ - astFields.head → Result(ErrorRegistry(path :+ astFields.head.outputName, resolveError(updateCtx, e), astFields.head.position), None) + astFields.head → Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position), None) } case (astFields, Some((field, updateCtx, PartialValue(v, es)))) ⇒ try { astFields.head → - resolveValue(path :+ astFields.head.outputName, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v)) - .appendErrors(path :+ astFields.head.outputName, es, astFields.head.position) + resolveValue(path + astFields.head, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v)) + .appendErrors(path + astFields.head, es, astFields.head.position) } catch { case NonFatal(e) ⇒ - astFields.head → Result(ErrorRegistry(path :+ astFields.head.outputName, resolveError(updateCtx, e), astFields.head.position).append(path :+ astFields.head.outputName, es, astFields.head.position), None) + astFields.head → Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position).append(path + astFields.head, es, astFields.head.position), None) } case (astFields, Some((field, updateCtx, TryValue(v)))) ⇒ v match { case Success(success) ⇒ try { - astFields.head → resolveValue(path :+ astFields.head.outputName, astFields, field.fieldType, field, resolveVal(updateCtx, success), resolveUc(updateCtx, success)) + astFields.head → resolveValue(path + astFields.head, astFields, field.fieldType, field, resolveVal(updateCtx, success), resolveUc(updateCtx, success)) } catch { case NonFatal(e) ⇒ - astFields.head → Result(ErrorRegistry(path :+ astFields.head.outputName, resolveError(updateCtx, e), astFields.head.position), None) + astFields.head → Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position), None) } case Failure(e) ⇒ - astFields.head → Result(ErrorRegistry(path :+ astFields.head.outputName, resolveError(updateCtx, e), astFields.head.position), None) + astFields.head → Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position), None) } case (astFields, Some((field, updateCtx, DeferredValue(deferred)))) ⇒ val promise = Promise[Any]() @@ -242,17 +242,17 @@ class Resolver[Ctx]( .flatMap { v ⇒ val uc = resolveUc(updateCtx, v) - resolveValue(path :+ astFields.head.outputName, astFields, field.fieldType, field, resolveVal(updateCtx, v), uc) match { + resolveValue(path + astFields.head, astFields, field.fieldType, field, resolveVal(updateCtx, v), uc) match { case r: Result ⇒ Future.successful(r) case er: DeferredResult ⇒ resolveDeferred(uc, er) } } .recover { - case e ⇒ Result(ErrorRegistry(path :+ astFields.head.outputName, resolveError(updateCtx, e), astFields.head.position), None) + case e ⇒ Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position), None) }) case (astFields, Some((field, updateCtx, FutureValue(future)))) ⇒ - val resolved = future.map(v ⇒ resolveValue(path :+ astFields.head.outputName, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v))).recover { - case e ⇒ Result(ErrorRegistry(path :+ astFields.head.outputName, resolveError(updateCtx, e), astFields.head.position), None) + val resolved = future.map(v ⇒ resolveValue(path + astFields.head, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v))).recover { + case e ⇒ Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position), None) } val deferred = resolved flatMap { @@ -267,10 +267,10 @@ class Resolver[Ctx]( astFields.head → DeferredResult(Vector(deferred), value) case (astFields, Some((field, updateCtx, PartialFutureValue(future)))) ⇒ val resolved = future.map {case PartialValue(v, es) ⇒ - resolveValue(path :+ astFields.head.outputName, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v)) - .appendErrors(path :+ astFields.head.outputName, es, astFields.head.position)} + resolveValue(path + astFields.head, astFields, field.fieldType, field, resolveVal(updateCtx, v), resolveUc(updateCtx, v)) + .appendErrors(path + astFields.head, es, astFields.head.position)} .recover { - case e ⇒ Result(ErrorRegistry(path :+ astFields.head.outputName, resolveError(updateCtx, e), astFields.head.position), None) + case e ⇒ Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position), None) } val deferred = resolved flatMap { @@ -290,20 +290,20 @@ class Resolver[Ctx]( promise.future.flatMap { v ⇒ val uc = resolveUc(updateCtx, v) - resolveValue(path :+ field.name, astFields, field.fieldType, field, resolveVal(updateCtx, v), uc) match { + resolveValue(path + astFields.head, astFields, field.fieldType, field, resolveVal(updateCtx, v), uc) match { case r: Result ⇒ Future.successful(r) case er: DeferredResult ⇒ resolveDeferred(uc, er) } } .recover{ - case e ⇒ Result(ErrorRegistry(path :+ field.name, resolveError(updateCtx, e), astFields.head.position), None) + case e ⇒ Result(ErrorRegistry(path + astFields.head, resolveError(updateCtx, e), astFields.head.position), None) }) } val simpleRes = resolvedValues.collect {case (af, r: Result) ⇒ af → r} val resSoFar = simpleRes.foldLeft(Result(errors, Some(marshaller.emptyMapNode(fieldsNamesOrdered)))) { - case (res, (astField, other)) ⇒ res addToMap (other, astField.outputName, isOptional(tpe, astField.name), path :+ astField.outputName, astField.position) + case (res, (astField, other)) ⇒ res addToMap (other, astField.outputName, isOptional(tpe, astField.name), path + astField, astField.position) } val complexRes = resolvedValues.collect{case (af, r: DeferredResult) ⇒ af → r} @@ -313,7 +313,7 @@ class Resolver[Ctx]( val allDeferred = complexRes.flatMap(_._2.deferred) val finalValue = Future.sequence(complexRes.map {case (astField, DeferredResult(_, future)) ⇒ future map (astField → _)}) map { results ⇒ results.foldLeft(resSoFar) { - case (res, (astField, other)) ⇒ res addToMap (other, astField.outputName, isOptional(tpe, astField.name), path :+ astField.outputName, astField.position) + case (res, (astField, other)) ⇒ res addToMap (other, astField.outputName, isOptional(tpe, astField.name), path + astField, astField.position) }.buildValue } @@ -350,7 +350,7 @@ class Resolver[Ctx]( } def resolveValue( - path: Vector[String], + path: ExecutionPath, astFields: Vector[ast.Field], tpe: OutputType[_], field: Field[Ctx, _], @@ -430,11 +430,11 @@ class Resolver[Ctx]( abst.typeOf(value, schema) match { case Some(obj) ⇒ resolveValue(path, astFields, obj, field, value, userCtx) case None ⇒ Result(ErrorRegistry(path, - new ExecutionError(s"Can't find appropriate subtype for field at path ${path mkString ", "}", exceptionHandler, sourceMapper, astFields.head.position.toList)), None) + new ExecutionError(s"Can't find appropriate subtype for field at path $path", exceptionHandler, sourceMapper, astFields.head.position.toList)), None) } } - def resolveSimpleListValue(simpleRes: Seq[Result], path: Vector[String], optional: Boolean, astPosition: Option[Position]): Result = { + def resolveSimpleListValue(simpleRes: Seq[Result], path: ExecutionPath, optional: Boolean, astPosition: Option[Position]): Result = { // this is very hot place, so resorting to mutability to minimize the footprint var errorReg = ErrorRegistry.empty @@ -466,7 +466,7 @@ class Resolver[Ctx]( def resolveField( userCtx: Ctx, tpe: ObjectType[Ctx, _], - path: Vector[String], + path: ExecutionPath, value: Any, errors: ErrorRegistry, name: String, @@ -628,8 +628,8 @@ class Resolver[Ctx]( } } - def collectProjections(path: Vector[String], field: Field[Ctx, _], astFields: Vector[ast.Field], maxLevel: Int): Vector[ProjectedName] = { - def loop(path: Vector[String], tpe: OutputType[_], astFields: Vector[ast.Field], currLevel: Int): Vector[ProjectedName] = + def collectProjections(path: ExecutionPath, field: Field[Ctx, _], astFields: Vector[ast.Field], maxLevel: Int): Vector[ProjectedName] = { + def loop(path: ExecutionPath, tpe: OutputType[_], astFields: Vector[ast.Field], currLevel: Int): Vector[ProjectedName] = if (currLevel > maxLevel) Vector.empty else tpe match { case OptionType(ofType) ⇒ loop(path, ofType, astFields, currLevel) @@ -647,7 +647,7 @@ class Resolver[Ctx]( case _ ⇒ field.name } - ProjectedName(projectedName, loop(path :+ projectedName, field.fieldType, fields, currLevel + 1)) + ProjectedName(projectedName, loop(path + projectedName, field.fieldType, fields, currLevel + 1)) } case Failure(_) ⇒ Vector.empty } @@ -669,16 +669,16 @@ class Resolver[Ctx]( tpe.isInstanceOf[OptionType[_]] trait Resolve { - def appendErrors(path: Vector[String], errors: Vector[Throwable], position: Option[Position]): Resolve + def appendErrors(path: ExecutionPath, errors: Vector[Throwable], position: Option[Position]): Resolve } case class DeferredResult(deferred: Vector[Future[Vector[Defer]]], futureValue: Future[Result]) extends Resolve { - def appendErrors(path: Vector[String], errors: Vector[Throwable], position: Option[Position]) = copy(futureValue = futureValue map (_.appendErrors(path, errors, position))) + def appendErrors(path: ExecutionPath, errors: Vector[Throwable], position: Option[Position]) = copy(futureValue = futureValue map (_.appendErrors(path, errors, position))) } case class Defer(promise: Promise[Any], deferred: Deferred[Any]) case class Result(errors: ErrorRegistry, value: Option[Any /* Either marshaller.Node or marshaller.MapBuilder */]) extends Resolve { - def addToMap(other: Result, key: String, optional: Boolean, path: Vector[String], position: Option[Position]) = + def addToMap(other: Result, key: String, optional: Boolean, path: ExecutionPath, position: Option[Position]) = copy( errors = if (!optional && other.value.isEmpty && other.errors.errorList.isEmpty) @@ -695,7 +695,7 @@ class Resolver[Ctx]( def builderValue = value.asInstanceOf[Option[marshaller.MapBuilder]] def buildValue = copy(value = builderValue map marshaller.mapNode) - def appendErrors(path: Vector[String], e: Vector[Throwable], position: Option[Position]) = copy(errors = errors.append(path, e, position)) + def appendErrors(path: ExecutionPath, e: Vector[Throwable], position: Option[Position]) = copy(errors = errors.append(path, e, position)) } } diff --git a/src/main/scala/sangria/execution/ResultResolver.scala b/src/main/scala/sangria/execution/ResultResolver.scala index fba80f52..b7a58398 100644 --- a/src/main/scala/sangria/execution/ResultResolver.scala +++ b/src/main/scala/sangria/execution/ResultResolver.scala @@ -23,7 +23,7 @@ class ResultResolver(val marshaller: ResultMarshaller, exceptionHandler: Executo } def resolveError(error: Throwable) = - marshalResult(None, marshalErrors(ErrorRegistry(Vector.empty, error))) + marshalResult(None, marshalErrors(ErrorRegistry(ExecutionPath.empty, error))) def handleSupportedError(e: Throwable) = { val handeled = exceptionHandler(marshaller → e) @@ -45,22 +45,22 @@ class ResultResolver(val marshaller: ResultMarshaller, exceptionHandler: Executo } case class ErrorRegistry(errorList: Vector[marshaller.Node]) { - def add(path: Vector[String], error: String) = + def add(path: ExecutionPath, error: String) = copy(errorList:+ errorNode(path, None, Seq("message" → marshaller.scalarNode(error, "String", Set.empty)))) - def add(path: Vector[String], error: Throwable) = + def add(path: ExecutionPath, error: Throwable) = copy(errorList ++ createErrorPaths(path, error)) - def append(path: Vector[String], errors: Vector[Throwable], position: Option[Position]) = + def append(path: ExecutionPath, errors: Vector[Throwable], position: Option[Position]) = copy(errors.map(e ⇒ errorNode(path, position map singleLocation, handleException(e))) ++ errorList) - def add(path: Vector[String], error: Throwable, position: Option[Position]) = + def add(path: ExecutionPath, error: Throwable, position: Option[Position]) = copy(errorList :+ errorNode(path, position map singleLocation, handleException(error))) def add(other: ErrorRegistry) = ErrorRegistry(errorList ++ other.errorList) - def createErrorPaths(path: Vector[String], e: Throwable) = e match { + def createErrorPaths(path: ExecutionPath, e: Throwable) = e match { case e: WithViolations if e.violations.nonEmpty ⇒ e.violations map { v ⇒ errorNode(path, getLocations(v), Seq("message" → marshaller.scalarNode(v.errorMessage, "String", Set.empty))) @@ -96,14 +96,14 @@ class ResultResolver(val marshaller: ResultMarshaller, exceptionHandler: Executo object ErrorRegistry { val empty = ErrorRegistry(Vector.empty) - def apply(path: Vector[String], error: Throwable): ErrorRegistry = empty.add(path, error) - def apply(path: Vector[String], error: Throwable, pos: Option[Position]): ErrorRegistry = empty.add(path, error, pos) + def apply(path: ExecutionPath, error: Throwable): ErrorRegistry = empty.add(path, error) + def apply(path: ExecutionPath, error: Throwable, pos: Option[Position]): ErrorRegistry = empty.add(path, error, pos) } - def errorNode(path: Vector[String], location: Option[marshaller.Node], errorFields: Seq[(String, marshaller.Node)]) = { + def errorNode(path: ExecutionPath, location: Option[marshaller.Node], errorFields: Seq[(String, marshaller.Node)]) = { val names = errorFields.map(_._1) - val namesWithPath = if (path.nonEmpty) names :+ "field" else names + val namesWithPath = if (path.nonEmpty) names :+ "path" else names val namesWithLoc = if (location.isDefined) namesWithPath :+ "locations" else namesWithPath val builder = errorFields.foldLeft(marshaller.emptyMapNode(namesWithLoc)) { @@ -112,7 +112,7 @@ class ResultResolver(val marshaller: ResultMarshaller, exceptionHandler: Executo val builderWithPath = if (path.nonEmpty) - marshaller.addMapNodeElem(builder, "field", marshaller.scalarNode(path mkString ".", "String", Set.empty), optional = false) + marshaller.addMapNodeElem(builder, "path", path.marshal(marshaller), optional = false) else builder diff --git a/src/main/scala/sangria/execution/ValueCollector.scala b/src/main/scala/sangria/execution/ValueCollector.scala index 1e1438e4..51f35b5e 100644 --- a/src/main/scala/sangria/execution/ValueCollector.scala +++ b/src/main/scala/sangria/execution/ValueCollector.scala @@ -16,7 +16,7 @@ class ValueCollector[Ctx, Input](schema: Schema[_, _], inputVars: Input, sourceM import coercionHelper._ - private val argumentCache = TrieMap[(Vector[String], List[ast.Argument]), Try[Args]]() + private val argumentCache = TrieMap[(ExecutionPath.PathCacheKey, List[ast.Argument]), Try[Args]]() def getVariableValues(definitions: List[ast.VariableDefinition]): Try[Map[String, VariableValue]] = if (!um.isMapNode(inputVars)) @@ -62,11 +62,11 @@ class ValueCollector[Ctx, Input](schema: Schema[_, _], inputVars: Input, sourceM private val emptyArgs = Success(Args.empty) - def getFieldArgumentValues(path: Vector[String], argumentDefs: List[Argument[_]], argumentAsts: List[ast.Argument], variables: Map[String, VariableValue]): Try[Args] = + def getFieldArgumentValues(path: ExecutionPath, argumentDefs: List[Argument[_]], argumentAsts: List[ast.Argument], variables: Map[String, VariableValue]): Try[Args] = if(argumentDefs.isEmpty) emptyArgs else - argumentCache.getOrElseUpdate(path → argumentAsts, getArgumentValues(argumentDefs, argumentAsts, variables)) + argumentCache.getOrElseUpdate(path.cacheKey → argumentAsts, getArgumentValues(argumentDefs, argumentAsts, variables)) def getArgumentValues(argumentDefs: List[Argument[_]], argumentAsts: List[ast.Argument], variables: Map[String, VariableValue]): Try[Args] = if (argumentDefs.isEmpty) diff --git a/src/main/scala/sangria/schema/Context.scala b/src/main/scala/sangria/schema/Context.scala index 34d773a0..9c6bef20 100644 --- a/src/main/scala/sangria/schema/Context.scala +++ b/src/main/scala/sangria/schema/Context.scala @@ -1,6 +1,6 @@ package sangria.schema -import sangria.execution.{FieldTag, DeprecationTracker, ValueCoercionHelper, Resolver} +import sangria.execution._ import sangria.marshalling._ import sangria.parser.SourceMapper @@ -246,7 +246,7 @@ case class Context[Ctx, Val]( sourceMapper: Option[SourceMapper], deprecationTracker: DeprecationTracker, astFields: Vector[ast.Field], - path: Vector[String]) extends WithArguments with WithInputTypeRendering[Ctx] + path: ExecutionPath) extends WithArguments with WithInputTypeRendering[Ctx] case class Args(raw: Map[String, Any]) extends AnyVal { def arg[T](arg: Argument[T]): T = raw.get(arg.name).fold(None.asInstanceOf[T])(_.asInstanceOf[T]) diff --git a/src/test/scala/sangria/execution/DeprecationTrackerSpec.scala b/src/test/scala/sangria/execution/DeprecationTrackerSpec.scala index f1032bc3..5e18a0d9 100644 --- a/src/test/scala/sangria/execution/DeprecationTrackerSpec.scala +++ b/src/test/scala/sangria/execution/DeprecationTrackerSpec.scala @@ -61,7 +61,7 @@ class DeprecationTrackerSpec extends WordSpec with Matchers with FutureResultSup Executor.execute(schema, query, deprecationTracker = deprecationTracker).await deprecationTracker.times.get should be (1) - deprecationTracker.ctx.get.path should be (List("deprecated")) + deprecationTracker.ctx.get.path should be (ExecutionPath.empty + "deprecated") deprecationTracker.ctx.get.field.name should be ("deprecated") } @@ -79,7 +79,7 @@ class DeprecationTrackerSpec extends WordSpec with Matchers with FutureResultSup Executor.execute(schema, query, deprecationTracker = deprecationTracker).await deprecationTracker.times.get should be (1) - deprecationTracker.ctx.get.path should be ("nested" :: "aa" :: "bb" :: Nil) + deprecationTracker.ctx.get.path should be (ExecutionPath.empty + "nested" + "aa" + "bb") deprecationTracker.ctx.get.field.name should be ("deprecated") deprecationTracker.ctx.get.parentType.name should be ("TestType") } @@ -100,7 +100,7 @@ class DeprecationTrackerSpec extends WordSpec with Matchers with FutureResultSup Executor.execute(schema, query, deprecationTracker = deprecationTracker).await deprecationTracker.times.get should be (1) - deprecationTracker.ctx.get.path should be ("foo" :: Nil) + deprecationTracker.ctx.get.path should be (ExecutionPath.empty + "foo") deprecationTracker.ctx.get.field.name should be ("foo") deprecationTracker.ctx.get.parentType.name should be ("TestType") } diff --git a/src/test/scala/sangria/execution/ExceptionHandlingSpec.scala b/src/test/scala/sangria/execution/ExceptionHandlingSpec.scala index c000f3f5..6586f77c 100644 --- a/src/test/scala/sangria/execution/ExceptionHandlingSpec.scala +++ b/src/test/scala/sangria/execution/ExceptionHandlingSpec.scala @@ -46,15 +46,15 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with FutureResultSupp "errors" → List( Map( "message" → "Internal server error", - "field" → "error", + "path" → List("error"), "locations" → List(Map("line" → 6, "column" → 11))), Map( "message" → "Internal server error", - "field" → "tryError", + "path" → List("tryError"), "locations" → List(Map("line" → 4, "column" → 11))), Map( "message" → "Internal server error", - "field" → "futureError", + "path" → List("futureError"), "locations" → List(Map("line" → 7, "column" → 11)))))) } @@ -81,11 +81,11 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with FutureResultSupp "errors" → List( Map( "message" → "Boom!", - "field" → "error", + "path" → List("error"), "locations" → List(Map("line" → 3, "column" → 11))), Map( "message" → "Boom!", - "field" → "futureError", + "path" → List("futureError"), "locations" → List(Map("line" → 4, "column" → 11)))))) } @@ -111,13 +111,13 @@ class ExceptionHandlingSpec extends WordSpec with Matchers with FutureResultSupp "errors" → List( Map( "message" → "Boom!", - "field" → "error", + "path" → List("error"), "foo" → List("bar", 1234), "baz" → "Test", "locations" → List(Map("line" → 3, "column" → 11))), Map( "message" → "Boom!", - "field" → "futureError", + "path" → List("futureError"), "foo" → List("bar", 1234), "baz" → "Test", "locations" → List(Map("line" → 4, "column" → 11)))))) diff --git a/src/test/scala/sangria/execution/ExecutorSpec.scala b/src/test/scala/sangria/execution/ExecutorSpec.scala index d8889b92..f7691338 100644 --- a/src/test/scala/sangria/execution/ExecutorSpec.scala +++ b/src/test/scala/sangria/execution/ExecutorSpec.scala @@ -218,17 +218,17 @@ class ExecutorSpec extends WordSpec with Matchers with FutureResultSupport { "errors" → List( Map( "message" → "Max query depth 6 is reached.", - "field" → "d1.deep.deep.deep.deep.deep.b", + "path" → List("d1", "deep", "deep", "deep", "deep", "deep", "b"), "locations" → List(Map("line" → 10, "column" → 23)) ), Map( "message" → "Max query depth 6 is reached.", - "field" → "d2.deep.deep.deep.deep.deep.b", + "path" → List("d2", "deep", "deep", "deep", "deep", "deep", "b"), "locations" → List(Map("line" → 24, "column" → 23)) ), Map( "message" → "Max query depth 6 is reached.", - "field" → "d2.deep.deep.deep.deep.deep.d", + "path" → List("d2", "deep", "deep", "deep", "deep", "deep", "d"), "locations" → List(Map("line" → 25, "column" → 23)) ) ) @@ -360,23 +360,23 @@ class ExecutorSpec extends WordSpec with Matchers with FutureResultSupport { errors should (have(size(5)) and contain(Map( - "field" → "syncError", + "path" → List("syncError"), "locations" → List(Map("line" → 4, "column" → 14)), "message" → "Error getting syncError")) and contain(Map( - "field" → "asyncReject", + "path" → List("asyncReject"), "locations" → List(Map("line" → 6, "column" → 11)), "message" → "Error getting asyncReject")) and contain(Map( "message" → "Error getting asyncDeferError", - "field" → "asyncDeferError", + "path" → List("asyncDeferError"), "locations" → List(Map("line" → 7, "column" → 12)))) and contain(Map( "message" → "Error getting syncDeferError", - "field" → "syncDeferError", + "path" → List("syncDeferError"), "locations" → List(Map("line" → 9, "column" → 15)))) and contain(Map( - "field" → "asyncError", + "path" → List("asyncError"), "locations" → List(Map("line" → 8, "column" → 15)), "message" → "Error getting asyncError"))) } @@ -628,11 +628,11 @@ class ExecutorSpec extends WordSpec with Matchers with FutureResultSupport { "errors" → List( Map( "message" → "error in resolver", - "field" → "defFail", + "path" → List("defFail"), "locations" → List(Map("line" → 3, "column" → 11))), Map( "message" → "error in resolver", - "field" → "defFutFail", + "path" → List("defFutFail"), "locations" → List(Map("line" → 4, "column" → 11)))))) } @@ -682,10 +682,10 @@ class ExecutorSpec extends WordSpec with Matchers with FutureResultSupport { errors should ( have(size(4)) and - contain(Map("message" → "error 1", "field" → "eager", "locations" → Vector(Map("line" → 1, "column" → 2)))) and - contain(Map("message" → "error 2", "field" → "eager", "locations" → Vector(Map("line" → 1, "column" → 2)))) and - contain(Map("message" → "error 3", "field" → "future", "locations" → Vector(Map("line" → 1, "column" → 9)))) and - contain(Map("message" → "error 4", "field" → "future", "locations" → Vector(Map("line" → 1, "column" → 9))))) + contain(Map("message" → "error 1", "path" → List("eager"), "locations" → Vector(Map("line" → 1, "column" → 2)))) and + contain(Map("message" → "error 2", "path" → List("eager"), "locations" → Vector(Map("line" → 1, "column" → 2)))) and + contain(Map("message" → "error 3", "path" → List("future"), "locations" → Vector(Map("line" → 1, "column" → 9)))) and + contain(Map("message" → "error 4", "path" → List("future"), "locations" → Vector(Map("line" → 1, "column" → 9))))) } } } diff --git a/src/test/scala/sangria/execution/ListsSpec.scala b/src/test/scala/sangria/execution/ListsSpec.scala index 1cd9a0b1..f5d8be75 100644 --- a/src/test/scala/sangria/execution/ListsSpec.scala +++ b/src/test/scala/sangria/execution/ListsSpec.scala @@ -65,7 +65,7 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { FutureValue(Future.failed(new IllegalStateException("Boom"))), Map( "data" → Map("nest" → Map("test" → null)), - "errors" → List(Map("message" → "Boom", "field" → "nest.test", "locations" → List(Map("line" → 1, "column" → 10)))))) + "errors" → List(Map("message" → "Boom", "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) } } @@ -80,7 +80,7 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) } @@ -98,7 +98,7 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) "Rejected" in check(tpe, FutureValue(Future.failed(new IllegalStateException("Boom"))), @@ -106,7 +106,7 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { "data" → Map("nest" → null), "errors" → List(Map( "message" → "Boom", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) } } @@ -120,7 +120,7 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { "data" → Map("nest" → Map("test" → null)), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) "Returns null" in check(tpe, Value(null), Map("data" → Map("nest" → Map("test" → null)))) } @@ -135,14 +135,14 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { "data" → Map("nest" → Map("test" → null)), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) "Returns null" in check(tpe, FutureValue(success(null)), Map("data" → Map("nest" → Map("test" → null)))) "Rejected" in check(tpe, FutureValue(Future.failed(new IllegalStateException("Boom"))), Map( "data" → Map("nest" → Map("test" → null)), - "errors" → List(Map("message" → "Boom", "field" → "nest.test", "locations" → List(Map("line" → 1, "column" → 10)))))) + "errors" → List(Map("message" → "Boom", "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) } } @@ -155,13 +155,13 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) "Returns null" in check(tpe, Value(null), Map( "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) } @@ -175,19 +175,19 @@ class ListsSpec extends WordSpec with Matchers with FutureResultSupport { "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) "Returns null" in check(tpe, FutureValue(success(null)), Map( "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.test", + "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) "Rejected" in check(tpe, FutureValue(Future.failed(new IllegalStateException("Boom"))), Map( "data" → Map("nest" → null), - "errors" → List(Map("message" → "Boom", "field" → "nest.test", "locations" → List(Map("line" → 1, "column" → 10)))))) + "errors" → List(Map("message" → "Boom", "path" → List("nest", "test"), "locations" → List(Map("line" → 1, "column" → 10)))))) } } } diff --git a/src/test/scala/sangria/execution/MutationSpec.scala b/src/test/scala/sangria/execution/MutationSpec.scala index 8d3533a8..e97a5933 100644 --- a/src/test/scala/sangria/execution/MutationSpec.scala +++ b/src/test/scala/sangria/execution/MutationSpec.scala @@ -229,20 +229,20 @@ class MutationSpec extends WordSpec with Matchers with GraphQlSupport { List( Map( "message" → "Cannot change the number", - "field" → "third", + "path" → List("third"), "locations" → List(Map("line" → 9, "column" → 11))), // todo fix duplicate errors Map( "message" → "Cannot change the number", - "field" → "third", + "path" → List("third"), "locations" → List(Map("line" → 9, "column" → 11))), Map("message" → "Cannot change the number", - "field" → "sixth", + "path" → List("sixth"), "locations" → List(Map("line" → 18, "column" → 11))), Map("message" → "error in resolver", - "field" → "defFail", + "path" → List("defFail"), "locations" → List(Map("line" → 24, "column" → 11))), Map("message" → "error in resolver", - "field" → "defFutFail", + "path" → List("defFutFail"), "locations" → List(Map("line" → 30, "column" → 11)))), userContext = UserContext(10), resolver = new Resolver diff --git a/src/test/scala/sangria/execution/NotNullSpec.scala b/src/test/scala/sangria/execution/NotNullSpec.scala index 9f83fddf..5667787b 100644 --- a/src/test/scala/sangria/execution/NotNullSpec.scala +++ b/src/test/scala/sangria/execution/NotNullSpec.scala @@ -62,7 +62,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G """, Map( "data" → Map("sync" → null), - "errors" → List(Map("message" → "sync", "field" → "sync", "locations" → List(Map("line" → 3, "column" → 11)))))) + "errors" → List(Map("message" → "sync", "path" → List("sync"), "locations" → List(Map("line" → 3, "column" → 11)))))) "nulls a nullable field that throws in a promise" in check( new ThrowingSubject, @@ -73,7 +73,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G """, Map( "data" → Map("promise" → null), - "errors" → List(Map("message" → "promise", "field" → "promise", "locations" → List(Map("line" → 3, "column" → 11)))))) + "errors" → List(Map("message" → "promise", "path" → List("promise"), "locations" → List(Map("line" → 3, "column" → 11)))))) "nulls a synchronously returned object that contains a non-nullable field that throws synchronously" in check( new ThrowingSubject, @@ -86,7 +86,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G """, Map( "data" → Map("nest" → null), - "errors" → List(Map("message" → "nonNullSync", "field" → "nest.nonNullSync", "locations" → List(Map("line" → 4, "column" → 13)))))) + "errors" → List(Map("message" → "nonNullSync", "path" → List("nest", "nonNullSync"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls a synchronously returned object that contains a non-nullable field that throws in a promise" in check( new ThrowingSubject, @@ -99,7 +99,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G """, Map( "data" → Map("nest" → null), - "errors" → List(Map("message" → "nonNullPromise", "field" → "nest.nonNullPromise", "locations" → List(Map("line" → 4, "column" → 13)))))) + "errors" → List(Map("message" → "nonNullPromise", "path" → List("nest", "nonNullPromise"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls an object returned in a promise that contains a non-nullable field that throws synchronously" in check( new ThrowingSubject, @@ -112,7 +112,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G """, Map( "data" → Map("promiseNest" → null), - "errors" → List(Map("message" → "nonNullSync", "field" → "promiseNest.nonNullSync", "locations" → List(Map("line" → 4, "column" → 13)))))) + "errors" → List(Map("message" → "nonNullSync", "path" → List("promiseNest", "nonNullSync"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls an object returned in a promise that contains a non-nullable field that throws in a promise" in check( new ThrowingSubject, @@ -125,7 +125,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G """, Map( "data" → Map("promiseNest" → null), - "errors" → List(Map("message" → "nonNullPromise", "field" → "promiseNest.nonNullPromise", "locations" → List(Map("line" → 4, "column" → 13)))))) + "errors" → List(Map("message" → "nonNullPromise", "path" → List("promiseNest", "nonNullPromise"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls a complex tree of nullable fields that throw" in checkErrors( new ThrowingSubject, @@ -184,18 +184,18 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G ) ), List( - Map("message" → "sync", "field" → "nest.sync", "locations" → List(Map("line" → 4, "column" → 13))), - Map("message" → "sync", "field" → "nest.nest.sync", "locations" → List(Map("line" → 7, "column" → 15))), - Map("message" → "sync", "field" → "nest.promiseNest.sync", "locations" → List(Map("line" → 11, "column" → 15))), - Map("message" → "sync", "field" → "promiseNest.sync", "locations" → List(Map("line" → 16, "column" → 13))), - Map("message" → "sync", "field" → "promiseNest.nest.sync", "locations" → List(Map("line" → 19, "column" → 15))), - Map("message" → "sync", "field" → "promiseNest.promiseNest.sync", "locations" → List(Map("line" → 23, "column" → 15))), - Map("message" → "promise", "field" → "nest.promise", "locations" → List(Map("line" → 5, "column" → 13))), - Map("message" → "promise", "field" → "nest.nest.promise", "locations" → List(Map("line" → 8, "column" → 15))), - Map("message" → "promise", "field" → "nest.promiseNest.promise", "locations" → List(Map("line" → 12, "column" → 15))), - Map("message" → "promise", "field" → "promiseNest.promise", "locations" → List(Map("line" → 17, "column" → 13))), - Map("message" → "promise", "field" → "promiseNest.nest.promise", "locations" → List(Map("line" → 20, "column" → 15))), - Map("message" → "promise", "field" → "promiseNest.promiseNest.promise", "locations" → List(Map("line" → 24, "column" → 15))) + Map("message" → "sync", "path" → List("nest", "sync"), "locations" → List(Map("line" → 4, "column" → 13))), + Map("message" → "sync", "path" → List("nest", "nest", "sync"), "locations" → List(Map("line" → 7, "column" → 15))), + Map("message" → "sync", "path" → List("nest", "promiseNest", "sync"), "locations" → List(Map("line" → 11, "column" → 15))), + Map("message" → "sync", "path" → List("promiseNest", "sync"), "locations" → List(Map("line" → 16, "column" → 13))), + Map("message" → "sync", "path" → List("promiseNest", "nest", "sync"), "locations" → List(Map("line" → 19, "column" → 15))), + Map("message" → "sync", "path" → List("promiseNest", "promiseNest", "sync"), "locations" → List(Map("line" → 23, "column" → 15))), + Map("message" → "promise", "path" → List("nest", "promise"), "locations" → List(Map("line" → 5, "column" → 13))), + Map("message" → "promise", "path" → List("nest", "nest", "promise"), "locations" → List(Map("line" → 8, "column" → 15))), + Map("message" → "promise", "path" → List("nest", "promiseNest", "promise"), "locations" → List(Map("line" → 12, "column" → 15))), + Map("message" → "promise", "path" → List("promiseNest", "promise"), "locations" → List(Map("line" → 17, "column" → 13))), + Map("message" → "promise", "path" → List("promiseNest", "nest", "promise"), "locations" → List(Map("line" → 20, "column" → 15))), + Map("message" → "promise", "path" → List("promiseNest", "promiseNest", "promise"), "locations" → List(Map("line" → 24, "column" → 15))) )) "nulls the first nullable object after a field throws in a long chain of fields that are non-null" in checkErrors( @@ -257,19 +257,19 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G List( Map( "message" → "nonNullSync", - "field" → "nest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullSync", + "path" → List("nest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullSync"), "locations" → List(Map("line" → 8, "column" → 21))), Map( "message" → "nonNullSync", - "field" → "promiseNest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullSync", + "path" → List("promiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullSync"), "locations" → List(Map("line" → 19, "column" → 21))), Map( "message" → "nonNullPromise", - "field" → "anotherNest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullPromise", + "path" → List("anotherNest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullPromise"), "locations" → List(Map("line" → 30, "column" → 21))), Map( "message" → "nonNullPromise", - "field" → "anotherPromiseNest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullPromise", + "path" → List("anotherPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullPromise"), "locations" → List(Map("line" → 41, "column" → 21))) )) @@ -304,7 +304,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.nonNullSync", + "path" → List("nest", "nonNullSync"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls a synchronously returned object that contains a non-nullable field that returns null in a promise" in check( @@ -320,7 +320,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G "data" → Map("nest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.nonNullPromise", + "path" → List("nest", "nonNullPromise"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls an object returned in a promise that contains a non-nullable field that returns null synchronously" in check( @@ -336,7 +336,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G "data" → Map("promiseNest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "promiseNest.nonNullSync", + "path" → List("promiseNest", "nonNullSync"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls an object returned in a promise that contains a non-nullable field that returns null ina a promise" in check( @@ -352,7 +352,7 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G "data" → Map("promiseNest" → null), "errors" → List(Map( "message" → "Cannot return null for non-nullable type", - "field" → "promiseNest.nonNullPromise", + "path" → List("promiseNest", "nonNullPromise"), "locations" → List(Map("line" → 4, "column" → 13)))))) "nulls a complex tree of nullable fields that return null" in checkErrors( @@ -472,19 +472,19 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G List( Map( "message" → "Cannot return null for non-nullable type", - "field" → "nest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullSync", + "path" → List("nest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullSync"), "locations" → List(Map("line" → 8, "column" → 21))), Map( "message" → "Cannot return null for non-nullable type", - "field" → "promiseNest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullSync", + "path" → List("promiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullSync"), "locations" → List(Map("line" → 19, "column" → 21))), Map( "message" → "Cannot return null for non-nullable type", - "field" → "anotherNest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullPromise", + "path" → List("anotherNest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullPromise"), "locations" → List(Map("line" → 30, "column" → 21))), Map( "message" → "Cannot return null for non-nullable type", - "field" → "anotherPromiseNest.nonNullNest.nonNullPromiseNest.nonNullNest.nonNullPromiseNest.nonNullPromise", + "path" → List("anotherPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullNest", "nonNullPromiseNest", "nonNullPromise"), "locations" → List(Map("line" → 41, "column" → 21))) )) @@ -493,27 +493,27 @@ class NotNullSpec extends WordSpec with Matchers with FutureResultSupport with G "query Q { nonNullSync }", Map( "data" → null, - "errors" → List(Map("message" → "nonNullSync", "field" → "nonNullSync", "locations" → List(Map("line" → 1, "column" → 11)))))) + "errors" → List(Map("message" → "nonNullSync", "path" → List("nonNullSync"), "locations" → List(Map("line" → 1, "column" → 11)))))) "nulls the top level if async non-nullable field errors" in check( new ThrowingSubject, "query Q { nonNullPromise }", Map( "data" → null, - "errors" → List(Map("message" → "nonNullPromise", "field" → "nonNullPromise", "locations" → List(Map("line" → 1, "column" → 11)))))) + "errors" → List(Map("message" → "nonNullPromise", "path" → List("nonNullPromise"), "locations" → List(Map("line" → 1, "column" → 11)))))) "nulls the top level if sync non-nullable field returns null" in check( new NullingSubject, "query Q { nonNullSync }", Map( "data" → null, - "errors" → List(Map("message" → "Cannot return null for non-nullable type", "field" → "nonNullSync", "locations" → List(Map("line" → 1, "column" → 11)))))) + "errors" → List(Map("message" → "Cannot return null for non-nullable type", "path" → List("nonNullSync"), "locations" → List(Map("line" → 1, "column" → 11)))))) "nulls the top level if async non-nullable field resolves null" in check( new NullingSubject, "query Q { nonNullPromise }", Map( "data" → null, - "errors" → List(Map("message" → "Cannot return null for non-nullable type", "field" → "nonNullPromise", "locations" → List(Map("line" → 1, "column" → 11)))))) + "errors" → List(Map("message" → "Cannot return null for non-nullable type", "path" → List("nonNullPromise"), "locations" → List(Map("line" → 1, "column" → 11)))))) } }