Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option to allow unused types #445

Merged
merged 3 commits into from
Nov 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 20 additions & 3 deletions src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,30 @@ 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())

// 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<ObjectTypeDefinition>().distinct()

if (unusedDefinitions.isEmpty()) {
break
}

val unusedDefinition = unusedDefinitions.first()

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())
}

return validateAndCreateResult(rootTypeHolder)
}

Expand Down Expand Up @@ -208,7 +225,7 @@ internal class SchemaClassScanner(
}.flatten().distinct()
}

private fun handleInterfaceOrUnionSubTypes(types: List<ObjectTypeDefinition>, failureMessage: (ObjectTypeDefinition) -> String) {
private fun handleDictionaryTypes(types: List<ObjectTypeDefinition>, failureMessage: (ObjectTypeDefinition) -> String) {
types.forEach { type ->
val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty()
if (!unvalidatedTypes.contains(type) && !dictionaryContainsType) {
Expand Down
11 changes: 9 additions & 2 deletions src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ data class SchemaParserOptions internal constructor(
val introspectionEnabled: Boolean,
val coroutineContextProvider: CoroutineContextProvider,
val typeDefinitionFactories: List<TypeDefinitionFactory>,
val fieldVisibility: GraphqlFieldVisibility?
val fieldVisibility: GraphqlFieldVisibility?,
val includeUnusedTypes: Boolean
) {
companion object {
@JvmStatic
Expand All @@ -56,6 +57,7 @@ data class SchemaParserOptions internal constructor(
private var coroutineContextProvider: CoroutineContextProvider? = null
private var typeDefinitionFactories: MutableList<TypeDefinitionFactory> = mutableListOf(RelayConnectionFactory())
private var fieldVisibility: GraphqlFieldVisibility? = null
private var includeUnusedTypes = false

fun contextClass(contextClass: Class<*>) = this.apply {
this.contextClass = contextClass
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -162,7 +168,8 @@ data class SchemaParserOptions internal constructor(
introspectionEnabled,
coroutineContextProvider,
typeDefinitionFactories,
fieldVisibility
fieldVisibility,
includeUnusedTypes
)
}
}
Expand Down
110 changes: 105 additions & 5 deletions src/test/groovy/graphql/kickstart/tools/SchemaClassScannerSpec.groovy
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -409,4 +405,108 @@ 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<Product> 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<Product> 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
}
}
}
2 changes: 2 additions & 0 deletions src/test/groovy/graphql/kickstart/tools/TestInterfaces.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,5 @@ interface Vehicle {
}

interface VehicleInformation {}

interface SomeInterface { String getValue() }