From 7996b4503139e56dd77e85e53ea5b3a9b68bba79 Mon Sep 17 00:00:00 2001 From: Danilo Barboza <277004+danilo-barboza@users.noreply.github.com> Date: Sun, 4 Oct 2020 12:03:40 +0200 Subject: [PATCH 1/3] feat: Add option to allow unused types --- .../kickstart/tools/SchemaClassScanner.kt | 14 +++ .../kickstart/tools/SchemaParserOptions.kt | 11 +- .../tools/SchemaClassScannerSpec.groovy | 103 ++++++++++++++++++ .../kickstart/tools/TestInterfaces.groovy | 2 + 4 files changed, 128 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 860689c5..0b147559 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -84,6 +84,20 @@ internal class SchemaClassScanner( handleInterfaceOrUnionSubTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) + if (options.includeUnusedTypes) { + do { + val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes)) + .filter { definition -> definition.name != "PageInfo" } + .filterIsInstance().distinct() + + if (unusedDefinitions.isEmpty()) break + + val unusedDefinition = unusedDefinitions.first() + + handleInterfaceOrUnionSubTypes(listOf(unusedDefinition)) { "Object type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." } + } while (scanQueue()) + } + return validateAndCreateResult(rootTypeHolder) } diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt index f64b1d46..411236e0 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt @@ -29,7 +29,8 @@ data class SchemaParserOptions internal constructor( val introspectionEnabled: Boolean, val coroutineContextProvider: CoroutineContextProvider, val typeDefinitionFactories: List, - val fieldVisibility: GraphqlFieldVisibility? + val fieldVisibility: GraphqlFieldVisibility?, + val includeUnusedTypes: Boolean ) { companion object { @JvmStatic @@ -56,6 +57,7 @@ data class SchemaParserOptions internal constructor( private var coroutineContextProvider: CoroutineContextProvider? = null private var typeDefinitionFactories: MutableList = mutableListOf(RelayConnectionFactory()) private var fieldVisibility: GraphqlFieldVisibility? = null + private var includeUnusedTypes = false fun contextClass(contextClass: Class<*>) = this.apply { this.contextClass = contextClass @@ -125,6 +127,10 @@ data class SchemaParserOptions internal constructor( this.fieldVisibility = fieldVisibility } + fun includeUnusedTypes(includeUnusedTypes: Boolean) = this.apply { + this.includeUnusedTypes = includeUnusedTypes + } + @ExperimentalCoroutinesApi fun build(): SchemaParserOptions { val coroutineContextProvider = coroutineContextProvider @@ -162,7 +168,8 @@ data class SchemaParserOptions internal constructor( introspectionEnabled, coroutineContextProvider, typeDefinitionFactories, - fieldVisibility + fieldVisibility, + includeUnusedTypes ) } } diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy index ca7d652c..ca035fb2 100644 --- a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy +++ b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy @@ -409,4 +409,107 @@ class SchemaClassScannerSpec extends Specification { String id } } + + def "scanner should handle unused types when option is true"() { + when: + ScannedSchemaObjects objects = SchemaParser.newParser() + .schemaString(''' + # Let's say this is the Products service from Apollo Federation Introduction + + type Query { + allProducts: [Product] + } + + type Product { + name: String + } + + #these directives are defined in the Apollo Federation Specification: https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ + type User @key(fields: "id") @extends { + id: ID! @external + recentPurchasedProducts: [Product] + address: Address + } + + type Address { + street: String + } + ''') + .resolvers(new GraphQLQueryResolver() { + List allProducts() { null } + }) + .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) + .dictionary(User) + .scan() + + then: + objects.definitions.find { it.name == "User" } != null + objects.definitions.find { it.name == "Address" } != null + } + + class Product { + String name + } + + class User { + String id + List recentPurchasedProducts + Address address + } + + class Address { + String street + } + + def "scanner should handle unused types with interfaces when option is true"() { + when: + ScannedSchemaObjects objects = SchemaParser.newParser() + .schemaString(''' + type Query { + whatever: Whatever + } + + type Whatever { + value: String + } + + type Unused { + someInterface: SomeInterface + } + + interface SomeInterface { + value: String + } + + type Implementation implements SomeInterface { + value: String + } + ''') + .resolvers(new GraphQLQueryResolver() { + Whatever whatever() { null } + }) + .options(SchemaParserOptions.newOptions().includeUnusedTypes(true).build()) + .dictionary(Unused, Implementation) + .scan() + + then: + objects.definitions.find { it.name == "Unused" } != null + objects.definitions.find { it.name == "SomeInterface" } != null + objects.definitions.find { it.name == "Implementation" } != null + } + + class Whatever { + String value + } + + class Unused { + SomeInterface someInterface + } + + class Implementation implements SomeInterface { + @Override + String getValue() { + return null + } + } } diff --git a/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy b/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy index 4ee9b896..496dab49 100644 --- a/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy +++ b/src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy @@ -9,3 +9,5 @@ interface Vehicle { } interface VehicleInformation {} + +interface SomeInterface { String getValue() } From cfe461078c71c7422ffb30ebe6137e7451bc08d1 Mon Sep 17 00:00:00 2001 From: Danilo Barboza <277004+danilo-barboza@users.noreply.github.com> Date: Sun, 4 Oct 2020 12:11:36 +0200 Subject: [PATCH 2/3] chore: rename generic function --- .../kotlin/graphql/kickstart/tools/SchemaClassScanner.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 0b147559..99422841 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -77,11 +77,11 @@ internal class SchemaClassScanner( do { do { // Require all implementors of discovered interfaces to be discovered or provided. - handleInterfaceOrUnionSubTypes(getAllObjectTypesImplementingDiscoveredInterfaces()) { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } + handleDictionaryTypes(getAllObjectTypesImplementingDiscoveredInterfaces()) { "Object type '${it.name}' implements a known interface, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) // Require all members of discovered unions to be discovered. - handleInterfaceOrUnionSubTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } + handleDictionaryTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) if (options.includeUnusedTypes) { @@ -94,7 +94,7 @@ internal class SchemaClassScanner( val unusedDefinition = unusedDefinitions.first() - handleInterfaceOrUnionSubTypes(listOf(unusedDefinition)) { "Object type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." } + handleDictionaryTypes(listOf(unusedDefinition)) { "Object type '${it.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) } @@ -222,7 +222,7 @@ internal class SchemaClassScanner( }.flatten().distinct() } - private fun handleInterfaceOrUnionSubTypes(types: List, failureMessage: (ObjectTypeDefinition) -> String) { + private fun handleDictionaryTypes(types: List, failureMessage: (ObjectTypeDefinition) -> String) { types.forEach { type -> val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty() if (!unvalidatedTypes.contains(type) && !dictionaryContainsType) { From c1fa6335501346d0fe588fbecc2a089e6b993625 Mon Sep 17 00:00:00 2001 From: Vojtech Polivka Date: Tue, 17 Nov 2020 14:07:30 -0800 Subject: [PATCH 3/3] Clean up --- .../kotlin/graphql/kickstart/tools/SchemaClassScanner.kt | 5 ++++- .../kickstart/tools/SchemaClassScannerSpec.groovy | 9 +++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt index 99422841..b45ada00 100644 --- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt +++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt @@ -84,13 +84,16 @@ internal class SchemaClassScanner( handleDictionaryTypes(getAllObjectTypeMembersOfDiscoveredUnions()) { "Object type '${it.name}' is a member of a known union, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary." } } while (scanQueue()) + // Find unused types and include them if required if (options.includeUnusedTypes) { do { val unusedDefinitions = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes)) .filter { definition -> definition.name != "PageInfo" } .filterIsInstance().distinct() - if (unusedDefinitions.isEmpty()) break + if (unusedDefinitions.isEmpty()) { + break + } val unusedDefinition = unusedDefinitions.first() diff --git a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy index ca035fb2..876ba3c6 100644 --- a/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy +++ b/src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy @@ -1,10 +1,6 @@ package graphql.kickstart.tools -import graphql.language.InputObjectTypeDefinition -import graphql.language.InputObjectTypeExtensionDefinition -import graphql.language.InterfaceTypeDefinition -import graphql.language.ObjectTypeDefinition -import graphql.language.ScalarTypeDefinition +import graphql.language.* import graphql.schema.Coercing import graphql.schema.GraphQLScalarType import spock.lang.Specification @@ -424,7 +420,8 @@ class SchemaClassScannerSpec extends Specification { name: String } - #these directives are defined in the Apollo Federation Specification: https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ + # these directives are defined in the Apollo Federation Specification: + # https://www.apollographql.com/docs/apollo-server/federation/federation-spec/ type User @key(fields: "id") @extends { id: ID! @external recentPurchasedProducts: [Product]