Skip to content

Add support for directives #204

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

Merged
merged 11 commits into from
Nov 17, 2018
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
14 changes: 7 additions & 7 deletions example/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.4.RELEASE</version>
<version>2.0.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<graphql-spring-boot-starter.version>5.0.2</graphql-spring-boot-starter.version>
<graphql-spring-boot-starter.version>5.0.6</graphql-spring-boot-starter.version>
</properties>

<dependencies>
Expand All @@ -46,24 +46,24 @@
</dependency>

<dependency>
<groupId>com.graphql-java</groupId>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-spring-boot-starter</artifactId>
<version>${graphql-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphiql-spring-boot-starter</artifactId>
<version>${graphql-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>voyager-spring-boot-starter</artifactId>
<version>${graphql-spring-boot-starter.version}</version>
</dependency>
<dependency>
<groupId>com.graphql-java</groupId>
<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.2.3</version>
<version>5.3.5</version>
</dependency>
</dependencies>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.coxautodev.graphql.tools.example.resolvers;

import com.coxautodev.graphql.tools.GraphQLMutationResolver;
import com.coxautodev.graphql.tools.example.types.Human;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.UUID;

@Component
public class Mutation implements GraphQLMutationResolver {

public Human createHuman(Map<String,String> createHumanInput) {
String name = null;
if (createHumanInput.containsKey("name")) {
name = createHumanInput.get("name");
}
String homePlanet = "Jakku";
if (createHumanInput.containsKey("homePlanet")) {
homePlanet = createHumanInput.get("homePlanet");
}
return new Human(UUID.randomUUID().toString(), name, null, homePlanet);
}

}
14 changes: 13 additions & 1 deletion example/src/main/resources/swapi.graphqls
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,16 @@ type Droid implements Character {
appearsIn: [Episode]
# The primary function of the droid
primaryFunction: String
}
}

type Mutation {
# Creates a new human character
createHuman(input: CreateHumanInput!): Human
}

input CreateHumanInput {
# The name of the human
name: String
# The home planet of the human, or null if unknown
homePlanet: String
}
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

<groupId>com.graphql-java-kickstart</groupId>
<artifactId>graphql-java-tools</artifactId>
<version>5.3.6-SNAPSHOT</version>
<version>5.4.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>GraphQL Java Tools</name>
Expand Down
43 changes: 31 additions & 12 deletions src/main/kotlin/com/coxautodev/graphql/tools/SchemaClassScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import graphql.language.FieldDefinition
import graphql.language.InputObjectTypeDefinition
import graphql.language.InputValueDefinition
import graphql.language.InterfaceTypeDefinition
import graphql.language.ListType
import graphql.language.NonNullType
import graphql.language.ObjectTypeDefinition
import graphql.language.ObjectTypeExtensionDefinition
import graphql.language.ScalarTypeDefinition
Expand All @@ -18,7 +20,6 @@ import graphql.language.UnionTypeDefinition
import graphql.schema.GraphQLScalarType
import graphql.schema.idl.ScalarInfo
import org.slf4j.LoggerFactory
import java.lang.reflect.Field
import java.lang.reflect.Method

/**
Expand Down Expand Up @@ -134,7 +135,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
val dictionary = try {
Maps.unmodifiableBiMap(HashBiMap.create<TypeDefinition<*>, JavaType>().also {
dictionary.filter {
it.value.javaType != null
it.value.javaType != null
&& it.value.typeClass() != java.lang.Object::class.java
&& !java.util.Map::class.java.isAssignableFrom(it.value.typeClass())
&& it.key !is InputObjectTypeDefinition
Expand Down Expand Up @@ -216,9 +217,9 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
}
}

private fun getResolverInfoFromTypeDictionary(typeName: String) : ResolverInfo? {
private fun getResolverInfoFromTypeDictionary(typeName: String): ResolverInfo? {
val dictionaryType = initialDictionary[typeName]?.get()
return if(dictionaryType != null) {
return if (dictionaryType != null) {
resolverInfosByDataClass[dictionaryType] ?: DataClassResolverInfo(dictionaryType);
} else {
null
Expand All @@ -230,33 +231,51 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
*/
private fun scanQueueItemForPotentialMatches(item: QueueItem) {
val resolverInfoList = this.resolverInfos.filter { it.dataClassType == item.clazz }
val resolverInfo: ResolverInfo? = if (resolverInfoList.size > 1) {
val resolverInfo: ResolverInfo = (if (resolverInfoList.size > 1) {
MultiResolverInfo(resolverInfoList)
} else {
if(item.clazz.equals(Object::class.java)) {
if (item.clazz.equals(Object::class.java)) {
getResolverInfoFromTypeDictionary(item.type.name)
} else {
resolverInfosByDataClass[item.clazz] ?: DataClassResolverInfo(item.clazz)
}
}
if(resolverInfo == null) {
throw throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")
}
}) ?: throw throw SchemaClassScannerError("The GraphQL schema type '${item.type.name}' maps to a field of type java.lang.Object however there is no matching entry for this type in the type dictionary. You may need to add this type to the dictionary before building the schema.")

scanResolverInfoForPotentialMatches(item.type, resolverInfo)
}

private fun scanResolverInfoForPotentialMatches(type: ObjectTypeDefinition, resolverInfo: ResolverInfo) {
type.getExtendedFieldDefinitions(extensionDefinitions).forEach { field ->
// val searchField = applyDirective(field)
val fieldResolver = fieldResolverScanner.findFieldResolver(field, resolverInfo)

fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver

fieldResolver.scanForMatches().forEach { potentialMatch ->
handleFoundType(typeClassMatcher.match(potentialMatch))
// if (potentialMatch.graphQLType is TypeName && !definitionsByName.containsKey((potentialMatch.graphQLType.name))) {
// val typeDefinition = ObjectTypeDefinition.newObjectTypeDefinition()
// .name(potentialMatch.graphQLType.name)
// .build()
// handleFoundType(TypeClassMatcher.ValidMatch(typeDefinition, typeClassMatcher.toRealType(potentialMatch), potentialMatch.reference))
// } else {
handleFoundType(typeClassMatcher.match(potentialMatch))
// }
}
}
}

// private fun applyDirective(field: FieldDefinition): FieldDefinition {
// val connectionDirectives = field.directives.filter { it.name == "connection" }
// if (connectionDirectives.isNotEmpty()) {
// val directive = connectionDirectives.first()
// val originalType:TypeName = field.type as TypeName
// val wrappedField = field.deepCopy()
// wrappedField.type = TypeName(originalType.name + "Connection")
// return wrappedField
// }
// return field
// }

private fun handleFoundType(match: TypeClassMatcher.Match) {
when (match) {
is TypeClassMatcher.ScalarMatch -> {
Expand Down Expand Up @@ -287,7 +306,7 @@ internal class SchemaClassScanner(initialDictionary: BiMap<String, Class<*>>, al
if (options.preferGraphQLResolver && realEntry.hasResolverRef()) {
log.warn("The real entry ${realEntry.joinReferences()} is a GraphQLResolver so ignoring this one ${javaType.unwrap()} $reference")
} else {
if(java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) {
if (java.util.Map::class.java.isAssignableFrom(javaType.unwrap())) {
throw SchemaClassScannerError("Two different property map classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType}:\n| ${reference.getDescription()}")
}
throw SchemaClassScannerError("Two different classes used for type ${type.name}:\n${realEntry.joinReferences()}\n\n- ${javaType.unwrap()}:\n| ${reference.getDescription()}")
Expand Down
55 changes: 49 additions & 6 deletions src/main/kotlin/com/coxautodev/graphql/tools/SchemaParser.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.coxautodev.graphql.tools

import graphql.introspection.Introspection
import graphql.language.AbstractNode
import graphql.language.ArrayValue
import graphql.language.BooleanValue
Expand All @@ -22,6 +23,7 @@ import graphql.language.TypeDefinition
import graphql.language.TypeName
import graphql.language.UnionTypeDefinition
import graphql.language.Value
import graphql.schema.GraphQLDirective
import graphql.schema.GraphQLEnumType
import graphql.schema.GraphQLFieldDefinition
import graphql.schema.GraphQLInputObjectType
Expand All @@ -36,15 +38,20 @@ import graphql.schema.GraphQLType
import graphql.schema.GraphQLTypeReference
import graphql.schema.GraphQLUnionType
import graphql.schema.TypeResolverProxy
import graphql.schema.idl.DirectiveBehavior
import graphql.schema.idl.RuntimeWiring
import graphql.schema.idl.ScalarInfo
import graphql.schema.idl.SchemaGeneratorHelper
import java.util.ArrayList
import java.util.HashSet
import kotlin.reflect.KClass

/**
* Parses a GraphQL Schema and maps object fields to provided class methods.
*
* @author Andrew Potter
*/
class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, private val options: SchemaParserOptions) {
class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, private val options: SchemaParserOptions, private val runtimeWiring: RuntimeWiring) {

companion object {
const val DEFAULT_DEPRECATION_MESSAGE = "No longer supported"
Expand Down Expand Up @@ -79,6 +86,9 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
private val permittedTypesForInputObject: Set<String> =
(inputObjectDefinitions.map { it.name } + enumDefinitions.map { it.name }).toSet()

private val schemaGeneratorHelper = SchemaGeneratorHelper()
private val directiveGenerator = DirectiveBehavior()

/**
* Parses the given schema with respect to the given dictionary and returns GraphQL objects.
*/
Expand Down Expand Up @@ -124,19 +134,40 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
.definition(definition)
.description(getDocumentation(definition))

builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.OBJECT))

definition.implements.forEach { implementsDefinition ->
val interfaceName = (implementsDefinition as TypeName).name
builder.withInterface(interfaces.find { it.name == interfaceName } ?: throw SchemaError("Expected interface type with name '$interfaceName' but found none!"))
}

definition.getExtendedFieldDefinitions(extensionDefinitions).forEach { fieldDefinition ->
fieldDefinition.description
builder.field { field ->
createField(field, fieldDefinition)
field.dataFetcher(fieldResolversByType[definition]?.get(fieldDefinition)?.createDataFetcher() ?: throw SchemaError("No resolver method found for object type '${definition.name}' and field '${fieldDefinition.name}', this is most likely a bug with graphql-java-tools"))

val wiredField = directiveGenerator.onField(field.build(), DirectiveBehavior.Params(runtimeWiring))
GraphQLFieldDefinition.Builder(wiredField)
}
}

return builder.build()
val objectType = builder.build()

return directiveGenerator.onObject(objectType, DirectiveBehavior.Params(runtimeWiring))
}

private fun buildDirectives(directives: List<Directive>, directiveDefinitions: Set<GraphQLDirective>, directiveLocation: Introspection.DirectiveLocation): Array<GraphQLDirective> {
val names = HashSet<String>()

val output = ArrayList<GraphQLDirective>()
for (directive in directives) {
if (!names.contains(directive.name)) {
names.add(directive.name)
output.add(schemaGeneratorHelper.buildDirective(directive, directiveDefinitions, directiveLocation))
}
}
return output.toTypedArray()
}

private fun createInputObject(definition: InputObjectTypeDefinition): GraphQLInputObjectType {
Expand All @@ -145,17 +176,20 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
.definition(definition)
.description(getDocumentation(definition))

builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.INPUT_OBJECT))

definition.inputValueDefinitions.forEach { inputDefinition ->
builder.field { field ->
field.name(inputDefinition.name)
field.definition(inputDefinition)
field.description(getDocumentation(inputDefinition))
field.defaultValue(inputDefinition.defaultValue)
field.type(determineInputType(inputDefinition.type))
field.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.INPUT_FIELD_DEFINITION))
}
}

return builder.build()
return directiveGenerator.onInputObject(builder.build(), DirectiveBehavior.Params(runtimeWiring))
}

private fun createEnumObject(definition: EnumTypeDefinition): GraphQLEnumType {
Expand All @@ -168,6 +202,8 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
.definition(definition)
.description(getDocumentation(definition))

builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.ENUM))

definition.enumValueDefinitions.forEach { enumDefinition ->
val enumName = enumDefinition.name
val enumValue = type.unwrap().enumConstants.find { (it as Enum<*>).name == enumName } ?: throw SchemaError("Expected value for name '$enumName' in enum '${type.unwrap().simpleName}' but found none!")
Expand All @@ -179,7 +215,7 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
}
}

