SchemaMappingInspector support for unions and interfaces
See gh-924
This commit is contained in:
@@ -66,6 +66,13 @@ public class ClassNameTypeResolver implements TypeResolver {
|
||||
this.mappings.put(clazz, graphQlTypeName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the map with configured {@link #addMapping(Class, String) explicit mappings}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public Map<Class<?>, String> getMappings() {
|
||||
return this.mappings;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
|
||||
@@ -76,6 +76,8 @@ final class DefaultSchemaResourceGraphQlSourceBuilder
|
||||
@Nullable
|
||||
private Consumer<SchemaReport> schemaReportConsumer;
|
||||
|
||||
private Consumer<SchemaMappingInspector.Initializer> inspectorInitializerConsumer = initializer -> {};
|
||||
|
||||
@Nullable
|
||||
private Consumer<GraphQLSchema> schemaReportRunner;
|
||||
|
||||
@@ -110,6 +112,15 @@ final class DefaultSchemaResourceGraphQlSourceBuilder
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlSource.SchemaResourceBuilder inspectSchemaMappings(
|
||||
Consumer<SchemaMappingInspector.Initializer> initializerConsumer, Consumer<SchemaReport> reportConsumer) {
|
||||
|
||||
this.inspectorInitializerConsumer = initializerConsumer.andThen(initializerConsumer);
|
||||
this.schemaReportConsumer = reportConsumer;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultSchemaResourceGraphQlSourceBuilder schemaFactory(
|
||||
BiFunction<TypeDefinitionRegistry, RuntimeWiring, GraphQLSchema> schemaFactory) {
|
||||
@@ -152,7 +163,12 @@ final class DefaultSchemaResourceGraphQlSourceBuilder
|
||||
|
||||
if (this.schemaReportConsumer != null) {
|
||||
this.schemaReportRunner = schema -> {
|
||||
SchemaReport report = SchemaMappingInspector.inspect(schema, runtimeWiring);
|
||||
SchemaMappingInspector.Initializer initializer = SchemaMappingInspector.initializer();
|
||||
if (this.typeResolver instanceof ClassNameTypeResolver cntr) {
|
||||
initializer.classResolver(SchemaMappingInspector.ClassResolver.fromClassNameTypeResolver(cntr));
|
||||
}
|
||||
this.inspectorInitializerConsumer.accept(initializer);
|
||||
SchemaReport report = initializer.inspect(schema, runtimeWiring.getDataFetchers());
|
||||
this.schemaReportConsumer.accept(report);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -210,6 +210,20 @@ public interface GraphQlSource {
|
||||
*/
|
||||
SchemaResourceBuilder inspectSchemaMappings(Consumer<SchemaReport> reportConsumer);
|
||||
|
||||
/**
|
||||
* Variant of {@link #inspectSchemaMappings(Consumer)} with the option to
|
||||
* initialize the {@link SchemaMappingInspector}, e.g. in order to assist
|
||||
* with finding Java representations of GraphQL union member types and
|
||||
* interface implementation types.
|
||||
* @param initializerConsumer callback to initialize the {@code SchemaMappingInspector}
|
||||
* @param reportConsumer a hook to inspect the report
|
||||
* @return the current builder
|
||||
* @since 1.3.0
|
||||
*/
|
||||
SchemaResourceBuilder inspectSchemaMappings(
|
||||
Consumer<SchemaMappingInspector.Initializer> initializerConsumer,
|
||||
Consumer<SchemaReport> reportConsumer);
|
||||
|
||||
/**
|
||||
* Configure a function to create the {@link GraphQLSchema} from the
|
||||
* given {@link TypeDefinitionRegistry} and {@link RuntimeWiring}.
|
||||
@@ -219,7 +233,8 @@ public interface GraphQlSource {
|
||||
* @param schemaFactory the function to create the schema
|
||||
* @return the current builder
|
||||
*/
|
||||
SchemaResourceBuilder schemaFactory(BiFunction<TypeDefinitionRegistry, RuntimeWiring, GraphQLSchema> schemaFactory);
|
||||
SchemaResourceBuilder schemaFactory(
|
||||
BiFunction<TypeDefinitionRegistry, RuntimeWiring, GraphQLSchema> schemaFactory);
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,15 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Predicate;
|
||||
|
||||
import graphql.schema.DataFetcher;
|
||||
import graphql.schema.FieldCoordinates;
|
||||
import graphql.schema.GraphQLEnumType;
|
||||
import graphql.schema.GraphQLFieldDefinition;
|
||||
import graphql.schema.GraphQLFieldsContainer;
|
||||
import graphql.schema.GraphQLInterfaceType;
|
||||
import graphql.schema.GraphQLList;
|
||||
import graphql.schema.GraphQLNamedOutputType;
|
||||
import graphql.schema.GraphQLNamedType;
|
||||
@@ -38,6 +41,7 @@ import graphql.schema.GraphQLObjectType;
|
||||
import graphql.schema.GraphQLScalarType;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.GraphQLType;
|
||||
import graphql.schema.GraphQLUnionType;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -91,6 +95,8 @@ public class SchemaMappingInspector {
|
||||
|
||||
private final Map<String, Map<String, DataFetcher>> dataFetchers;
|
||||
|
||||
private final InterfaceUnionLookup interfaceUnionLookup;
|
||||
|
||||
private final Set<String> inspectedTypes = new HashSet<>();
|
||||
|
||||
private final ReportBuilder reportBuilder = new ReportBuilder();
|
||||
@@ -99,11 +105,15 @@ public class SchemaMappingInspector {
|
||||
private SchemaReport report;
|
||||
|
||||
|
||||
private SchemaMappingInspector(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> dataFetchers) {
|
||||
private SchemaMappingInspector(
|
||||
GraphQLSchema schema, Map<String, Map<String, DataFetcher>> dataFetchers,
|
||||
List<ClassResolver> classResolvers, Function<GraphQLObjectType, String> classNameFunction) {
|
||||
|
||||
Assert.notNull(schema, "GraphQLSchema is required");
|
||||
Assert.notNull(dataFetchers, "DataFetcher map is required");
|
||||
this.schema = schema;
|
||||
this.dataFetchers = dataFetchers;
|
||||
this.interfaceUnionLookup = new InterfaceUnionLookup(schema, dataFetchers, classResolvers, classNameFunction);
|
||||
}
|
||||
|
||||
|
||||
@@ -180,23 +190,40 @@ public class SchemaMappingInspector {
|
||||
return;
|
||||
}
|
||||
|
||||
// Can we inspect GraphQL type?
|
||||
if (!(typePair.outputType() instanceof GraphQLFieldsContainer fieldContainer)) {
|
||||
if (isNotScalarOrEnumType(typePair.outputType())) {
|
||||
FieldCoordinates coordinates = FieldCoordinates.coordinates(parent.getName(), field.getName());
|
||||
addSkippedType(typePair.outputType(), coordinates, "Unsupported schema type");
|
||||
MultiValueMap<GraphQLType, ResolvableType> typePairs = new LinkedMultiValueMap<>();
|
||||
if (typePair.outputType() instanceof GraphQLUnionType unionType) {
|
||||
typePairs.putAll(this.interfaceUnionLookup.resolveUnion(unionType));
|
||||
}
|
||||
else if (typePair.outputType() instanceof GraphQLInterfaceType interfaceType) {
|
||||
typePairs.putAll(this.interfaceUnionLookup.resolveInterface(interfaceType));
|
||||
}
|
||||
|
||||
if (typePairs.isEmpty()) {
|
||||
typePairs.add(typePair.outputType(), typePair.resolvableType());
|
||||
}
|
||||
|
||||
for (Map.Entry<GraphQLType, List<ResolvableType>> entry : typePairs.entrySet()) {
|
||||
for (ResolvableType resolvableType : entry.getValue()) {
|
||||
|
||||
// Can we inspect GraphQL type?
|
||||
if (!(entry.getKey() instanceof GraphQLFieldsContainer fieldContainer)) {
|
||||
if (isNotScalarOrEnumType(entry.getKey())) {
|
||||
FieldCoordinates coordinates = FieldCoordinates.coordinates(parent.getName(), field.getName());
|
||||
addSkippedType(entry.getKey(), coordinates, "Unsupported schema type");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Can we inspect Java type?
|
||||
if (resolvableType.resolve(Object.class) == Object.class) {
|
||||
FieldCoordinates coordinates = FieldCoordinates.coordinates(parent.getName(), field.getName());
|
||||
addSkippedType(entry.getKey(), coordinates, "No Java type information");
|
||||
continue;
|
||||
}
|
||||
|
||||
checkFieldsContainer(fieldContainer, resolvableType);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Can we inspect Java type?
|
||||
if (typePair.resolvableType().resolve(Object.class) == Object.class) {
|
||||
FieldCoordinates coordinates = FieldCoordinates.coordinates(parent.getName(), field.getName());
|
||||
addSkippedType(typePair.outputType(), coordinates, "No Java type information");
|
||||
return;
|
||||
}
|
||||
|
||||
checkFieldsContainer(fieldContainer, typePair.resolvableType());
|
||||
}
|
||||
|
||||
private void checkFieldArguments(GraphQLFieldDefinition field, SelfDescribingDataFetcher<?> dataFetcher) {
|
||||
@@ -269,8 +296,284 @@ public class SchemaMappingInspector {
|
||||
* {@code DataFetcher} registrations.
|
||||
* @since 1.2.5
|
||||
*/
|
||||
public static SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> dataFetchers) {
|
||||
return new SchemaMappingInspector(schema, dataFetchers).getOrCreateReport();
|
||||
public static SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers) {
|
||||
return initializer().inspect(schema, fetchers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an initializer to configure the {@link SchemaMappingInspector}
|
||||
* and perform the inspection.
|
||||
* @since 1.3
|
||||
*/
|
||||
public static Initializer initializer() {
|
||||
return new DefaultInitializer();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Helps to configure {@link SchemaMappingInspector}.
|
||||
* @since 1.3
|
||||
*/
|
||||
public interface Initializer {
|
||||
|
||||
/**
|
||||
* Provide a function to derive the simple class name that corresponds to a
|
||||
* GraphQL union member type, or a GraphQL interface implementation type.
|
||||
* This is then used to find a Java class in the same package as that of
|
||||
* the return type of the controller method for the interface or union.
|
||||
* <p>The default, {@link GraphQLObjectType#getName()} is used
|
||||
* @param function the function to use
|
||||
* @return the same initializer instance
|
||||
*/
|
||||
Initializer classNameFunction(Function<GraphQLObjectType, String> function);
|
||||
|
||||
/**
|
||||
* Add a custom {@link ClassResolver} to use to find the Java class for a
|
||||
* GraphQL union member type, or a GraphQL interface implementation type.
|
||||
* @param resolver the resolver to add
|
||||
* @return the same initializer instance
|
||||
*/
|
||||
Initializer classResolver(ClassResolver resolver);
|
||||
|
||||
/**
|
||||
* Perform the inspection and return a report.
|
||||
* @param schema the schema to inspect
|
||||
* @param fetchers the registered data fetchers
|
||||
* @return the produced report
|
||||
*/
|
||||
SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers);
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Strategy to resolve the Java class(es) for a {@code GraphQLObjectType}, effectively
|
||||
* the reverse of {@link graphql.schema.TypeResolver}, for schema inspection purposes.
|
||||
*/
|
||||
public interface ClassResolver {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param objectType the {@code GraphQLObjectType} to resolve
|
||||
* @param interfaceOrUnionType either an interface the object implements,
|
||||
* or a union the object is a member of
|
||||
*/
|
||||
List<Class<?>> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnionType);
|
||||
|
||||
|
||||
/**
|
||||
* Create a resolver by re-using the explicit, reverse mappings of
|
||||
* {@link ClassNameTypeResolver}.
|
||||
*/
|
||||
static ClassResolver fromClassNameTypeResolver(ClassNameTypeResolver resolver) {
|
||||
MappingClassResolver mappingResolver = new MappingClassResolver();
|
||||
resolver.getMappings().forEach((key, value) -> mappingResolver.addMapping(value, key));
|
||||
return mappingResolver;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Default implementation of {@link Initializer}.
|
||||
*/
|
||||
private static class DefaultInitializer implements Initializer {
|
||||
|
||||
private Function<GraphQLObjectType, String> classNameFunction = GraphQLObjectType::getName;
|
||||
|
||||
private final List<ClassResolver> classResolvers = new ArrayList<>();
|
||||
|
||||
public DefaultInitializer() {
|
||||
this.classResolvers.add((objectType, interfaceOrUnionType) -> Collections.emptyList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Initializer classNameFunction(Function<GraphQLObjectType, String> function) {
|
||||
this.classNameFunction = function;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Initializer classResolver(ClassResolver resolver) {
|
||||
this.classResolvers.add(resolver);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers) {
|
||||
return new SchemaMappingInspector(
|
||||
schema, fetchers, this.classResolvers, this.classNameFunction).getOrCreateReport();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ClassResolver with explicit mappings.
|
||||
*/
|
||||
private static class MappingClassResolver implements ClassResolver {
|
||||
|
||||
private final MultiValueMap<String, Class<?>> map = new LinkedMultiValueMap<>();
|
||||
|
||||
public void addMapping(String typeName, Class<?> clazz) {
|
||||
this.map.add(typeName, clazz);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnionType) {
|
||||
return this.map.getOrDefault(objectType.getName(), Collections.emptyList());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* ClassResolver that uses a function to derive the simple class name from
|
||||
* the GraphQL object type, and then prepends a prefixes such as a package
|
||||
* name and/or an outer class name.
|
||||
*/
|
||||
private static class ReflectionClassResolver implements ClassResolver {
|
||||
|
||||
private final Function<GraphQLObjectType, String> classNameFunction;
|
||||
|
||||
private final MultiValueMap<String, String> classPrefixes = new LinkedMultiValueMap<>();
|
||||
|
||||
public ReflectionClassResolver(Function<GraphQLObjectType, String> classNameFunction) {
|
||||
this.classNameFunction = classNameFunction;
|
||||
}
|
||||
|
||||
public void addClassPrefix(String interfaceOrUnionTypeName, String classPrefix) {
|
||||
this.classPrefixes.add(interfaceOrUnionTypeName, classPrefix);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Class<?>> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnion) {
|
||||
String className = this.classNameFunction.apply(objectType);
|
||||
for (String prefix : classPrefixes.getOrDefault(interfaceOrUnion.getName(), Collections.emptyList())) {
|
||||
try {
|
||||
Class<?> clazz = Class.forName(prefix + className);
|
||||
return Collections.singletonList(clazz);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
// Ignore
|
||||
}
|
||||
}
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Provides methods to look up GraphQL Object and Java type pairs associated
|
||||
* with GraphQL interface and union types.
|
||||
*/
|
||||
private static class InterfaceUnionLookup {
|
||||
|
||||
private final static Predicate<String> PACKAGE_PREDICATE = name -> !name.startsWith("java.");
|
||||
|
||||
private static final LinkedMultiValueMap<GraphQLType, ResolvableType> EMPTY_MULTI_VALUE_MAP = new LinkedMultiValueMap<>(0);
|
||||
|
||||
/** Interface or union type name to implementing or member GraphQL-Java types pairs. */
|
||||
private final Map<String, MultiValueMap<GraphQLType, ResolvableType>> mappings = new LinkedHashMap<>();
|
||||
|
||||
InterfaceUnionLookup(
|
||||
GraphQLSchema schema, Map<String, Map<String, DataFetcher>> dataFetchers,
|
||||
List<ClassResolver> classResolvers, Function<GraphQLObjectType, String> classNameFunction) {
|
||||
|
||||
addReflectionClassResolver(schema, dataFetchers, classNameFunction, classResolvers);
|
||||
|
||||
for (GraphQLNamedType type : schema.getAllTypesAsList()) {
|
||||
if (type instanceof GraphQLUnionType union) {
|
||||
for (GraphQLNamedOutputType member : union.getTypes()) {
|
||||
addTypeMapping(union, (GraphQLObjectType) member, classResolvers);
|
||||
}
|
||||
}
|
||||
else if (type instanceof GraphQLObjectType objectType) {
|
||||
for (GraphQLNamedOutputType interfaceType : objectType.getInterfaces()) {
|
||||
addTypeMapping(interfaceType, objectType, classResolvers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addReflectionClassResolver(
|
||||
GraphQLSchema schema, Map<String, Map<String, DataFetcher>> dataFetchers,
|
||||
Function<GraphQLObjectType, String> classNameFunction, List<ClassResolver> classResolvers) {
|
||||
|
||||
ReflectionClassResolver resolver = new ReflectionClassResolver(classNameFunction);
|
||||
classResolvers.add(resolver);
|
||||
|
||||
for (Map.Entry<String, Map<String, DataFetcher>> typeEntry : dataFetchers.entrySet()) {
|
||||
String typeName = typeEntry.getKey();
|
||||
GraphQLType parentType = schema.getType(typeName);
|
||||
if (parentType == null) {
|
||||
continue; // Unmapped registration
|
||||
}
|
||||
for (Map.Entry<String, DataFetcher> fieldEntry : typeEntry.getValue().entrySet()) {
|
||||
FieldCoordinates coordinates = FieldCoordinates.coordinates(typeName, fieldEntry.getKey());
|
||||
GraphQLFieldDefinition field = schema.getFieldDefinition(coordinates);
|
||||
if (field == null) {
|
||||
continue; // Unmapped registration
|
||||
}
|
||||
TypePair pair = TypePair.resolveTypePair(parentType, field, fieldEntry.getValue(), schema);
|
||||
GraphQLType outputType = pair.outputType();
|
||||
if (outputType instanceof GraphQLUnionType || outputType instanceof GraphQLInterfaceType) {
|
||||
String outputTypeName = ((GraphQLNamedOutputType) outputType).getName();
|
||||
Class<?> clazz = pair.resolvableType().resolve(Object.class);
|
||||
if (PACKAGE_PREDICATE.test(clazz.getPackageName())) {
|
||||
int index = clazz.getName().indexOf(clazz.getSimpleName());
|
||||
resolver.addClassPrefix(outputTypeName, clazz.getName().substring(0, index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void addTypeMapping(
|
||||
GraphQLNamedOutputType interfaceOrUnionType, GraphQLObjectType objectType,
|
||||
List<ClassResolver> classResolvers) {
|
||||
|
||||
List<ResolvableType> resolvableTypes = new ArrayList<>();
|
||||
|
||||
for (ClassResolver resolver : classResolvers) {
|
||||
List<Class<?>> classes = resolver.resolveClass(objectType, interfaceOrUnionType);
|
||||
if (!classes.isEmpty()) {
|
||||
for (Class<?> clazz : classes) {
|
||||
ResolvableType resolvableType = ResolvableType.forClass(clazz);
|
||||
resolvableTypes.add(resolvableType);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (resolvableTypes.isEmpty()) {
|
||||
resolvableTypes.add(ResolvableType.NONE);
|
||||
}
|
||||
|
||||
for (ResolvableType resolvableType : resolvableTypes) {
|
||||
String name = interfaceOrUnionType.getName();
|
||||
this.mappings.computeIfAbsent(name, n -> new LinkedMultiValueMap<>()).add(objectType, resolvableType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the implementation GraphQL and Java type pairs for the interface.
|
||||
* @param interfaceType the interface type to resolve type pairs for
|
||||
* @return {@code MultiValueMap} with one or more pairs, possibly one
|
||||
* pair with {@link ResolvableType#NONE}.
|
||||
*/
|
||||
public MultiValueMap<GraphQLType, ResolvableType> resolveInterface(GraphQLInterfaceType interfaceType) {
|
||||
return this.mappings.getOrDefault(interfaceType.getName(), EMPTY_MULTI_VALUE_MAP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the member GraphQL and Java type pairs for the union.
|
||||
* @param unionType the union type to resolve type pairs for
|
||||
* @return {@code MultiValueMap} with one or more pairs, possibly one
|
||||
* pair with {@link ResolvableType#NONE}.
|
||||
*/
|
||||
public MultiValueMap<GraphQLType, ResolvableType> resolveUnion(GraphQLUnionType unionType) {
|
||||
return this.mappings.getOrDefault(unionType.getName(), EMPTY_MULTI_VALUE_MAP);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.execution.SchemaMappingInspector.ClassResolver;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
/**
|
||||
* Tests for {@link SchemaMappingInspector} with interface types.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class SchemaMappingInspectorInterfaceTests extends SchemaMappingInspectorTestSupport {
|
||||
|
||||
private final static String schema = """
|
||||
type Query {
|
||||
vehicles: [Vehicle!]!
|
||||
}
|
||||
interface Vehicle {
|
||||
name: String!
|
||||
price: Int!
|
||||
}
|
||||
type Car implements Vehicle {
|
||||
name: String!
|
||||
price: Int!
|
||||
engineType: String!
|
||||
}
|
||||
type Bike implements Vehicle {
|
||||
name: String!
|
||||
price: Int!
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
@Nested
|
||||
class InterfaceFieldsNotOnJavaInterface {
|
||||
|
||||
@Test
|
||||
void reportUnmappedFields() {
|
||||
SchemaReport report = inspectSchema(schema, VehicleController.class);
|
||||
assertThatReport(report)
|
||||
.hasSkippedTypeCount(0)
|
||||
.hasUnmappedFieldCount(3)
|
||||
.containsUnmappedFields("Car", "price", "engineType")
|
||||
.containsUnmappedFields("Bike", "price");
|
||||
}
|
||||
|
||||
interface Vehicle {
|
||||
String name();
|
||||
}
|
||||
record Car(String name) implements Vehicle {}
|
||||
record Bike(String name) implements Vehicle {}
|
||||
|
||||
@Controller
|
||||
static class VehicleController {
|
||||
|
||||
@QueryMapping
|
||||
List<Vehicle> vehicles() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class GraphQlAndJavaTypeNameMismatch {
|
||||
|
||||
@Test
|
||||
void useClassNameFunction() {
|
||||
|
||||
SchemaReport report = inspectSchema(schema,
|
||||
initializer -> initializer.classNameFunction(type -> type.getName() + "Impl"),
|
||||
VehicleController.class);
|
||||
|
||||
assertThatReport(report)
|
||||
.hasSkippedTypeCount(0)
|
||||
.hasUnmappedFieldCount(3)
|
||||
.containsUnmappedFields("Car", "price", "engineType")
|
||||
.containsUnmappedFields("Bike", "price");
|
||||
}
|
||||
|
||||
@Test
|
||||
void useClassNameTypeResolver() {
|
||||
|
||||
ClassNameTypeResolver typeResolver = new ClassNameTypeResolver();
|
||||
typeResolver.addMapping(CarImpl.class, "Car");
|
||||
|
||||
SchemaReport report = inspectSchema(schema,
|
||||
initializer -> initializer.classResolver(ClassResolver.fromClassNameTypeResolver(typeResolver)),
|
||||
VehicleController.class);
|
||||
|
||||
assertThatReport(report)
|
||||
.hasUnmappedFieldCount(2).containsUnmappedFields("Car", "price", "engineType")
|
||||
.hasSkippedTypeCount(1).containsSkippedTypes("Bike");
|
||||
}
|
||||
|
||||
interface Vehicle {
|
||||
String name();
|
||||
}
|
||||
record CarImpl(String name) implements Vehicle {}
|
||||
record BikeImpl(String name) implements Vehicle {}
|
||||
|
||||
@Controller
|
||||
static class VehicleController {
|
||||
|
||||
@QueryMapping
|
||||
List<Vehicle> vehicles() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class SkippedTypes {
|
||||
|
||||
@Test
|
||||
void reportSkippedImplementations() {
|
||||
SchemaReport report = inspectSchema(schema, VehicleController.class);
|
||||
assertThatReport(report).hasSkippedTypeCount(2).containsSkippedTypes("Car", "Bike");
|
||||
}
|
||||
|
||||
interface Vehicle {
|
||||
String name();
|
||||
}
|
||||
|
||||
@Controller
|
||||
static class VehicleController {
|
||||
|
||||
@QueryMapping
|
||||
List<Vehicle> vehicles() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.function.Consumer;
|
||||
|
||||
import graphql.schema.FieldCoordinates;
|
||||
import graphql.schema.GraphQLNamedType;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.SchemaGenerator;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||
|
||||
/**
|
||||
* Base class for {@link SchemaMappingInspector} tests.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class SchemaMappingInspectorTestSupport {
|
||||
|
||||
protected SchemaReport inspectSchema(String schemaContent, Class<?>... controllers) {
|
||||
return inspectSchema(schemaContent, initializer -> {}, controllers);
|
||||
}
|
||||
|
||||
protected SchemaReport inspectSchema(
|
||||
String schemaContent, Consumer<SchemaMappingInspector.Initializer> consumer, Class<?>... controllers) {
|
||||
|
||||
GraphQLSchema schema = SchemaGenerator.createdMockedSchema(schemaContent);
|
||||
RuntimeWiring runtimeWiring = createRuntimeWiring(controllers);
|
||||
SchemaMappingInspector.Initializer initializer = SchemaMappingInspector.initializer();
|
||||
consumer.accept(initializer);
|
||||
return initializer.inspect(schema, runtimeWiring.getDataFetchers());
|
||||
}
|
||||
|
||||
private RuntimeWiring createRuntimeWiring(Class<?>... controllerTypes) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
for (Class<?> controllerType : controllerTypes) {
|
||||
context.registerBean(controllerType);
|
||||
}
|
||||
context.registerBean(BatchLoaderRegistry.class, () -> new DefaultBatchLoaderRegistry());
|
||||
context.refresh();
|
||||
|
||||
AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer();
|
||||
configurer.setApplicationContext(context);
|
||||
configurer.afterPropertiesSet();
|
||||
|
||||
RuntimeWiring.Builder wiringBuilder = RuntimeWiring.newRuntimeWiring();
|
||||
configurer.configure(wiringBuilder);
|
||||
return wiringBuilder.build();
|
||||
}
|
||||
|
||||
protected static SchemaInspectionReportAssert assertThatReport(SchemaReport actual) {
|
||||
return new SchemaInspectionReportAssert(actual);
|
||||
}
|
||||
|
||||
|
||||
protected static class SchemaInspectionReportAssert
|
||||
extends AbstractAssert<SchemaInspectionReportAssert, SchemaReport> {
|
||||
|
||||
public SchemaInspectionReportAssert(SchemaReport actual) {
|
||||
super(actual, SchemaInspectionReportAssert.class);
|
||||
}
|
||||
|
||||
public void isEmpty() {
|
||||
isNotNull();
|
||||
if (!this.actual.unmappedFields().isEmpty()) {
|
||||
failWithMessage("Report contains missing fields: %s", this.actual.unmappedFields());
|
||||
}
|
||||
if (!this.actual.unmappedRegistrations().isEmpty()) {
|
||||
failWithMessage("Report contains missing DataFetcher registrations for %s", this.actual.unmappedRegistrations());
|
||||
}
|
||||
if (!this.actual.skippedTypes().isEmpty()) {
|
||||
failWithMessage("Report contains skipped types: %s", this.actual.skippedTypes());
|
||||
}
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasUnmappedFieldCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.unmappedFields().size() != expected) {
|
||||
failWithMessage("Expected %s unmapped fields, found %s.", expected, this.actual.unmappedFields());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasUnmappedDataFetcherCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.unmappedRegistrations().size() != expected) {
|
||||
failWithMessage("Expected %s unmapped fields, found %s.", expected, this.actual.unmappedFields());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasUnmappedArgumentCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.unmappedArguments().size() != expected) {
|
||||
failWithMessage("Expected %s unmapped arguments, found %s.", expected, this.actual.unmappedArguments());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasSkippedTypeCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.skippedTypes().size() != expected) {
|
||||
failWithMessage("Expected %s skipped types, found %s.", expected, this.actual.skippedTypes());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsUnmappedFields(String typeName, String... fieldNames) {
|
||||
isNotNull();
|
||||
List<String> expected = Arrays.asList(fieldNames);
|
||||
List<String> actual = this.actual.unmappedFields().stream()
|
||||
.filter(coordinates -> coordinates.getTypeName().equals(typeName))
|
||||
.map(FieldCoordinates::getFieldName)
|
||||
.toList();
|
||||
if (!actual.containsAll(expected)) {
|
||||
failWithMessage("Expected unmapped fields for %s: %s, found %s", typeName, expected, actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsUnmappedDataFetchersFor(String typeName, String... fieldNames) {
|
||||
isNotNull();
|
||||
List<FieldCoordinates> expected = Arrays.stream(fieldNames)
|
||||
.map(field -> FieldCoordinates.coordinates(typeName, field))
|
||||
.toList();
|
||||
if (!this.actual.unmappedRegistrations().keySet().containsAll(expected)) {
|
||||
failWithMessage("Expected unmapped DataFetchers for %s, found %s", expected, this.actual.unmappedRegistrations());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsUnmappedArguments(String... arguments) {
|
||||
isNotNull();
|
||||
List<String> expected = Arrays.asList(arguments);
|
||||
List<String> actual = this.actual.unmappedArguments().entrySet().stream()
|
||||
.flatMap(entry -> entry.getValue().stream())
|
||||
.toList();
|
||||
if (!actual.containsAll(expected)) {
|
||||
failWithMessage("Expected unmapped arguments: %s, found %s", expected, actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsSkippedTypes(String... fieldCoordinates) {
|
||||
isNotNull();
|
||||
List<String> expected = Arrays.asList(fieldCoordinates);
|
||||
List<String> actual = this.actual.skippedTypes().stream()
|
||||
.map(skippedType -> ((GraphQLNamedType) skippedType.type()).getName())
|
||||
.toList();
|
||||
if (!actual.containsAll(expected)) {
|
||||
failWithMessage("Expected skipped types: %s, found %s", expected, actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,25 +16,20 @@
|
||||
|
||||
package org.springframework.graphql.execution;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import graphql.schema.FieldCoordinates;
|
||||
import graphql.schema.GraphQLNamedType;
|
||||
import graphql.schema.GraphQLSchema;
|
||||
import graphql.schema.idl.RuntimeWiring;
|
||||
import graphql.schema.idl.SchemaGenerator;
|
||||
import org.assertj.core.api.AbstractAssert;
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.data.domain.ScrollPosition;
|
||||
import org.springframework.data.domain.Window;
|
||||
import org.springframework.graphql.Author;
|
||||
@@ -45,7 +40,6 @@ import org.springframework.graphql.data.method.annotation.MutationMapping;
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SchemaMapping;
|
||||
import org.springframework.graphql.data.method.annotation.SubscriptionMapping;
|
||||
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -57,7 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Oliver Drotbohm
|
||||
*/
|
||||
class SchemaMappingInspectorTests {
|
||||
class SchemaMappingInspectorTests extends SchemaMappingInspectorTestSupport{
|
||||
|
||||
|
||||
@Nested
|
||||
@@ -447,27 +441,6 @@ class SchemaMappingInspectorTests {
|
||||
assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportHasSkippedUnionType() {
|
||||
String schema = """
|
||||
type Query {
|
||||
fooBar: FooBar
|
||||
}
|
||||
|
||||
union FooBar = Foo | Bar
|
||||
|
||||
type Foo {
|
||||
name: String
|
||||
}
|
||||
|
||||
type Bar {
|
||||
name: String
|
||||
}
|
||||
""";
|
||||
SchemaReport report = inspectSchema(schema, UnionController.class);
|
||||
assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(1).containsSkippedTypes("FooBar");
|
||||
}
|
||||
|
||||
@Test
|
||||
void reportHasSkippedTypeForUnknownDataFetcherType() {
|
||||
String schemaContent = """
|
||||
@@ -567,33 +540,6 @@ class SchemaMappingInspectorTests {
|
||||
|
||||
}
|
||||
|
||||
private SchemaReport inspectSchema(String schemaContent, Class<?>... controllers) {
|
||||
GraphQLSchema schema = SchemaGenerator.createdMockedSchema(schemaContent);
|
||||
RuntimeWiring runtimeWiring = createRuntimeWiring(controllers);
|
||||
return SchemaMappingInspector.inspect(schema, runtimeWiring);
|
||||
}
|
||||
|
||||
private RuntimeWiring createRuntimeWiring(Class<?>... controllerTypes) {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
|
||||
for (Class<?> controllerType : controllerTypes) {
|
||||
context.registerBean(controllerType);
|
||||
}
|
||||
context.registerBean(BatchLoaderRegistry.class, () -> new DefaultBatchLoaderRegistry());
|
||||
context.refresh();
|
||||
|
||||
AnnotatedControllerConfigurer configurer = new AnnotatedControllerConfigurer();
|
||||
configurer.setApplicationContext(context);
|
||||
configurer.afterPropertiesSet();
|
||||
|
||||
RuntimeWiring.Builder wiringBuilder = RuntimeWiring.newRuntimeWiring();
|
||||
configurer.configure(wiringBuilder);
|
||||
return wiringBuilder.build();
|
||||
}
|
||||
|
||||
static SchemaInspectionReportAssert assertThatReport(SchemaReport actual) {
|
||||
return new SchemaInspectionReportAssert(actual);
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
static class EmptyController {
|
||||
@@ -705,116 +651,4 @@ class SchemaMappingInspectorTests {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@Controller
|
||||
static class UnionController {
|
||||
|
||||
@QueryMapping
|
||||
Object fooBar() {
|
||||
return "Hello";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class SchemaInspectionReportAssert
|
||||
extends AbstractAssert<SchemaInspectionReportAssert, SchemaReport> {
|
||||
|
||||
public SchemaInspectionReportAssert(SchemaReport actual) {
|
||||
super(actual, SchemaInspectionReportAssert.class);
|
||||
}
|
||||
|
||||
public void isEmpty() {
|
||||
isNotNull();
|
||||
if (!this.actual.unmappedFields().isEmpty()) {
|
||||
failWithMessage("Report contains missing fields: %s", this.actual.unmappedFields());
|
||||
}
|
||||
if (!this.actual.unmappedRegistrations().isEmpty()) {
|
||||
failWithMessage("Report contains missing DataFetcher registrations for %s", this.actual.unmappedRegistrations());
|
||||
}
|
||||
if (!this.actual.skippedTypes().isEmpty()) {
|
||||
failWithMessage("Report contains skipped types: %s", this.actual.skippedTypes());
|
||||
}
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasUnmappedFieldCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.unmappedFields().size() != expected) {
|
||||
failWithMessage("Expected %s unmapped fields, found %s.", expected, this.actual.unmappedFields());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasUnmappedDataFetcherCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.unmappedRegistrations().size() != expected) {
|
||||
failWithMessage("Expected %s unmapped fields, found %s.", expected, this.actual.unmappedFields());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasUnmappedArgumentCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.unmappedArguments().size() != expected) {
|
||||
failWithMessage("Expected %s unmapped arguments, found %s.", expected, this.actual.unmappedArguments());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert hasSkippedTypeCount(int expected) {
|
||||
isNotNull();
|
||||
if (this.actual.skippedTypes().size() != expected) {
|
||||
failWithMessage("Expected %s skipped types, found %s.", expected, this.actual.skippedTypes());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsUnmappedFields(String typeName, String... fieldNames) {
|
||||
isNotNull();
|
||||
List<String> expected = Arrays.asList(fieldNames);
|
||||
List<String> actual = this.actual.unmappedFields().stream()
|
||||
.filter(coordinates -> coordinates.getTypeName().equals(typeName))
|
||||
.map(FieldCoordinates::getFieldName)
|
||||
.toList();
|
||||
if (!actual.containsAll(expected)) {
|
||||
failWithMessage("Expected unmapped fields for %s: %s, found %s", typeName, expected, actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsUnmappedDataFetchersFor(String typeName, String... fieldNames) {
|
||||
isNotNull();
|
||||
List<FieldCoordinates> expected = Arrays.stream(fieldNames)
|
||||
.map(field -> FieldCoordinates.coordinates(typeName, field))
|
||||
.toList();
|
||||
if (!this.actual.unmappedRegistrations().keySet().containsAll(expected)) {
|
||||
failWithMessage("Expected unmapped DataFetchers for %s, found %s", expected, this.actual.unmappedRegistrations());
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsUnmappedArguments(String... arguments) {
|
||||
isNotNull();
|
||||
List<String> expected = Arrays.asList(arguments);
|
||||
List<String> actual = this.actual.unmappedArguments().entrySet().stream()
|
||||
.flatMap(entry -> entry.getValue().stream())
|
||||
.toList();
|
||||
if (!actual.containsAll(expected)) {
|
||||
failWithMessage("Expected unmapped arguments: %s, found %s", expected, actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public SchemaInspectionReportAssert containsSkippedTypes(String... fieldCoordinates) {
|
||||
isNotNull();
|
||||
List<String> expected = Arrays.asList(fieldCoordinates);
|
||||
List<String> actual = this.actual.skippedTypes().stream()
|
||||
.map(skippedType -> ((GraphQLNamedType) skippedType.type()).getName())
|
||||
.toList();
|
||||
if (!actual.containsAll(expected)) {
|
||||
failWithMessage("Expected skipped types: %s, found %s", expected, actual);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2020-2024 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.List;
|
||||
|
||||
import org.junit.jupiter.api.Nested;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.graphql.data.method.annotation.QueryMapping;
|
||||
import org.springframework.graphql.execution.SchemaMappingInspector.ClassResolver;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
/**
|
||||
* Tests for {@link SchemaMappingInspector} with union types.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
*/
|
||||
public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTestSupport {
|
||||
|
||||
private final static String schema = """
|
||||
type Query {
|
||||
search: [SearchResult!]!
|
||||
}
|
||||
union SearchResult = Photo | Video
|
||||
type Photo {
|
||||
height: Int
|
||||
width: Int
|
||||
}
|
||||
type Video {
|
||||
title: String
|
||||
}
|
||||
""";
|
||||
|
||||
|
||||
@Nested
|
||||
class InterfaceFieldsNotOnJavaInterface {
|
||||
|
||||
@Test
|
||||
void reportUnmappedFields() {
|
||||
SchemaReport report = inspectSchema(schema, SearchController.class);
|
||||
assertThatReport(report)
|
||||
.hasSkippedTypeCount(0)
|
||||
.hasUnmappedFieldCount(3)
|
||||
.containsUnmappedFields("Photo", "height", "width")
|
||||
.containsUnmappedFields("Video", "title");
|
||||
}
|
||||
|
||||
|
||||
sealed interface ResultItem permits Photo, Video {}
|
||||
record Photo() implements ResultItem {}
|
||||
record Video() implements ResultItem {}
|
||||
|
||||
@Controller
|
||||
static class SearchController {
|
||||
|
||||
@QueryMapping
|
||||
List<ResultItem> search() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class GraphQlAndJavaTypeNameMismatch {
|
||||
|
||||
@Test
|
||||
void useClassNameFunction() {
|
||||
|
||||
SchemaReport report = inspectSchema(schema,
|
||||
initializer -> initializer.classNameFunction(type -> type.getName() + "Impl"),
|
||||
SearchController.class);
|
||||
|
||||
assertThatReport(report)
|
||||
.hasSkippedTypeCount(0)
|
||||
.hasUnmappedFieldCount(3)
|
||||
.containsUnmappedFields("Photo", "height", "width")
|
||||
.containsUnmappedFields("Video", "title");
|
||||
}
|
||||
|
||||
@Test
|
||||
void useClassNameTypeResolver() {
|
||||
|
||||
ClassNameTypeResolver typeResolver = new ClassNameTypeResolver();
|
||||
typeResolver.addMapping(PhotoImpl.class, "Photo");
|
||||
|
||||
SchemaReport report = inspectSchema(schema,
|
||||
initializer -> initializer.classResolver(ClassResolver.fromClassNameTypeResolver(typeResolver)),
|
||||
SearchController.class);
|
||||
|
||||
assertThatReport(report)
|
||||
.hasUnmappedFieldCount(2).containsUnmappedFields("Photo", "height", "width")
|
||||
.hasSkippedTypeCount(1).containsSkippedTypes("Video");
|
||||
}
|
||||
|
||||
sealed interface ResultItem permits PhotoImpl, VideoImpl {}
|
||||
record PhotoImpl() implements ResultItem {}
|
||||
record VideoImpl() implements ResultItem {}
|
||||
|
||||
@Controller
|
||||
static class SearchController {
|
||||
|
||||
@QueryMapping
|
||||
List<ResultItem> search() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Nested
|
||||
class SkippedTypes {
|
||||
|
||||
@Test
|
||||
void reportSkippedImplementations() {
|
||||
SchemaReport report = inspectSchema(schema, SearchController.class);
|
||||
assertThatReport(report).hasSkippedTypeCount(2).containsSkippedTypes("Photo", "Video");
|
||||
}
|
||||
|
||||
interface ResultItem {}
|
||||
|
||||
@Controller
|
||||
static class SearchController {
|
||||
|
||||
@QueryMapping
|
||||
List<ResultItem> search() {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user