From f568967748f3c4ce6e306a42923550e7bdec8a72 Mon Sep 17 00:00:00 2001 From: Oleg Ilyenko Date: Fri, 7 Aug 2015 22:00:35 +0200 Subject: [PATCH] UniqueFragmentNames and UniqueOperationNames rules. closes #40 --- .../scala/sangria/validation/Violation.scala | 8 ++ .../rules/UniqueFragmentNames.scala | 28 ++++++ .../rules/UniqueOperationNames.scala | 28 ++++++ .../rules/UniqueFragmentNamesSpec.scala | 98 +++++++++++++++++++ .../rules/UniqueOperationNamesSpec.scala | 90 +++++++++++++++++ 5 files changed, 252 insertions(+) create mode 100644 src/main/scala/sangria/validation/rules/UniqueFragmentNames.scala create mode 100644 src/main/scala/sangria/validation/rules/UniqueOperationNames.scala create mode 100644 src/test/scala/sangria/validation/rules/UniqueFragmentNamesSpec.scala create mode 100644 src/test/scala/sangria/validation/rules/UniqueOperationNamesSpec.scala diff --git a/src/main/scala/sangria/validation/Violation.scala b/src/main/scala/sangria/validation/Violation.scala index cda49a80..1d81f1b2 100644 --- a/src/main/scala/sangria/validation/Violation.scala +++ b/src/main/scala/sangria/validation/Violation.scala @@ -164,3 +164,11 @@ 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" } + +case class DuplicateFragmentNameViolation(fragName: String, sourceMapper: Option[SourceMapper], positions: List[Position]) extends AstNodeViolation { + lazy val errorMessage = s"There can only be one fragment named '$fragName'.$astLocation" +} + +case class DuplicateOperationNameViolation(opName: String, sourceMapper: Option[SourceMapper], positions: List[Position]) extends AstNodeViolation { + lazy val errorMessage = s"There can only be one operation named '$opName'.$astLocation" +} diff --git a/src/main/scala/sangria/validation/rules/UniqueFragmentNames.scala b/src/main/scala/sangria/validation/rules/UniqueFragmentNames.scala new file mode 100644 index 00000000..fa17e7ce --- /dev/null +++ b/src/main/scala/sangria/validation/rules/UniqueFragmentNames.scala @@ -0,0 +1,28 @@ +package sangria.validation.rules + +import sangria.ast +import sangria.ast.AstVisitorCommand._ +import sangria.validation._ + +import scala.collection.mutable.{Set => MutableSet} + +/** + * Unique fragment names + * + * A GraphQL document is only valid if all defined fragments have unique names. + */ +class UniqueFragmentNames extends ValidationRule { + override def visitor(ctx: ValidationContext) = new AstValidatingVisitor { + val knownFragmentNames = MutableSet[String]() + + override val onEnter: ValidationVisit = { + case fragDef: ast.FragmentDefinition => + if (knownFragmentNames contains fragDef.name) + Left(Vector(DuplicateFragmentNameViolation(fragDef.name, ctx.sourceMapper, fragDef.position.toList))) + else { + knownFragmentNames += fragDef.name + Right(Continue) + } + } + } +} \ No newline at end of file diff --git a/src/main/scala/sangria/validation/rules/UniqueOperationNames.scala b/src/main/scala/sangria/validation/rules/UniqueOperationNames.scala new file mode 100644 index 00000000..4a03e62e --- /dev/null +++ b/src/main/scala/sangria/validation/rules/UniqueOperationNames.scala @@ -0,0 +1,28 @@ +package sangria.validation.rules + +import sangria.ast +import sangria.ast.AstVisitorCommand._ +import sangria.validation._ + +import scala.collection.mutable.{Set => MutableSet} + +/** + * Unique operation names + * + * A GraphQL document is only valid if all defined operations have unique names. + */ +class UniqueOperationNames extends ValidationRule { + override def visitor(ctx: ValidationContext) = new AstValidatingVisitor { + val knownOpNames = MutableSet[String]() + + override val onEnter: ValidationVisit = { + case ast.OperationDefinition(_, Some(name), _, _, _, pos) => + if (knownOpNames contains name) + Left(Vector(DuplicateOperationNameViolation(name, ctx.sourceMapper, pos.toList))) + else { + knownOpNames += name + Right(Continue) + } + } + } +} \ No newline at end of file diff --git a/src/test/scala/sangria/validation/rules/UniqueFragmentNamesSpec.scala b/src/test/scala/sangria/validation/rules/UniqueFragmentNamesSpec.scala new file mode 100644 index 00000000..6f9a4ecd --- /dev/null +++ b/src/test/scala/sangria/validation/rules/UniqueFragmentNamesSpec.scala @@ -0,0 +1,98 @@ +package sangria.validation.rules + +import org.scalatest.WordSpec +import sangria.util.{Pos, ValidationSupport} + +class UniqueFragmentNamesSpec extends WordSpec with ValidationSupport { + + override val defaultRule = Some(new UniqueFragmentNames) + + "Validate: Unique fragment names" should { + "no fragments" in expectPasses( + """ + { + field + } + """) + + "one fragment" in expectPasses( + """ + { + ...fragA + } + + fragment fragA on Type { + field + } + """) + + "many fragments" in expectPasses( + """ + { + ...fragA + ...fragB + ...fragC + } + fragment fragA on Type { + fieldA + } + fragment fragB on Type { + fieldB + } + fragment fragC on Type { + fieldC + } + """) + + "inline fragments are always unique" in expectPasses( + """ + { + ...on Type { + fieldA + } + ...on Type { + fieldB + } + } + """) + + "fragment and operation named the same" in expectPasses( + """ + query Foo { + ...Foo + } + fragment Foo on Type { + field + } + """) + + "fragments named the same" in expectFails( + """ + { + ...fragA + } + fragment fragA on Type { + fieldA + } + fragment fragA on Type { + fieldB + } + """, + List( + "There can only be one fragment named 'fragA'." -> Some(Pos(8, 9)) + )) + + "fragments named the same without being referenced" in expectFails( + """ + fragment fragA on Type { + fieldA + } + fragment fragA on Type { + fieldB + } + """, + List( + "There can only be one fragment named 'fragA'." -> Some(Pos(5, 9)) + )) + } +} diff --git a/src/test/scala/sangria/validation/rules/UniqueOperationNamesSpec.scala b/src/test/scala/sangria/validation/rules/UniqueOperationNamesSpec.scala new file mode 100644 index 00000000..07b7de7a --- /dev/null +++ b/src/test/scala/sangria/validation/rules/UniqueOperationNamesSpec.scala @@ -0,0 +1,90 @@ +package sangria.validation.rules + +import org.scalatest.WordSpec +import sangria.util.{Pos, ValidationSupport} + +class UniqueOperationNamesSpec extends WordSpec with ValidationSupport { + + override val defaultRule = Some(new UniqueOperationNames) + + "Validate: Unique operation names" should { + "no operations" in expectPasses( + """ + fragment fragA on Type { + field + } + """) + + "one anon operation" in expectPasses( + """ + { + field + } + """) + + "one named operation" in expectPasses( + """ + query Foo { + field + } + """) + + "multiple operations" in expectPasses( + """ + query Foo { + field + } + + query Bar { + field + } + """) + + "multiple operations of different types" in expectPasses( + """ + query Foo { + field + } + + mutation Bar { + field + } + """) + + "fragment and operation named the same" in expectPasses( + """ + query Foo { + ...Foo + } + fragment Foo on Type { + field + } + """) + + "multiple operations of same name" in expectFails( + """ + query Foo { + fieldA + } + query Foo { + fieldB + } + """, + List( + "There can only be one operation named 'Foo'." -> Some(Pos(5, 9)) + )) + + "multiple operations of same name of different types" in expectFails( + """ + query Foo { + fieldA + } + mutation Foo { + fieldB + } + """, + List( + "There can only be one operation named 'Foo'." -> Some(Pos(5, 9)) + )) + } +}