diff --git a/src/main/scala/sangria/ast/QueryAst.scala b/src/main/scala/sangria/ast/QueryAst.scala index 1a8730d0..e18c586a 100644 --- a/src/main/scala/sangria/ast/QueryAst.scala +++ b/src/main/scala/sangria/ast/QueryAst.scala @@ -457,6 +457,7 @@ case class DirectiveDefinition( arguments: Vector[InputValueDefinition], locations: Vector[DirectiveLocation], description: Option[StringValue] = None, + repeatable: Boolean = false, comments: Vector[Comment] = Vector.empty, location: Option[AstLocation] = None) extends TypeSystemDefinition with WithDescription @@ -911,7 +912,7 @@ object AstVisitor { tc.foreach(c => loop(c)) breakOrSkip(onLeave(n)) } - case n @ DirectiveDefinition(_, args, locations, description, comment, _) => + case n @ DirectiveDefinition(_, args, locations, description, _, comment, _) => if (breakOrSkip(onEnter(n))) { args.foreach(d => loop(d)) locations.foreach(d => loop(d)) diff --git a/src/main/scala/sangria/introspection/IntrospectionParser.scala b/src/main/scala/sangria/introspection/IntrospectionParser.scala index fa71fd45..f2ce3fd5 100644 --- a/src/main/scala/sangria/introspection/IntrospectionParser.scala +++ b/src/main/scala/sangria/introspection/IntrospectionParser.scala @@ -82,7 +82,8 @@ object IntrospectionParser { name = mapStringField(directive, "name", path), description = mapStringFieldOpt(directive, "description"), locations = um.getListValue(mapField(directive, "locations")).map(v => DirectiveLocation.fromString(stringValue(v, path :+ "locations"))).toSet, - args = mapFieldOpt(directive, "args") map um.getListValue getOrElse Vector.empty map (arg => parseInputValue(arg, path :+ "args"))) + args = mapFieldOpt(directive, "args") map um.getListValue getOrElse Vector.empty map (arg => parseInputValue(arg, path :+ "args")), + repeatable = mapBooleanFieldOpt(directive, "isRepeatable") getOrElse false) private def parseType[In : InputUnmarshaller](tpe: In, path: Vector[String]) = mapStringField(tpe, "kind", path) match { @@ -148,11 +149,14 @@ object IntrospectionParser { private def mapBooleanField[In : InputUnmarshaller](map: In, name: String, path: Vector[String] = Vector.empty): Boolean = booleanValue(mapField(map, name, path), path :+ name) + private def mapBooleanFieldOpt[In : InputUnmarshaller](map: In, name: String, path: Vector[String] = Vector.empty): Option[Boolean] = + mapFieldOpt(map, name) filter um.isDefined map (booleanValue(_, path :+ name)) + private def mapFieldOpt[In : InputUnmarshaller](map: In, name: String): Option[In] = um.getMapValue(map, name) filter um.isDefined private def mapStringFieldOpt[In : InputUnmarshaller](map: In, name: String, path: Vector[String] = Vector.empty): Option[String] = - mapFieldOpt(map, name) filter um.isDefined map (s => stringValue(s, path :+ name) ) + mapFieldOpt(map, name) filter um.isDefined map (stringValue(_, path :+ name)) private def um[T: InputUnmarshaller] = implicitly[InputUnmarshaller[T]] diff --git a/src/main/scala/sangria/introspection/model.scala b/src/main/scala/sangria/introspection/model.scala index d2bc102c..1046d52e 100644 --- a/src/main/scala/sangria/introspection/model.scala +++ b/src/main/scala/sangria/introspection/model.scala @@ -116,4 +116,5 @@ case class IntrospectionDirective( name: String, description: Option[String], locations: Set[DirectiveLocation.Value], - args: Seq[IntrospectionInputValue]) + args: Seq[IntrospectionInputValue], + repeatable: Boolean) diff --git a/src/main/scala/sangria/introspection/package.scala b/src/main/scala/sangria/introspection/package.scala index 210757e6..3ba44ad2 100644 --- a/src/main/scala/sangria/introspection/package.scala +++ b/src/main/scala/sangria/introspection/package.scala @@ -254,7 +254,9 @@ package object introspection { Field("name", StringType, resolve = _.value.name), Field("description", OptionType(StringType), resolve = _.value.description), Field("locations", ListType(__DirectiveLocation), resolve = _.value.locations.toVector.sorted), - Field("args", ListType(__InputValue), resolve = _.value.arguments))) + Field("args", ListType(__InputValue), resolve = _.value.arguments), + Field("isRepeatable", BooleanType, Some("Permits using the directive multiple times at the same location."), + resolve = _.value.repeatable))) val __Schema = ObjectType( name = "__Schema", @@ -309,10 +311,10 @@ package object introspection { def introspectionQuery: ast.Document = introspectionQuery() - def introspectionQuery(schemaDescription: Boolean = true): ast.Document = - QueryParser.parse(introspectionQueryString(schemaDescription)) + def introspectionQuery(schemaDescription: Boolean = true, directiveRepeatableFlag: Boolean = true): ast.Document = + QueryParser.parse(introspectionQueryString(schemaDescription, directiveRepeatableFlag)) - def introspectionQueryString(schemaDescription: Boolean = true): String = + def introspectionQueryString(schemaDescription: Boolean = true, directiveRepeatableFlag: Boolean = true): String = s"""query IntrospectionQuery { | __schema { | queryType { name } @@ -328,6 +330,7 @@ package object introspection { | args { | ...InputValue | } + | ${if (directiveRepeatableFlag) "isRepeatable" else ""} | } | ${if (schemaDescription) "description" else ""} | } diff --git a/src/main/scala/sangria/macros/AstLiftable.scala b/src/main/scala/sangria/macros/AstLiftable.scala index 65db5f43..752ac3a4 100644 --- a/src/main/scala/sangria/macros/AstLiftable.scala +++ b/src/main/scala/sangria/macros/AstLiftable.scala @@ -76,8 +76,8 @@ trait AstLiftable { case FragmentDefinition(n, t, d, s, v, c, tc, p) => q"_root_.sangria.ast.FragmentDefinition($n, $t, $d, $s, $v, $c, $tc, $p)" - case DirectiveDefinition(n, a, l, desc, c, p) => - q"_root_.sangria.ast.DirectiveDefinition($n, $a, $l, $desc, $c, $p)" + case DirectiveDefinition(n, a, l, desc, r, c, p) => + q"_root_.sangria.ast.DirectiveDefinition($n, $a, $l, $desc, $r, $c, $p)" case SchemaDefinition(o, d, desc, c, tc, p) => q"_root_.sangria.ast.SchemaDefinition($o, $d, $desc, $c, $tc, $p)" diff --git a/src/main/scala/sangria/parser/QueryParser.scala b/src/main/scala/sangria/parser/QueryParser.scala index 5bec6f8b..5928abe4 100644 --- a/src/main/scala/sangria/parser/QueryParser.scala +++ b/src/main/scala/sangria/parser/QueryParser.scala @@ -320,9 +320,11 @@ trait TypeSystemDefinitions { this: Parser with Tokens with Ignored with Directi wsNoComment('{') ~ (test(legacyEmptyFields) ~ InputValueDefinition.* | InputValueDefinition.+) ~ Comments ~ wsNoComment('}') ~> (_ -> _) } + def repeatable = rule { capture(Keyword("repeatable")).? ~> (_.isDefined)} + def DirectiveDefinition = rule { - Description ~ Comments ~ trackPos ~ directive ~ '@' ~ NameStrict ~ (ArgumentsDefinition.? ~> (_ getOrElse Vector.empty)) ~ on ~ DirectiveLocations ~> ( - (descr, comment, location, name, args, locations) => ast.DirectiveDefinition(name, args, locations, descr, comment, location)) + Description ~ Comments ~ trackPos ~ directive ~ '@' ~ NameStrict ~ (ArgumentsDefinition.? ~> (_ getOrElse Vector.empty)) ~ repeatable ~ on ~ DirectiveLocations ~> ( + (descr, comment, location, name, args, rep, locations) => ast.DirectiveDefinition(name, args, locations, descr, rep, comment, location)) } def DirectiveLocations = rule { ws('|').? ~ DirectiveLocation.+(wsNoComment('|')) ~> (_.toVector) } diff --git a/src/main/scala/sangria/renderer/QueryRenderer.scala b/src/main/scala/sangria/renderer/QueryRenderer.scala index 684830c9..72e4c096 100644 --- a/src/main/scala/sangria/renderer/QueryRenderer.scala +++ b/src/main/scala/sangria/renderer/QueryRenderer.scala @@ -557,7 +557,7 @@ object QueryRenderer { renderDirs(dirs, config, indent, frontSep = true) + renderOperationTypeDefinitions(ops, ext, indent, config, frontSep = true) - case dd @ DirectiveDefinition(name, args, locations, description, _, _) => + case dd @ DirectiveDefinition(name, args, locations, description, rep, _, _) => val locsRendered = locations.zipWithIndex map { case (l, idx) => (if (idx != 0 && shouldRenderComment(l, None, config)) config.lineBreak else "") + (if (shouldRenderComment(l, None, config)) config.lineBreak else if (idx != 0) config.separator else "") + @@ -568,7 +568,9 @@ object QueryRenderer { renderComment(dd, description orElse prev, indent, config) + indent.str + "directive" + config.separator + "@" + name + renderInputValueDefs(args, indent, config) + (if (args.isEmpty) config.mandatorySeparator else "") + - "on" + (if (shouldRenderComment(locations.head, None, config)) "" else config.mandatorySeparator) + + (if (rep) "repeatable" + config.mandatorySeparator else "") + + "on" + + (if (shouldRenderComment(locations.head, None, config)) "" else config.mandatorySeparator) + locsRendered.mkString(config.separator + "|") case dl @ DirectiveLocation(name, _, _) => diff --git a/src/main/scala/sangria/renderer/SchemaRenderer.scala b/src/main/scala/sangria/renderer/SchemaRenderer.scala index 2071de92..47f2ecda 100644 --- a/src/main/scala/sangria/renderer/SchemaRenderer.scala +++ b/src/main/scala/sangria/renderer/SchemaRenderer.scala @@ -214,10 +214,10 @@ object SchemaRenderer { ast.DirectiveLocation(__DirectiveLocation.byValue(loc).name) def renderDirective(dir: Directive) = - ast.DirectiveDefinition(dir.name, renderArgs(dir.arguments), dir.locations.toVector.map(renderDirectiveLocation).sortBy(_.name), renderDescription(dir.description)) + ast.DirectiveDefinition(dir.name, renderArgs(dir.arguments), dir.locations.toVector.map(renderDirectiveLocation).sortBy(_.name), renderDescription(dir.description), dir.repeatable) def renderDirective(dir: IntrospectionDirective) = - ast.DirectiveDefinition(dir.name, renderArgsI(dir.args), dir.locations.toVector.map(renderDirectiveLocation).sortBy(_.name), renderDescription(dir.description)) + ast.DirectiveDefinition(dir.name, renderArgsI(dir.args), dir.locations.toVector.map(renderDirectiveLocation).sortBy(_.name), renderDescription(dir.description), dir.repeatable) def schemaAstFromIntrospection(introspectionSchema: IntrospectionSchema, filter: SchemaFilter = SchemaFilter.default): ast.Document = { val schemaDef = if (filter.renderSchema) renderSchemaDefinition(introspectionSchema) else None diff --git a/src/main/scala/sangria/schema/AstSchemaBuilder.scala b/src/main/scala/sangria/schema/AstSchemaBuilder.scala index 3ef79a89..4e1fcd14 100644 --- a/src/main/scala/sangria/schema/AstSchemaBuilder.scala +++ b/src/main/scala/sangria/schema/AstSchemaBuilder.scala @@ -674,6 +674,7 @@ class DefaultAstSchemaBuilder[Ctx] extends AstSchemaBuilder[Ctx] { description = directiveDescription(definition), locations = locations, arguments = arguments, + repeatable = definition.repeatable, shouldInclude = directiveShouldInclude(definition))) def transformInputObjectType[T]( diff --git a/src/main/scala/sangria/schema/IntrospectionSchemaBuilder.scala b/src/main/scala/sangria/schema/IntrospectionSchemaBuilder.scala index 797cb4ba..63ac0f47 100644 --- a/src/main/scala/sangria/schema/IntrospectionSchemaBuilder.scala +++ b/src/main/scala/sangria/schema/IntrospectionSchemaBuilder.scala @@ -258,6 +258,7 @@ class DefaultIntrospectionSchemaBuilder[Ctx] extends IntrospectionSchemaBuilder[ description = directiveDescription(definition), locations = definition.locations, arguments = arguments, + repeatable = definition.repeatable, shouldInclude = directiveShouldInclude(definition))) def objectTypeInstanceCheck(definition: IntrospectionObjectType): Option[(Any, Class[_]) => Boolean] = diff --git a/src/main/scala/sangria/schema/ResolverBasedAstSchemaBuilder.scala b/src/main/scala/sangria/schema/ResolverBasedAstSchemaBuilder.scala index 20357e1d..800c80c9 100644 --- a/src/main/scala/sangria/schema/ResolverBasedAstSchemaBuilder.scala +++ b/src/main/scala/sangria/schema/ResolverBasedAstSchemaBuilder.scala @@ -102,6 +102,26 @@ class ResolverBasedAstSchemaBuilder[Ctx](val resolvers: Seq[AstSchemaResolver[Ct case r @ AnyFieldResolver(fn) if fn.isDefinedAt(origin) => r } + override def buildSchema( + definition: Option[ast.SchemaDefinition], + extensions: List[ast.SchemaExtensionDefinition], + queryType: ObjectType[Ctx, Any], + mutationType: Option[ObjectType[Ctx, Any]], + subscriptionType: Option[ObjectType[Ctx, Any]], + additionalTypes: List[Type with Named], + directives: List[Directive], + mat: AstSchemaMaterializer[Ctx]) = + Schema[Ctx, Any]( + query = queryType, + mutation = mutationType, + subscription = subscriptionType, + additionalTypes = additionalTypes, + description = definition.flatMap(_.description.map(_.value)), + directives = directives, + astDirectives = definition.fold(Vector.empty[ast.Directive])(_.directives) ++ extensions.flatMap(_.directives), + astNodes = Vector(mat.document) ++ extensions ++ definition.toVector, + validationRules = SchemaValidationRule.default :+ new ResolvedDirectiveValidationRule(this.directives.filterNot(_.repeatable).map(_.name).toSet)) + override def resolveField( origin: MatOrigin, typeDefinition: Either[ast.TypeDefinition, ObjectLikeType[Ctx, _]], diff --git a/src/main/scala/sangria/schema/Schema.scala b/src/main/scala/sangria/schema/Schema.scala index 54439cc3..3b186b49 100644 --- a/src/main/scala/sangria/schema/Schema.scala +++ b/src/main/scala/sangria/schema/Schema.scala @@ -754,6 +754,7 @@ case class Directive( description: Option[String] = None, arguments: List[Argument[_]] = Nil, locations: Set[DirectiveLocation.Value] = Set.empty, + repeatable: Boolean = false, shouldInclude: DirectiveContext => Boolean = _ => true) extends HasArguments with Named { def rename(newName: String) = copy(name = newName).asInstanceOf[this.type] def toAst: ast.DirectiveDefinition = SchemaRenderer.renderDirective(this) diff --git a/src/main/scala/sangria/schema/SchemaComparator.scala b/src/main/scala/sangria/schema/SchemaComparator.scala index c64f608b..42017584 100644 --- a/src/main/scala/sangria/schema/SchemaComparator.scala +++ b/src/main/scala/sangria/schema/SchemaComparator.scala @@ -59,6 +59,12 @@ object SchemaComparator { } private def findInDirective(oldDir: Directive, newDir: Directive): Vector[SchemaChange] = { + val repeatableChanged = + if (oldDir.repeatable != newDir.repeatable) + Vector(SchemaChange.DirectiveRepeatableChanged(newDir, oldDir.repeatable, newDir.repeatable, !newDir.repeatable)) + else + Vector.empty + val locationChanges = findInDirectiveLocations(oldDir, newDir) val fieldChanges = findInArgs(oldDir.arguments, newDir.arguments, added = SchemaChange.DirectiveArgumentAdded(newDir, _, _), @@ -69,7 +75,7 @@ object SchemaComparator { dirAdded = SchemaChange.DirectiveArgumentAstDirectiveAdded(newDir, _, _), dirRemoved = SchemaChange.DirectiveArgumentAstDirectiveRemoved(newDir, _, _)) - locationChanges ++ fieldChanges + repeatableChanged ++ locationChanges ++ fieldChanges } private def findInDirectiveLocations(oldDir: Directive, newDir: Directive): Vector[SchemaChange] = { @@ -659,6 +665,9 @@ object SchemaChange { case class DirectiveArgumentAdded(directive: Directive, argument: Argument[_], breaking: Boolean) extends AbstractChange(s"Argument `${argument.name}` was added to `${directive.name}` directive", breaking) + case class DirectiveRepeatableChanged(directive: Directive, oldRepeatable: Boolean, newRepeatable: Boolean, breaking: Boolean) + extends AbstractChange(if (newRepeatable) s"Directive `${directive.name}` was made repeatable per location" else s"Directive `${directive.name}` was made unique per location", breaking) + case class InputFieldTypeChanged(tpe: InputObjectType[_], field: InputField[_], breaking: Boolean, oldFiledType: InputType[_], newFieldType: InputType[_]) extends AbstractChange(s"`${tpe.name}.${field.name}` input field type changed from `${SchemaRenderer.renderTypeName(oldFiledType)}` to `${SchemaRenderer.renderTypeName(newFieldType)}`", breaking) with TypeChange diff --git a/src/main/scala/sangria/schema/SchemaValidationRule.scala b/src/main/scala/sangria/schema/SchemaValidationRule.scala index 345efc63..6c56a2b2 100644 --- a/src/main/scala/sangria/schema/SchemaValidationRule.scala +++ b/src/main/scala/sangria/schema/SchemaValidationRule.scala @@ -493,5 +493,37 @@ class FullSchemaTraversalValidationRule(validators: SchemaElementValidator*) ext def validName(name: String): Boolean = !reservedNames.contains(name) } +/** + * Validates uniqueness of directives on types and the schema definition. + * + * It is not fully covered by `UniqueDirectivesPerLocation` since it onl looks at one AST node at a time, + * so it does not cover type + type extension scenario. + */ +class ResolvedDirectiveValidationRule(knownUniqueDirectives: Set[String]) extends SchemaValidationRule { + def validate[Ctx, Val](schema: Schema[Ctx, Val]): List[Violation] = { + val uniqueDirectives = knownUniqueDirectives ++ schema.directives.filterNot(_.repeatable).map(_.name) + val sourceMapper = SchemaElementValidator.sourceMapper(schema) + + val schemaViolations = validateUniqueDirectives(schema, uniqueDirectives, sourceMapper) + + val typeViolations = + schema.typeList.collect { + case withDirs: HasAstInfo => validateUniqueDirectives(withDirs, uniqueDirectives, sourceMapper) + } + + schemaViolations.toList ++ typeViolations.flatten + } + + private def validateUniqueDirectives(withDirs: HasAstInfo, uniqueDirectives: Set[String], sourceMapper: Option[SourceMapper]) = { + val duplicates = withDirs.astDirectives + .filter(d => uniqueDirectives.contains(d.name)) + .groupBy(_.name) + .filter(_._2.size > 1) + .toVector + + duplicates.map{case (dirName, dups) => DuplicateDirectiveViolation(dirName, sourceMapper, dups.flatMap(_.location).toList)} + } +} + case class SchemaValidationException(violations: Vector[Violation], eh: ExceptionHandler = ExceptionHandler.empty) extends ExecutionError( s"Schema does not pass validation. Violations:\n\n${violations map (_.errorMessage) mkString "\n\n"}", eh) with WithViolations with QueryAnalysisError diff --git a/src/main/scala/sangria/validation/rules/UniqueDirectivesPerLocation.scala b/src/main/scala/sangria/validation/rules/UniqueDirectivesPerLocation.scala index ea419529..a3b7c5d0 100644 --- a/src/main/scala/sangria/validation/rules/UniqueDirectivesPerLocation.scala +++ b/src/main/scala/sangria/validation/rules/UniqueDirectivesPerLocation.scala @@ -14,6 +14,8 @@ import scala.collection.mutable.{Map => MutableMap} */ class UniqueDirectivesPerLocation extends ValidationRule { override def visitor(ctx: ValidationContext) = new AstValidatingVisitor { + val repeatableDirectives = ctx.schema.directivesByName.mapValues(d => d.repeatable) + override val onEnter: ValidationVisit = { // Many different AST nodes may contain directives. Rather than listing // them all, just listen for entering any node, and check to see if it @@ -22,11 +24,13 @@ class UniqueDirectivesPerLocation extends ValidationRule { val knownDirectives = MutableMap[String, ast.Directive]() val errors = node.directives.foldLeft(Vector.empty[Violation]) { - case (errors, d) if knownDirectives contains d.name => - errors :+ DuplicateDirectiveViolation(d.name, ctx.sourceMapper, knownDirectives(d.name).location.toList ++ d.location.toList ) - case (errors, d) => + case (es, d) if repeatableDirectives.getOrElse(d.name, true) => + es + case (es, d) if knownDirectives contains d.name => + es :+ DuplicateDirectiveViolation(d.name, ctx.sourceMapper, knownDirectives(d.name).location.toList ++ d.location.toList ) + case (es, d) => knownDirectives(d.name) = d - errors + es } if (errors.nonEmpty) Left(errors) diff --git a/src/test/resources/queries/schema-kitchen-sink-pretty.graphql b/src/test/resources/queries/schema-kitchen-sink-pretty.graphql index edfe9b79..9d1e877e 100644 --- a/src/test/resources/queries/schema-kitchen-sink-pretty.graphql +++ b/src/test/resources/queries/schema-kitchen-sink-pretty.graphql @@ -84,4 +84,6 @@ extend type Foo @onType "cool skip" directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @myRepeatableDir(name: String!) repeatable on OBJECT | INTERFACE + directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT \ No newline at end of file diff --git a/src/test/resources/queries/schema-kitchen-sink.graphql b/src/test/resources/queries/schema-kitchen-sink.graphql index 5b6c0211..2e0d5b20 100644 --- a/src/test/resources/queries/schema-kitchen-sink.graphql +++ b/src/test/resources/queries/schema-kitchen-sink.graphql @@ -83,6 +83,10 @@ extend type Foo @onType "cool skip" directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT +directive @myRepeatableDir(name: String!) repeatable on + | OBJECT + | INTERFACE + directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD diff --git a/src/test/scala/sangria/introspection/IntrospectionSpec.scala b/src/test/scala/sangria/introspection/IntrospectionSpec.scala index dcd548e3..6c737bd5 100644 --- a/src/test/scala/sangria/introspection/IntrospectionSpec.scala +++ b/src/test/scala/sangria/introspection/IntrospectionSpec.scala @@ -4,6 +4,7 @@ import org.scalatest.{Matchers, WordSpec} import sangria.execution.Executor import sangria.parser.QueryParser import sangria.schema._ +import sangria.macros._ import sangria.util.{DebugUtil, FutureResultSupport} import sangria.validation.QueryValidator @@ -110,6 +111,19 @@ class IntrospectionSpec extends WordSpec with Matchers with FutureResultSupport "name" -> "__InputValue", "ofType" -> null)))), "isDeprecated" -> false, + "deprecationReason" -> null), + Map( + "name" -> "isRepeatable", + "description" -> "Permits using the directive multiple times at the same location.", + "args" -> Vector.empty, + "type" -> Map( + "kind" -> "NON_NULL", + "name" -> null, + "ofType" -> Map( + "kind" -> "SCALAR", + "name" -> "Boolean", + "ofType" -> null)), + "isDeprecated" -> false, "deprecationReason" -> null)), "inputFields" -> null, "interfaces" -> Vector.empty, @@ -749,7 +763,8 @@ class IntrospectionSpec extends WordSpec with Matchers with FutureResultSupport "kind" -> "SCALAR", "name" -> "Boolean", "ofType" -> null)), - "defaultValue" -> null))), + "defaultValue" -> null)), + "isRepeatable" -> false), Map( "name" -> "skip", "description" -> "Directs the executor to skip this field or fragment when the `if` argument is true.", @@ -768,7 +783,8 @@ class IntrospectionSpec extends WordSpec with Matchers with FutureResultSupport "kind" -> "SCALAR", "name" -> "Boolean", "ofType" -> null)), - "defaultValue" -> null))), + "defaultValue" -> null)), + "isRepeatable" -> false), Map( "name" -> "deprecated", "description" -> "Marks an element of a GraphQL schema as no longer supported.", @@ -783,10 +799,46 @@ class IntrospectionSpec extends WordSpec with Matchers with FutureResultSupport "kind" -> "SCALAR", "name" -> "String", "ofType" -> null), - "defaultValue" -> "\"No longer supported\"")))), + "defaultValue" -> "\"No longer supported\"")), + "isRepeatable" -> false)), "description" -> null)))) } + "includes repeatable flag on directives" in { + val testType = ObjectType("TestType", fields[Unit, Unit](Field("foo", OptionType(StringType), resolve = _ => None))) + val repeatableDirective = Directive("test", repeatable = true, locations = Set(DirectiveLocation.Object, DirectiveLocation.Interface)) + val schema = Schema(testType, directives = repeatableDirective :: BuiltinDirectives) + + val query = + gql""" + { + __schema { + directives { + name + isRepeatable + } + } + } + """ + + Executor.execute(schema, query).await should be (Map( + "data" -> Map( + "__schema" -> Map( + "directives" -> Vector( + Map( + "name" -> "test", + "isRepeatable" -> true), + Map( + "name" -> "include", + "isRepeatable" -> false), + Map( + "name" -> "skip", + "isRepeatable" -> false), + Map( + "name" -> "deprecated", + "isRepeatable" -> false)))))) + } + "introspects on input object" in { val inputType = InputObjectType("TestInputObject", List( InputField("a", OptionInputType(StringType), defaultValue = "foo"), diff --git a/src/test/scala/sangria/macros/LiteralMacroSpec.scala b/src/test/scala/sangria/macros/LiteralMacroSpec.scala index 4478aedb..78f3215b 100644 --- a/src/test/scala/sangria/macros/LiteralMacroSpec.scala +++ b/src/test/scala/sangria/macros/LiteralMacroSpec.scala @@ -1026,6 +1026,7 @@ class LiteralMacroSpec extends WordSpec with Matchers { DirectiveLocation("FRAGMENT_SPREAD", Vector.empty, None), DirectiveLocation("INLINE_FRAGMENT", Vector.empty, None)), None, + false, Vector.empty, None ), @@ -1038,6 +1039,7 @@ class LiteralMacroSpec extends WordSpec with Matchers { DirectiveLocation("FRAGMENT_SPREAD", Vector.empty, None), DirectiveLocation("INLINE_FRAGMENT", Vector.empty, None)), None, + false, Vector.empty, None )), diff --git a/src/test/scala/sangria/parser/QueryParserSpec.scala b/src/test/scala/sangria/parser/QueryParserSpec.scala index 73e27b58..6a1ecc07 100644 --- a/src/test/scala/sangria/parser/QueryParserSpec.scala +++ b/src/test/scala/sangria/parser/QueryParserSpec.scala @@ -1373,6 +1373,7 @@ class QueryParserSpec extends WordSpec with Matchers with StringMatchers { DirectiveLocation("FRAGMENT_SPREAD", Vector.empty, Some(AstLocation(76, 4, 13))), DirectiveLocation("INLINE_FRAGMENT", Vector.empty, Some(AstLocation(104, 5, 13)))), None, + false, Vector.empty, Some(AstLocation(9, 2, 9)) )), diff --git a/src/test/scala/sangria/parser/SchemaParserSpec.scala b/src/test/scala/sangria/parser/SchemaParserSpec.scala index 233a8244..b725679b 100644 --- a/src/test/scala/sangria/parser/SchemaParserSpec.scala +++ b/src/test/scala/sangria/parser/SchemaParserSpec.scala @@ -21,157 +21,157 @@ class SchemaParserSpec extends WordSpec with Matchers with StringMatchers { Vector( SchemaDefinition( Vector( - OperationTypeDefinition(OperationType.Query, NamedType("QueryType", Some(AstLocation(306, 9, 10))), Vector.empty, Some(AstLocation(299, 9, 3))), - OperationTypeDefinition(OperationType.Mutation, NamedType("MutationType", Some(AstLocation(328, 10, 13))), Vector.empty, Some(AstLocation(318, 10, 3)))), + OperationTypeDefinition(OperationType.Query, NamedType("QueryType", Some(AstLocation("", 306, 9, 10))), Vector.empty, Some(AstLocation("", 299, 9, 3))), + OperationTypeDefinition(OperationType.Mutation, NamedType("MutationType", Some(AstLocation("", 328, 10, 13))), Vector.empty, Some(AstLocation("", 318, 10, 3)))), Vector.empty, None, Vector( - Comment(" Copyright (c) 2015, Facebook, Inc.", Some(AstLocation(0, 1, 1))), - Comment(" All rights reserved.", Some(AstLocation(37, 2, 1))), - Comment("", Some(AstLocation(60, 3, 1))), - Comment(" This source code is licensed under the BSD-style license found in the", Some(AstLocation(62, 4, 1))), - Comment(" LICENSE file in the root directory of this source tree. An additional grant", Some(AstLocation(134, 5, 1))), - Comment(" of patent rights can be found in the PATENTS file in the same directory.", Some(AstLocation(212, 6, 1)))), + Comment(" Copyright (c) 2015, Facebook, Inc.", Some(AstLocation("", 0, 1, 1))), + Comment(" All rights reserved.", Some(AstLocation("", 37, 2, 1))), + Comment("", Some(AstLocation("", 60, 3, 1))), + Comment(" This source code is licensed under the BSD-style license found in the", Some(AstLocation("", 62, 4, 1))), + Comment(" LICENSE file in the root directory of this source tree. An additional grant", Some(AstLocation("", 134, 5, 1))), + Comment(" of patent rights can be found in the PATENTS file in the same directory.", Some(AstLocation("", 212, 6, 1)))), Vector.empty, - Some(AstLocation(288, 8, 1)) + Some(AstLocation("", 288, 8, 1)) ), ObjectTypeDefinition( "Foo", Vector( - NamedType("Bar", Some(AstLocation(390, 16, 21)))), + NamedType("Bar", Some(AstLocation("", 390, 16, 21)))), Vector( - FieldDefinition("one", NamedType("Type", Some(AstLocation(403, 17, 8))), Vector.empty, Vector.empty, None, Vector.empty, Some(AstLocation(398, 17, 3))), - FieldDefinition("two", NamedType("Type", Some(AstLocation(437, 18, 30))), Vector(InputValueDefinition("argument", NotNullType(NamedType("InputType", Some(AstLocation(424, 18, 17))), Some(AstLocation(424, 18, 17))), None, Vector.empty, None, Vector.empty, Some(AstLocation(414, 18, 7)))), Vector.empty, None, Vector.empty, Some(AstLocation(410, 18, 3))), - FieldDefinition("three", NamedType("Int", Some(AstLocation(487, 19, 46))), Vector(InputValueDefinition("argument", NamedType("InputType", Some(AstLocation(460, 19, 19))), None, Vector.empty, None, Vector.empty, Some(AstLocation(450, 19, 9))), InputValueDefinition("other", NamedType("String", Some(AstLocation(478, 19, 37))), None, Vector.empty, None, Vector.empty, Some(AstLocation(471, 19, 30)))), Vector.empty, None, Vector.empty, Some(AstLocation(444, 19, 3))), - FieldDefinition("four", NamedType("String", Some(AstLocation(528, 20, 38))), Vector(InputValueDefinition("argument", NamedType("String", Some(AstLocation(508, 20, 18))), Some(StringValue("string", false, None, Vector.empty, Some(AstLocation(517, 20, 27)))), Vector.empty, None, Vector.empty, Some(AstLocation(498, 20, 8)))), Vector.empty, None, Vector.empty, Some(AstLocation(493, 20, 3))), - FieldDefinition("five", NamedType("String", Some(AstLocation(586, 21, 52))), Vector(InputValueDefinition("argument", ListType(NamedType("String", Some(AstLocation(553, 21, 19))), Some(AstLocation(552, 21, 18))), Some(ListValue( + FieldDefinition("one", NamedType("Type", Some(AstLocation("", 403, 17, 8))), Vector.empty, Vector.empty, None, Vector.empty, Some(AstLocation("", 398, 17, 3))), + FieldDefinition("two", NamedType("Type", Some(AstLocation("", 437, 18, 30))), Vector(InputValueDefinition("argument", NotNullType(NamedType("InputType", Some(AstLocation("", 424, 18, 17))), Some(AstLocation("", 424, 18, 17))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 414, 18, 7)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 410, 18, 3))), + FieldDefinition("three", NamedType("Int", Some(AstLocation("", 487, 19, 46))), Vector(InputValueDefinition("argument", NamedType("InputType", Some(AstLocation("", 460, 19, 19))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 450, 19, 9))), InputValueDefinition("other", NamedType("String", Some(AstLocation("", 478, 19, 37))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 471, 19, 30)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 444, 19, 3))), + FieldDefinition("four", NamedType("String", Some(AstLocation("", 528, 20, 38))), Vector(InputValueDefinition("argument", NamedType("String", Some(AstLocation("", 508, 20, 18))), Some(StringValue("string", false, None, Vector.empty, Some(AstLocation("", 517, 20, 27)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 498, 20, 8)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 493, 20, 3))), + FieldDefinition("five", NamedType("String", Some(AstLocation("", 586, 21, 52))), Vector(InputValueDefinition("argument", ListType(NamedType("String", Some(AstLocation("", 553, 21, 19))), Some(AstLocation("", 552, 21, 18))), Some(ListValue( Vector( - StringValue("string", false, None, Vector.empty, Some(AstLocation(564, 21, 30))), - StringValue("string", false, None, Vector.empty, Some(AstLocation(574, 21, 40)))), + StringValue("string", false, None, Vector.empty, Some(AstLocation("", 564, 21, 30))), + StringValue("string", false, None, Vector.empty, Some(AstLocation("", 574, 21, 40)))), Vector.empty, - Some(AstLocation(563, 21, 29)) - )), Vector.empty, None, Vector.empty, Some(AstLocation(542, 21, 8)))), Vector.empty, None, Vector.empty, Some(AstLocation(537, 21, 3))), - FieldDefinition("six", NamedType("Type", Some(AstLocation(678, 26, 46))), Vector(InputValueDefinition("argument", NamedType("InputType", Some(AstLocation(649, 26, 17))), Some(ObjectValue( + Some(AstLocation("", 563, 21, 29)) + )), Vector.empty, None, Vector.empty, Some(AstLocation("", 542, 21, 8)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 537, 21, 3))), + FieldDefinition("six", NamedType("Type", Some(AstLocation("", 678, 26, 46))), Vector(InputValueDefinition("argument", NamedType("InputType", Some(AstLocation("", 649, 26, 17))), Some(ObjectValue( Vector( ObjectField( "key", - StringValue("value", false, None, Vector.empty, Some(AstLocation(667, 26, 35))), + StringValue("value", false, None, Vector.empty, Some(AstLocation("", 667, 26, 35))), Vector.empty, - Some(AstLocation(662, 26, 30)) + Some(AstLocation("", 662, 26, 30)) )), Vector.empty, - Some(AstLocation(661, 26, 29)) - )), Vector.empty, None, Vector.empty, Some(AstLocation(639, 26, 7)))), Vector.empty, Some(StringValue("More \"\"\" descriptions \\", true, Some("\n More \"\"\" descriptions \\\n "), Vector.empty, Some(AstLocation(596, 23, 3)))), Vector.empty, Some(AstLocation(635, 26, 3)))), + Some(AstLocation("", 661, 26, 29)) + )), Vector.empty, None, Vector.empty, Some(AstLocation("", 639, 26, 7)))), Vector.empty, Some(StringValue("More \"\"\" descriptions \\", true, Some("\n More \"\"\" descriptions \\\n "), Vector.empty, Some(AstLocation("", 596, 23, 3)))), Vector.empty, Some(AstLocation("", 635, 26, 3)))), Vector.empty, - Some(StringValue("type description!", true, Some("\ntype description!\n"), Vector.empty, Some(AstLocation(344, 13, 1)))), + Some(StringValue("type description!", true, Some("\ntype description!\n"), Vector.empty, Some(AstLocation("", 344, 13, 1)))), Vector.empty, Vector.empty, - Some(AstLocation(370, 16, 1)) + Some(AstLocation("", 370, 16, 1)) ), ObjectTypeDefinition( "AnnotatedObject", Vector.empty, Vector( - FieldDefinition("annotatedField", NamedType("Type", Some(AstLocation(781, 30, 49))), Vector(InputValueDefinition("arg", NamedType("Type", Some(AstLocation(755, 30, 23))), Some(StringValue("default", false, None, Vector.empty, Some(AstLocation(762, 30, 30)))), Vector(Directive( + FieldDefinition("annotatedField", NamedType("Type", Some(AstLocation("", 781, 30, 49))), Vector(InputValueDefinition("arg", NamedType("Type", Some(AstLocation("", 755, 30, 23))), Some(StringValue("default", false, None, Vector.empty, Some(AstLocation("", 762, 30, 30)))), Vector(Directive( "onArg", Vector.empty, Vector.empty, - Some(AstLocation(772, 30, 40)) - )), None, Vector.empty, Some(AstLocation(750, 30, 18)))), Vector(Directive( + Some(AstLocation("", 772, 30, 40)) + )), None, Vector.empty, Some(AstLocation("", 750, 30, 18)))), Vector(Directive( "onField", Vector.empty, Vector.empty, - Some(AstLocation(786, 30, 54)) - )), None, Vector.empty, Some(AstLocation(735, 30, 3)))), + Some(AstLocation("", 786, 30, 54)) + )), None, Vector.empty, Some(AstLocation("", 735, 30, 3)))), Vector( Directive( "onObject", Vector( Argument( "arg", - StringValue("value", false, None, Vector.empty, Some(AstLocation(722, 29, 37))), + StringValue("value", false, None, Vector.empty, Some(AstLocation("", 722, 29, 37))), Vector.empty, - Some(AstLocation(717, 29, 32)) + Some(AstLocation("", 717, 29, 32)) )), Vector.empty, - Some(AstLocation(707, 29, 22)) + Some(AstLocation("", 707, 29, 22)) )), None, Vector.empty, Vector.empty, - Some(AstLocation(686, 29, 1)) + Some(AstLocation("", 686, 29, 1)) ), InterfaceTypeDefinition( "Bar", Vector( - FieldDefinition("one", NamedType("Type", Some(AstLocation(875, 37, 8))), Vector.empty, Vector.empty, None, Vector.empty, Some(AstLocation(870, 37, 3))), - FieldDefinition("four", NamedType("String", Some(AstLocation(917, 38, 38))), Vector(InputValueDefinition("argument", NamedType("String", Some(AstLocation(897, 38, 18))), Some(StringValue("string", false, None, Vector.empty, Some(AstLocation(906, 38, 27)))), Vector.empty, None, Vector.empty, Some(AstLocation(887, 38, 8)))), Vector.empty, None, Vector.empty, Some(AstLocation(882, 38, 3)))), + FieldDefinition("one", NamedType("Type", Some(AstLocation("", 875, 37, 8))), Vector.empty, Vector.empty, None, Vector.empty, Some(AstLocation("", 870, 37, 3))), + FieldDefinition("four", NamedType("String", Some(AstLocation("", 917, 38, 38))), Vector(InputValueDefinition("argument", NamedType("String", Some(AstLocation("", 897, 38, 18))), Some(StringValue("string", false, None, Vector.empty, Some(AstLocation("", 906, 38, 27)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 887, 38, 8)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 882, 38, 3)))), Vector.empty, - Some(StringValue(" It's an interface!", false, None, Vector(Comment(" comment above", Some(AstLocation(798, 33, 1)))), Some(AstLocation(814, 34, 1)))), + Some(StringValue(" It's an interface!", false, None, Vector(Comment(" comment above", Some(AstLocation("", 798, 33, 1)))), Some(AstLocation("", 814, 34, 1)))), Vector( - Comment(" comment below", Some(AstLocation(836, 35, 1)))), + Comment(" comment below", Some(AstLocation("", 836, 35, 1)))), Vector.empty, - Some(AstLocation(852, 36, 1)) + Some(AstLocation("", 852, 36, 1)) ), InterfaceTypeDefinition( "AnnotatedInterface", Vector( - FieldDefinition("annotatedField", NamedType("Type", Some(AstLocation(1007, 42, 37))), Vector(InputValueDefinition("arg", NamedType("Type", Some(AstLocation(993, 42, 23))), None, Vector(Directive( + FieldDefinition("annotatedField", NamedType("Type", Some(AstLocation("", 1007, 42, 37))), Vector(InputValueDefinition("arg", NamedType("Type", Some(AstLocation("", 993, 42, 23))), None, Vector(Directive( "onArg", Vector.empty, Vector.empty, - Some(AstLocation(998, 42, 28)) - )), None, Vector.empty, Some(AstLocation(988, 42, 18)))), Vector(Directive( + Some(AstLocation("", 998, 42, 28)) + )), None, Vector.empty, Some(AstLocation("", 988, 42, 18)))), Vector(Directive( "onField", Vector.empty, Vector.empty, - Some(AstLocation(1012, 42, 42)) - )), None, Vector.empty, Some(AstLocation(973, 42, 3)))), + Some(AstLocation("", 1012, 42, 42)) + )), None, Vector.empty, Some(AstLocation("", 973, 42, 3)))), Vector( Directive( "onInterface", Vector.empty, Vector.empty, - Some(AstLocation(956, 41, 30)) + Some(AstLocation("", 956, 41, 30)) )), None, Vector.empty, Vector.empty, - Some(AstLocation(927, 41, 1)) + Some(AstLocation("", 927, 41, 1)) ), UnionTypeDefinition( "Feed", Vector( - NamedType("Story", Some(AstLocation(1037, 45, 14))), - NamedType("Article", Some(AstLocation(1045, 45, 22))), - NamedType("Advert", Some(AstLocation(1055, 45, 32)))), + NamedType("Story", Some(AstLocation("", 1037, 45, 14))), + NamedType("Article", Some(AstLocation("", 1045, 45, 22))), + NamedType("Advert", Some(AstLocation("", 1055, 45, 32)))), Vector.empty, None, Vector.empty, - Some(AstLocation(1024, 45, 1)) + Some(AstLocation("", 1024, 45, 1)) ), UnionTypeDefinition( "AnnotatedUnion", Vector( - NamedType("A", Some(AstLocation(1095, 47, 33))), - NamedType("B", Some(AstLocation(1099, 47, 37)))), + NamedType("A", Some(AstLocation("", 1095, 47, 33))), + NamedType("B", Some(AstLocation("", 1099, 47, 37)))), Vector( Directive( "onUnion", Vector.empty, Vector.empty, - Some(AstLocation(1084, 47, 22)) + Some(AstLocation("", 1084, 47, 22)) )), None, Vector.empty, - Some(AstLocation(1063, 47, 1)) + Some(AstLocation("", 1063, 47, 1)) ), ScalarTypeDefinition( "CustomScalar", Vector.empty, None, Vector.empty, - Some(AstLocation(1102, 49, 1)) + Some(AstLocation("", 1102, 49, 1)) ), ScalarTypeDefinition( "AnnotatedScalar", @@ -180,22 +180,22 @@ class SchemaParserSpec extends WordSpec with Matchers with StringMatchers { "onScalar", Vector.empty, Vector.empty, - Some(AstLocation(1146, 51, 24)) + Some(AstLocation("", 1146, 51, 24)) )), None, Vector.empty, - Some(AstLocation(1123, 51, 1)) + Some(AstLocation("", 1123, 51, 1)) ), EnumTypeDefinition( "Site", Vector( - EnumValueDefinition("DESKTOP", Vector.empty, Some(StringValue("description 1", false, None, Vector.empty, Some(AstLocation(1171, 54, 3)))), Vector.empty, Some(AstLocation(1189, 55, 3))), - EnumValueDefinition("MOBILE", Vector.empty, Some(StringValue("description 2", true, Some("\n description 2\n "), Vector.empty, Some(AstLocation(1199, 56, 3)))), Vector.empty, Some(AstLocation(1227, 59, 3)))), + EnumValueDefinition("DESKTOP", Vector.empty, Some(StringValue("description 1", false, None, Vector.empty, Some(AstLocation("", 1171, 54, 3)))), Vector.empty, Some(AstLocation("", 1189, 55, 3))), + EnumValueDefinition("MOBILE", Vector.empty, Some(StringValue("description 2", true, Some("\n description 2\n "), Vector.empty, Some(AstLocation("", 1199, 56, 3)))), Vector.empty, Some(AstLocation("", 1227, 59, 3)))), Vector.empty, None, Vector.empty, Vector.empty, - Some(AstLocation(1157, 53, 1)) + Some(AstLocation("", 1157, 53, 1)) ), EnumTypeDefinition( "AnnotatedEnum", @@ -204,62 +204,62 @@ class SchemaParserSpec extends WordSpec with Matchers with StringMatchers { "onEnumValue", Vector.empty, Vector.empty, - Some(AstLocation(1284, 63, 19)) - )), None, Vector.empty, Some(AstLocation(1268, 63, 3))), - EnumValueDefinition("OTHER_VALUE", Vector.empty, None, Vector.empty, Some(AstLocation(1299, 64, 3)))), + Some(AstLocation("", 1284, 63, 19)) + )), None, Vector.empty, Some(AstLocation("", 1268, 63, 3))), + EnumValueDefinition("OTHER_VALUE", Vector.empty, None, Vector.empty, Some(AstLocation("", 1299, 64, 3)))), Vector( Directive( "onEnum", Vector.empty, Vector.empty, - Some(AstLocation(1256, 62, 20)) + Some(AstLocation("", 1256, 62, 20)) )), None, Vector.empty, Vector.empty, - Some(AstLocation(1237, 62, 1)) + Some(AstLocation("", 1237, 62, 1)) ), InputObjectTypeDefinition( "InputType", Vector( - InputValueDefinition("key", NotNullType(NamedType("String", Some(AstLocation(1339, 68, 8))), Some(AstLocation(1339, 68, 8))), None, Vector.empty, None, Vector.empty, Some(AstLocation(1334, 68, 3))), - InputValueDefinition("answer", NamedType("Int", Some(AstLocation(1357, 69, 11))), Some(BigIntValue(42, Vector.empty, Some(AstLocation(1363, 69, 17)))), Vector.empty, None, Vector.empty, Some(AstLocation(1349, 69, 3)))), + InputValueDefinition("key", NotNullType(NamedType("String", Some(AstLocation("", 1339, 68, 8))), Some(AstLocation("", 1339, 68, 8))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 1334, 68, 3))), + InputValueDefinition("answer", NamedType("Int", Some(AstLocation("", 1357, 69, 11))), Some(BigIntValue(42, Vector.empty, Some(AstLocation("", 1363, 69, 17)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 1349, 69, 3)))), Vector.empty, None, Vector.empty, Vector.empty, - Some(AstLocation(1314, 67, 1)) + Some(AstLocation("", 1314, 67, 1)) ), InputObjectTypeDefinition( "AnnotatedInput", Vector( - InputValueDefinition("annotatedField", NamedType("Type", Some(AstLocation(1447, 74, 19))), None, Vector(Directive( + InputValueDefinition("annotatedField", NamedType("Type", Some(AstLocation("", 1447, 74, 19))), None, Vector(Directive( "onField", Vector.empty, Vector.empty, - Some(AstLocation(1452, 74, 24)) - )), None, Vector(Comment(" field comment", Some(AstLocation(1413, 73, 3)))), Some(AstLocation(1431, 74, 3)))), + Some(AstLocation("", 1452, 74, 24)) + )), None, Vector(Comment(" field comment", Some(AstLocation("", 1413, 73, 3)))), Some(AstLocation("", 1431, 74, 3)))), Vector( Directive( "onInputObjectType", Vector.empty, Vector.empty, - Some(AstLocation(1390, 72, 22)) + Some(AstLocation("", 1390, 72, 22)) )), None, Vector.empty, Vector.empty, - Some(AstLocation(1369, 72, 1)) + Some(AstLocation("", 1369, 72, 1)) ), ObjectTypeExtensionDefinition( "Foo", Vector.empty, Vector( - FieldDefinition("seven", NamedType("Type", Some(AstLocation(1511, 78, 30))), Vector(InputValueDefinition("argument", ListType(NamedType("String", Some(AstLocation(1501, 78, 20))), Some(AstLocation(1500, 78, 19))), None, Vector.empty, None, Vector.empty, Some(AstLocation(1490, 78, 9)))), Vector.empty, None, Vector.empty, Some(AstLocation(1484, 78, 3)))), + FieldDefinition("seven", NamedType("Type", Some(AstLocation("", 1511, 78, 30))), Vector(InputValueDefinition("argument", ListType(NamedType("String", Some(AstLocation("", 1501, 78, 20))), Some(AstLocation("", 1500, 78, 19))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 1490, 78, 9)))), Vector.empty, None, Vector.empty, Some(AstLocation("", 1484, 78, 3)))), Vector.empty, Vector.empty, Vector.empty, - Some(AstLocation(1464, 77, 1)) + Some(AstLocation("", 1464, 77, 1)) ), ObjectTypeExtensionDefinition( "Foo", @@ -270,41 +270,55 @@ class SchemaParserSpec extends WordSpec with Matchers with StringMatchers { "onType", Vector.empty, Vector.empty, - Some(AstLocation(1535, 81, 17)) + Some(AstLocation("", 1535, 81, 17)) )), Vector.empty, Vector.empty, - Some(AstLocation(1519, 81, 1)) + Some(AstLocation("", 1519, 81, 1)) ), DirectiveDefinition( "skip", Vector( - InputValueDefinition("if", NotNullType(NamedType("Boolean", Some(AstLocation(1577, 84, 21))), Some(AstLocation(1577, 84, 21))), None, Vector.empty, None, Vector.empty, Some(AstLocation(1573, 84, 17)))), + InputValueDefinition("if", NotNullType(NamedType("Boolean", Some(AstLocation("", 1577, 84, 21))), Some(AstLocation("", 1577, 84, 21))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 1573, 84, 17)))), Vector( - DirectiveLocation("FIELD", Vector.empty, Some(AstLocation(1590, 84, 34))), - DirectiveLocation("FRAGMENT_SPREAD", Vector.empty, Some(AstLocation(1598, 84, 42))), - DirectiveLocation("INLINE_FRAGMENT", Vector.empty, Some(AstLocation(1616, 84, 60)))), - Some(StringValue("cool skip", false, None, Vector.empty, Some(AstLocation(1545, 83, 1)))), + DirectiveLocation("FIELD", Vector.empty, Some(AstLocation("", 1590, 84, 34))), + DirectiveLocation("FRAGMENT_SPREAD", Vector.empty, Some(AstLocation("", 1598, 84, 42))), + DirectiveLocation("INLINE_FRAGMENT", Vector.empty, Some(AstLocation("", 1616, 84, 60)))), + Some(StringValue("cool skip", false, None, Vector.empty, Some(AstLocation("", 1545, 83, 1)))), + false, Vector.empty, - Some(AstLocation(1557, 84, 1)) + Some(AstLocation("", 1557, 84, 1)) + ), + DirectiveDefinition( + "myRepeatableDir", + Vector( + InputValueDefinition("name", NotNullType(NamedType("String", Some(AstLocation("", 1666, 86, 34))), Some(AstLocation("", 1666, 86, 34))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 1660, 86, 28)))), + Vector( + DirectiveLocation("OBJECT", Vector.empty, Some(AstLocation("", 1693, 87, 5))), + DirectiveLocation("INTERFACE", Vector.empty, Some(AstLocation("", 1704, 88, 5)))), + None, + true, + Vector.empty, + Some(AstLocation("", 1633, 86, 1)) ), DirectiveDefinition( "include", Vector( - InputValueDefinition("if", NotNullType(NamedType("Boolean", Some(AstLocation(1656, 86, 24))), Some(AstLocation(1656, 86, 24))), None, Vector.empty, None, Vector.empty, Some(AstLocation(1652, 86, 20)))), + InputValueDefinition("if", NotNullType(NamedType("Boolean", Some(AstLocation("", 1738, 90, 24))), Some(AstLocation("", 1738, 90, 24))), None, Vector.empty, None, Vector.empty, Some(AstLocation("", 1734, 90, 20)))), Vector( - DirectiveLocation("FIELD", Vector.empty, Some(AstLocation(1671, 87, 6))), - DirectiveLocation("FRAGMENT_SPREAD", Vector.empty, Some(AstLocation(1682, 88, 6))), - DirectiveLocation("INLINE_FRAGMENT", Vector.empty, Some(AstLocation(1703, 89, 6)))), + DirectiveLocation("FIELD", Vector.empty, Some(AstLocation("", 1753, 91, 6))), + DirectiveLocation("FRAGMENT_SPREAD", Vector.empty, Some(AstLocation("", 1764, 92, 6))), + DirectiveLocation("INLINE_FRAGMENT", Vector.empty, Some(AstLocation("", 1785, 93, 6)))), None, + false, Vector.empty, - Some(AstLocation(1633, 86, 1)) + Some(AstLocation("", 1715, 90, 1)) )), Vector.empty, - Some(AstLocation(0, 1, 1)), + Some(AstLocation("", 0, 1, 1)), None ) - + parseQuery(query) should be(Success(expectedAst)) } @@ -786,6 +800,52 @@ class SchemaParserSpec extends WordSpec with Matchers with StringMatchers { """) } + "Directive definition" in { + val Success(ast) = parseQuery("directive @foo on OBJECT | INTERFACE") + + ast.withoutSourceMapper should be ( + Document( + Vector( + DirectiveDefinition( + "foo", + Vector.empty, + Vector( + DirectiveLocation("OBJECT", Vector.empty, Some(AstLocation("", 18, 1, 19))), + DirectiveLocation("INTERFACE", Vector.empty, Some(AstLocation("", 27, 1, 28)))), + None, + false, + Vector.empty, + Some(AstLocation("", 0, 1, 1)) + )), + Vector.empty, + Some(AstLocation("", 0, 1, 1)), + None + )) + } + + "Repeatable directive definition" in { + val Success(ast) = parseQuery("directive @foo repeatable on OBJECT | INTERFACE") + + ast.withoutSourceMapper should be ( + Document( + Vector( + DirectiveDefinition( + "foo", + Vector.empty, + Vector( + DirectiveLocation("OBJECT", Vector.empty, Some(AstLocation("", 29, 1, 30))), + DirectiveLocation("INTERFACE", Vector.empty, Some(AstLocation("", 38, 1, 39)))), + None, + true, + Vector.empty, + Some(AstLocation("", 0, 1, 1)) + )), + Vector.empty, + Some(AstLocation("", 0, 1, 1)), + None + )) + } + "Allow legacy empty fields syntax" in { val Success(ast) = QueryParser.parse( """ diff --git a/src/test/scala/sangria/renderer/QueryRendererSpec.scala b/src/test/scala/sangria/renderer/QueryRendererSpec.scala index 2055f4e4..cb75aace 100644 --- a/src/test/scala/sangria/renderer/QueryRendererSpec.scala +++ b/src/test/scala/sangria/renderer/QueryRendererSpec.scala @@ -710,6 +710,7 @@ class QueryRendererSpec extends WordSpec with Matchers with StringMatchers { |extend type Foo {seven(argument:[String]):Type} |extend type Foo @onType |"cool skip" directive@skip(if:Boolean!)on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT + |directive@myRepeatableDir(name:String!)repeatable on OBJECT|INTERFACE |directive@include(if:Boolean!)on FIELD|FRAGMENT_SPREAD|INLINE_FRAGMENT""".stripMargin) (after being strippedOfCarriageReturns) prettyRendered should equal (FileUtil loadQuery "schema-kitchen-sink-pretty.graphql") diff --git a/src/test/scala/sangria/renderer/SchemaRenderSpec.scala b/src/test/scala/sangria/renderer/SchemaRenderSpec.scala index 3604bd88..6fab118d 100644 --- a/src/test/scala/sangria/renderer/SchemaRenderSpec.scala +++ b/src/test/scala/sangria/renderer/SchemaRenderSpec.scala @@ -617,6 +617,9 @@ class SchemaRenderSpec extends WordSpec with Matchers with FutureResultSupport w | description: String | locations: [__DirectiveLocation!]! | args: [__InputValue!]! + | + | "Permits using the directive multiple times at the same location." + | isRepeatable: Boolean! |} | |"A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies." @@ -787,6 +790,9 @@ class SchemaRenderSpec extends WordSpec with Matchers with FutureResultSupport w | description: String | locations: [__DirectiveLocation!]! | args: [__InputValue!]! + | + | # Permits using the directive multiple times at the same location. + | isRepeatable: Boolean! |} | |# A Directive can be adjacent to many parts of the GraphQL language, a __DirectiveLocation describes one such possible adjacencies. diff --git a/src/test/scala/sangria/schema/AstSchemaMaterializerSpec.scala b/src/test/scala/sangria/schema/AstSchemaMaterializerSpec.scala index ab9e29d4..504a2e0b 100644 --- a/src/test/scala/sangria/schema/AstSchemaMaterializerSpec.scala +++ b/src/test/scala/sangria/schema/AstSchemaMaterializerSpec.scala @@ -438,6 +438,21 @@ class AstSchemaMaterializerSpec extends WordSpec with Matchers with FutureResult cycleOutput(schema) should equal (schema) (after being strippedOfCarriageReturns) } + + "Supports repeatable directives" in { + val schemaStr = + """type Query { + | str: String + |} + | + |directive @foo(arg: Int) repeatable on FIELD""".stripMargin + + cycleOutput(schemaStr) should equal (schemaStr) (after being strippedOfCarriageReturns) + + val schema = Schema.buildFromAst(QueryParser.parse(schemaStr)) + + schema.directivesByName("foo").repeatable should be (true) + } } "Failures" should { diff --git a/src/test/scala/sangria/schema/IntrospectionSchemaMaterializerSpec.scala b/src/test/scala/sangria/schema/IntrospectionSchemaMaterializerSpec.scala index 482f7de3..2a0d0993 100644 --- a/src/test/scala/sangria/schema/IntrospectionSchemaMaterializerSpec.scala +++ b/src/test/scala/sangria/schema/IntrospectionSchemaMaterializerSpec.scala @@ -220,9 +220,14 @@ class IntrospectionSchemaMaterializerSpec extends WordSpec with Matchers with Fu query = ObjectType("Simple", "This is a simple type", fields[Any, Any]( Field("string", OptionType(StringType), Some("This is a string field"), resolve = _ => None))), - directives = BuiltinDirectives ++ List(Directive("customDirective", Some("This is a custom directive"), - shouldInclude = _ => true, - locations = Set(DirectiveLocation.Field))))) + directives = BuiltinDirectives ++ List( + Directive("customDirective", Some("This is a custom directive"), + shouldInclude = _ => true, + locations = Set(DirectiveLocation.Field)), + Directive("customRepeatableDirective", Some("This is a custom repeatable directive"), + shouldInclude = _ => true, + repeatable = true, + locations = Set(DirectiveLocation.Field))))) "builds a schema aware of deprecation" in testSchema( Schema(ObjectType("Simple", "This is a simple type", fields[Any, Any]( diff --git a/src/test/scala/sangria/schema/ResolverBasedAstSchemaBuilderSpec.scala b/src/test/scala/sangria/schema/ResolverBasedAstSchemaBuilderSpec.scala index 619cad08..b1409d3c 100644 --- a/src/test/scala/sangria/schema/ResolverBasedAstSchemaBuilderSpec.scala +++ b/src/test/scala/sangria/schema/ResolverBasedAstSchemaBuilderSpec.scala @@ -866,5 +866,115 @@ class ResolverBasedAstSchemaBuilderSpec extends WordSpec with Matchers with Futu } """.parseJson) } + + "validate unique directives on extensions" in { + val TestDir = Directive("dir", locations = + Set(DL.Object, DL.Interface, DL.Union, DL.InputObject, DL.Scalar, DL.Schema, DL.Enum)) + + val TestRepDir = Directive("dir", repeatable = true, locations = + Set(DL.Object, DL.Interface, DL.Union, DL.InputObject, DL.Scalar, DL.Schema, DL.Enum)) + + val builder = resolverBased[Unit](AdditionalDirectives(Seq(TestDir))) + val builderRep = resolverBased[Unit](AdditionalDirectives(Seq(TestRepDir))) + + def testForUnique(schemaAst: ast.Document) = { + val error = intercept [SchemaValidationException] ( + Schema.buildFromAst(schemaAst, builder.validateSchemaWithException(schemaAst))) + + error.violations should have size 1 + + error.violations.head.errorMessage should include( + "The directive 'dir' can only be used once at this location.") + + // should be successful + Schema.buildFromAst(schemaAst, builderRep.validateSchemaWithException(schemaAst)) + } + + testForUnique( + gql""" + type Query @dir {field: String} + extend type Query @dir + """) + + testForUnique( + gql""" + type Query {field: String} + interface Foo @dir {field: String} + extend interface Foo @dir + """) + + testForUnique( + gql""" + type Query @dir {filed: String} + type Cat {field: String} + type Dog {field: String} + + union Foo @dir = Cat | Dog + + extend union Foo @dir + """) + + testForUnique( + gql""" + type Query @dir {filed: String} + enum Foo @dir {RED, BLUE} + extend enum Foo @dir + """) + + testForUnique( + gql""" + type Query @dir {filed: String} + scalar Foo @dir + extend scalar Foo @dir + """) + + testForUnique( + gql""" + type Query @dir {filed: String} + input Foo @dir {field: String} + extend input Foo @dir + """) + + testForUnique( + gql""" + type Query @dir {filed: String} + schema @dir {query: Query} + extend schema @dir + """) + } + + "validate unique directives on extensions with schema extension" in { + val TestDir = Directive("dir", locations = + Set(DL.Object, DL.Interface, DL.Union, DL.InputObject, DL.Scalar, DL.Schema, DL.Enum)) + + val builder = resolverBased[Unit](AdditionalDirectives(Seq(TestDir))) + + val mainSchemaAst = gql"type Query @dir {field: String}" + val extSchemaAst = gql"extend type Query @dir" + + val schema = Schema.buildFromAst(mainSchemaAst, builder.validateSchemaWithException(mainSchemaAst)) + + val error = intercept [SchemaValidationException] ( + schema.extend(extSchemaAst, builder.validateSchemaWithException(extSchemaAst))) + + error.violations should have size 1 + error.violations.head.errorMessage should include( + "The directive 'dir' can only be used once at this location.") + } + + "allow empty field list on types as long as provider contributes new fields" in { + val TestDir = Directive("dir", locations = Set(DL.Object)) + + val builder = resolverBased[Any](DirectiveFieldProvider(TestDir, c => List(MaterializedField(c.origin, + Field("hello", c.scalarType("String"), resolve = (_: Context[Any, Any]) => "world"))))) + + val schemaAst = gql"type Query @dir" + + val schema = Schema.buildFromAst(schemaAst, builder.validateSchemaWithException(schemaAst)) + + val query = gql"{hello}" + + Executor.execute(schema, query).await should be (Map("data" -> Map("hello" -> "world"))) + } } } diff --git a/src/test/scala/sangria/schema/SchemaComparatorSpec.scala b/src/test/scala/sangria/schema/SchemaComparatorSpec.scala index 7418081e..b831de38 100644 --- a/src/test/scala/sangria/schema/SchemaComparatorSpec.scala +++ b/src/test/scala/sangria/schema/SchemaComparatorSpec.scala @@ -153,6 +153,20 @@ class SchemaComparatorSpec extends WordSpec with Matchers { breakingChange[DirectiveLocationRemoved]("`Enum` directive location removed from `foo` directive"), nonBreakingChange[DirectiveArgumentAdded]("Argument `c` was added to `foo` directive")) + "detect changes repeatable directive definition" in checkChanges( + gql""" + directive @a repeatable on OBJECT + directive @b(foo: Int) on OBJECT + """, + + gql""" + directive @a on OBJECT + directive @b(foo: Int) repeatable on OBJECT + """, + + breakingChange[DirectiveRepeatableChanged]("Directive `a` was made unique per location"), + nonBreakingChange[DirectiveRepeatableChanged]("Directive `b` was made repeatable per location")) + "should detect changes in input types" in checkChanges( graphql""" input Sort {dir: Int} diff --git a/src/test/scala/sangria/util/ValidationSupport.scala b/src/test/scala/sangria/util/ValidationSupport.scala index 41cd90a4..169c339b 100644 --- a/src/test/scala/sangria/util/ValidationSupport.scala +++ b/src/test/scala/sangria/util/ValidationSupport.scala @@ -169,6 +169,13 @@ trait ValidationSupport extends Matchers { Directive("onFragmentSpread", locations = Set(DirectiveLocation.FragmentSpread), shouldInclude = alwaysInclude), Directive("onInlineFragment", locations = Set(DirectiveLocation.InlineFragment), shouldInclude = alwaysInclude), Directive("onVariableDefinition", locations = Set(DirectiveLocation.VariableDefinition), shouldInclude = alwaysInclude), + Directive("genericDirectiveA", locations = Set(DirectiveLocation.FragmentDefinition, DirectiveLocation.Field), shouldInclude = alwaysInclude), + Directive("genericDirectiveB", locations = Set(DirectiveLocation.FragmentDefinition, DirectiveLocation.Field), shouldInclude = alwaysInclude), + Directive("repeatableDirective", + repeatable = true, + arguments = Argument("id", IntType, "Some generic ID") :: Nil, + locations = Set(DirectiveLocation.Object), + shouldInclude = alwaysInclude), Directive("onSchema", locations = Set(DirectiveLocation.Schema), shouldInclude = alwaysInclude), Directive("onScalar", locations = Set(DirectiveLocation.Scalar), shouldInclude = alwaysInclude), Directive("onObject", locations = Set(DirectiveLocation.Object), shouldInclude = alwaysInclude), diff --git a/src/test/scala/sangria/validation/rules/UniqueDirectivesPerLocationSpec.scala b/src/test/scala/sangria/validation/rules/UniqueDirectivesPerLocationSpec.scala index 074407cb..e17f0657 100644 --- a/src/test/scala/sangria/validation/rules/UniqueDirectivesPerLocationSpec.scala +++ b/src/test/scala/sangria/validation/rules/UniqueDirectivesPerLocationSpec.scala @@ -17,74 +17,95 @@ class UniqueDirectivesPerLocationSpec extends WordSpec with ValidationSupport { "unique directives in different locations" in expectPasses( """ - fragment Test on Type @directiveA { - field @directiveB + fragment Test on Type @genericDirectiveA { + field @genericDirectiveB } """) "unique directives in same locations" in expectPasses( """ - fragment Test on Type @directiveA @directiveB { - field @directiveA @directiveB + fragment Test on Type @genericDirectiveA @genericDirectiveB { + field @genericDirectiveA @genericDirectiveB } """) "same directives in different locations" in expectPasses( """ - fragment Test on Type @directiveA { - field @directiveA + fragment Test on Type @genericDirectiveA { + field @genericDirectiveA } """) "same directives in similar locations" in expectPasses( """ fragment Test on Type { - field @directive - field @directive + field @genericDirectiveA + field @genericDirectiveA } """) - "duplicate directives in one location" in expectFailsPosList( + "repeatable directives in same location" in expectPasses( + """ + type Test @repeatableDirective(id: 1) @repeatableDirective(id: 2) { + field: String! + } + """) + + "repeatable directives in similar locations" in expectPasses( + """ + type Test @repeatableDirective(id: 1) { + field: String! + } + + extend type Test @repeatableDirective(id: 2) { + anotherField: String! + } + """) + + "unknown directives must be ignored" in expectPasses( + """ + type Test @unknownDirective @unknownDirective { + field: String! + } + + extend type Test @unknownDirective { + anotherField: String! + } + """) + + "duplicate directives in one location" in expectFailsSimple( """ fragment Test on Type { - field @directive @directive + field @genericDirectiveA @genericDirectiveA } """, - List( - "The directive 'directive' can only be used once at this location." -> List(Pos(3, 17), Pos(3, 28)) - )) + "The directive 'genericDirectiveA' can only be used once at this location." -> Seq(Pos(3, 17), Pos(3, 36))) - "many duplicate directives in one location" in expectFailsPosList( + "many duplicate directives in one location" in expectFailsSimple( """ fragment Test on Type { - field @directive @directive @directive + field @genericDirectiveA @genericDirectiveA @genericDirectiveA } """, - List( - "The directive 'directive' can only be used once at this location." -> List(Pos(3, 17), Pos(3, 28)), - "The directive 'directive' can only be used once at this location." -> List(Pos(3, 17), Pos(3, 39)) - )) + "The directive 'genericDirectiveA' can only be used once at this location." -> Seq(Pos(3, 17), Pos(3, 36)), + "The directive 'genericDirectiveA' can only be used once at this location." -> Seq(Pos(3, 17), Pos(3, 55))) - "different duplicate directives in one location" in expectFailsPosList( + "different duplicate directives in one location" in expectFailsSimple( """ fragment Test on Type { - field @directiveA @directiveB @directiveA @directiveB + field @genericDirectiveA @genericDirectiveB @genericDirectiveA @genericDirectiveB } """, - List( - "The directive 'directiveA' can only be used once at this location." -> List(Pos(3, 17), Pos(3, 41)), - "The directive 'directiveB' can only be used once at this location." -> List(Pos(3, 29), Pos(3, 53)) - )) + "The directive 'genericDirectiveA' can only be used once at this location." -> Seq(Pos(3, 17), Pos(3, 55)), + "The directive 'genericDirectiveB' can only be used once at this location." -> Seq(Pos(3, 36), Pos(3, 74))) - "duplicate directives in many locations" in expectFailsPosList( + "duplicate directives in many locations" in expectFailsSimple( """ - fragment Test on Type @directive @directive { - field @directive @directive + fragment Test on Type @genericDirectiveA @genericDirectiveA { + field @genericDirectiveA @genericDirectiveA } """, - List( - "The directive 'directive' can only be used once at this location." -> List(Pos(2, 31), Pos(2, 42)), - "The directive 'directive' can only be used once at this location." -> List(Pos(3, 17), Pos(3, 28)) - )) + "The directive 'genericDirectiveA' can only be used once at this location." -> Seq(Pos(2, 31), Pos(2, 50)), + "The directive 'genericDirectiveA' can only be used once at this location." -> Seq(Pos(3, 17), Pos(3, 36))) } }