diff --git a/adapters/http4s/src/test/scala/caliban/Http4sAdapterSpec.scala b/adapters/http4s/src/test/scala/caliban/Http4sAdapterSpec.scala index 9c80f4a5f..91ce988e3 100644 --- a/adapters/http4s/src/test/scala/caliban/Http4sAdapterSpec.scala +++ b/adapters/http4s/src/test/scala/caliban/Http4sAdapterSpec.scala @@ -130,7 +130,7 @@ object Http4sAdapterSpec extends DefaultRunnableSpec { val fileURL: URL = getClass.getResource(s"/$fileName") val query: String = - """{ "query": "mutation ($file: Upload!) { uploadFile(file: $file) { hash, path, filename, mimetype } }", "variables": { "file": null }}""" + """{ "query": "mutation ($file: UploadInput!) { uploadFile(file: $file) { hash, path, filename, mimetype } }", "variables": { "file": null }}""" val request = basicRequest .post(uri) @@ -169,7 +169,7 @@ object Http4sAdapterSpec extends DefaultRunnableSpec { val file2URL: URL = getClass.getResource(s"/$file2Name") val query: String = - """{ "query": "mutation ($files: [Upload!]!) { uploadFiles(files: $files) { hash, path, filename, mimetype } }", "variables": { "files": [null, null] }}""" + """{ "query": "mutation ($files: [UploadInput!]!) { uploadFiles(files: $files) { hash, path, filename, mimetype } }", "variables": { "files": [null, null] }}""" val request = basicRequest .post(uri) diff --git a/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala b/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala index 0c51e1019..96b0e57c7 100644 --- a/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala +++ b/adapters/play/src/test/scala/caliban/PlayAdapterSpec.scala @@ -133,7 +133,7 @@ object PlayAdapterSpec extends DefaultRunnableSpec { val fileURL: URL = getClass.getResource(s"/$fileName") val query: String = - """{ "query": "mutation ($file: Upload!) { uploadFile(file: $file) { hash, path, filename, mimetype } }", "variables": { "file": null }}""" + """{ "query": "mutation ($file: UploadInput!) { uploadFile(file: $file) { hash, path, filename, mimetype } }", "variables": { "file": null }}""" val request = basicRequest .post(uri) @@ -172,7 +172,7 @@ object PlayAdapterSpec extends DefaultRunnableSpec { val file2URL: URL = getClass.getResource(s"/$file2Name") val query: String = - """{ "query": "mutation ($files: [Upload!]!) { uploadFiles(files: $files) { hash, path, filename, mimetype } }", "variables": { "files": [null, null] }}""" + """{ "query": "mutation ($files: [UploadInput!]!) { uploadFiles(files: $files) { hash, path, filename, mimetype } }", "variables": { "files": [null, null] }}""" val request = basicRequest .post(uri) diff --git a/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala b/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala index 17a4cf464..b7b6243be 100644 --- a/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala +++ b/benchmarks/src/main/scala/caliban/GraphQLBenchmarks.scala @@ -343,7 +343,6 @@ class GraphQLBenchmarks { object SangriaNewValidator { import sangria.validation.RuleBasedQueryValidator - import sangria.validation.ValidationRule import sangria.validation.rules._ val allRules = diff --git a/core/src/main/scala/caliban/introspection/Introspector.scala b/core/src/main/scala/caliban/introspection/Introspector.scala index 9635ec1fe..b2425d21e 100644 --- a/core/src/main/scala/caliban/introspection/Introspector.scala +++ b/core/src/main/scala/caliban/introspection/Introspector.scala @@ -21,7 +21,7 @@ object Introspector extends IntrospectionDerivation { "The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument." ), Set(__DirectiveLocation.FIELD, __DirectiveLocation.FRAGMENT_SPREAD, __DirectiveLocation.INLINE_FRAGMENT), - List(__InputValue("if", None, () => Types.boolean, None)) + List(__InputValue("if", None, () => Types.makeNonNull(Types.boolean), None)) ), __Directive( "include", @@ -29,7 +29,7 @@ object Introspector extends IntrospectionDerivation { "The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument." ), Set(__DirectiveLocation.FIELD, __DirectiveLocation.FRAGMENT_SPREAD, __DirectiveLocation.INLINE_FRAGMENT), - List(__InputValue("if", None, () => Types.boolean, None)) + List(__InputValue("if", None, () => Types.makeNonNull(Types.boolean), None)) ) ) diff --git a/core/src/main/scala/caliban/parsing/adt/Type.scala b/core/src/main/scala/caliban/parsing/adt/Type.scala index 6e84eaa1d..618215e1d 100644 --- a/core/src/main/scala/caliban/parsing/adt/Type.scala +++ b/core/src/main/scala/caliban/parsing/adt/Type.scala @@ -10,6 +10,12 @@ sealed trait Type { self => case Type.NamedType(name, nonNull) => if (nonNull) s"$name!" else name case Type.ListType(ofType, nonNull) => if (nonNull) s"[$ofType]!" else s"[$ofType]" } + + def toNullable: Type = + self match { + case Type.NamedType(name, _) => Type.NamedType(name, nonNull = false) + case Type.ListType(ofType, _) => Type.ListType(ofType, nonNull = false) + } } object Type { diff --git a/core/src/main/scala/caliban/validation/DefaultValueValidator.scala b/core/src/main/scala/caliban/validation/DefaultValueValidator.scala index 087264210..1529dd48c 100644 --- a/core/src/main/scala/caliban/validation/DefaultValueValidator.scala +++ b/core/src/main/scala/caliban/validation/DefaultValueValidator.scala @@ -2,11 +2,11 @@ package caliban.validation import caliban.CalibanError.ValidationError import caliban.InputValue._ -import caliban.{ InputValue, Value } import caliban.Value._ import caliban.introspection.adt._ import caliban.introspection.adt.__TypeKind._ import caliban.parsing.Parser +import caliban.{ InputValue, Value } import zio.IO object DefaultValueValidator { @@ -21,7 +21,7 @@ object DefaultValueValidator { "The default value for a field must be written using GraphQL input syntax." ) ) - _ <- Validator.validateInputValues(field, value) + _ <- Validator.validateInputValues(field, value, Context.empty) _ <- validateInputTypes(field, value, errorContext) } yield () } diff --git a/core/src/main/scala/caliban/validation/FieldMap.scala b/core/src/main/scala/caliban/validation/FieldMap.scala index 17fe49567..dabb91767 100644 --- a/core/src/main/scala/caliban/validation/FieldMap.scala +++ b/core/src/main/scala/caliban/validation/FieldMap.scala @@ -11,10 +11,10 @@ object FieldMap { implicit class FieldMapOps(val self: FieldMap) extends AnyVal { def |+|(that: FieldMap): FieldMap = (self.keySet ++ that.keySet).map { k => - k -> (self.get(k).getOrElse(Set.empty) ++ that.get(k).getOrElse(Set.empty)) + k -> (self.getOrElse(k, Set.empty) ++ that.getOrElse(k, Set.empty)) }.toMap - def show = + def show: String = self.map { case (k, fields) => s"$k -> ${fields.map(_.fieldDef.name).mkString(", ")}" }.mkString("\n") @@ -40,7 +40,7 @@ object FieldMap { def apply(context: Context, parentType: __Type, selectionSet: Iterable[Selection]): FieldMap = selectionSet.foldLeft(FieldMap.empty)({ case (fields, selection) => selection match { - case FragmentSpread(name, directives) => + case FragmentSpread(name, _) => context.fragments .get(name) .map { definition => diff --git a/core/src/main/scala/caliban/validation/FragmentValidator.scala b/core/src/main/scala/caliban/validation/FragmentValidator.scala index a9b449375..e8282efc5 100644 --- a/core/src/main/scala/caliban/validation/FragmentValidator.scala +++ b/core/src/main/scala/caliban/validation/FragmentValidator.scala @@ -3,9 +3,10 @@ package caliban.validation import caliban.CalibanError.ValidationError import caliban.introspection.adt._ import caliban.parsing.adt.Selection -import zio.{ Chunk, IO, UIO } -import Utils._ -import Utils.syntax._ +import caliban.validation.Utils._ +import caliban.validation.Utils.syntax._ +import zio.{ Chunk, IO } + import scala.collection.mutable object FragmentValidator { @@ -18,7 +19,7 @@ object FragmentValidator { val parentsCache = scala.collection.mutable.Map.empty[Iterable[Selection], Chunk[String]] val groupsCache = scala.collection.mutable.Map.empty[Set[SelectedField], Chunk[Set[SelectedField]]] - def sameResponseShapeByName(context: Context, parentType: __Type, set: Iterable[Selection]): Chunk[String] = + def sameResponseShapeByName(set: Iterable[Selection]): Chunk[String] = shapeCache.get(set) match { case Some(value) => value case None => @@ -31,22 +32,22 @@ object FragmentValidator { .getOrElse("")}.${f2.fieldDef.name}. Try using an alias." ) } else - sameResponseShapeByName(context, parentType, f1.selection.selectionSet ++ f2.selection.selectionSet) + sameResponseShapeByName(f1.selection.selectionSet ++ f2.selection.selectionSet) } }) shapeCache.update(set, res) res } - def sameForCommonParentsByName(context: Context, parentType: __Type, set: Iterable[Selection]): Chunk[String] = + def sameForCommonParentsByName(set: Iterable[Selection]): Chunk[String] = parentsCache.get(set) match { case Some(value) => value case None => val fields = FieldMap(context, parentType, set) - val res = Chunk.fromIterable(fields.flatMap({ case (name, fields) => - groupByCommonParents(context, parentType, fields).flatMap { group => + val res = Chunk.fromIterable(fields.flatMap({ case (_, fields) => + groupByCommonParents(fields).flatMap { group => val merged = group.flatMap(_.selection.selectionSet) - requireSameNameAndArguments(group) ++ sameForCommonParentsByName(context, parentType, merged) + requireSameNameAndArguments(group) ++ sameForCommonParentsByName(merged) } })) parentsCache.update(set, res) @@ -82,11 +83,7 @@ object FragmentValidator { else List() } - def groupByCommonParents( - context: Context, - parentType: __Type, - fields: Set[SelectedField] - ): Chunk[Set[SelectedField]] = + def groupByCommonParents(fields: Set[SelectedField]): Chunk[Set[SelectedField]] = groupsCache.get(fields) match { case Some(value) => value case None => @@ -116,14 +113,7 @@ object FragmentValidator { res } - val fields = FieldMap( - context, - parentType, - selectionSet - ) - - val conflicts = sameResponseShapeByName(context, parentType, selectionSet) ++ - sameForCommonParentsByName(context, parentType, selectionSet) + val conflicts = sameResponseShapeByName(selectionSet) ++ sameForCommonParentsByName(selectionSet) IO.whenCase(conflicts) { case Chunk(head, _*) => IO.fail(ValidationError(head, "")) diff --git a/core/src/main/scala/caliban/validation/Utils.scala b/core/src/main/scala/caliban/validation/Utils.scala index fda66a30c..c92d794bc 100644 --- a/core/src/main/scala/caliban/validation/Utils.scala +++ b/core/src/main/scala/caliban/validation/Utils.scala @@ -1,22 +1,9 @@ package caliban.validation -import caliban.CalibanError.ValidationError -import caliban.InputValue.VariableValue -import caliban.Value.NullValue -import caliban.execution.{ ExecutionRequest, Field => F } -import caliban.introspection.Introspector import caliban.introspection.adt._ import caliban.introspection.adt.__TypeKind._ -import caliban.parsing.SourceMapper -import caliban.parsing.adt.Definition.ExecutableDefinition.{ FragmentDefinition, OperationDefinition } -import caliban.parsing.adt.Definition.{ TypeSystemDefinition, TypeSystemExtension } -import caliban.parsing.adt.OperationType._ -import caliban.parsing.adt.Selection.{ Field, FragmentSpread, InlineFragment } import caliban.parsing.adt.Type.NamedType -import caliban.parsing.adt._ -import caliban.schema.{ RootSchema, RootSchemaBuilder, RootType, Types } -import caliban.{ InputValue, Rendering, Value } -import zio.{ Chunk, IO } +import zio.Chunk object Utils { def isObjectType(t: __Type): Boolean = @@ -63,11 +50,11 @@ object Utils { def isListType(t: __Type): Boolean = t.kind == __TypeKind.LIST - def getFields(t: __Type) = t.fields(__DeprecatedArgs(Some(true))) - def getType(t: Option[NamedType], parentType: __Type, context: Context) = + def getFields(t: __Type): Option[List[__Field]] = t.fields(__DeprecatedArgs(Some(true))) + def getType(t: Option[NamedType], parentType: __Type, context: Context): __Type = t.fold(Option(parentType))(t => context.rootType.types.get(t.name)).getOrElse(parentType) - def getType(t: NamedType, context: Context) = + def getType(t: NamedType, context: Context): Option[__Type] = context.rootType.types.get(t.name) def cross[A](a: Iterable[A]): Chunk[(A, A)] = @@ -82,7 +69,7 @@ object Utils { self.flatMap(a => that.map(b => (a, b))) } - implicit class Tuple2Syntax[+A, +B](val self: Tuple2[Option[A], Option[B]]) extends AnyVal { + implicit class Tuple2Syntax[+A, +B](val self: (Option[A], Option[B])) extends AnyVal { def mapN[C](f: (A, B) => C): Option[C] = self._1.flatMap(a => self._2.map(b => f(a, b))) } diff --git a/core/src/main/scala/caliban/validation/Validator.scala b/core/src/main/scala/caliban/validation/Validator.scala index c0672da34..9c4aa731e 100644 --- a/core/src/main/scala/caliban/validation/Validator.scala +++ b/core/src/main/scala/caliban/validation/Validator.scala @@ -15,9 +15,11 @@ import caliban.parsing.adt.Selection.{ Field, FragmentSpread, InlineFragment } import caliban.parsing.adt.Type.NamedType import caliban.parsing.adt._ import caliban.schema.{ RootSchema, RootSchemaBuilder, RootType, Types } +import caliban.validation.Utils.isObjectType import caliban.{ InputValue, Rendering, Value } import zio.IO -import Utils.{ isObjectType } + +import scala.annotation.tailrec object Validator { @@ -38,7 +40,7 @@ object Validator { validateRootQuery(schema) } - private[caliban] def validateType(t: __Type) = + private[caliban] def validateType(t: __Type): IO[ValidationError, Unit] = t.kind match { case __TypeKind.ENUM => validateEnum(t) case __TypeKind.UNION => validateUnion(t) @@ -252,12 +254,22 @@ object Validator { "GraphQL servers define what directives they support. For each usage of a directive, the directive must be available on that server." ) case Some(directive) => - IO.when(!directive.locations.contains(location))( - failValidation( - s"Directive '${d.name}' is used in invalid location '$location'.", - "GraphQL servers define what directives they support and where they support them. For each usage of a directive, the directive must be used in a location that the server has declared support for." + IO.foreach_(d.arguments) { case (arg, argValue) => + directive.args.find(_.name == arg) match { + case None => + failValidation( + s"Argument '$arg' is not defined on directive '${d.name}' ($location).", + "Every argument provided to a field or directive must be defined in the set of possible arguments of that field or directive." + ) + case Some(inputValue) => validateInputValues(inputValue, argValue, context) + } + } *> + IO.when(!directive.locations.contains(location))( + failValidation( + s"Directive '${d.name}' is used in invalid location '$location'.", + "GraphQL servers define what directives they support and where they support them. For each usage of a directive, the directive must be used in a location that the server has declared support for." + ) ) - ) } } } yield () @@ -434,11 +446,16 @@ object Validator { ) .flatMap { f => validateFields(context, field.selectionSet, Types.innerType(f.`type`())) *> - validateArguments(field, f, currentType) + validateArguments(field, f, currentType, context) } } - private def validateArguments(field: Field, f: __Field, currentType: __Type): IO[ValidationError, Unit] = + private def validateArguments( + field: Field, + f: __Field, + currentType: __Type, + context: Context + ): IO[ValidationError, Unit] = IO.foreach_(field.arguments) { case (arg, argValue) => f.args.find(_.name == arg) match { case None => @@ -446,7 +463,7 @@ object Validator { s"Argument '$arg' is not defined on field '${field.name}' of type '${currentType.name.getOrElse("")}'.", "Every argument provided to a field or directive must be defined in the set of possible arguments of that field or directive." ) - case Some(inputValue) => validateInputValues(inputValue, argValue) + case Some(inputValue) => validateInputValues(inputValue, argValue, context) } } *> IO.foreach_(f.args.filter(a => a.`type`().kind == __TypeKind.NON_NULL))(arg => @@ -470,7 +487,8 @@ object Validator { private[caliban] def validateInputValues( inputValue: __InputValue, - argValue: InputValue + argValue: InputValue, + context: Context ): IO[ValidationError, Unit] = { val t = inputValue.`type`() val inputType = if (t.kind == __TypeKind.NON_NULL) t.ofType.getOrElse(t) else t @@ -484,14 +502,14 @@ object Validator { s"Input field '$k' is not defined on type '${inputType.name.getOrElse("?")}'.", "Every input field provided in an input object value must be defined in the set of possible fields of that input object’s expected type." ) - case Some(value) => validateInputValues(value, v) + case Some(value) => validateInputValues(value, v, context) } } *> IO .foreach_(inputFields)(inputField => IO.when( inputField.defaultValue.isEmpty && inputField.`type`().kind == __TypeKind.NON_NULL && - fields.get(inputField.name).getOrElse(NullValue) == NullValue + fields.getOrElse(inputField.name, NullValue) == NullValue )( failValidation( s"Required field '${inputField.name}' on object '${inputType.name.getOrElse("?")}' was not provided.", @@ -499,10 +517,91 @@ object Validator { ) ) ) + case VariableValue(variableName) => + context.variableDefinitions.get(variableName) match { + case Some(variableDefinition) => checkVariableUsageAllowed(variableDefinition, inputValue) + case None => + failValidation( + s"Variable '$variableName' is not defined.", + "Variables are scoped on a per‐operation basis. That means that any variable used within the context of an operation must be defined at the top level of that operation" + ) + } case _ => IO.unit } } + private def checkVariableUsageAllowed( + variableDefinition: VariableDefinition, + inputValue: __InputValue + ): IO[ValidationError, Unit] = { + val locationType = inputValue.`type`() + val variableType = variableDefinition.variableType + if (!locationType.isNullable && !variableType.nonNull) { + val hasNonNullVariableDefaultValue = variableDefinition.defaultValue.exists(_ != NullValue) + val hasLocationDefaultValue = inputValue.defaultValue.nonEmpty + if (!hasNonNullVariableDefaultValue && !hasLocationDefaultValue) + failValidation( + s"Variable '${variableDefinition.name}' usage is not allowed because it is nullable and doesn't have a default value.", + "Variable usages must be compatible with the arguments they are passed to." + ) + else { + val nullableLocationType = locationType.ofType.getOrElse(locationType) + checkTypesCompatible(variableDefinition.name, variableType, nullableLocationType) + } + } else checkTypesCompatible(variableDefinition.name, variableType, locationType) + } + + @tailrec + private def checkTypesCompatible( + variableName: String, + variableType: Type, + locationType: __Type + ): IO[ValidationError, Unit] = { + val explanation = "Variable usages must be compatible with the arguments they are passed to." + if (!locationType.isNullable) { + if (variableType.nullable) + failValidation( + s"Variable '$variableName' usage is not allowed because it is nullable but it shouldn't be.", + explanation + ) + else { + val nullableLocationType = locationType.ofType.getOrElse(locationType) + val nullableVariableType = variableType.toNullable + checkTypesCompatible(variableName, nullableVariableType, nullableLocationType) + } + } else if (variableType.nonNull) { + val nullableVariableType = variableType.toNullable + checkTypesCompatible(variableName, nullableVariableType, locationType) + } else if (locationType.kind == __TypeKind.LIST) { + variableType match { + case _: Type.NamedType => + failValidation( + s"Variable '$variableName' usage is not allowed because it is a not a list but it should be.", + explanation + ) + case Type.ListType(ofType, _) => + val itemLocationType = locationType.ofType.getOrElse(locationType) + val itemVariableType = ofType + checkTypesCompatible(variableName, itemVariableType, itemLocationType) + } + } else + variableType match { + case Type.ListType(_, _) => + failValidation( + s"Variable '$variableName' usage is not allowed because it is a list but it should not be.", + explanation + ) + case Type.NamedType(name, _) => + IO.when(!locationType.name.contains(name))( + failValidation( + s"Variable '$variableName' usage is not allowed because its type doesn't match the schema ($name instead of ${locationType.name + .getOrElse("")}).", + explanation + ) + ) + } + } + private def validateLeafFieldSelection(selections: List[Selection], currentType: __Type): IO[ValidationError, Unit] = IO.whenCase(currentType.kind) { case __TypeKind.SCALAR | __TypeKind.ENUM if selections.nonEmpty => diff --git a/core/src/main/scala/caliban/validation/package.scala b/core/src/main/scala/caliban/validation/package.scala index 63566e1fd..11a12149f 100644 --- a/core/src/main/scala/caliban/validation/package.scala +++ b/core/src/main/scala/caliban/validation/package.scala @@ -2,9 +2,10 @@ package caliban import caliban.parsing.adt.Definition.ExecutableDefinition.{ FragmentDefinition, OperationDefinition } import caliban.introspection.adt.{ __Field, __Type } -import caliban.parsing.adt.{ Document, Selection } +import caliban.parsing.SourceMapper +import caliban.parsing.adt.{ Document, Selection, VariableDefinition } import caliban.parsing.adt.Selection.Field -import caliban.schema.RootType +import caliban.schema.{ RootType, Types } package object validation { case class SelectedField( @@ -21,5 +22,13 @@ package object validation { operations: List[OperationDefinition], fragments: Map[String, FragmentDefinition], selectionSets: List[Selection] - ) + ) { + lazy val variableDefinitions: Map[String, VariableDefinition] = + operations.flatMap(_.variableDefinitions.map(d => d.name -> d)).toMap + } + + object Context { + val empty: Context = + Context(Document(Nil, SourceMapper.empty), RootType(Types.boolean, None, None), Nil, Map.empty, Nil) + } } diff --git a/core/src/test/scala/caliban/execution/ExecutionSpec.scala b/core/src/test/scala/caliban/execution/ExecutionSpec.scala index ed19ded2c..bf7eee577 100644 --- a/core/src/test/scala/caliban/execution/ExecutionSpec.scala +++ b/core/src/test/scala/caliban/execution/ExecutionSpec.scala @@ -227,7 +227,7 @@ object ExecutionSpec extends DefaultRunnableSpec { testM("variable in object") { val interpreter = graphQL(resolver).interpreter val query = gqldoc(""" - query test($name: String) { + query test($name: String!) { exists(character: { name: $name, nicknames: [], origin: EARTH }) }""") diff --git a/core/src/test/scala/caliban/execution/FragmentSpec.scala b/core/src/test/scala/caliban/execution/FragmentSpec.scala index fc5712d0e..75eab1a11 100644 --- a/core/src/test/scala/caliban/execution/FragmentSpec.scala +++ b/core/src/test/scala/caliban/execution/FragmentSpec.scala @@ -1,6 +1,5 @@ package caliban.execution -import caliban.CalibanError import caliban.GraphQL._ import caliban.Macros.gqldoc import caliban.RootResolver @@ -51,8 +50,8 @@ object FragmentSpec extends DefaultRunnableSpec { }""") for { - interpteter <- graphQL(resolver).interpreter - res <- interpteter.execute(query) + interpreter <- graphQL(resolver).interpreter + res <- interpreter.execute(query) } yield assert(res.data.toString)(equalTo("""{"amos":{"role":{"shipName":"Rocinante"}}}""")) }, testM("inline fragment") { @@ -177,8 +176,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query) + interpreter <- gql.interpreter + res <- interpreter.execute(query) } yield assert(res.errors)(isEmpty) }, testM("merge identical fields with alias") { @@ -200,8 +199,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query) + interpreter <- gql.interpreter + res <- interpreter.execute(query) } yield assert(res.errors)(isEmpty) }, testM("alias conflict") { @@ -223,8 +222,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query) + interpreter <- gql.interpreter + res <- interpreter.execute(query) } yield assert(res.errors.headOption)(isSome(anything)) } ), @@ -250,8 +249,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query) + interpreter <- gql.interpreter + res <- interpreter.execute(query) } yield assert(res.errors)(isEmpty) }, testM("identical fields with identical values") { @@ -268,15 +267,15 @@ object FragmentSpec extends DefaultRunnableSpec { | doesKnowCommand(value: $dogCommand) | doesKnowCommand(value: $dogCommand) |} - |query DogQuery($dogCommand: DogCommand){ + |query DogQuery($dogCommand: DogCommand!){ | dog { | ...mergeIdenticalFieldsWithIdenticalValues | } |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) + interpreter <- gql.interpreter + res <- interpreter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) } yield assert(res.errors)(isEmpty) }, testM("identical fields with args") { @@ -301,8 +300,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query) + interpreter <- gql.interpreter + res <- interpreter.execute(query) } yield assert(res.errors.headOption)(isSome(anything)) }, testM("conflicting value and arg") { @@ -326,8 +325,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) + interpreter <- gql.interpreter + res <- interpreter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) } yield assert(res.errors.headOption)(isSome(anything)) }, testM("conflicting args") { @@ -351,8 +350,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) + interpreter <- gql.interpreter + res <- interpreter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) } yield assert(res.errors.headOption)(isSome(anything)) }, testM("conflicting args") { @@ -376,8 +375,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) + interpreter <- gql.interpreter + res <- interpreter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) } yield assert(res.errors.headOption)(isSome(anything)) } ), @@ -407,8 +406,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) + interpreter <- gql.interpreter + res <- interpreter.execute(query, variables = Map("dogCommand" -> StringValue("SIT"))) } yield assert(res.errors)(isEmpty) }, testM("safe differing args") { @@ -442,8 +441,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query) + interpreter <- gql.interpreter + res <- interpreter.execute(query) } yield assert(res.errors)(isEmpty) }, testM("conflicting different responses") { @@ -473,8 +472,8 @@ object FragmentSpec extends DefaultRunnableSpec { |}""".stripMargin for { - interpteter <- gql.interpreter - res <- interpteter.execute(query) + interpreter <- gql.interpreter + res <- interpreter.execute(query) } yield assert(res.errors.headOption)(isSome(anything)) } ) diff --git a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala index 2cd5b4226..6c6ce3e55 100644 --- a/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala +++ b/core/src/test/scala/caliban/introspection/IntrospectionSpec.scala @@ -124,7 +124,7 @@ object IntrospectionSpec extends DefaultRunnableSpec { assertM(interpreter.flatMap(_.execute(fullIntrospectionQuery)).map(_.data.toString))( equalTo( - """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"Use `characters`"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}]}]}}""" + """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"character","description":null,"args":[{"name":"name","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"defaultValue":null}],"type":{"kind":"OBJECT","name":"Character","ofType":null},"isDeprecated":true,"deprecationReason":"Use `characters`"}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]}]}}""" ) ) }, @@ -151,7 +151,7 @@ object IntrospectionSpec extends DefaultRunnableSpec { assertM(interpreter.flatMap(_.execute(fullIntrospectionQuery)).map(_.data.toString))( equalTo( - """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"SCALAR","name":"Boolean","ofType":null},"defaultValue":null}]}]}}""" + """{"__schema":{"queryType":{"name":"QueryIO"},"mutationType":null,"subscriptionType":null,"types":[{"kind":"SCALAR","name":"Boolean","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Captain","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"CaptainShipName","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"SCALAR","name":"CaptainShipName","description":"Description of custom scalar emphasizing proper captain ship names","fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Character","description":null,"fields":[{"name":"name","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"nicknames","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}}}},"isDeprecated":false,"deprecationReason":null},{"name":"origin","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"ENUM","name":"Origin","ofType":null}},"isDeprecated":false,"deprecationReason":null},{"name":"role","description":null,"args":[],"type":{"kind":"UNION","name":"Role","ofType":null},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Engineer","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"Mechanic","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"ENUM","name":"Origin","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":[{"name":"BELT","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"EARTH","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MARS","description":null,"isDeprecated":false,"deprecationReason":null},{"name":"MOON","description":null,"isDeprecated":true,"deprecationReason":"Use: EARTH | MARS | BELT"}],"possibleTypes":null},{"kind":"OBJECT","name":"Pilot","description":null,"fields":[{"name":"shipName","description":null,"args":[],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"String","ofType":null}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"OBJECT","name":"QueryIO","description":"Queries","fields":[{"name":"characters","description":"Return all characters from a given origin","args":[{"name":"origin","description":null,"type":{"kind":"ENUM","name":"Origin","ofType":null},"defaultValue":null}],"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"LIST","name":null,"ofType":{"kind":"NON_NULL","name":null,"ofType":{"kind":"OBJECT","name":"Character","ofType":null}}}},"isDeprecated":false,"deprecationReason":null}],"inputFields":null,"interfaces":[],"enumValues":null,"possibleTypes":null},{"kind":"UNION","name":"Role","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":[{"kind":"OBJECT","name":"Captain","ofType":null},{"kind":"OBJECT","name":"Engineer","ofType":null},{"kind":"OBJECT","name":"Mechanic","ofType":null},{"kind":"OBJECT","name":"Pilot","ofType":null}]},{"kind":"SCALAR","name":"String","description":null,"fields":null,"inputFields":null,"interfaces":null,"enumValues":null,"possibleTypes":null}],"directives":[{"name":"skip","description":"The @skip directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional exclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]},{"name":"include","description":"The @include directive may be provided for fields, fragment spreads, and inline fragments, and allows for conditional inclusion during execution as described by the if argument.","locations":["FIELD","FRAGMENT_SPREAD","INLINE_FRAGMENT"],"args":[{"name":"if","description":null,"type":{"kind":"NON_NULL","name":null,"ofType":{"kind":"SCALAR","name":"Boolean","ofType":null}},"defaultValue":null}]}]}}""" ) ) }, diff --git a/core/src/test/scala/caliban/validation/ValidationSpec.scala b/core/src/test/scala/caliban/validation/ValidationSpec.scala index e5782f8ff..226ed317d 100644 --- a/core/src/test/scala/caliban/validation/ValidationSpec.scala +++ b/core/src/test/scala/caliban/validation/ValidationSpec.scala @@ -5,7 +5,7 @@ import caliban.CalibanError.ValidationError import caliban.GraphQL._ import caliban.Macros.gqldoc import caliban.TestUtils._ -import caliban.Value.StringValue +import caliban.Value.{ BooleanValue, StringValue } import zio.IO import zio.test.Assertion._ import zio.test.environment.TestEnvironment @@ -249,7 +249,7 @@ object ValidationSpec extends DefaultRunnableSpec { }, testM("variable used in object") { val query = gqldoc(""" - query($x: String) { + query($x: String!) { exists(character: { name: $x, nicknames: [], origin: EARTH }) }""") assertM(interpreter.flatMap(_.execute(query, None, Map("x" -> StringValue("y")))).map(_.errors.headOption))( @@ -287,6 +287,60 @@ object ValidationSpec extends DefaultRunnableSpec { } }""") check(query, "Directive 'skip' is defined twice.") + }, + testM("variable type doesn't match") { + val query = gqldoc(""" + query($x: Int!) { + exists(character: { name: $x, nicknames: [], origin: EARTH }) + }""") + check( + query, + "Variable 'x' usage is not allowed because its type doesn't match the schema (Int instead of String)." + ) + }, + testM("variable cardinality is the same") { + val query = gqldoc(""" + query($x: [String]!) { + exists(character: { name: $x, nicknames: [], origin: EARTH }) + }""") + check(query, "Variable 'x' usage is not allowed because it is a list but it should not be.") + }, + testM("variable nullability is the same") { + val query = gqldoc(""" + query($x: String) { + exists(character: { name: $x, nicknames: [], origin: EARTH }) + }""") + check(query, "Variable 'x' usage is not allowed because it is nullable and doesn't have a default value.") + }, + testM("variable nullability with default") { + val query = gqldoc(""" + query($x: String = "test") { + exists(character: { name: $x, nicknames: [], origin: EARTH }) + }""") + assertM(interpreter.flatMap(_.execute(query, None, Map())).map(_.errors.headOption))(isNone) + }, + testM("directive with variable of the wrong type") { + val query = gqldoc(""" + query($x: String!) { + characters { + name @skip(if: $x) + } + }""") + check( + query, + "Variable 'x' usage is not allowed because its type doesn't match the schema (String instead of Boolean)." + ) + }, + testM("directive with variable of the right type") { + val query = gqldoc(""" + query($x: Boolean!) { + characters { + name @skip(if: $x) + } + }""") + assertM(interpreter.flatMap(_.execute(query, None, Map("x" -> BooleanValue(true)))).map(_.errors.headOption))( + isNone + ) } ) }