Skip to content

Commit 233097a

Browse files
committed
Add option to allow applying undeclared directives
- New SchemaParserOptions.allowUndeclaredDirectives, off by default - Undeclared directives are kept as applied directives only, with argument types guessed from their values - Fails with a SchemaError when an argument type can't be guessed
1 parent e89f76a commit 233097a

3 files changed

Lines changed: 115 additions & 4 deletions

File tree

‎src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt‎

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package graphql.kickstart.tools
22

33
import graphql.Directives
4+
import graphql.Scalars
45
import graphql.introspection.Introspection
56
import graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION
67
import graphql.kickstart.tools.directive.DirectiveWiringHelper
@@ -355,7 +356,8 @@ class SchemaParser internal constructor(
355356
return directives.map { directive ->
356357
val graphQLDirective = schemaDirectives.find { d -> d.name == directive.name }
357358
?: BUILT_IN_DIRECTIVES[directive.name]
358-
?: throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.")
359+
?: if (options.allowUndeclaredDirectives) return@map buildUndeclaredAppliedDirective(directive)
360+
else throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.")
359361
val graphQLArguments = graphQLDirective.arguments.associateBy { it.name }
360362

361363
GraphQLAppliedDirective.newDirective()
@@ -397,6 +399,36 @@ class SchemaParser internal constructor(
397399
}.toTypedArray()
398400
}
399401

402+
private fun buildUndeclaredAppliedDirective(directive: Directive): GraphQLAppliedDirective {
403+
return GraphQLAppliedDirective.newDirective()
404+
.name(directive.name)
405+
.definition(directive)
406+
.comparatorRegistry(runtimeWiring.comparatorRegistry)
407+
.apply {
408+
directive.arguments.forEach { arg ->
409+
argument(GraphQLAppliedDirectiveArgument.newArgument()
410+
.name(arg.name)
411+
.type(guessDirectiveArgumentType(directive, arg.name, arg.value))
412+
.valueLiteral(arg.value)
413+
.build()
414+
)
415+
}
416+
}
417+
.build()
418+
}
419+
420+
// there's no directive definition to look the type up in, so it's guessed from the value
421+
private fun guessDirectiveArgumentType(directive: Directive, argumentName: String, value: Value<*>): GraphQLInputType {
422+
return when (value) {
423+
is StringValue, is NullValue -> Scalars.GraphQLString
424+
is IntValue -> Scalars.GraphQLInt
425+
is FloatValue -> Scalars.GraphQLFloat
426+
is BooleanValue -> Scalars.GraphQLBoolean
427+
is ArrayValue -> GraphQLList(value.values.firstOrNull()?.let { guessDirectiveArgumentType(directive, argumentName, it) } ?: Scalars.GraphQLString)
428+
else -> throw SchemaError("Can't guess the type of argument ${directive.name}#$argumentName of undeclared directive ${directive.name}, please declare the directive.")
429+
}
430+
}
431+
400432
// TODO remove this once directives are fully replaced with applied directives
401433
private fun buildDirectives(
402434
directives: List<Directive>,
@@ -411,7 +443,9 @@ class SchemaParser internal constructor(
411443
names.add(directive.name)
412444
val graphQLDirective = this.schemaDirectives.find { d -> d.name == directive.name }
413445
?: BUILT_IN_DIRECTIVES[directive.name]
414-
?: throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.")
446+
// graphql-java rejects legacy directives without a definition, so undeclared ones are only kept as applied directives
447+
?: if (options.allowUndeclaredDirectives) continue
448+
else throw SchemaError("Found applied directive ${directive.name} without corresponding directive definition.")
415449
val graphQLArguments = graphQLDirective.arguments.associateBy { it.name }
416450
output.add(
417451
GraphQLDirective.newDirective()

‎src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ data class SchemaParserOptions internal constructor(
3636
val typeDefinitionFactories: List<TypeDefinitionFactory>,
3737
val fieldVisibility: GraphqlFieldVisibility?,
3838
val includeUnusedTypes: Boolean,
39-
val useCommentsForDescriptions: Boolean
39+
val useCommentsForDescriptions: Boolean,
40+
val allowUndeclaredDirectives: Boolean
4041
) {
4142
companion object {
4243
@JvmStatic
@@ -67,6 +68,7 @@ data class SchemaParserOptions internal constructor(
6768
private var fieldVisibility: GraphqlFieldVisibility? = null
6869
private var includeUnusedTypes = false
6970
private var useCommentsForDescriptions = true
71+
private var allowUndeclaredDirectives = false
7072

7173
fun contextClass(contextClass: Class<*>) = this.apply {
7274
this.contextClass = contextClass
@@ -153,6 +155,14 @@ data class SchemaParserOptions internal constructor(
153155
this.useCommentsForDescriptions = useCommentsForDescriptions
154156
}
155157

158+
/**
159+
* Allows applying directives that have no definition in the schema, e.g. the Apollo Federation directives imported through `@link`.
160+
* They're only added as applied directives, with argument types guessed from their values.
161+
*/
162+
fun allowUndeclaredDirectives(allowUndeclaredDirectives: Boolean) = this.apply {
163+
this.allowUndeclaredDirectives = allowUndeclaredDirectives
164+
}
165+
156166
@ExperimentalCoroutinesApi
157167
fun build(): SchemaParserOptions {
158168
val coroutineContextProvider = coroutineContextProvider
@@ -194,7 +204,8 @@ data class SchemaParserOptions internal constructor(
194204
typeDefinitionFactories,
195205
fieldVisibility,
196206
includeUnusedTypes,
197-
useCommentsForDescriptions
207+
useCommentsForDescriptions,
208+
allowUndeclaredDirectives
198209
)
199210
}
200211
}

‎src/test/kotlin/graphql/kickstart/tools/DirectiveTest.kt‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,72 @@ class DirectiveTest {
383383
assertEquals(error.message, "Found applied directive link without corresponding directive definition.")
384384
}
385385

386+
@Test
387+
fun `should allow undeclared directives when enabled`() {
388+
val schema = SchemaParser.newParser()
389+
.schemaString(
390+
"""
391+
extend schema @link(url: "https://specs.apollo.dev/federation/v2.3", import: ["@key", "@shareable"])
392+
393+
type Query {
394+
books: [Book!] @shareable
395+
}
396+
397+
type Book @key(fields: "id", resolvable: true) @custom(weight: 1.5, limit: 3, tag: null) {
398+
id: Int!
399+
name: String!
400+
}
401+
""")
402+
.resolvers(QueryResolver())
403+
.options(SchemaParserOptions.newOptions().allowUndeclaredDirectives(true).build())
404+
.build()
405+
.makeExecutableSchema()
406+
407+
val link = schema.getSchemaAppliedDirective("link")
408+
assertEquals(link.getArgument("import")?.getValue<List<String>>(), listOf("@key", "@shareable"))
409+
assertNotNull(schema.queryType.getField("books").getAppliedDirective("shareable"))
410+
411+
val book = schema.getObjectType("Book")!!
412+
val key = book.getAppliedDirective("key")
413+
assertEquals(key.getArgument("fields")?.getValue<String>(), "id")
414+
assertEquals(key.getArgument("resolvable")?.getValue<Boolean>(), true)
415+
val custom = book.getAppliedDirective("custom")
416+
assertEquals(custom.getArgument("weight")?.getValue<Double>(), 1.5)
417+
assertEquals(custom.getArgument("limit")?.getValue<Int>(), 3)
418+
assertNull(custom.getArgument("tag")?.getValue<String>())
419+
// graphql-java rejects legacy directives without a definition
420+
assert(book.directives.isEmpty())
421+
422+
val printed = SchemaPrinter(SchemaPrinter.Options.defaultOptions().includeSchemaDefinition(true)).print(schema)
423+
assert(printed.contains("""type Book @custom(limit : 3, tag : null, weight : 1.5) @key(fields : "id", resolvable : true) {""")) {
424+
printed
425+
}
426+
}
427+
428+
@Test
429+
fun `should fail on undeclared directive with an argument whose type can't be guessed`() {
430+
val error = assertThrows(SchemaError::class.java) {
431+
SchemaParser.newParser()
432+
.schemaString(
433+
"""
434+
type Query {
435+
books: [Book!] @policy(purpose: SECURITY)
436+
}
437+
438+
type Book {
439+
id: Int!
440+
name: String!
441+
}
442+
""")
443+
.resolvers(QueryResolver())
444+
.options(SchemaParserOptions.newOptions().allowUndeclaredDirectives(true).build())
445+
.build()
446+
.makeExecutableSchema()
447+
}
448+
449+
assertEquals(error.message, "Can't guess the type of argument policy#purpose of undeclared directive policy, please declare the directive.")
450+
}
451+
386452
private class BookQueryResolver : GraphQLQueryResolver {
387453
fun book(filter: BookFilter): Book? = null
388454
}

0 commit comments

Comments
 (0)