Improve class mapping in SchemaMappingInspector

This commit is contained in:
rstoyanchev
2024-05-03 20:01:56 +01:00
parent dceb3aff94
commit 0ab80ee5ce
5 changed files with 69 additions and 54 deletions

View File

@@ -280,21 +280,26 @@ For unions, the inspection iterates over member types and tries to find the corr
classes. For interfaces, the inspection iterates over implementation types and looks
for the corresponding classes.
By default, corresponding `Class` can be found if the class name matches that of the
GraphQL union member of interface implementation type, _and_ the `Class` is located in
the same package (and/or outer class) as the return type of the controller method for the
union or interface. In addition, if `ClassNameTypeResolver` is configured as a
By default, corresponding Java classes can be detected out-of-the-box in the following cases:
- The ``Class``'s simple name matches the GraphQL union member of interface implementation
type name, _and_ the `Class` is located in the same package as the return type of the
controller method, or controller class, mapped to the union or interface field.
- The `Class` is inspected in other parts of the schema where the mapped field is of a
concrete union member or interface implementation type.
- You have registered a
xref:request-execution.adoc#execution.graphqlsource.default-type-resolver[TypeResolver]
with explicit class mapping registrations, those are also checked.
that has explicit `Class` to GraphQL type mappings .
If a union member or an interface implementation type is listed as skipped, you have
the following additional options:
In none the above help, and GraphQL types are reported as skipped in the schema inspection
report, you can make the following customizations:
- Register a function to resolve the `Class` name for a given GraphQL type to account
for class naming conventions.
- Register a `ClassResolver` with any custom resolution logic.
- Explicitly map a GraphQL type name to a Java class or classes.
- Configure a function that customizes how a GraphQL type name is adapted to a simple
`Class` name. This can help with a specific Java class naming conventions.
- Provide a `ClassNameTypeResolver` to map a GraphQL type a Java classes.
Use the following for such customizations:
For example:
[source,java,indent=0,subs="verbatim,quotes"]
----
@@ -302,10 +307,8 @@ GraphQlSource.Builder builder = ...
builder.schemaResources(..)
.inspectSchemaMappings(
initializer -> initializer.classNameFunction(type -> type.getName() + "Impl")
report -> {
logger.debug(report);
})
initializer -> initializer.classMapping("Author", Author.class)
logger::debug);
----

View File

@@ -238,10 +238,7 @@ final class DefaultSchemaResourceGraphQlSourceBuilder
// Add explicit mappings from ClassNameTypeResolver's
runtimeWiring.getTypeResolvers().values().stream().distinct().forEach((resolver) -> {
if (resolver instanceof ClassNameTypeResolver cntr) {
Map<Class<?>, String> mappings = cntr.getMappings();
if (!mappings.isEmpty()) {
initializer.classResolver(SchemaMappingInspector.ClassResolver.create(mappings));
}
cntr.getMappings().forEach((aClass, name) -> initializer.classMapping(name, aClass));
}
});

View File

