diff --git a/AGENTS.md b/AGENTS.md
index e6f2005d..2da586e8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -5,9 +5,10 @@ resolver classes. Published to Maven Central. Kotlin, with a few Java files, tar
## Layout
-- `src/main/kotlin/graphql/kickstart/tools`: `SchemaParser` and its builder and options, `SchemaClassScanner` (matches schema
- types to classes), `resolver/` (field resolvers and data fetchers), `directive/`, `relay/`, `proxy/` (finds the class
- behind a Spring, Guice, Weld or Javassist proxied resolver), `util/`
+- `src/main/kotlin/graphql/kickstart/tools`: `SchemaParser` and its builder and options, `MergedDefinitions.kt` (merges each
+ SDL type with its extensions into a `MergedType`. Other code, apart from the Relay factory, reads SDL types only through
+ it), `SchemaClassScanner` (matches schema types to classes), `resolver/` (field resolvers and data fetchers), `directive/`,
+ `relay/`, `proxy/` (finds the class behind a Spring, Guice, Weld or Javassist proxied resolver), `util/`
- `src/test/kotlin`: JUnit tests, one class per feature. A test for an issue goes into the class for its feature.
`EndToEndTest` and `EndToEndSpecHelper` hold the shared schema and resolvers
- `.claude/skills/verify-tools`: the probe that checks a change from a consuming app's side
@@ -94,7 +95,8 @@ happening. "docs only" means nothing does yet, so the rule depends on you readin
| A resolver method's parameter layout, type variables and source are decided once, at schema build | docs only |
| A change to how resolvers are found, bound or unwrapped keeps what a method, a property and a Map field return | `ResolverCompatibilityTest` and the bounded check in `ReturnValueUnwrappingTest`. Each lists exactly today's property and Map gaps as known findings, and fails when one is fixed until it's removed, or when a new failure would hide under one |
| One output has one builder. Don't add a second path that builds the same thing | docs only |
-| Read an SDL type's members merged with its extensions, never from the raw definition | docs only |
+| Read an SDL type's members merged with its extensions, never from the raw definition | `MergedType` in `MergedDefinitions.kt` and `DefinitionsBoundaryTest`, which fails on a raw member, directive or child-node read, an extension class, an SDL node walk or a `TypeDefinitionRegistry` call outside it. A read by reflection gets past it |
+| Every kind of SDL type starts its graphql-java type from the same recipe | `MergedType.prepare`, `DefinitionsBoundaryTest` and `TypeKindRecipeTest` |
| Reuse an existing structure before you add a parallel one | docs only |
| Argument conversion goes through one entry point | docs only |
| An input field's Java type comes from the mapping Jackson uses, not from guessed getters | docs only |
diff --git a/pom.xml b/pom.xml
index 65cf1627..06adcd74 100644
--- a/pom.xml
+++ b/pom.xml
@@ -85,6 +85,11 @@
+
+ com.tngtech.archunit
+ archunit
+ 1.5.1
+
ch.qos.logback
logback-classic
@@ -181,6 +186,11 @@
+
+ com.tngtech.archunit
+ archunit
+ test
+
ch.qos.logback
logback-classic
@@ -261,6 +271,11 @@
test-jar
+
+
+ logback-test.xml
+
+
diff --git a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
index 40a52802..4faff924 100644
--- a/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/DictionaryTypeResolver.kt
@@ -3,7 +3,6 @@ package graphql.kickstart.tools
import graphql.TypeResolutionEnvironment
import graphql.kickstart.tools.util.JavaType
import graphql.kickstart.tools.util.unwrap
-import graphql.language.TypeDefinition
import graphql.schema.GraphQLInterfaceType
import graphql.schema.GraphQLObjectType
import graphql.schema.GraphQLUnionType
@@ -14,7 +13,7 @@ import java.lang.reflect.ParameterizedType
* @author Andrew Potter
*/
internal abstract class DictionaryTypeResolver(
- private val dictionary: Map>
+ private val dictionary: Map>
) : TypeResolver {
// Raw class of each parameterized entry, e.g. Box for Box, unless several entries share it
private val rawClassDictionary by lazy {
@@ -25,7 +24,7 @@ internal abstract class DictionaryTypeResolver(
.toMap()
}
- private fun getTypeDefinition(clazz: Class): TypeDefinition<*>? {
+ private fun getTypeDefinition(clazz: Class): MergedType<*>? {
return dictionary[clazz]
?: (if (clazz.superclass == null) null else getTypeDefinition(clazz.superclass))
?: clazz.interfaces.mapNotNull { getTypeDefinition(it) }.firstOrNull()
@@ -45,7 +44,7 @@ internal abstract class DictionaryTypeResolver(
}
internal class InterfaceTypeResolver(
- dictionary: Map>,
+ dictionary: Map>,
private val thisInterface: GraphQLInterfaceType
) : DictionaryTypeResolver(
dictionary
@@ -54,7 +53,7 @@ internal class InterfaceTypeResolver(
}
internal class UnionTypeResolver(
- dictionary: Map>,
+ dictionary: Map>,
private val thisUnion: GraphQLUnionType
) : DictionaryTypeResolver(
dictionary
diff --git a/src/main/kotlin/graphql/kickstart/tools/MergedDefinitions.kt b/src/main/kotlin/graphql/kickstart/tools/MergedDefinitions.kt
new file mode 100644
index 00000000..1cce7227
--- /dev/null
+++ b/src/main/kotlin/graphql/kickstart/tools/MergedDefinitions.kt
@@ -0,0 +1,229 @@
+package graphql.kickstart.tools
+
+import graphql.Directives
+import graphql.introspection.Introspection.DirectiveLocation
+import graphql.kickstart.tools.util.describe
+import graphql.kickstart.tools.util.getDocumentation
+import graphql.kickstart.tools.util.unwrap
+import graphql.language.*
+import graphql.schema.*
+import graphql.schema.idl.ScalarInfo
+
+internal class MergedDefinitions(definitions: List>) {
+ val directiveDefinitions: List = definitions.filterIsInstance()
+ val rootInfo: RootTypeInfo = RootTypeInfo.fromSchemaDefinitions(definitions.filterIsInstance())
+
+ val scalarNames: Set = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.keys + definitions.filterIsInstance().map { it.name }
+
+ private val typeDefinitions = definitions.filterIsInstance>()
+ private val baseTypeNames = typeDefinitions.filter { it !is SDLExtensionDefinition }.map { it.name }.toSet()
+
+ private val loneExtensions = typeDefinitions
+ .filter { it is SDLExtensionDefinition && it !is ObjectTypeDefinition && it !is InputObjectTypeDefinition && it.name !in baseTypeNames }
+ .distinctBy { it.name }
+ .toSet()
+
+ private val sources = typeDefinitions.filter { it !is SDLExtensionDefinition || it in loneExtensions }
+
+ private val sourcesByName = sources.associateBy { it.name }
+
+ private val extensionsByName = typeDefinitions
+ .filter { it is SDLExtensionDefinition && it !in loneExtensions && !extendsOtherKind(it) }
+ .groupBy { it.name }
+
+ private val typesBySource: List, MergedType<*>>> = sources.mapNotNull { source -> merge(source)?.let { source to it } }
+
+ private val specificationScalars: List> = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.values.map { definition ->
+ definition to MergedType.Scalar(definition, extensionsByName[definition.name].orEmpty().filterIsInstance())
+ }
+
+ private val specificationScalarsByName: Map = specificationScalars.associate { (_, type) -> type.name to type }
+
+ private val sourceOf: Map, TypeDefinition<*>> = (typesBySource + specificationScalars).associate { (source, type) -> type to source }
+
+ val byName: Map> = typesBySource.associate { (_, type) -> type.name to type }
+
+ val objectsWithRepeats: List = typesBySource.map { it.second }.filterIsInstance()
+
+ val ignoredExtensionWarnings: List = typeDefinitions.filter(::extendsOtherKind).map { extension ->
+ "Type '${extension.name}' is defined as ${sourcesByName.getValue(extension.name).kind()}, but extended as ${extension.kind()}. The extension is ignored."
+ }
+
+ fun resolve(name: String?): MergedType<*>? = specificationScalarsByName[name] ?: byName[name]
+
+ fun definitionsOf(types: Set>): Set> = types.mapNotNullTo(linkedSetOf()) { sourceOf[it] }
+
+ fun findUsage(reference: TypeName, types: Collection>): String? =
+ (types.mapNotNull { sourceOf[it] } + directiveDefinitions + extensionsByName.values.flatten()).asSequence().flatMap { definition ->
+ when (definition) {
+ is ObjectTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions)
+ is InterfaceTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions)
+ is InputObjectTypeDefinition -> definition.inputValueDefinitions.map { "input field '${definition.name}.${it.name}'" to it.type }
+ is DirectiveDefinition -> definition.inputValueDefinitions.map { "argument '${it.name}' of directive '@${definition.name}'" to it.type }
+ else -> emptyList()
+ }
+ }.find { (_, type) -> type.unwrap() === reference }?.first
+
+ private fun fieldUsages(typeName: String, fields: List): List>> = fields.flatMap { field ->
+ listOf("field '$typeName.${field.name}'" to field.type) +
+ field.inputValueDefinitions.map { "argument '${it.name}' of field '$typeName.${field.name}'" to it.type }
+ }
+
+ private fun merge(source: TypeDefinition<*>): MergedType<*>? {
+ val extensions = extensionsByName[source.name].orEmpty()
+ return when (source) {
+ is ObjectTypeDefinition -> MergedType.Object(source, extensions.filterIsInstance())
+ is InterfaceTypeDefinition -> MergedType.Interface(source, extensions.filterIsInstance())
+ is UnionTypeDefinition -> MergedType.Union(source, extensions.filterIsInstance())
+ is EnumTypeDefinition -> MergedType.Enum(source, extensions.filterIsInstance())
+ is InputObjectTypeDefinition -> MergedType.Input(source, extensions.filterIsInstance())
+ is ScalarTypeDefinition -> MergedType.Scalar(source, extensions.filterIsInstance())
+ else -> null
+ }
+ }
+
+ private fun extendsOtherKind(definition: TypeDefinition<*>): Boolean {
+ val type = sourcesByName[definition.name]
+ return definition is SDLExtensionDefinition && type != null && type.kind() != definition.kind()
+ }
+}
+
+private fun TypeDefinition<*>.kind(): String = when (this) {
+ is ObjectTypeDefinition -> "an object type"
+ is InterfaceTypeDefinition -> "an interface"
+ is UnionTypeDefinition -> "a union"
+ is EnumTypeDefinition -> "an enum"
+ is InputObjectTypeDefinition -> "an input object type"
+ is ScalarTypeDefinition -> "a scalar"
+ else -> javaClass.simpleName
+}
+
+internal class TypeBuildContext(
+ val options: SchemaParserOptions,
+ val directives: SdlDirectives,
+ val customScalars: CustomScalarMap,
+)
+
+internal sealed class MergedType>(
+ private val definition: TypeDefinition<*>,
+ extensions: List>,
+) {
+ val name: String get() = definition.name
+
+ val definitionClassName: String get() = definition.javaClass.simpleName
+
+ val kindPhrase: String get() = definition.kind()
+
+ protected val directives: List = definition.directives + extensions.flatMap { it.directives }
+
+ protected abstract val location: DirectiveLocation
+
+ protected abstract val declarations: List>
+
+ protected abstract fun newBuilder(context: TypeBuildContext): B
+
+ fun prepare(context: TypeBuildContext): B {
+ rejectRepeated()
+ val builder = newBuilder(context).name(name)
+ getDocumentation(definition as AbstractNode<*>, context.options)?.let { builder.description(it) }
+ return with(context.directives) { builder.withSdlDirectives(directives, location) }
+ }
+
+ private fun rejectRepeated() {
+ val noun = when (this) {
+ is Object, is Interface, is Input -> "field"
+ is Enum -> "value"
+ is Union -> "member"
+ is Scalar -> return
+ }
+ val names = mutableSetOf()
+ val repeat = declarations.firstOrNull { !names.add(it.name) } ?: return
+ throw SchemaError("Type '$name' declares $noun '${repeat.name}' more than once${repeat.sourceLocation.describe()}.")
+ }
+
+ sealed interface Implementing {
+ val fields: List
+ val interfaces: List>
+ }
+
+ class Object(
+ private val definition: ObjectTypeDefinition,
+ private val extensions: List,
+ ) : MergedType(definition, extensions), Implementing {
+ override val fields: List = definition.fieldDefinitions + extensions.flatMap { it.fieldDefinitions }
+ override val interfaces: List> = definition.implements + extensions.flatMap { it.implements }
+ override val location = DirectiveLocation.OBJECT
+ override val declarations get() = fields
+ override fun newBuilder(context: TypeBuildContext): GraphQLObjectType.Builder =
+ GraphQLObjectType.newObject().definition(definition).extensionDefinitions(extensions)
+ }
+
+ class Interface(
+ private val definition: InterfaceTypeDefinition,
+ private val extensions: List,
+ ) : MergedType(definition, extensions), Implementing {
+ override val fields: List = definition.fieldDefinitions + extensions.flatMap { it.fieldDefinitions }
+ override val interfaces: List> = definition.implements + extensions.flatMap { it.implements }
+ override val location = DirectiveLocation.INTERFACE
+ override val declarations get() = fields
+ override fun newBuilder(context: TypeBuildContext): GraphQLInterfaceType.Builder =
+ GraphQLInterfaceType.newInterface().definition(definition).extensionDefinitions(extensions)
+ }
+
+ class Union(
+ private val definition: UnionTypeDefinition,
+ private val extensions: List,
+ ) : MergedType(definition, extensions) {
+ val members: List> = definition.memberTypes + extensions.flatMap { it.memberTypes }
+ override val location = DirectiveLocation.UNION
+ override val declarations get() = members.map { it as TypeName }
+ override fun newBuilder(context: TypeBuildContext): GraphQLUnionType.Builder =
+ GraphQLUnionType.newUnionType().definition(definition).extensionDefinitions(extensions)
+
+ fun declarationTextOf(member: Type<*>): String =
+ (listOf(definition) + extensions).first { declaration -> declaration.memberTypes.any { it === member } }.toString()
+ }
+
+ class Enum(
+ private val definition: EnumTypeDefinition,
+ private val extensions: List,
+ ) : MergedType(definition, extensions) {
+ val values: List = definition.enumValueDefinitions + extensions.flatMap { it.enumValueDefinitions }
+ override val location = DirectiveLocation.ENUM
+ override val declarations get() = values
+ override fun newBuilder(context: TypeBuildContext): GraphQLEnumType.Builder =
+ GraphQLEnumType.newEnum().definition(definition).extensionDefinitions(extensions)
+ }
+
+ class Input(
+ private val definition: InputObjectTypeDefinition,
+ private val extensions: List,
+ ) : MergedType(definition, extensions) {
+ val fields: List = definition.inputValueDefinitions + extensions.flatMap { it.inputValueDefinitions }
+ override val location = DirectiveLocation.INPUT_OBJECT
+ override val declarations get() = fields
+ override fun newBuilder(context: TypeBuildContext): GraphQLInputObjectType.Builder =
+ GraphQLInputObjectType.newInputObject().definition(definition).extensionDefinitions(extensions)
+ }
+
+ class Scalar(
+ private val definition: ScalarTypeDefinition,
+ private val extensions: List,
+ ) : MergedType(definition, extensions) {
+ override val location = DirectiveLocation.SCALAR
+ override val declarations = emptyList>()
+
+ // like graphql-java, only the schema's directives are applied
+ override fun newBuilder(context: TypeBuildContext): GraphQLScalarType.Builder {
+ val provided = context.customScalars.getValue(name)
+ return GraphQLScalarType.newScalar(provided)
+ .definition(definition)
+ .extensionDefinitions(extensions)
+ .specifiedByUrl(specifiedByUrl() ?: provided.specifiedByUrl)
+ .clearDirectives()
+ }
+
+ private fun specifiedByUrl(): String? =
+ (directives.find { it.name == Directives.SpecifiedByDirective.name }?.getArgument("url")?.value as? StringValue)?.value
+ }
+}
diff --git a/src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt b/src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt
index a4d5c764..7a76bdd7 100644
--- a/src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt
@@ -2,7 +2,6 @@ package graphql.kickstart.tools
import graphql.kickstart.tools.util.JavaType
import graphql.kickstart.tools.util.unwrap
-import graphql.kickstart.tools.util.withExtensions
import graphql.language.*
import graphql.schema.idl.ScalarInfo
import org.slf4j.Logger
@@ -13,16 +12,14 @@ import java.lang.reflect.Method
internal class ScanDictionary(
private val options: SchemaParserOptions,
private val log: Logger,
- private val typeExtensions: TypeExtensions,
- private val definitionsByName: Map>,
+ private val typesByName: Map>,
private val typeClassMatcher: TypeClassMatcher,
private val initialDictionary: Map,
- private val enqueue: (ObjectTypeDefinition, JavaType) -> Unit,
- private val handleFoundScalarType: (ScalarTypeDefinition) -> Unit,
- private val dictionary: MutableMap, DictionaryEntry> = mutableMapOf(),
-) : Map, ScanDictionary.EntryView> by dictionary {
- private val interfaceDefinitionsByName = definitionsByName.values.filterIsInstance().associateBy { it.name }
- private val enumsWithoutClass = mutableMapOf()
+ private val enqueue: (MergedType.Object, JavaType) -> Unit,
+ private val handleFoundScalarType: (MergedType.Scalar) -> Unit,
+ private val dictionary: MutableMap, DictionaryEntry> = mutableMapOf(),
+) : Map, ScanDictionary.EntryView> by dictionary {
+ private val enumsWithoutClass = mutableMapOf()
fun handleFoundType(match: TypeClassMatcher.Match) = when (match) {
is TypeClassMatcher.ScalarMatch -> handleFoundScalarType(match.type)
@@ -32,7 +29,7 @@ internal class ScanDictionary(
/**
* Enter a found type into the dictionary if it doesn't exist yet, add a reference pointing back to where it was discovered.
*/
- fun handleFoundType(type: TypeDefinition<*>, javaType: JavaType?, reference: Reference) {
+ fun handleFoundType(type: MergedType<*>, javaType: JavaType?, reference: Reference) {
val realEntry = dictionary.getOrPut(type) { DictionaryEntry() }
var typeWasSet = false
@@ -62,14 +59,15 @@ internal class ScanDictionary(
/**
* Handle a newly found type, adding it to the list of actually used types and putting it in the scanning queue if it's an object type.
*/
- private fun handleNewType(graphQLType: TypeDefinition<*>, javaType: JavaType) {
+ private fun handleNewType(graphQLType: MergedType<*>, javaType: JavaType) {
when (graphQLType) {
- is ObjectTypeDefinition -> {
+ is MergedType.Object -> {
enqueue(graphQLType, javaType)
scanInterfacesOfType(graphQLType)
}
- is InputObjectTypeDefinition -> handleInputObjectFields(graphQLType, javaType)
+ is MergedType.Input -> handleInputObjectFields(graphQLType, javaType)
+ else -> {}
}
}
@@ -77,10 +75,9 @@ internal class ScanDictionary(
* Finds the types of the input object's fields through the properties of its class. A null [javaType] means the input object
* has no known class, e.g. because it's only reachable through a Map.
*/
- private fun handleInputObjectFields(graphQLType: InputObjectTypeDefinition, javaType: JavaType?) {
+ private fun handleInputObjectFields(graphQLType: MergedType.Input, javaType: JavaType?) {
val clazz = javaType?.unwrap()
- graphQLType.withExtensions(typeExtensions)
- .flatMap { it.inputValueDefinitions }
+ graphQLType.fields
.forEach { inputValueDefinition ->
val inputGraphQLType = inputValueDefinition.type.unwrap()
if (ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(inputGraphQLType.name)) {
@@ -108,10 +105,10 @@ internal class ScanDictionary(
* Without a Java property for an input field its type can still be built, as long as it doesn't need a class of its own.
* At runtime the value is whatever the input object's class (or the Map) deserializes it into.
*/
- private fun handleInputValueWithoutJavaType(graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, inputGraphQLType: TypeName, clazz: Class<*>?) {
- when (val definition = definitionsByName[inputGraphQLType.name]) {
- is ScalarTypeDefinition -> handleFoundScalarType(definition)
- is InputObjectTypeDefinition -> {
+ private fun handleInputValueWithoutJavaType(graphQLType: MergedType.Input, inputValueDefinition: InputValueDefinition, inputGraphQLType: TypeName, clazz: Class<*>?) {
+ when (val definition = typesByName[inputGraphQLType.name]) {
+ is MergedType.Scalar -> handleFoundScalarType(definition)
+ is MergedType.Input -> {
val isNewType = !dictionary.containsKey(definition)
// registered without a class, so a class found for it later still gets set and scanned
handleFoundType(definition, null, InputObjectReference(inputValueDefinition))
@@ -120,7 +117,7 @@ internal class ScanDictionary(
}
}
// the enum class may still be found through another field, so this is only reported once scanning is done
- is EnumTypeDefinition -> enumsWithoutClass.putIfAbsent(definition, enumWithoutClassMessage(definition, graphQLType, inputValueDefinition, clazz))
+ is MergedType.Enum -> enumsWithoutClass.putIfAbsent(definition, enumWithoutClassMessage(definition, graphQLType, inputValueDefinition, clazz))
// undefined types and output types are reported by the SchemaParser
else -> {}
}
@@ -133,7 +130,7 @@ internal class ScanDictionary(
private fun isLibraryClass(clazz: Class<*>) =
listOf("java.", "kotlin.", JACKSON_PACKAGE_PREFIX).any { clazz.name.startsWith(it) }
- private fun enumWithoutClassMessage(enumType: EnumTypeDefinition, graphQLType: InputObjectTypeDefinition, inputValueDefinition: InputValueDefinition, clazz: Class<*>?): String {
+ private fun enumWithoutClassMessage(enumType: MergedType.Enum, graphQLType: MergedType.Input, inputValueDefinition: InputValueDefinition, clazz: Class<*>?): String {
val field = inputValueDefinition.name
val reason = when {
clazz == null -> "no class was found for input type '${graphQLType.name}'"
@@ -150,10 +147,10 @@ internal class ScanDictionary(
?.let { throw SchemaClassScannerError(it.value) }
}
- fun scanInterfacesOfType(graphQLType: ObjectTypeDefinition) {
- graphQLType.withExtensions(typeExtensions).flatMap { it.implements }.forEach {
+ fun scanInterfacesOfType(graphQLType: MergedType.Object) {
+ graphQLType.interfaces.forEach {
if (it is TypeName) {
- handleFoundType(interfaceDefinitionsByName[it.name]
+ handleFoundType(typesByName[it.name] as? MergedType.Interface
?: throw SchemaClassScannerError("Object type ${graphQLType.name} declared interface ${it.name}, but no interface with that name was found in the schema!"), null, InterfaceReference(graphQLType))
}
}
@@ -238,11 +235,11 @@ internal class ScanDictionary(
override fun getDescription() = "unused type"
}
- private class InterfaceReference(private val type: ObjectTypeDefinition) : Reference() {
+ private class InterfaceReference(private val type: MergedType.Object) : Reference() {
override fun getDescription() = "interface declarations of ${type.name}"
}
- class UnionMemberReference(private val union: UnionTypeDefinition) : Reference() {
+ class UnionMemberReference(private val union: MergedType.Union) : Reference() {
override fun getDescription() = "member of union ${union.name}"
}
diff --git a/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt b/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt
index 1a60dce9..a17009b1 100644
--- a/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/ScannedSchemaObjects.kt
@@ -2,11 +2,7 @@ package graphql.kickstart.tools
import graphql.kickstart.tools.resolver.FieldResolver
import graphql.kickstart.tools.util.JavaType
-import graphql.language.EnumTypeDefinition
import graphql.language.FieldDefinition
-import graphql.language.ObjectTypeDefinition
-import graphql.language.SDLNamedDefinition
-import graphql.language.TypeDefinition
import graphql.schema.GraphQLScalarType
/**
@@ -14,24 +10,33 @@ import graphql.schema.GraphQLScalarType
*/
internal data class ScannedSchemaObjects(
val dictionary: TypeClassDictionary,
- val definitions: Set>,
- val typeExtensions: TypeExtensions,
+ val definitions: MergedDefinitions,
+ val types: Set>,
val customScalars: CustomScalarMap,
- val rootInfo: RootTypeInfo,
- val fieldResolversByType: Map>,
- val unusedDefinitions: Set>,
- val directiveOnlyEnums: Set
+ val fieldResolversByType: Map>,
+ val unusedTypes: Set>,
+ val directiveOnlyEnums: Set
)
-internal class TypeClassDictionary(val classByType: Map, JavaType>) {
- val typeByClass: Map> = classByType.entries
- .groupBy({ it.value }, { it.key })
- .mapValues { (javaType, types) ->
- types.singleOrNull() ?: throw SchemaClassScannerError(
- "Class '${javaType.typeName}' is used for types ${types.joinToString { "'${it.name}'" }}, but only input object types can share a class!"
- )
+internal class TypeClassDictionary(val classByType: Map, JavaType>) {
+ val typeByClass: Map>
+
+ init {
+ requireOnlyInputTypesShareAClass(classByType) { javaType, types ->
+ "Class '${javaType.typeName}' is used for types ${types.joinToString { "'${it.name}'" }}, but only input object types can share a class!"
+ }
+ typeByClass = classByType.entries.associate { it.value to it.key }
+ }
+
+ companion object {
+ fun requireOnlyInputTypesShareAClass(classByType: Map, J>, message: (J, List>) -> String) {
+ classByType.entries
+ .filter { it.key !is MergedType.Input }
+ .groupBy({ it.value }, { it.key })
+ .entries.firstOrNull { it.value.size > 1 }
+ ?.let { (javaType, types) -> throw SchemaClassScannerError(message(javaType, types)) }
}
+ }
}
internal typealias CustomScalarMap = Map
-internal typealias TypeExtensions = Map>>
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
index 42f312e2..ec170918 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
@@ -25,14 +25,14 @@ import java.lang.reflect.Modifier
*/
internal class SchemaClassScanner(
initialDictionary: Map>,
- allDefinitions: List>,
+ private val definitions: MergedDefinitions,
resolvers: List>,
private val scalars: CustomScalarMap,
private val options: SchemaParserOptions
) {
private val log = LoggerFactory.getLogger(javaClass)
- private val rootInfo = RootTypeInfo.fromSchemaDefinitions(allDefinitions.filterIsInstance())
+ private val rootInfo = definitions.rootInfo
private val queryResolvers = resolvers.filterIsInstance()
private val mutationResolvers = resolvers.filterIsInstance()
@@ -41,54 +41,32 @@ internal class SchemaClassScanner(
private val resolverInfos = resolvers.asSequence().minus(queryResolvers).minus(mutationResolvers).minus(subscriptionResolvers).map { NormalResolverInfo(it, options) }.toList()
private val initialDictionary = initialDictionary.mapValues { InitialDictionaryEntry(it.value) }
- private val directiveDefinitions = allDefinitions.filterIsInstance()
- private val scalarDefinitions = allDefinitions.filterIsInstance()
-
- private val typeDefinitions = allDefinitions.filterIsInstance>()
- private val baseTypeNames = typeDefinitions.filter { it !is SDLExtensionDefinition }.map { it.name }.toSet()
-
- private val loneExtensions = typeDefinitions
- .filter { it is SDLExtensionDefinition && it !is ObjectTypeDefinition && it !is InputObjectTypeDefinition && it.name !in baseTypeNames }
- .distinctBy { it.name }
-
- private val definitionsByName = typeDefinitions.filter { it !is SDLExtensionDefinition || it in loneExtensions }.associateBy { it.name }
- private val typeExtensions: TypeExtensions = typeDefinitions
- .filter { it is SDLExtensionDefinition && it !in loneExtensions && !extendsOtherKind(it) }
- .groupBy { it.name }
- private val objectDefinitions = allDefinitions.filterIsInstance().filter { it !is ObjectTypeExtensionDefinition }
- private val objectDefinitionsByName = objectDefinitions.associateBy { it.name }
-
- private val scalarNames = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.keys + scalarDefinitions.map { it.name }
- private val fieldResolverScanner = FieldResolverScanner(options, scalarNames)
- private val typeClassMatcher = TypeClassMatcher(definitionsByName)
- private val unvalidatedTypes = mutableSetOf>()
+ private val directiveDefinitions = definitions.directiveDefinitions
+ private val typesByName = definitions.byName
+ private val objectTypesByName = definitions.objectsWithRepeats.associateBy { it.name }
+
+ private val fieldResolverScanner = FieldResolverScanner(options, definitions.scalarNames)
+ private val typeClassMatcher = TypeClassMatcher(definitions)
+ private val unvalidatedTypes = mutableSetOf>()
private val queue = linkedSetOf()
- private val dictionary = ScanDictionary(options, log, typeExtensions, definitionsByName, typeClassMatcher, this.initialDictionary, ::enqueue, ::handleFoundScalarType)
- private val sealedSubclassMatcher = SealedSubclassMatcher(dictionary, typeExtensions)
+ private val dictionary = ScanDictionary(options, log, typesByName, typeClassMatcher, this.initialDictionary, ::enqueue, ::handleFoundScalarType)
+ private val sealedSubclassMatcher = SealedSubclassMatcher(dictionary)
- private val fieldResolversByType = mutableMapOf>()
+ private val fieldResolversByType = mutableMapOf>()
private val notCallableOnResolver = mutableListOf()
init {
initialDictionary.forEach { (name, clazz) ->
- if (!definitionsByName.containsKey(name)) {
+ if (!typesByName.containsKey(name)) {
throw SchemaClassScannerError("Class in supplied dictionary '${clazz.name}' specified type name '$name', but a type definition with that name was not found!")
}
}
- typeDefinitions.filter { extendsOtherKind(it) }.forEach { extension ->
- val definition = definitionsByName.getValue(extension.name)
- log.warn("Type '${extension.name}' is defined as ${definition.kind()}, but extended as ${extension.kind()}. The extension is ignored.")
- }
+ definitions.ignoredExtensionWarnings.forEach { log.warn(it) }
- initialDictionary
- .filterKeys { definitionsByName[it] !is InputObjectTypeDefinition }
- .entries.groupBy({ it.value }, { it.key })
- .forEach { (clazz, names) ->
- if (names.size > 1) {
- throw SchemaClassScannerError("Class in supplied dictionary '${clazz.name}' specified type names ${names.joinToString { "'$it'" }}, but only input object types can share a class!")
- }
- }
+ TypeClassDictionary.requireOnlyInputTypesShareAClass(initialDictionary.mapKeys { typesByName.getValue(it.key) }) { clazz, types ->
+ "Class in supplied dictionary '${clazz.name}' specified type names ${types.joinToString { "'${it.name}'" }}, but only input object types can share a class!"
+ }
if (options.allowUnimplementedResolvers) {
log.warn("Option 'allowUnimplementedResolvers' should only be set to true during development, as it can cause schema errors to be moved to query time instead of schema creation time. Make sure this is turned off in production.")
@@ -101,7 +79,7 @@ internal class SchemaClassScanner(
fun scanForClasses(): ScannedSchemaObjects {
// Figure out what query, mutation and subscription types are called
- val rootTypeHolder = RootTypesHolder(options, rootInfo, definitionsByName, queryResolvers, mutationResolvers, subscriptionResolvers)
+ val rootTypeHolder = RootTypesHolder(options, rootInfo, typesByName, queryResolvers, mutationResolvers, subscriptionResolvers)
handleRootType(rootTypeHolder.query)
handleRootType(rootTypeHolder.mutation)
@@ -134,7 +112,7 @@ internal class SchemaClassScanner(
if (options.includeUnusedTypes) {
// One type at a time, since scanning it can discover other unused types along with their classes
while (true) {
- val unusedDefinition = (definitionsByName.values - (dictionary.keys.toSet() + unvalidatedTypes))
+ val unusedDefinition = (typesByName.values - (dictionary.keys.toSet() + unvalidatedTypes))
.filter { definition -> definition.name != "PageInfo" }
.firstOrNull { canIncludeUnusedType(it) }
?: break
@@ -149,19 +127,19 @@ internal class SchemaClassScanner(
return validateAndCreateResult(rootTypeHolder)
}
- private fun canIncludeUnusedType(definition: TypeDefinition<*>): Boolean = when (definition) {
- is ObjectTypeDefinition, is InterfaceTypeDefinition, is UnionTypeDefinition -> true
+ private fun canIncludeUnusedType(definition: MergedType<*>): Boolean = when (definition) {
+ is MergedType.Object, is MergedType.Interface, is MergedType.Union -> true
// Enums can't be built without a class, so the ones missing from the dictionary are only reported as unused
- is EnumTypeDefinition -> initialDictionary.containsKey(definition.name)
+ is MergedType.Enum -> initialDictionary.containsKey(definition.name)
else -> false
}
- private fun handleUnusedType(type: TypeDefinition<*>) {
+ private fun handleUnusedType(type: MergedType<*>) {
val initialEntry = initialDictionary[type.name]
when {
initialEntry != null -> dictionary.handleFoundType(type, initialEntry.get(), DictionaryReference())
// Interfaces and unions are resolved through the classes of their implementations, so they don't need one
- type is InterfaceTypeDefinition || type is UnionTypeDefinition -> dictionary.handleFoundType(type, null, UnusedTypeReference())
+ type is MergedType.Interface || type is MergedType.Union -> dictionary.handleFoundType(type, null, UnusedTypeReference())
else -> throw SchemaClassScannerError("Object type '${type.name}' is unused and includeUnusedTypes is true. Please pass a class for type '${type.name}' in the parser's dictionary.")
}
}
@@ -202,15 +180,15 @@ internal class SchemaClassScanner(
private fun handleDirectiveInput(inputType: Type<*>) {
val inputTypeName = inputType.unwrap().name
- val typeDefinition = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS[inputTypeName]
- ?: definitionsByName[inputTypeName]
+ val typeDefinition = definitions.resolve(inputTypeName)
?: error("No ${TypeDefinition::class.java.simpleName} for type name $inputTypeName")
when (typeDefinition) {
- is ScalarTypeDefinition -> handleFoundScalarType(typeDefinition)
- is EnumTypeDefinition -> handleDirectiveEnum(typeDefinition)
- is InputObjectTypeDefinition -> handleDictionaryTypes(listOf(typeDefinition)) {
+ is MergedType.Scalar -> handleFoundScalarType(typeDefinition)
+ is MergedType.Enum -> handleDirectiveEnum(typeDefinition)
+ is MergedType.Input -> handleDictionaryTypes(listOf(typeDefinition)) {
"Input object type '${it.name}' is used in a directive, but no class could be found for that type name. Please pass a class for type '${it.name}' in the parser's dictionary."
}
+ else -> {}
}
}
@@ -219,7 +197,7 @@ internal class SchemaClassScanner(
* it goes into [unvalidatedTypes] and, unless a field uses it too, is built with its value names as values, see
* [validateAndCreateResult].
*/
- private fun handleDirectiveEnum(type: EnumTypeDefinition) {
+ private fun handleDirectiveEnum(type: MergedType.Enum) {
if (dictionary.containsKey(type)) {
return
}
@@ -233,17 +211,16 @@ internal class SchemaClassScanner(
}
/**
- * The names of the types used by the fields, arguments and input fields of [definitions], including fields added by extensions.
+ * The names of the types used by the fields, arguments and input fields of [types].
*/
- private fun typeNamesUsedByFields(definitions: Set>): Set {
- return definitions
- .flatMap { it.withExtensions(typeExtensions) }
- .flatMap { definition ->
- when (definition) {
- is ImplementingTypeDefinition<*> -> definition.fieldDefinitions.flatMap { field ->
+ private fun typeNamesUsedByFields(types: Set>): Set {
+ return types
+ .flatMap { type ->
+ when (type) {
+ is MergedType.Implementing -> type.fields.flatMap { field ->
field.inputValueDefinitions.map { it.type } + field.type
}
- is InputObjectTypeDefinition -> definition.inputValueDefinitions.map { it.type }
+ is MergedType.Input -> type.fields.map { it.type }
else -> emptyList()
}
}
@@ -271,12 +248,12 @@ internal class SchemaClassScanner(
it.value.javaType != null
&& it.value.typeClass() != Any::class.java
&& !Map::class.java.isAssignableFrom(it.value.typeClass())
- && it.key !is InputObjectTypeDefinition
- && it.key !is UnionTypeDefinition
+ && it.key !is MergedType.Input
+ && it.key !is MergedType.Union
}.mapValues { it.value.javaType!! }
)
// SchemaParser builds the scalars, this only checks that each one was provided
- val scalars = observedDefinitions.filterIsInstance()
+ val scalars = observedDefinitions.filterIsInstance()
.filter {
// Filter for any defined scalars OR scalars that aren't defined but also aren't standard
scalars.containsKey(it.name) || !ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS.containsKey(it.name)
@@ -285,7 +262,7 @@ internal class SchemaClassScanner(
?: throw SchemaClassScannerError("Expected a user-defined GraphQL scalar type with name '${definition.name}' but found none!"))
}
- val unusedDefinitions = (definitionsByName.values - observedDefinitions).toSet()
+ val unusedDefinitions = (typesByName.values - observedDefinitions).toSet()
unusedDefinitions
.filter { definition -> definition.name != "PageInfo" }
.forEach { definition ->
@@ -304,15 +281,13 @@ internal class SchemaClassScanner(
validateRootResolversWereUsed(rootTypeHolder.mutation, fieldResolvers)
validateRootResolversWereUsed(rootTypeHolder.subscription, fieldResolvers)
- val definitions = observedDefinitions + directiveDefinitions
-
// The enums in unvalidatedTypes come from directives without a class, so they can only be built when no field uses them.
// A field hands the enum's values to user code even when the scanner found no class through it, e.g. an input field bound
// to a Map or an unimplemented field's argument.
val typeNamesUsedByFields = typeNamesUsedByFields(observedDefinitions)
- val directiveOnlyEnums = unvalidatedTypes.filterIsInstance().filter { it.name !in typeNamesUsedByFields }.toSet()
+ val directiveOnlyEnums = unvalidatedTypes.filterIsInstance().filter { it.name !in typeNamesUsedByFields }.toSet()
- return ScannedSchemaObjects(dictionary, definitions, typeExtensions, scalars, rootInfo, fieldResolversByType.toMap(), unusedDefinitions, directiveOnlyEnums)
+ return ScannedSchemaObjects(dictionary, definitions, observedDefinitions, scalars, fieldResolversByType.toMap(), unusedDefinitions, directiveOnlyEnums)
}
private fun validateRootResolversWereUsed(rootType: RootType?, fieldResolvers: List) {
@@ -328,22 +303,20 @@ internal class SchemaClassScanner(
}
}
- private fun getAllObjectTypesImplementingDiscoveredInterfaces(): List {
- return dictionary.keys.filterIsInstance().map { iface ->
- objectDefinitions.filter { obj ->
- obj.withExtensions(typeExtensions).flatMap { it.implements }.filterIsInstance().any { it.name == iface.name }
+ private fun getAllObjectTypesImplementingDiscoveredInterfaces(): List {
+ return dictionary.keys.filterIsInstance().map { iface ->
+ definitions.objectsWithRepeats.filter { obj ->
+ obj.interfaces.filterIsInstance().any { it.name == iface.name }
}
}.flatten().distinctBy { it.name }
}
- private fun getAllObjectTypeMembersOfDiscoveredUnions(): List {
- val unionTypeNames = dictionary.keys.filterIsInstance().map { union -> union.name }.toSet()
- return dictionary.keys.filterIsInstance().map { union ->
- union.withExtensions(typeExtensions).flatMap { definition ->
- definition.memberTypes.filterIsInstance().filter { !unionTypeNames.contains(it.name) }.map {
- objectDefinitionsByName[it.name]
- ?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: $definition")
- }
+ private fun getAllObjectTypeMembersOfDiscoveredUnions(): List {
+ val unionTypeNames = dictionary.keys.filterIsInstance().map { union -> union.name }.toSet()
+ return dictionary.keys.filterIsInstance().map { union ->
+ union.members.filterIsInstance().filter { !unionTypeNames.contains(it.name) }.map {
+ objectTypesByName[it.name]
+ ?: throw SchemaClassScannerError("No object type found with name '${it.name}' for union: ${union.declarationTextOf(it)}")
}
}.flatten().distinct()
}
@@ -352,10 +325,10 @@ internal class SchemaClassScanner(
* Unions can list other unions as members. Returns whether any new unions were found.
*/
private fun handleNestedUnions(): Boolean {
- val newUnions = dictionary.keys.filterIsInstance().flatMap { union ->
- union.withExtensions(typeExtensions).flatMap { it.memberTypes }.filterIsInstance()
- .map { definitionsByName[it.name] }
- .filterIsInstance()
+ val newUnions = dictionary.keys.filterIsInstance().flatMap { union ->
+ union.members.filterIsInstance()
+ .map { typesByName[it.name] }
+ .filterIsInstance()
.filter { !dictionary.containsKey(it) }
.map { it to UnionMemberReference(union) }
}
@@ -369,10 +342,10 @@ internal class SchemaClassScanner(
* union or interface may not be reachable from any object field. Returns whether any new types were found.
*/
private fun handleInterfaceFieldTypes(): Boolean {
- val newTypes = dictionary.keys.filterIsInstance().flatMap { iface ->
- iface.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }.mapNotNull { field ->
- val type = definitionsByName[field.type.unwrap().name]
- if ((type is UnionTypeDefinition || type is InterfaceTypeDefinition) && !dictionary.containsKey(type)) {
+ val newTypes = dictionary.keys.filterIsInstance().flatMap { iface ->
+ iface.fields.mapNotNull { field ->
+ val type = typesByName[field.type.unwrap().name]
+ if ((type is MergedType.Union || type is MergedType.Interface) && !dictionary.containsKey(type)) {
type to FieldTypeReference("${iface.name}.${field.name}")
} else {
null
@@ -384,8 +357,8 @@ internal class SchemaClassScanner(
return newTypes.isNotEmpty()
}
- private fun handleDictionaryTypes(types: List>, failureMessage: (TypeDefinition<*>) -> String) {
- val typesWithoutClass = mutableListOf>()
+ private fun handleDictionaryTypes(types: List>, failureMessage: (MergedType<*>) -> String) {
+ val typesWithoutClass = mutableListOf>()
types.forEach { type ->
val dictionaryContainsType = dictionary.filter { it.key.name == type.name }.isNotEmpty()
if (!unvalidatedTypes.contains(type) && !dictionaryContainsType) {
@@ -398,7 +371,7 @@ internal class SchemaClassScanner(
}
}
- val sealedMatches = typesWithoutClass.filterIsInstance().mapNotNull { sealedSubclassMatcher.sealedSubclassMatches(it).singleOrNull() }
+ val sealedMatches = typesWithoutClass.filterIsInstance().mapNotNull { sealedSubclassMatcher.sealedSubclassMatches(it).singleOrNull() }
if (sealedMatches.isEmpty()) {
typesWithoutClass.firstOrNull()?.let { throw SchemaClassScannerError(failureMessage(it) + sealedSubclassMatcher.sealedSubclassHint(it)) }
}
@@ -448,8 +421,8 @@ internal class SchemaClassScanner(
private fun isResolverForSupertype(resolverInfo: NormalResolverInfo, dataClass: Class<*>) =
resolverInfo.dataClassType != Any::class.java && resolverInfo.dataClassType.isAssignableFrom(dataClass)
- private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) {
- type.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }.forEach { field ->
+ private fun scanResolverInfoForPotentialMatches(type: MergedType.Object, resolverInfo: ResolverInfo) {
+ type.fields.forEach { field ->
val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)
collectIfNotCallableOnResolver(type, fieldResolver)
@@ -462,7 +435,7 @@ internal class SchemaClassScanner(
}
}
- private fun collectIfNotCallableOnResolver(type: ObjectTypeDefinition, fieldResolver: FieldResolver) {
+ private fun collectIfNotCallableOnResolver(type: MergedType.Object, fieldResolver: FieldResolver) {
val source = fieldResolver.search.source ?: return
val member: Member = when (fieldResolver) {
is MethodFieldResolver -> fieldResolver.method
@@ -492,7 +465,7 @@ internal class SchemaClassScanner(
)
}
- private fun requireParameterForEachArgument(type: ObjectTypeDefinition, fieldResolver: FieldResolver) {
+ private fun requireParameterForEachArgument(type: MergedType.Object, fieldResolver: FieldResolver) {
if (fieldResolver !is MethodFieldResolver) {
return
}
@@ -512,37 +485,22 @@ internal class SchemaClassScanner(
}
}
- private fun handleFoundScalarType(type: ScalarTypeDefinition) {
+ private fun handleFoundScalarType(type: MergedType.Scalar) {
unvalidatedTypes.add(type)
}
- private fun extendsOtherKind(definition: TypeDefinition<*>): Boolean {
- val type = definitionsByName[definition.name]
- return definition is SDLExtensionDefinition && type != null && type.kind() != definition.kind()
- }
-
- private fun TypeDefinition<*>.kind(): String = when (this) {
- is ObjectTypeDefinition -> "an object type"
- is InterfaceTypeDefinition -> "an interface"
- is UnionTypeDefinition -> "a union"
- is EnumTypeDefinition -> "an enum"
- is InputObjectTypeDefinition -> "an input object type"
- is ScalarTypeDefinition -> "a scalar"
- else -> javaClass.simpleName
- }
-
- private fun enqueue(graphQLType: ObjectTypeDefinition, javaType: JavaType) {
+ private fun enqueue(graphQLType: MergedType.Object, javaType: JavaType) {
queue.add(QueueItem(graphQLType, javaType))
}
- private data class QueueItem(val type: ObjectTypeDefinition, val clazz: JavaType)
+ private data class QueueItem(val type: MergedType.Object, val clazz: JavaType)
private data class NotCallableOnResolver(val message: String, val isField: Boolean)
private class RootTypesHolder(
options: SchemaParserOptions,
rootInfo: RootTypeInfo,
- definitionsByName: Map>,
+ typesByName: Map>,
queryResolvers: List,
mutationResolvers: List,
subscriptionResolvers: List
@@ -551,9 +509,9 @@ internal class SchemaClassScanner(
private val mutationName = rootInfo.getMutationName()
private val subscriptionName = rootInfo.getSubscriptionName()
- private val queryDefinition = definitionsByName[queryName]
- private val mutationDefinition = definitionsByName[mutationName]
- private val subscriptionDefinition = definitionsByName[subscriptionName]
+ private val queryDefinition = typesByName[queryName]
+ private val mutationDefinition = typesByName[mutationName]
+ private val subscriptionDefinition = typesByName[subscriptionName]
private val queryResolverInfo = RootResolverInfo(queryResolvers, options)
private val mutationResolverInfo = RootResolverInfo(mutationResolvers, options)
@@ -563,7 +521,7 @@ internal class SchemaClassScanner(
val mutation = createRootType("mutation", mutationDefinition, mutationName, rootInfo.isMutationRequired(), mutationResolvers, GraphQLMutationResolver::class.java, mutationResolverInfo)
val subscription = createRootType("subscription", subscriptionDefinition, subscriptionName, rootInfo.isSubscriptionRequired(), subscriptionResolvers, GraphQLSubscriptionResolver::class.java, subscriptionResolverInfo)
- private fun createRootType(name: String, type: TypeDefinition<*>?, typeName: String, required: Boolean, resolvers: List, resolverInterface: Class<*>, resolverInfo: RootResolverInfo): RootType? {
+ private fun createRootType(name: String, type: MergedType<*>?, typeName: String, required: Boolean, resolvers: List, resolverInterface: Class<*>, resolverInfo: RootResolverInfo): RootType? {
if (type == null) {
if (required) {
throw SchemaClassScannerError("Type definition for root $name type '$typeName' not found!")
@@ -572,8 +530,8 @@ internal class SchemaClassScanner(
return null
}
- if (type !is ObjectTypeDefinition) {
- throw SchemaClassScannerError("Expected root query type's type to be ${ObjectTypeDefinition::class.java.simpleName}, but it was ${type.javaClass.simpleName}")
+ if (type !is MergedType.Object) {
+ throw SchemaClassScannerError("Expected root query type's type to be ${ObjectTypeDefinition::class.java.simpleName}, but it was ${type.definitionClassName}")
}
// Find query resolver class
@@ -585,7 +543,7 @@ internal class SchemaClassScanner(
}
}
- internal class RootType(val name: String, val type: ObjectTypeDefinition, val resolvers: List, val resolverInterface: Class<*>, val resolverInfo: RootResolverInfo)
+ internal class RootType(val name: String, val type: MergedType.Object, val resolvers: List, val resolverInterface: Class<*>, val resolverInfo: RootResolverInfo)
}
internal class SchemaClassScannerError(message: String, throwable: Throwable? = null) : RuntimeException(message, throwable)
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
index a49b9aa1..501a41bc 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParser.kt
@@ -1,24 +1,15 @@
package graphql.kickstart.tools
-import graphql.Directives
import graphql.introspection.Introspection
import graphql.introspection.Introspection.DirectiveLocation.ARGUMENT_DEFINITION
-import graphql.introspection.Introspection.DirectiveLocation.ENUM
import graphql.introspection.Introspection.DirectiveLocation.ENUM_VALUE
import graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION
import graphql.introspection.Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION
-import graphql.introspection.Introspection.DirectiveLocation.INPUT_OBJECT
-import graphql.introspection.Introspection.DirectiveLocation.INTERFACE
-import graphql.introspection.Introspection.DirectiveLocation.OBJECT
-import graphql.introspection.Introspection.DirectiveLocation.SCALAR
-import graphql.introspection.Introspection.DirectiveLocation.UNION
import graphql.kickstart.tools.directive.DirectiveWiringHelper
import graphql.kickstart.tools.resolver.MethodFieldResolver
import graphql.kickstart.tools.util.describe
-import graphql.kickstart.tools.util.extensionsOf
import graphql.kickstart.tools.util.getDocumentation
import graphql.kickstart.tools.util.unwrap
-import graphql.kickstart.tools.util.withExtensions
import graphql.language.*
import graphql.schema.*
import graphql.schema.idl.RuntimeWiring
@@ -45,21 +36,21 @@ class SchemaParser internal constructor(
private val dictionary = scanResult.dictionary
private val definitions = scanResult.definitions
- private val typeExtensions = scanResult.typeExtensions
+ private val types = scanResult.types
private val customScalars = scanResult.customScalars
- private val rootInfo = scanResult.rootInfo
+ private val rootInfo = definitions.rootInfo
private val fieldResolversByType = scanResult.fieldResolversByType
- private val unusedDefinitions = scanResult.unusedDefinitions
+ private val unusedDefinitions = definitions.definitionsOf(scanResult.unusedTypes)
private val directiveOnlyEnums = scanResult.directiveOnlyEnums
- private val objectDefinitions = definitions.filterIsInstance()
- private val inputObjectDefinitions = definitions.filterIsInstance()
- private val enumDefinitions = definitions.filterIsInstance()
- private val interfaceDefinitions = definitions.filterIsInstance()
- private val directiveDefinitions = definitions.filterIsInstance()
+ private val objectDefinitions = types.filterIsInstance()
+ private val inputObjectDefinitions = types.filterIsInstance()
+ private val enumDefinitions = types.filterIsInstance()
+ private val interfaceDefinitions = types.filterIsInstance()
+ private val directiveDefinitions = definitions.directiveDefinitions
- private val unionDefinitions = definitions.filterIsInstance()
- private val scalarDefinitions = definitions.filterIsInstance().filter { it.name in customScalars }.associateBy { it.name }
+ private val unionDefinitions = types.filterIsInstance()
+ private val scalarDefinitions = types.filterIsInstance().filter { it.name in customScalars }.associateBy { it.name }
private val permittedTypesForObject: Set = (objectDefinitions.map { it.name } +
enumDefinitions.map { it.name } +
@@ -67,12 +58,13 @@ class SchemaParser internal constructor(
unionDefinitions.map { it.name }).toSet()
private val permittedTypesForInputObject: Set =
(inputObjectDefinitions.map { it.name } + enumDefinitions.map { it.name }).toSet()
- private val typeReferenceErrors = TypeReferenceErrors(definitions, unusedDefinitions, typeExtensions)
+ private val typeReferenceErrors = TypeReferenceErrors(definitions, types, scanResult.unusedTypes)
private val codeRegistryBuilder = GraphQLCodeRegistry.newCodeRegistry()
private val directiveWiringHelper = DirectiveWiringHelper(runtimeWiring, codeRegistryBuilder)
private lateinit var sdlDirectives: SdlDirectives
+ private lateinit var typeBuildContext: TypeBuildContext
/**
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
@@ -86,6 +78,7 @@ class SchemaParser internal constructor(
// type references, so that an input object used by a directive applied to it doesn't contain itself
determineType(GraphQLInputType::class, it, permittedTypesForInputObject) as GraphQLInputType
}
+ typeBuildContext = TypeBuildContext(options, sdlDirectives, customScalars)
val scalars = scalarDefinitions.values.map { createScalar(it) }
inputObjectDefinitions.forEach {
if (inputObjects.none { io -> io.name == it.name }) {
@@ -160,27 +153,17 @@ class SchemaParser internal constructor(
@Suppress("unused")
fun getUnusedDefinitions(): Set> = unusedDefinitions
- private fun createObject(objectDefinition: ObjectTypeDefinition, interfaces: List, inputObjects: List): GraphQLObjectType {
- val name = objectDefinition.name
- val extensions = typeExtensions.extensionsOf(name)
- val directives = sdlDirectives.directivesOf(objectDefinition, extensions)
- val fieldDefinitions = objectDefinition.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }
- rejectRepeated(name, "field", fieldDefinitions)
- val builder = GraphQLObjectType.newObject()
- .name(name)
- .definition(objectDefinition)
- .extensionDefinitions(extensions)
- .description(getDocumentation(objectDefinition, options))
- .withSdlDirectives(directives, OBJECT)
+ private fun createObject(objectDefinition: MergedType.Object, interfaces: List, inputObjects: List): GraphQLObjectType {
+ val builder = objectDefinition.prepare(typeBuildContext)
.apply {
- objectDefinition.withExtensions(typeExtensions).flatMap { it.implements }.forEach { implementsDefinition ->
+ objectDefinition.interfaces.forEach { implementsDefinition ->
val interfaceName = (implementsDefinition as TypeName).name
withInterface(interfaces.find { it.name == interfaceName }
?: throw SchemaError("Expected interface type with name '$interfaceName' but found none!"))
}
}
.apply {
- fieldDefinitions.forEach { fieldDefinition ->
+ objectDefinition.fields.forEach { fieldDefinition ->
field { field ->
createField(field, fieldDefinition, inputObjects)
codeRegistryBuilder.dataFetcher(
@@ -200,59 +183,38 @@ class SchemaParser internal constructor(
return builder.build()
}
- private fun createInputObject(definition: InputObjectTypeDefinition, inputObjects: List,
+ private fun createInputObject(definition: MergedType.Input, inputObjects: List,
inputObjectsInProgress: MutableSet): GraphQLInputObjectType {
- val extensionDefinitions = typeExtensions.extensionsOf(definition.name)
- val directives = sdlDirectives.directivesOf(definition, extensionDefinitions)
- val inputValueDefinitions = definition.withExtensions(typeExtensions).flatMap { it.inputValueDefinitions }
- rejectRepeated(definition.name, "field", inputValueDefinitions)
-
+ val builder = definition.prepare(typeBuildContext)
inputObjectsInProgress.add(definition.name)
- val builder = GraphQLInputObjectType.newInputObject()
- .name(definition.name)
- .definition(definition)
- .extensionDefinitions(extensionDefinitions)
- .description(getDocumentation(definition, options))
- .withSdlDirectives(directives, INPUT_OBJECT)
- .apply {
- inputValueDefinitions.forEach { fieldDefinition ->
- field(
- GraphQLInputObjectField.newInputObjectField()
- .name(fieldDefinition.name)
- .definition(fieldDefinition)
- .description(getDocumentation(fieldDefinition, options))
- .apply { fieldDefinition.defaultValue?.let { v -> defaultValueLiteral(v) } }
- .apply { sdlDirectives.getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } }
- .type(determineInputType(fieldDefinition.type, inputObjects, inputObjectsInProgress))
- .withSdlDirectives(fieldDefinition.directives, INPUT_FIELD_DEFINITION)
- .build()
- )
- }
- }
+ definition.fields.forEach { fieldDefinition ->
+ builder.field(
+ GraphQLInputObjectField.newInputObjectField()
+ .name(fieldDefinition.name)
+ .definition(fieldDefinition)
+ .description(getDocumentation(fieldDefinition, options))
+ .apply { fieldDefinition.defaultValue?.let { v -> defaultValueLiteral(v) } }
+ .apply { sdlDirectives.getDeprecated(fieldDefinition.directives)?.let { deprecate(it) } }
+ .type(determineInputType(fieldDefinition.type, inputObjects, inputObjectsInProgress))
+ .withSdlDirectives(fieldDefinition.directives, INPUT_FIELD_DEFINITION)
+ .build()
+ )
+ }
return builder.build()
}
- private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType {
+ private fun createEnumObject(definition: MergedType.Enum): GraphQLEnumType {
val name = definition.name
// enums only used by directives can do without a class, see SchemaClassScanner.handleDirectiveEnum
val enumClass = dictionary.classByType[definition]?.unwrap()
if (enumClass == null && definition !in directiveOnlyEnums) throw SchemaError("Expected enum with name '$name' but found none!")
if (enumClass != null && !enumClass.isEnum) throw SchemaError("Type '$name' is declared as an enum in the GraphQL schema but is not a Java enum!")
- val extensions = typeExtensions.extensionsOf(name)
- val directives = sdlDirectives.directivesOf(definition, extensions)
- val values = definition.withExtensions(typeExtensions).flatMap { it.enumValueDefinitions }
- rejectRepeated(name, "value", values)
- val builder = GraphQLEnumType.newEnum()
- .name(name)
- .definition(definition)
- .extensionDefinitions(extensions)
- .description(getDocumentation(definition, options))
- .withSdlDirectives(directives, ENUM)
+ val builder = definition.prepare(typeBuildContext)
.apply {
- values.forEach { valueDefinition ->
+ definition.values.forEach { valueDefinition ->
val enumName = valueDefinition.name
val enumValue = if (enumClass == null) {
enumName
@@ -277,41 +239,17 @@ class SchemaParser internal constructor(
return builder.build()
}
- private fun createScalar(definition: ScalarTypeDefinition): GraphQLScalarType {
- val name = definition.name
- val provided = customScalars.getValue(name)
- val extensions = typeExtensions.extensionsOf(name)
- val directives = sdlDirectives.directivesOf(definition, extensions)
- val builder = GraphQLScalarType.newScalar(provided)
- .definition(definition)
- .extensionDefinitions(extensions)
- .description(getDocumentation(definition, options) ?: provided.description)
- .specifiedByUrl(getSpecifiedByUrl(directives) ?: provided.specifiedByUrl)
- // like graphql-java, only the schema's directives are applied
- .clearDirectives()
- .withSdlDirectives(directives, SCALAR)
-
- return builder.build()
- }
+ private fun createScalar(definition: MergedType.Scalar): GraphQLScalarType = definition.prepare(typeBuildContext).build()
- private fun createInterfaceObject(interfaceDefinition: InterfaceTypeDefinition, inputObjects: List): GraphQLInterfaceType {
- val extensions = typeExtensions.extensionsOf(interfaceDefinition.name)
- val directives = sdlDirectives.directivesOf(interfaceDefinition, extensions)
- val fieldDefinitions = interfaceDefinition.withExtensions(typeExtensions).flatMap { it.fieldDefinitions }
- rejectRepeated(interfaceDefinition.name, "field", fieldDefinitions)
- val builder = GraphQLInterfaceType.newInterface()
- .name(interfaceDefinition.name)
- .definition(interfaceDefinition)
- .extensionDefinitions(extensions)
- .description(getDocumentation(interfaceDefinition, options))
- .withSdlDirectives(directives, INTERFACE)
+ private fun createInterfaceObject(interfaceDefinition: MergedType.Interface, inputObjects: List): GraphQLInterfaceType {
+ val builder = interfaceDefinition.prepare(typeBuildContext)
.apply {
- fieldDefinitions.forEach { fieldDefinition ->
+ interfaceDefinition.fields.forEach { fieldDefinition ->
field { field -> createField(field, fieldDefinition, inputObjects) }
}
}
.apply {
- interfaceDefinition.withExtensions(typeExtensions).flatMap { it.implements }.forEach { implementsDefinition ->
+ interfaceDefinition.interfaces.forEach { implementsDefinition ->
val interfaceName = (implementsDefinition as TypeName).name
withInterface(GraphQLTypeReference(interfaceName))
}
@@ -320,16 +258,8 @@ class SchemaParser internal constructor(
return builder.build()
}
- private fun createUnionObject(definition: UnionTypeDefinition, types: List): GraphQLUnionType {
- val extensions = typeExtensions.extensionsOf(definition.name)
- val directives = sdlDirectives.directivesOf(definition, extensions)
- rejectRepeated(definition.name, "member", definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.map { it as TypeName })
- val builder = GraphQLUnionType.newUnionType()
- .name(definition.name)
- .definition(definition)
- .extensionDefinitions(extensions)
- .description(getDocumentation(definition, options))
- .withSdlDirectives(directives, UNION)
+ private fun createUnionObject(definition: MergedType.Union, types: List): GraphQLUnionType {
+ val builder = definition.prepare(typeBuildContext)
.apply {
getLeafUnionObjects(definition, types).forEach { possibleType(it) }
}
@@ -338,18 +268,18 @@ class SchemaParser internal constructor(
}
private fun getLeafUnionObjects(
- definition: UnionTypeDefinition,
+ definition: MergedType.Union,
types: List,
- path: List = listOf(definition),
+ path: List = listOf(definition),
): List {
val name = definition.name
val leafObjects = mutableListOf()
- definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.forEach {
+ definition.members.forEach {
val typeName = (it as TypeName).name
// Is this a nested union? If so, expand
- val nestedUnion: UnionTypeDefinition? = unionDefinitions.find { otherDefinition -> typeName == otherDefinition.name }
+ val nestedUnion: MergedType.Union? = unionDefinitions.find { otherDefinition -> typeName == otherDefinition.name }
if (nestedUnion != null) {
if (nestedUnion in path) {
@@ -365,12 +295,6 @@ class SchemaParser internal constructor(
return leafObjects
}
- private fun rejectRepeated(typeName: String, kind: String, declarations: List>) {
- val names = mutableSetOf()
- val repeat = declarations.firstOrNull { !names.add(it.name) } ?: return
- throw SchemaError("Type '$typeName' declares $kind '${repeat.name}' more than once${repeat.sourceLocation.describe()}.")
- }
-
private fun createField(field: GraphQLFieldDefinition.Builder, fieldDefinition: FieldDefinition, inputObjects: List): GraphQLFieldDefinition.Builder {
return field
.name(fieldDefinition.name)
@@ -401,9 +325,6 @@ class SchemaParser internal constructor(
private fun > B.withSdlDirectives(directives: List, location: Introspection.DirectiveLocation): B =
with(sdlDirectives) { withSdlDirectives(directives, location) }
- private fun getSpecifiedByUrl(directives: List): String? =
- (directives.find { it.name == Directives.SpecifiedByDirective.name }?.getArgument("url")?.value as? StringValue)?.value
-
// custom scalars are in the dictionary, so they're referenced by name like the other types
private fun scalarType(name: String?): GraphQLInputType? =
scalarDefinitions[name]?.let { GraphQLTypeReference(it.name) } ?: GRAPHQL_SCALARS[name]
diff --git a/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt b/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt
index 448228c2..35727c57 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SchemaParserBuilder.kt
@@ -163,7 +163,7 @@ class SchemaParserBuilder {
val definitions = appendDynamicDefinitions(parseDefinitions())
val customScalars = scalars.associateBy { it.name }
- return SchemaClassScanner(dictionary.toMap(), definitions, resolvers, customScalars, options)
+ return SchemaClassScanner(dictionary.toMap(), MergedDefinitions(definitions), resolvers, customScalars, options)
.scanForClasses()
}
diff --git a/src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt b/src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt
index ee531bf3..92dbc42e 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SdlDirectives.kt
@@ -28,9 +28,6 @@ internal class SdlDirectives(
createDirectives()
}
- fun directivesOf(definition: TypeDefinition<*>, extensions: List>): List =
- definition.directives + extensions.flatMap { it.directives }
-
fun failOnProblems() {
if (problems.isNotEmpty()) throw SchemaError(problems.joinToString("\n"))
}
diff --git a/src/main/kotlin/graphql/kickstart/tools/SealedSubclassMatcher.kt b/src/main/kotlin/graphql/kickstart/tools/SealedSubclassMatcher.kt
index 671e940d..713999ae 100644
--- a/src/main/kotlin/graphql/kickstart/tools/SealedSubclassMatcher.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/SealedSubclassMatcher.kt
@@ -3,21 +3,15 @@ package graphql.kickstart.tools
import graphql.kickstart.tools.ScanDictionary.SealedSubclassReference
import graphql.kickstart.tools.util.JavaType
import graphql.kickstart.tools.util.unwrap
-import graphql.kickstart.tools.util.withExtensions
-import graphql.language.InterfaceTypeDefinition
-import graphql.language.ObjectTypeDefinition
-import graphql.language.TypeDefinition
import graphql.language.TypeName
-import graphql.language.UnionTypeDefinition
import org.apache.commons.lang3.reflect.TypeUtils
import java.lang.reflect.ParameterizedType
internal class SealedSubclassMatcher(
- private val dictionary: Map, ScanDictionary.EntryView>,
- private val typeExtensions: TypeExtensions,
+ private val dictionary: Map, ScanDictionary.EntryView>,
) {
- fun sealedSubclassHint(type: TypeDefinition<*>): String {
- if (type !is ObjectTypeDefinition) {
+ fun sealedSubclassHint(type: MergedType<*>): String {
+ if (type !is MergedType.Object) {
return ""
}
@@ -35,7 +29,7 @@ internal class SealedSubclassMatcher(
}
}
- fun sealedSubclassMatches(type: ObjectTypeDefinition): List =
+ fun sealedSubclassMatches(type: MergedType.Object): List =
sealedTypesOf(type)
.flatMap { sealedType ->
sealedSubclasses(sealedType.unwrap())
@@ -44,12 +38,12 @@ internal class SealedSubclassMatcher(
}
.distinctBy { it.javaType.unwrap() }
- private fun sealedTypesOf(type: ObjectTypeDefinition): List =
+ private fun sealedTypesOf(type: MergedType.Object): List =
dictionary
.filterKeys { definition ->
when (definition) {
- is UnionTypeDefinition -> definition.withExtensions(typeExtensions).flatMap { it.memberTypes }.filterIsInstance().any { it.name == type.name }
- is InterfaceTypeDefinition -> type.withExtensions(typeExtensions).flatMap { it.implements }.filterIsInstance().any { it.name == definition.name }
+ is MergedType.Union -> definition.members.filterIsInstance().any { it.name == type.name }
+ is MergedType.Interface -> type.interfaces.filterIsInstance().any { it.name == definition.name }
else -> false
}
}
diff --git a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt
index fff48337..93cffe0e 100644
--- a/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt
@@ -6,7 +6,6 @@ import graphql.kickstart.tools.util.GraphQLLangType
import graphql.kickstart.tools.util.JavaType
import graphql.kickstart.tools.util.isSubtypeOf
import graphql.language.*
-import graphql.schema.idl.ScalarInfo
import graphql.schema.idl.TypeUtil
import java.lang.reflect.ParameterizedType
import java.util.*
@@ -14,32 +13,30 @@ import java.util.*
/**
* @author Andrew Potter
*/
-internal class TypeClassMatcher(private val definitionsByName: Map>) {
+internal class TypeClassMatcher(private val definitions: MergedDefinitions) {
fun match(potentialMatch: PotentialMatch): Match {
val unwrapping = potentialMatch.unwrapping
return when (val graphQLType = unwrapping.graphQLType) {
is TypeName -> {
- val typeDefinition = ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS_DEFINITIONS[graphQLType.name]
- ?: definitionsByName[graphQLType.name]
+ val typeDefinition = definitions.resolve(graphQLType.name)
?: throw potentialMatch.error("No ${TypeDefinition::class.java.simpleName} for type name ${graphQLType.name}")
- if (typeDefinition is ScalarTypeDefinition) {
+ if (typeDefinition is MergedType.Scalar) {
ScalarMatch(typeDefinition)
} else {
ValidMatch(typeDefinition, unwrapping.javaType, potentialMatch.reference)
}
}
- is TypeDefinition<*> -> ValidMatch(graphQLType, unwrapping.javaType, potentialMatch.reference)
else -> throw potentialMatch.error("Unknown type: ${unwrapping.javaType.javaClass.name}")
}
}
internal sealed interface Match
- internal data class ScalarMatch(val type: ScalarTypeDefinition) : Match
+ internal data class ScalarMatch(val type: MergedType.Scalar) : Match
- internal data class ValidMatch(val type: TypeDefinition<*>, val javaType: JavaType, val reference: ScanDictionary.Reference) : Match
+ internal data class ValidMatch(val type: MergedType<*>, val javaType: JavaType, val reference: ScanDictionary.Reference) : Match
internal enum class Location(val prettyName: String) {
RETURN_TYPE("return type"),
diff --git a/src/main/kotlin/graphql/kickstart/tools/TypeReferenceErrors.kt b/src/main/kotlin/graphql/kickstart/tools/TypeReferenceErrors.kt
index 679cfbe5..36da3e70 100644
--- a/src/main/kotlin/graphql/kickstart/tools/TypeReferenceErrors.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/TypeReferenceErrors.kt
@@ -1,69 +1,47 @@
package graphql.kickstart.tools
import graphql.kickstart.tools.util.describe
-import graphql.kickstart.tools.util.unwrap
-import graphql.language.*
+import graphql.language.TypeName
import graphql.schema.GraphQLInputType
import kotlin.reflect.KClass
internal class TypeReferenceErrors(
- private val definitions: Set>,
- private val unusedDefinitions: Set>,
- private val typeExtensions: TypeExtensions,
+ private val definitions: MergedDefinitions,
+ private val types: Set>,
+ private val unusedTypes: Set>,
) {
fun unexpectedTypeError(typeName: TypeName, expectedType: KClass<*>): SchemaError {
val isInput = expectedType == GraphQLInputType::class
- val definition = unusedDefinitions.find { it.name == typeName.name }
- ?: definitions.filterIsInstance>().find { it.name == typeName.name }
- val outputOnlyKind = when (definition) {
- is ObjectTypeDefinition -> "an object type"
- is InterfaceTypeDefinition -> "an interface"
- is UnionTypeDefinition -> "a union"
- else -> null
- }
+ val definition = unusedTypes.find { it.name == typeName.name }
+ ?: types.find { it.name == typeName.name }
+ val outputOnlyKind = definition?.takeIf { it is MergedType.Object || it is MergedType.Interface || it is MergedType.Union }?.kindPhrase
val problem = when {
definition == null -> "is not defined in the schema."
isInput && outputOnlyKind != null ->
"is $outputOnlyKind, which can't be used as an input type. Input types are input objects, enums and scalars."
- !isInput && definition is InputObjectTypeDefinition ->
+ !isInput && definition is MergedType.Input ->
"is an input object type, which can't be used as an output type. Output types are object types, interfaces, unions, enums and scalars."
- definition in unusedDefinitions ->
+ definition in unusedTypes ->
"is defined in the schema, but was not reached while scanning the resolvers and the classes they use, so it wasn't built. " +
unreachedTypeHint(definition)
else -> "can't be used as ${if (isInput) "an input" else "an output"} type."
}
- val usage = findUsage(typeName)?.let { " used by $it" }.orEmpty()
+ val usage = definitions.findUsage(typeName, types)?.let { " used by $it" }.orEmpty()
return SchemaError("Type '${typeName.name}'$usage${typeName.sourceLocation.describe()} $problem")
}
// what makes the scanner reach the type depends on its kind, see SchemaClassScanner.canIncludeUnusedType
- private fun unreachedTypeHint(definition: TypeDefinition<*>): String = when (definition) {
- is InterfaceTypeDefinition, is UnionTypeDefinition ->
+ private fun unreachedTypeHint(definition: MergedType<*>): String = when (definition) {
+ is MergedType.Interface, is MergedType.Union ->
"Return it from a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes."
- is EnumTypeDefinition ->
+ is MergedType.Enum ->
"Use it in a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes and pass its enum class in the parser's dictionary."
- is InputObjectTypeDefinition ->
+ is MergedType.Input ->
"Use it in an argument of a resolver method, directly or through another input type. SchemaParserOptions.includeUnusedTypes doesn't include input types."
- is ScalarTypeDefinition ->
+ is MergedType.Scalar ->
"Use it in a resolver method or one of those classes. SchemaParserOptions.includeUnusedTypes doesn't include scalars."
- else ->
+ is MergedType.Object ->
"Return it from a resolver method or one of those classes, or set SchemaParserOptions.includeUnusedTypes and pass a class for it in the parser's dictionary."
}
-
- // determineType only gets the type reference, so the definition using it is looked up for the error message
- private fun findUsage(typeName: TypeName): String? = (definitions + typeExtensions.values.flatten()).asSequence().flatMap { definition ->
- when (definition) {
- is ObjectTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions)
- is InterfaceTypeDefinition -> fieldUsages(definition.name, definition.fieldDefinitions)
- is InputObjectTypeDefinition -> definition.inputValueDefinitions.map { "input field '${definition.name}.${it.name}'" to it.type }
- is DirectiveDefinition -> definition.inputValueDefinitions.map { "argument '${it.name}' of directive '@${definition.name}'" to it.type }
- else -> emptyList()
- }
- }.find { (_, type) -> type.unwrap() === typeName }?.first
-
- private fun fieldUsages(typeName: String, fields: List): List>> = fields.flatMap { field ->
- listOf("field '$typeName.${field.name}'" to field.type) +
- field.inputValueDefinitions.map { "argument '${it.name}' of field '$typeName.${field.name}'" to it.type }
- }
}
diff --git a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt
index 5c8c98fb..14c156ed 100644
--- a/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt
+++ b/src/main/kotlin/graphql/kickstart/tools/util/Utils.kt
@@ -2,7 +2,6 @@ package graphql.kickstart.tools.util
import graphql.kickstart.tools.GraphQLResolver
import graphql.kickstart.tools.SchemaParserOptions
-import graphql.kickstart.tools.TypeExtensions
import graphql.language.*
import graphql.schema.DataFetchingEnvironment
import graphql.schema.idl.TypeUtil
@@ -28,13 +27,6 @@ internal typealias GraphQLLangType = Type<*>
internal fun Type<*>.unwrap(): TypeName = TypeUtil.unwrapAll(this)
-internal inline fun > TypeExtensions.extensionsOf(name: String): List =
- this[name].orEmpty().filterIsInstance()
-
-// Every extension class is a subclass of its type's class, e.g. UnionTypeExtensionDefinition of UnionTypeDefinition.
-internal inline fun > T.withExtensions(typeExtensions: TypeExtensions): List =
- listOf(this) + typeExtensions.extensionsOf(name)
-
internal fun JavaType.unwrap(): Class =
if (this is ParameterizedType) {
this.rawType as Class<*>
diff --git a/src/test/kotlin/graphql/kickstart/tools/DefinitionsBoundaryTest.kt b/src/test/kotlin/graphql/kickstart/tools/DefinitionsBoundaryTest.kt
new file mode 100644
index 00000000..98557d61
--- /dev/null
+++ b/src/test/kotlin/graphql/kickstart/tools/DefinitionsBoundaryTest.kt
@@ -0,0 +1,146 @@
+package graphql.kickstart.tools
+
+import com.tngtech.archunit.base.DescribedPredicate
+import com.tngtech.archunit.core.domain.JavaAccess
+import com.tngtech.archunit.core.domain.JavaClass
+import com.tngtech.archunit.core.domain.JavaClasses
+import com.tngtech.archunit.core.domain.JavaConstructorCall
+import com.tngtech.archunit.core.domain.JavaConstructorReference
+import com.tngtech.archunit.core.importer.ClassFileImporter
+import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
+import graphql.kickstart.tools.directive.DirectiveWiringHelper
+import graphql.kickstart.tools.relay.RelayConnectionFactory
+import graphql.language.AstTransformer
+import graphql.language.NodeTraverser
+import graphql.language.NodeVisitor
+import graphql.language.SDLExtensionDefinition
+import graphql.language.SchemaExtensionDefinition
+import graphql.language.TypeDefinition
+import graphql.schema.GraphQLEnumType
+import graphql.schema.GraphQLInputObjectType
+import graphql.schema.GraphQLInterfaceType
+import graphql.schema.GraphQLObjectType
+import graphql.schema.GraphQLScalarType
+import graphql.schema.GraphQLUnionType
+import graphql.schema.idl.TypeDefinitionRegistry
+import org.junit.Test
+import java.nio.file.Paths
+import kotlin.reflect.KClass
+
+class DefinitionsBoundaryTest {
+
+ companion object {
+ private val classes: JavaClasses = ClassFileImporter()
+ .importPath(Paths.get(MergedDefinitions::class.java.protectionDomain.codeSource.location.toURI()))
+ }
+
+ private fun declaredOutside(vararg allowed: KClass<*>) = object : DescribedPredicate("are not ${allowed.joinToString { it.java.name }}") {
+ private val names = allowed.map { it.java.name }
+ override fun test(javaClass: JavaClass) = javaClass.name.substringBefore('$') !in names
+ }
+
+ private val typeMembers = setOf(
+ "getFieldDefinitions", "getImplements", "getMemberTypes", "getEnumValueDefinitions", "getInputValueDefinitions",
+ "getDirectives", "getDirectivesByName", "hasDirective", "getChildren", "getNamedChildren",
+ )
+
+ private val readsTypeMembers = object : DescribedPredicate>("reads the members, directives or child nodes of what may be an SDL type definition") {
+ override fun test(access: JavaAccess<*>): Boolean {
+ val owner = access.target.owner
+ return access.target.name in typeMembers &&
+ (owner.isAssignableTo(TypeDefinition::class.java) || owner.isAssignableFrom(TypeDefinition::class.java))
+ }
+ }
+
+ private val extensionClass = object : DescribedPredicate("are graphql-java type extension definitions") {
+ override fun test(javaClass: JavaClass) =
+ javaClass.isAssignableTo(SDLExtensionDefinition::class.java) && !javaClass.isAssignableTo(SchemaExtensionDefinition::class.java)
+ }
+
+ private val nodeWalker = object : DescribedPredicate("walk graphql-java SDL nodes") {
+ override fun test(javaClass: JavaClass) = javaClass.isAssignableTo(NodeVisitor::class.java) ||
+ javaClass.isEquivalentTo(NodeTraverser::class.java) || javaClass.isEquivalentTo(AstTransformer::class.java)
+ }
+
+ private val objectMethods = Any::class.java.methods.mapTo(hashSetOf()) { it.name }
+
+ private val readsTypeRegistry = object : DescribedPredicate>("reads a TypeDefinitionRegistry") {
+ override fun test(access: JavaAccess<*>) =
+ access.target.owner.isAssignableTo(TypeDefinitionRegistry::class.java) && access.target.name !in objectMethods
+ }
+
+ private val namedTypes = listOf(
+ GraphQLObjectType::class, GraphQLInterfaceType::class, GraphQLUnionType::class,
+ GraphQLEnumType::class, GraphQLInputObjectType::class, GraphQLScalarType::class,
+ ).mapTo(hashSetOf()) { it.java.name }
+ private val builderFactories = setOf("newObject", "newInterface", "newUnionType", "newEnum", "newInputObject", "newScalar")
+
+ private val startsNamedTypeBuilder = object : DescribedPredicate>("starts a builder for a graphql-java named type") {
+ override fun test(access: JavaAccess<*>): Boolean {
+ val owner = access.target.owner.name
+ val isFactory = access.target.name in builderFactories && owner in namedTypes
+ val isConstructor = (access is JavaConstructorCall || access is JavaConstructorReference) &&
+ owner.substringBefore('$') in namedTypes && owner.endsWith("\$Builder")
+ return isFactory || isConstructor
+ }
+ }
+
+ private val transformsNamedType = object : DescribedPredicate>("transforms a graphql-java named type") {
+ override fun test(access: JavaAccess<*>) = access.target.name == "transform" && access.target.owner.name in namedTypes
+ }
+
+ private val readMergedType = "Look the type up with MergedDefinitions.byName or resolve(name) and read MergedType.fields, interfaces, members or values, which hold the extensions' items too"
+
+ @Test
+ fun `imports the compiled main classes`() {
+ assert(classes.size > 100) { "expected the compiled main classes, found ${classes.size}" }
+ }
+
+ @Test
+ fun `only the merged definitions read the members and directives of SDL types`() {
+ noClasses().that(declaredOutside(MergedDefinitions::class, MergedType::class, RelayConnectionFactory::class))
+ .should().accessTargetWhere(readsTypeMembers)
+ .because("each type is merged with its extensions once in MergedDefinitions.kt. $readMergedType. " +
+ "MergedType.prepare applies the merged directives, and a built graphql-java type lists them in appliedDirectives. " +
+ "A field, argument or enum value's own directives are fine to read through a FieldDefinition, InputValueDefinition or EnumValueDefinition receiver, not a DirectivesContainer or Node one")
+ .check(classes)
+ }
+
+ @Test
+ fun `only the merged definitions see type extension classes`() {
+ noClasses().that(declaredOutside(MergedDefinitions::class, MergedType::class))
+ .should().dependOnClassesThat(extensionClass)
+ .because("each type is merged with its extensions once in MergedDefinitions.kt. $readMergedType. MergedType.prepare passes the extension definitions to the builder")
+ .check(classes)
+ }
+
+ @Test
+ fun `nothing walks SDL nodes`() {
+ noClasses().should().dependOnClassesThat(nodeWalker)
+ .because("a walk visits each type definition apart from its extensions. $readMergedType")
+ .check(classes)
+ }
+
+ @Test
+ fun `nothing reads a TypeDefinitionRegistry`() {
+ noClasses().should().accessTargetWhere(readsTypeRegistry)
+ .because("a TypeDefinitionRegistry keeps each type apart from its extensions. $readMergedType")
+ .check(classes)
+ }
+
+ @Test
+ fun `only the merged definitions start named-type builders`() {
+ noClasses().that(declaredOutside(MergedType::class))
+ .should().accessTargetWhere(startsNamedTypeBuilder)
+ .because("every named type starts from MergedType.prepare, which checks repeats and sets the name, definitions, description and directives")
+ .check(classes)
+ }
+
+ @Test
+ fun `only the directive wiring transforms built named types`() {
+ noClasses().that(declaredOutside(DirectiveWiringHelper::class))
+ .should().accessTargetWhere(transformsNamedType)
+ .because("transform copies a type that's already built. Start a new type from MergedType.prepare")
+ .check(classes)
+ }
+}
diff --git a/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt b/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt
index d289a380..673542da 100644
--- a/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt
+++ b/src/test/kotlin/graphql/kickstart/tools/NestedGenericWrapperTest.kt
@@ -229,7 +229,7 @@ class NestedGenericWrapperTest {
fun `resolves every return type the matcher accepts`() {
val options = SchemaParserOptions.newOptions().genericWrappers(tryWrapper, pageWrapper).build()
val typeResolver = GenericTypeResolver(Query::class.java, options)
- val matcher = TypeClassMatcher(mapOf("Payload" to ObjectTypeDefinition("Payload"), "Leaf" to ScalarTypeDefinition("Leaf")))
+ val matcher = TypeClassMatcher(MergedDefinitions(listOf(ObjectTypeDefinition("Payload"), ScalarTypeDefinition("Leaf"))))
val method = Query::class.java.getMethod("listOfWrapped")
val graphQLTypes = listOf(TypeName("Payload"), TypeName("Leaf")).flatMap { listOf(it, NonNullType(it), ListType(it), NonNullType(ListType(NonNullType(it))), ListType(ListType(it))) }
val containers = listOf(List::class.java, CompletableFuture::class.java, Try::class.java, Page::class.java, DataFetcherResult::class.java, Optional::class.java, Future::class.java)
@@ -273,7 +273,7 @@ class NestedGenericWrapperTest {
@Test
fun `rejects a DataFetcherResult or an Optional twice in one value`() {
val typeResolver = GenericTypeResolver(Query::class.java, SchemaParserOptions.newOptions().build())
- val matcher = TypeClassMatcher(mapOf("Payload" to ObjectTypeDefinition("Payload")))
+ val matcher = TypeClassMatcher(MergedDefinitions(listOf(ObjectTypeDefinition("Payload"))))
val method = Query::class.java.getMethod("listOfWrapped")
val result = DataFetcherResult::class.java
val optional = Optional::class.java
diff --git a/src/test/kotlin/graphql/kickstart/tools/TypeClassMatcherTest.kt b/src/test/kotlin/graphql/kickstart/tools/TypeClassMatcherTest.kt
index 06868a81..c8ce6299 100644
--- a/src/test/kotlin/graphql/kickstart/tools/TypeClassMatcherTest.kt
+++ b/src/test/kotlin/graphql/kickstart/tools/TypeClassMatcherTest.kt
@@ -28,13 +28,11 @@ class TypeClassMatcherTest {
private val customType: Type<*> = TypeName("CustomType")
private val unwrappedCustomType: Type<*> = TypeName("UnwrappedGenericCustomType")
- private val customDefinition: TypeDefinition<*> = ObjectTypeDefinition("CustomType")
- private val unwrappedCustomDefinition: TypeDefinition<*> = ObjectTypeDefinition("UnwrappedGenericCustomType")
+ private val definitions = MergedDefinitions(listOf(ObjectTypeDefinition("CustomType"), ObjectTypeDefinition("UnwrappedGenericCustomType")))
+ private val customDefinition: MergedType<*> = definitions.byName.getValue("CustomType")
+ private val unwrappedCustomDefinition: MergedType<*> = definitions.byName.getValue("UnwrappedGenericCustomType")
- private val matcher: TypeClassMatcher = TypeClassMatcher(mapOf(
- "CustomType" to customDefinition,
- "UnwrappedGenericCustomType" to unwrappedCustomDefinition
- ))
+ private val matcher: TypeClassMatcher = TypeClassMatcher(definitions)
private val options: SchemaParserOptions = SchemaParserOptions.newOptions().genericWrappers(
GenericWrapper(
diff --git a/src/test/kotlin/graphql/kickstart/tools/TypeKindRecipeTest.kt b/src/test/kotlin/graphql/kickstart/tools/TypeKindRecipeTest.kt
new file mode 100644
index 00000000..270e7014
--- /dev/null
+++ b/src/test/kotlin/graphql/kickstart/tools/TypeKindRecipeTest.kt
@@ -0,0 +1,138 @@
+package graphql.kickstart.tools
+
+import graphql.Scalars
+import graphql.language.Node
+import graphql.schema.*
+import org.junit.Assert.assertThrows
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import kotlin.reflect.KClass
+
+class TypeKindRecipeTest {
+
+ private val kinds: Map>, Kind> = mapOf(
+ MergedType.Object::class to Kind("Item", "ObjectTypeDefinition", Repeat("extend type Item { id: ID! }", "field", "id")),
+ MergedType.Interface::class to Kind("Node", "InterfaceTypeDefinition", Repeat("extend interface Node { id: ID! }", "field", "id")),
+ MergedType.Union::class to Kind("Result", "UnionTypeDefinition", Repeat("extend union Result = Item", "member", "Item")),
+ MergedType.Enum::class to Kind("Color", "EnumTypeDefinition", Repeat("extend enum Color { RED }", "value", "RED")),
+ MergedType.Input::class to Kind("Filter", "InputObjectTypeDefinition", Repeat("extend input Filter { id: ID }", "field", "id")),
+ MergedType.Scalar::class to Kind("Date", "ScalarTypeDefinition", repeat = null),
+ )
+
+ @Test
+ fun `every kind of type has a case`() {
+ fun leaves(kind: KClass>): List>> =
+ if (kind.isSealed) kind.sealedSubclasses.flatMap(::leaves) else listOf(kind)
+
+ val missing = leaves(MergedType::class).filter { it !in kinds }.map { it.qualifiedName!!.removePrefix("graphql.kickstart.tools.") }
+ assertTrue(
+ "${missing.joinToString()} has no case. Add a Kind for it to kinds, and add to sdl a definition with a description and @a and an extension with @b",
+ missing.isEmpty(),
+ )
+ }
+
+ @Test
+ fun `every kind of type gets the description, definitions and directives of its definition and extension`() {
+ val schema = parser(sdl).makeExecutableSchema().assertEveryFieldResolves()
+
+ val built = kinds.values.map { kind ->
+ val type = schema.getType(kind.name) as GraphQLDirectiveContainer
+ listOf(type.description, type.definition?.javaClass?.simpleName, (type as GraphQLNamedType).extensionDefinitions().size, type.appliedDirectives.map { it.name })
+ }
+ assertEquals(built, kinds.values.map { kind ->
+ listOf("${kind.name} description", kind.definitionClass, 1, listOf("a", "b"))
+ })
+ }
+
+ @Test
+ fun `every kind of type with members rejects a member that its extension repeats`() {
+ val repeating = kinds.values.filter { it.repeat != null }
+ val errors = repeating.map { kind ->
+ assertThrows(SchemaError::class.java) { parser(sdl + kind.repeat!!.extension).makeExecutableSchema() }.message
+ }
+
+ val appendedExtensionLine = sdl.lines().size
+ assertEquals(errors, repeating.map { kind ->
+ val repeat = kind.repeat!!
+ val column = repeat.extension.indexOf(repeat.member, repeat.extension.indexOf(kind.name) + kind.name.length) + 1
+ "Type '${kind.name}' declares ${repeat.noun} '${repeat.member}' more than once at line $appendedExtensionLine, column $column."
+ })
+ }
+
+ private class Kind(val name: String, val definitionClass: String, val repeat: Repeat?)
+
+ private class Repeat(val extension: String, val noun: String, val member: String)
+
+ private val sdl = """
+ |directive @a on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+ |directive @b on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR
+ |
+ |type Query { node: Node result: Result color: Color search(filter: Filter): String today: Date }
+ |
+ |"Item description"
+ |type Item implements Node @a { id: ID! }
+ |extend type Item @b { name: String }
+ |type Other { id: ID! }
+ |
+ |"Node description"
+ |interface Node @a { id: ID! }
+ |extend interface Node @b { name: String }
+ |
+ |"Result description"
+ |union Result @a = Item
+ |extend union Result @b = Other
+ |
+ |"Color description"
+ |enum Color @a { RED }
+ |extend enum Color @b { GREEN }
+ |
+ |"Filter description"
+ |input Filter @a { id: ID }
+ |extend input Filter @b { limit: Int }
+ |
+ |"Date description"
+ |scalar Date @a
+ |extend scalar Date @b
+ |""".trimMargin()
+
+ private val date = GraphQLScalarType.newScalar().name("Date").description("provided").coercing(Scalars.GraphQLString.coercing).build()
+
+ private fun parser(schema: String) = SchemaParser.newParser()
+ .schemaString(schema)
+ .resolvers(Query())
+ .dictionary(Item::class, Other::class)
+ .scalars(date)
+ .build()
+
+ private fun GraphQLNamedType.extensionDefinitions(): List> = when (this) {
+ is GraphQLObjectType -> extensionDefinitions
+ is GraphQLInterfaceType -> extensionDefinitions
+ is GraphQLUnionType -> extensionDefinitions
+ is GraphQLEnumType -> extensionDefinitions
+ is GraphQLInputObjectType -> extensionDefinitions
+ is GraphQLScalarType -> extensionDefinitions
+ else -> error("not a named type with extensions: $name")
+ }
+
+ class Query : GraphQLQueryResolver {
+ fun node(): Entity = Item("1", "item")
+ fun result(): Outcome = Other("2")
+ fun color() = Color.GREEN
+ fun search(filter: Filter) = "${filter.id} ${filter.limit}"
+ fun today() = "2026-11-04"
+ }
+
+ interface Entity {
+ val id: String
+ }
+
+ interface Outcome
+
+ class Item(override val id: String, val name: String?) : Entity, Outcome
+
+ class Other(val id: String) : Outcome
+
+ enum class Color { RED, GREEN }
+
+ class Filter(val id: String?, val limit: Int?)
+}
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 00000000..75ea21c4
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,13 @@
+
+
+
+ %d{HH:mm:ss.SSS} [%thread] %level %logger -%kvp- %msg%n
+
+
+
+
+
+
+
+
+