Support for default TypeResolver
See gh-154
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql.execution;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
|
||||
import graphql.TypeResolutionEnvironment;
|
||||
import graphql.schema.GraphQLObjectType;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.TypeResolver;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
* {@link TypeResolver} that tries to find a GraphQL Object type based on the
|
||||
* class name of an Object. If necessary, it walks up the base class and
|
||||
* interface hierarchy to find a match.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ClassNameTypeResolver implements TypeResolver {
|
||||
|
||||
private Function<Class<?>, String> classNameExtractor = Class::getSimpleName;
|
||||
|
||||
private final Map<Class<?>, String> mappings = new LinkedHashMap<>();
|
||||
|
||||
|
||||
/**
|
||||
* Customize how the name of a class, or base class/interface, is determined.
|
||||
* An application can use this to adapt to a common naming convention, e.g.
|
||||
* removing "Impl" as a suffix, or "Base" as prefix, and so on.
|
||||
* <p>By default, this is {@link Class#getSimpleName()}.
|
||||
* @param classNameExtractor the function to use
|
||||
*/
|
||||
public void setClassNameExtractor(Function<Class<?>, String> classNameExtractor) {
|
||||
Assert.notNull(classNameExtractor, "'classNameExtractor' is required");
|
||||
this.classNameExtractor = classNameExtractor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a mapping from a Java {@link Class} to a GraphQL Object type. The
|
||||
* given class can be a base class or an interface, in which case the mapping
|
||||
* applies to sub-classes too.
|
||||
* @param clazz the Java class to map
|
||||
* @param graphQlTypeName the matching GraphQL object type
|
||||
*/
|
||||
public void addMapping(Class<?> clazz, String graphQlTypeName) {
|
||||
this.mappings.put(clazz, graphQlTypeName);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public GraphQLObjectType getType(TypeResolutionEnvironment environment) {
|
||||
Class<?> clazz = environment.getObject().getClass();
|
||||
GraphQLObjectType type = getTypeForClass(clazz, environment.getSchema());
|
||||
Assert.state(type != null, "No GraphQL Object type for class: " + clazz.getName());
|
||||
return type;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private GraphQLObjectType getTypeForClass(Class<?> clazz, GraphQLSchema schema) {
|
||||
if (clazz.getName().startsWith("java.")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String name = getMapping(clazz);
|
||||
if (name != null) {
|
||||
GraphQLObjectType objectType = schema.getObjectType(name);
|
||||
if (objectType == null) {
|
||||
throw new IllegalStateException(
|
||||
"Invalid mapping for " + clazz.getName() + ". " +
|
||||
"No GraphQL Object type with name '" + name + "'.");
|
||||
}
|
||||
return objectType;
|
||||
}
|
||||
|
||||
name = this.classNameExtractor.apply(clazz);
|
||||
if (schema.containsType(name)) {
|
||||
return schema.getObjectType(name);
|
||||
}
|
||||
|
||||
for (Class<?> interfaceType : clazz.getInterfaces()) {
|
||||
GraphQLObjectType objectType = getTypeForClass(interfaceType, schema);
|
||||
if (objectType != null) {
|
||||
return objectType;
|
||||
}
|
||||
}
|
||||
|
||||
Class<?> superclass = clazz.getSuperclass();
|
||||
if (superclass != Object.class && superclass != null) {
|
||||
return getTypeForClass(superclass, schema);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private String getMapping(Class<?> targetClass) {
|
||||
for (Map.Entry<Class<?>, String> entry : this.mappings.entrySet()) {
|
||||
if (entry.getKey().isAssignableFrom(targetClass)) {
|
||||
return entry.getValue();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,10 +29,13 @@ import java.util.function.Consumer;
|
||||
import graphql.GraphQL;
|
||||
import graphql.execution.instrumentation.ChainedInstrumentation;
|
||||
import graphql.execution.instrumentation.Instrumentation;
|
||||
import graphql.language.InterfaceTypeDefinition;
|
||||
import graphql.language.UnionTypeDefinition;
|
||||
import graphql.schema.GraphQLCodeRegistry;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.GraphQLTypeVisitor;
|
||||
import graphql.schema.SchemaTraverser;
|
||||
import graphql.schema.TypeResolver;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.SchemaGenerator;
|
||||
import graphql.schema.idl.SchemaParser;
|
||||
@@ -55,6 +58,9 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
|
||||
private final List<RuntimeWiringConfigurer> runtimeWiringConfigurers = new ArrayList<>();
|
||||
|
||||
@Nullable
|
||||
private TypeResolver defaultTypeResolver;
|
||||
|
||||
private final List<DataFetcherExceptionResolver> exceptionResolvers = new ArrayList<>();
|
||||
|
||||
private final List<GraphQLTypeVisitor> typeVisitors = new ArrayList<>();
|
||||
@@ -80,6 +86,12 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlSource.Builder defaultTypeResolver(TypeResolver typeResolver) {
|
||||
this.defaultTypeResolver = typeResolver;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlSource.Builder exceptionResolvers(List<DataFetcherExceptionResolver> resolvers) {
|
||||
this.exceptionResolvers.addAll(resolvers);
|
||||
@@ -122,9 +134,12 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
this.runtimeWiringConfigurers.forEach(configurer -> configurer.configure(runtimeWiringBuilder));
|
||||
RuntimeWiring runtimeWiring = runtimeWiringBuilder.build();
|
||||
|
||||
registerDefaultTypeResolver(registry, runtimeWiring);
|
||||
|
||||
GraphQLSchema schema = (this.schemaFactory != null ?
|
||||
this.schemaFactory.apply(registry, runtimeWiring) :
|
||||
new SchemaGenerator().makeExecutableSchema(registry, runtimeWiring));
|
||||
|
||||
schema = applyTypeVisitors(schema);
|
||||
|
||||
GraphQL.Builder builder = GraphQL.newGraphQL(schema);
|
||||
@@ -139,6 +154,14 @@ class DefaultGraphQlSourceBuilder implements GraphQlSource.Builder {
|
||||
return new CachedGraphQlSource(graphQl, schema);
|
||||
}
|
||||
|
||||
private void registerDefaultTypeResolver(TypeDefinitionRegistry registry, RuntimeWiring runtimeWiring) {
|
||||
TypeResolver typeResolver =
|
||||
(this.defaultTypeResolver != null ? this.defaultTypeResolver : new ClassNameTypeResolver());
|
||||
registry.types().values().stream()
|
||||
.filter(def -> def instanceof UnionTypeDefinition || def instanceof InterfaceTypeDefinition)
|
||||
.forEach(def -> runtimeWiring.getTypeResolvers().putIfAbsent(def.getName(), typeResolver));
|
||||
}
|
||||
|
||||
private TypeDefinitionRegistry parseSchemaResource(Resource schemaResource) {
|
||||
Assert.notNull(schemaResource, "'schemaResource' not provided");
|
||||
Assert.isTrue(schemaResource.exists(), "'schemaResource' does not exist");
|
||||
|
||||
@@ -25,6 +25,7 @@ import graphql.GraphQL;
|
||||
import graphql.execution.instrumentation.Instrumentation;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.GraphQLTypeVisitor;
|
||||
import graphql.schema.TypeResolver;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.TypeDefinitionRegistry;
|
||||
|
||||
@@ -89,6 +90,25 @@ public interface GraphQlSource {
|
||||
*/
|
||||
Builder configureRuntimeWiring(RuntimeWiringConfigurer configurer);
|
||||
|
||||
/**
|
||||
* Configure the default {@link TypeResolver} to use for GraphQL Interface
|
||||
* and Union types that don't already have such a registration after all
|
||||
* {@link #configureRuntimeWiring(RuntimeWiringConfigurer) RuntimeWiringConfigurer's}
|
||||
* have been applied.
|
||||
* <p>A GraphQL {@code TypeResolver} is used to determine the GraphQL Object
|
||||
* type of values returned from DataFetcher's of GraphQL Interface or
|
||||
* Union fields.
|
||||
* <p>By default this is set to {@link ClassNameTypeResolver}, which
|
||||
* tries to match the simple class name of the Object value to a GraphQL
|
||||
* Object type, and it also tries the same for supertypes (base classes
|
||||
* and interfaces). See the Javadoc of {@code ClassNameTypeResolver} for
|
||||
* further ways to customize matching a Java class to a GraphQL Object type.
|
||||
* @param typeResolver the {@code TypeResolver} to use
|
||||
* @return the current builder
|
||||
* @see ClassNameTypeResolver
|
||||
*/
|
||||
Builder defaultTypeResolver(TypeResolver typeResolver);
|
||||
|
||||
/**
|
||||
* Add {@link DataFetcherExceptionResolver}'s to use for resolving exceptions from
|
||||
* {@link graphql.schema.DataFetcher}'s.
|
||||
|
||||
@@ -0,0 +1,279 @@
|
||||
/*
|
||||
* Copyright 2002-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql.execution;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import graphql.ExecutionResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.graphql.GraphQlTestUtils;
|
||||
import org.springframework.graphql.RequestInput;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Tests for interface and union type resolution via {@link ClassNameTypeResolver}.
|
||||
* @author Rossen Stoyanchev
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public class ClassNameTypeResolverTests {
|
||||
|
||||
private static final List<Animal> animalList = Arrays.asList(new Dog(), new Penguin());
|
||||
|
||||
private static final List<?> animalAndPlantList = Arrays.asList(new GrayWolf(), new GiantRedwood());
|
||||
|
||||
private static final String schema = "" +
|
||||
"type Query {" +
|
||||
" animals: [Animal!]!," +
|
||||
" sightings: [Sighting!]!" +
|
||||
"}" +
|
||||
"interface Animal {" +
|
||||
" name: String!" +
|
||||
"}" +
|
||||
"type Bird implements Animal {" +
|
||||
" name: String!" +
|
||||
" flightless: Boolean!" +
|
||||
"}" +
|
||||
"type Mammal implements Animal {" +
|
||||
" name: String!" +
|
||||
" herbivore: Boolean!" +
|
||||
"}" +
|
||||
"type Plant {" +
|
||||
" family: String!" +
|
||||
"}" +
|
||||
"type Vegetable {" +
|
||||
" family: String!" +
|
||||
"}" +
|
||||
"union Sighting = Bird | Mammal | Plant | Vegetable ";
|
||||
|
||||
|
||||
@Test
|
||||
void typeResolutionViaSuperHierarchy() {
|
||||
|
||||
GraphQlSource graphQlSource =
|
||||
GraphQlTestUtils.initGraphQlSource(schema, "Query", "animals", env -> animalList).build();
|
||||
|
||||
String query = "" +
|
||||
"query Animals {" +
|
||||
" animals {" +
|
||||
" __typename" +
|
||||
" name" +
|
||||
" ... on Bird {" +
|
||||
" flightless" +
|
||||
" }" +
|
||||
" ... on Mammal {" +
|
||||
" herbivore" +
|
||||
" }" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
ExecutionResult result = new ExecutionGraphQlService(graphQlSource)
|
||||
.execute(new RequestInput(query, null, null))
|
||||
.block();
|
||||
|
||||
List<Map<String, Object>> actualAnimals = GraphQlTestUtils.checkErrorsAndGetData(result, "animals");
|
||||
|
||||
for (int i = 0; i < animalList.size(); i++) {
|
||||
Map<String, Object> actualAnimal = actualAnimals.get(i);
|
||||
Animal animal = animalList.get(i);
|
||||
assertThat(actualAnimal.get("name")).isEqualTo(animal.getName());
|
||||
|
||||
if (animal instanceof Bird) {
|
||||
assertThat(actualAnimal.get("flightless")).isEqualTo(((Bird) animal).isFlightless());
|
||||
}
|
||||
else if (animal instanceof Mammal) {
|
||||
assertThat(actualAnimal.get("herbivore")).isEqualTo(((Mammal) animal).isHerbivore());
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void typeResolutionViaMapping() {
|
||||
|
||||
ClassNameTypeResolver typeResolver = new ClassNameTypeResolver();
|
||||
typeResolver.addMapping(Tree.class, "Plant");
|
||||
|
||||
GraphQlSource graphQlSource =
|
||||
GraphQlTestUtils.initGraphQlSource(schema, "Query", "sightings", env -> animalAndPlantList)
|
||||
.defaultTypeResolver(typeResolver)
|
||||
.build();
|
||||
|
||||
String query = "" +
|
||||
"query Sightings {" +
|
||||
" sightings {" +
|
||||
" __typename" +
|
||||
" ... on Bird {" +
|
||||
" name" +
|
||||
" }" +
|
||||
" ... on Mammal {" +
|
||||
" name" +
|
||||
" }" +
|
||||
" ... on Plant {" +
|
||||
" family" +
|
||||
" }" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
ExecutionResult result = new ExecutionGraphQlService(graphQlSource)
|
||||
.execute(new RequestInput(query, null, null))
|
||||
.block();
|
||||
|
||||
List<Map<String, Object>> actualSightings = GraphQlTestUtils.checkErrorsAndGetData(result, "sightings");
|
||||
|
||||
for (int i = 0; i < animalAndPlantList.size(); i++) {
|
||||
Map<String, Object> actualSighting = actualSightings.get(i);
|
||||
Object sighting = animalAndPlantList.get(i);
|
||||
|
||||
if (sighting instanceof Animal) {
|
||||
assertThat(actualSighting.get("name")).isEqualTo(((Animal) sighting).getName());
|
||||
}
|
||||
else if (sighting instanceof Tree) {
|
||||
assertThat(actualSighting.get("family")).isEqualTo(((Tree) sighting).getFamily());
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
interface Animal {
|
||||
|
||||
String getName();
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface Bird extends Animal {
|
||||
|
||||
boolean isFlightless();
|
||||
|
||||
}
|
||||
|
||||
|
||||
interface Mammal extends Animal {
|
||||
|
||||
boolean isHerbivore();
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class BaseAnimal implements Animal {
|
||||
|
||||
final String name;
|
||||
|
||||
BaseAnimal(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class BaseBird extends BaseAnimal implements Bird {
|
||||
|
||||
private final boolean flightless;
|
||||
|
||||
BaseBird(String name, boolean flightless) {
|
||||
super(name);
|
||||
this.flightless = flightless;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isFlightless() {
|
||||
return this.flightless;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class BaseMammal extends BaseAnimal implements Mammal {
|
||||
|
||||
private final boolean isHerbivore;
|
||||
|
||||
BaseMammal(String name, boolean isHerbivore) {
|
||||
super(name);
|
||||
this.isHerbivore = isHerbivore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isHerbivore() {
|
||||
return this.isHerbivore;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class Penguin extends BaseBird {
|
||||
|
||||
Penguin() {
|
||||
super("Penguin", true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class Dog extends BaseMammal {
|
||||
|
||||
Dog() {
|
||||
super("Dog", false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class GrayWolf extends BaseMammal {
|
||||
|
||||
GrayWolf() {
|
||||
super("Gray Wolf", false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
static class Tree {
|
||||
|
||||
private final String family;
|
||||
|
||||
Tree(String family) {
|
||||
this.family = family;
|
||||
}
|
||||
|
||||
public String getFamily() {
|
||||
return family;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static class GiantRedwood extends Tree {
|
||||
|
||||
GiantRedwood() {
|
||||
super("Redwood");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user