@@ -302,20 +302,43 @@ public final class SchemaMappingInspector {
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
* Provide an explicit mapping between a GraphQL type name and the Java
* class(es) that represent it at runtime to help inspect union member
* and interface implementation types when those associations cannot be
* discovered otherwise.
* <p>Out of the box, there a several ways through which schema inspection
* can locate such types automatically:
* <ul>
* <li>Java class representations are located in the same package as the
* type returned from the controller method for a union or interface field,
* and their {@link Class#getSimpleName() simple class names} match GraphQL
* type names, possibly with the help of a {@link #classNameFunction}.
* <li>Java class representations are located in the same package as the
* declaring class of the controller method for a union or interface field.
* <li>Controller methods return the Java class representations of schema
* fields for concrete union member or interface implementation types.
* </ul>
* @param graphQlTypeName the name of a GraphQL Object type
* @param aClass one or more Java class representations
* @return the same initializer instance
*/
Initializer classMapping(String graphQlTypeName, Class<?>... aClass);
/**
* Help to derive the {@link Class#getSimpleName() simple class name} for
* the Java representation of a GraphQL union member or interface implementing
* type. For more details, see {@link #classMapping(String, Class[])}.
* <p>By 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
* Alternative to {@link #classMapping(String, Class[])} with a custom
* {@link ClassResolver} to find the Java class(es) for a GraphQL union
* member or interface implementation type.
* @param resolver the resolver to use to find associated Java classes
* @return the same initializer instance
*/
Initializer classResolver(ClassResolver resolver);
@@ -345,14 +368,6 @@ public final class SchemaMappingInspector {
*/
List<Class<?>> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnionType);
/**
* Create a resolver from the given mappings.
* @param mappings from Class to GraphQL type name
*/
static ClassResolver create(Map<Class<?>, String> mappings) {
return new MappingClassResolver(mappings);
}
}
@@ -365,6 +380,8 @@ public final class SchemaMappingInspector {
private final List<ClassResolver> classResolvers = new ArrayList<>();
private final MultiValueMap<String, Class<?>> classMappings = new LinkedMultiValueMap<>();
@Override
public Initializer classNameFunction(Function<GraphQLObjectType, String> function) {
this.classNameFunction = function;
@@ -377,14 +394,20 @@ public final class SchemaMappingInspector {
return this;
}
@Override
public Initializer classMapping(String graphQlTypeName, Class<?>... classes) {
for (Class<?> aClass : classes) {
this.classMappings.add(graphQlTypeName, aClass);
}
return this;
}
@Override
public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers) {
ReflectionClassResolver reflectionResolver =
ReflectionClassResolver.create(schema, fetchers, this.classNameFunction);
List<ClassResolver> resolvers = new ArrayList<>(this.classResolvers);
resolvers.add(reflectionResolver);
resolvers.add(new MappingClassResolver(this.classMappings));
resolvers.add(ReflectionClassResolver.create(schema, fetchers, this.classNameFunction));
InterfaceUnionLookup lookup = InterfaceUnionLookup.create(schema, resolvers);
@@ -399,15 +422,15 @@ public final class SchemaMappingInspector {
*/
private static final class MappingClassResolver implements ClassResolver {
private final MultiValueMap<String, Class<?>> map = new LinkedMultiValueMap<>();
private final MultiValueMap<String, Class<?>> mappings = new LinkedMultiValueMap<>();
MappingClassResolver(Map<Class<?>, String> mappings) {
mappings.forEach((key, value) -> this.map.add(value, key));
MappingClassResolver(MultiValueMap<String, Class<?>> mappings) {
this.mappings.putAll(mappings);
}
@Override
public List<Class<?>> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnionType) {
return this.map.getOrDefault(objectType.getName(), Collections.emptyList());
return this.mappings.getOrDefault(objectType.getName(), Collections.emptyList());
}
}
@@ -478,7 +501,7 @@ public final class SchemaMappingInspector {
if (PACKAGE_PREDICATE.test(clazz.getPackageName())) {
addClassPrefix(outputTypeName, clazz, classPrefixes);
}
else if (dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribing) {
if (dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribing) {
if (selfDescribing.getReturnType().getSource() instanceof MethodParameter param) {
addClassPrefix(outputTypeName, param.getDeclaringClass(), classPrefixes);
}

View File

@@ -17,13 +17,11 @@
package org.springframework.graphql.execution;
import java.util.List;
import java.util.Map;
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;
/**
@@ -103,10 +101,8 @@ public class SchemaMappingInspectorInterfaceTests extends SchemaMappingInspector
@Test
void classNameTypeResolver() {
Map<Class<?>, String> mappings = Map.of(CarImpl.class, "Car");
SchemaReport report = inspectSchema(schema,
initializer -> initializer.classResolver(ClassResolver.create(mappings)),
initializer -> initializer.classMapping("Car", CarImpl.class),
VehicleController.class);
assertThatReport(report)

View File

@@ -17,14 +17,12 @@
package org.springframework.graphql.execution;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.graphql.execution.SchemaMappingInspector.ClassResolver;
import org.springframework.stereotype.Controller;
/**
@@ -129,10 +127,8 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
@Test
void classNameTypeResolver() {
Map<Class<?>, String> mappings = Map.of(PhotoImpl.class, "Photo");
SchemaReport report = inspectSchema(schema,
initializer -> initializer.classResolver(ClassResolver.create(mappings)),
initializer -> initializer.classMapping("Photo", PhotoImpl.class),
SearchController.class);
assertThatReport(report)