diff --git a/src/main/scala/sangria/validation/Violation.scala b/src/main/scala/sangria/validation/Violation.scala index 230fa153..cda49a80 100644 --- a/src/main/scala/sangria/validation/Violation.scala +++ b/src/main/scala/sangria/validation/Violation.scala @@ -161,3 +161,6 @@ case class FieldsConflictViolation(outputName: String, reason: Either[String, Ve } } +case class AnonOperationNotAloneViolation(sourceMapper: Option[SourceMapper], positions: List[Position]) extends AstNodeViolation { + lazy val errorMessage = s"This anonymous operation must be the only defined operation.$astLocation" +} diff --git a/src/main/scala/sangria/validation/rules/LoneAnonymousOperation.scala b/src/main/scala/sangria/validation/rules/LoneAnonymousOperation.scala new file mode 100644 index 00000000..fb32bb79 --- /dev/null +++ b/src/main/scala/sangria/validation/rules/LoneAnonymousOperation.scala @@ -0,0 +1,30 @@ +package sangria.validation.rules + +import sangria.ast +import sangria.ast.AstVisitorCommand._ +import sangria.validation._ + +import scala.language.postfixOps + +/** + * Lone anonymous operation + * + * A GraphQL document is only valid if when it contains an anonymous operation + * (the query short-hand) that it contains only that one operation definition. + */ +class LoneAnonymousOperation extends ValidationRule { + override def visitor(ctx: ValidationContext) = new AstValidatingVisitor { + var operationCount = 0 + + override val onEnter: ValidationVisit = { + case ast.Document(definitions, _, _) => + operationCount = definitions.count(_.isInstanceOf[ast.OperationDefinition]) + Right(Continue) + case op: ast.OperationDefinition => + if (op.name.isEmpty && operationCount > 1) + Left(Vector(AnonOperationNotAloneViolation(ctx.sourceMapper, op.position.toList))) + else + Right(Continue) + } + } +} \ No newline at end of file diff --git a/src/test/scala/sangria/validation/rules/LoneAnonymousOperationSpec.scala b/src/test/scala/sangria/validation/rules/LoneAnonymousOperationSpec.scala new file mode 100644 index 00000000..d7b47645 --- /dev/null +++ b/src/test/scala/sangria/validation/rules/LoneAnonymousOperationSpec.scala @@ -0,0 +1,73 @@ +package sangria.validation.rules + +import org.scalatest.WordSpec +import sangria.util.{Pos, ValidationSupport} + +class LoneAnonymousOperationSpec extends WordSpec with ValidationSupport { + + override val defaultRule = Some(new LoneAnonymousOperation) + + "Validate: Anonymous operation must be alone" should { + "no operations" in expectPasses( + """ + fragment fragA on Type { + field + } + """) + + "one anon operation" in expectPasses( + """ + { + field + } + """) + + "multiple named operations" in expectPasses( + """ + query Foo { + field + } + + query Bar { + field + } + """) + + "anon operation with fragment" in expectPasses( + """ + { + ...Foo + } + fragment Foo on Type { + field + } + """) + + "multiple anon operations" in expectFails( + """ + { + fieldA + } + { + fieldB + } + """, + List( + "This anonymous operation must be the only defined operation." -> Some(Pos(2, 9)), + "This anonymous operation must be the only defined operation." -> Some(Pos(5, 9)) + )) + + "anon operation with another operation" in expectFails( + """ + { + fieldA + } + mutation Foo { + fieldB + } + """, + List( + "This anonymous operation must be the only defined operation." -> Some(Pos(2, 9)) + )) + } +}