return builder.build()
return directiveGenerator.onEnum(builder.build(), DirectiveBehavior.Params(runtimeWiring))
}

private fun createInterfaceObject(definition: InterfaceTypeDefinition): GraphQLInterfaceType {
Expand All @@ -190,11 +226,13 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
.description(getDocumentation(definition))
.typeResolver(TypeResolverProxy())

builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.INTERFACE))

definition.fieldDefinitions.forEach { fieldDefinition ->
builder.field { field -> createField(field, fieldDefinition) }
}

return builder.build()
return directiveGenerator.onInterface(builder.build(), DirectiveBehavior.Params(runtimeWiring))
}

private fun createUnionObject(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): GraphQLUnionType {
Expand All @@ -205,8 +243,10 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
.description(getDocumentation(definition))
.typeResolver(TypeResolverProxy())

builder.withDirectives(*buildDirectives(definition.directives, setOf(), Introspection.DirectiveLocation.UNION))

getLeafUnionObjects(definition, types).forEach { builder.possibleType(it) }
return builder.build()
return directiveGenerator.onUnion(builder.build(), DirectiveBehavior.Params(runtimeWiring))
}

private fun getLeafUnionObjects(definition: UnionTypeDefinition, types: List<GraphQLObjectType>): List<GraphQLObjectType> {
Expand Down Expand Up @@ -241,8 +281,11 @@ class SchemaParser internal constructor(scanResult: ScannedSchemaObjects, privat
argument.description(getDocumentation(argumentDefinition))
argument.defaultValue(buildDefaultValue(argumentDefinition.defaultValue))
argument.type(determineInputType(argumentDefinition.type))
argument.withDirectives(*buildDirectives(argumentDefinition.directives, setOf(), Introspection.DirectiveLocation.ARGUMENT_DEFINITION))

}
}
field.withDirectives(*buildDirectives(fieldDefinition.directives, setOf(), Introspection.DirectiveLocation.FIELD_DEFINITION))
return field
}

Expand Down
Loading