diff --git a/modules/core/src/main/scala/sangria/validation/QueryValidator.scala b/modules/core/src/main/scala/sangria/validation/QueryValidator.scala index b3ff0ca6..635e2250 100644 --- a/modules/core/src/main/scala/sangria/validation/QueryValidator.scala +++ b/modules/core/src/main/scala/sangria/validation/QueryValidator.scala @@ -45,7 +45,8 @@ object QueryValidator { new VariablesAreInputTypes, new VariablesInAllowedPosition, new InputDocumentNonConflictingVariableInference, - new SingleFieldSubscriptions + new SingleFieldSubscriptions, + new ExactlyOneOfFieldGiven ) def ruleBased(rules: List[ValidationRule]): RuleBasedQueryValidator = diff --git a/modules/core/src/main/scala/sangria/validation/Violation.scala b/modules/core/src/main/scala/sangria/validation/Violation.scala index 1b12ccd5..5d2b79c4 100644 --- a/modules/core/src/main/scala/sangria/validation/Violation.scala +++ b/modules/core/src/main/scala/sangria/validation/Violation.scala @@ -1105,6 +1105,14 @@ case class NoQueryTypeViolation(sourceMapper: Option[SourceMapper], locations: L "Must provide schema definition with query type or a type named Query." } +case class NotExactlyOneOfField( + typeName: String, + sourceMapper: Option[SourceMapper], + locations: List[AstLocation] +) extends AstNodeViolation { + lazy val simpleErrorMessage = s"Exactly one key must be specified for oneOf type '${typeName}'." +} + case class OneOfMandatoryField( fieldName: String, typeName: String, diff --git a/modules/core/src/main/scala/sangria/validation/rules/ExactlyOneOfFieldGiven.scala b/modules/core/src/main/scala/sangria/validation/rules/ExactlyOneOfFieldGiven.scala new file mode 100644 index 00000000..d3ac6921 --- /dev/null +++ b/modules/core/src/main/scala/sangria/validation/rules/ExactlyOneOfFieldGiven.scala @@ -0,0 +1,37 @@ +package sangria.validation.rules + +import sangria.ast +import sangria.schema +import sangria.ast.AstVisitorCommand +import sangria.validation._ + +/** For oneOf input objects, exactly one field should be non-null. */ +class ExactlyOneOfFieldGiven extends ValidationRule { + override def visitor(ctx: ValidationContext) = new AstValidatingVisitor { + override val onEnter: ValidationVisit = { case ast.ObjectValue(fields, _, pos) => + ctx.typeInfo.inputType match { + case Some(inputType) => + inputType.namedInputType match { + case schema.InputObjectType(name, _, _, directives, _) if directives.exists { d => + d.name == schema.OneOfDirective.name + } => + val nonNullFields = fields.filter { field => + field.value match { + case ast.NullValue(_, _) => false + case _ => true + } + } + + nonNullFields.size match { + case 1 => AstVisitorCommand.RightContinue + case _ => + Left(Vector(NotExactlyOneOfField(name, ctx.sourceMapper, pos.toList))) + } + + case _ => AstVisitorCommand.RightContinue + } + case None => AstVisitorCommand.RightContinue + } + } + } +} diff --git a/modules/core/src/test/scala/sangria/util/ValidationSupport.scala b/modules/core/src/test/scala/sangria/util/ValidationSupport.scala index 7de201fd..95db047d 100644 --- a/modules/core/src/test/scala/sangria/util/ValidationSupport.scala +++ b/modules/core/src/test/scala/sangria/util/ValidationSupport.scala @@ -1,5 +1,6 @@ package sangria.util +import sangria.ast import sangria.parser.QueryParser import sangria.schema._ import sangria.validation._ @@ -145,6 +146,13 @@ trait ValidationSupport extends Matchers { ) ) + val OneOfInput = InputObjectType( + "OneOfInput", + List( + InputField("catName", OptionInputType(StringType)), + InputField("dogId", OptionInputType(IntType)) + )).withDirective(ast.Directive(OneOfDirective.name)) + val ComplicatedArgs = ObjectType( "ComplicatedArgs", List[TestField]( @@ -251,7 +259,13 @@ trait ValidationSupport extends Matchers { Field("catOrDog", OptionType(CatOrDog), resolve = _ => None), Field("dogOrHuman", OptionType(DogOrHuman), resolve = _ => None), Field("humanOrAlien", OptionType(HumanOrAlien), resolve = _ => None), - Field("complicatedArgs", OptionType(ComplicatedArgs), resolve = _ => None) + Field("complicatedArgs", OptionType(ComplicatedArgs), resolve = _ => None), + Field( + "oneOfQuery", + OptionType(CatOrDog), + arguments = List(Argument("input", OneOfInput)), + resolve = _ => None + ) ) ) diff --git a/modules/core/src/test/scala/sangria/validation/rules/ExactlyOneOfFieldGivenSpec.scala b/modules/core/src/test/scala/sangria/validation/rules/ExactlyOneOfFieldGivenSpec.scala new file mode 100644 index 00000000..cbff6a1c --- /dev/null +++ b/modules/core/src/test/scala/sangria/validation/rules/ExactlyOneOfFieldGivenSpec.scala @@ -0,0 +1,70 @@ +package sangria.validation.rules + +import sangria.util.{Pos, ValidationSupport} +import org.scalatest.wordspec.AnyWordSpec + +class ExactlyOneOfFieldGivenSpec extends AnyWordSpec with ValidationSupport { + + override val defaultRule = Some(new ExactlyOneOfFieldGiven) + + "Validate: exactly oneOf field given" should { + "with exactly one non-null field given" in expectPasses(""" + query OneOfQuery { + oneOfQuery(input: { + catName: "Gretel" + }) { + ... on Cat { + name + } + } + } + """) + + "with exactly one null field given" in expectFails( + """ + query OneOfQuery { + oneOfQuery(input: { + catName: null + }) { + ... on Cat { + name + } + } + } + """, + List("Exactly one key must be specified for oneOf type 'OneOfInput'." -> Some(Pos(3, 31))) + ) + + "with no fields given" in expectFails( + """ + query OneOfQuery { + oneOfQuery(input: {}) { + ... on Cat { + name + } + } + } + """, + List("Exactly one key must be specified for oneOf type 'OneOfInput'." -> Some(Pos(3, 31))) + ) + + "with more than one non-null fields given" in expectFails( + """ + query OneOfQuery { + oneOfQuery(input: { + catName: "Gretel", + dogId: 123 + }) { + ... on Cat { + name + } + ... on Dog { + name + } + } + } + """, + List("Exactly one key must be specified for oneOf type 'OneOfInput'." -> Some(Pos(3, 31))) + ) + } +}