Skip to content

Commit 0ab80ee

Browse files
committed
Improve class mapping in SchemaMappingInspector
1 parent dceb3af commit 0ab80ee

File tree

5 files changed

+69
-54
lines changed

5 files changed

+69
-54
lines changed

spring-graphql-docs/modules/ROOT/pages/request-execution.adoc

+18-15
Original file line numberDiff line numberDiff line change
@@ -280,32 +280,35 @@ For unions, the inspection iterates over member types and tries to find the corr
280280
classes. For interfaces, the inspection iterates over implementation types and looks
281281
for the corresponding classes.
282282

283-
By default, corresponding `Class` can be found if the class name matches that of the
284-
GraphQL union member of interface implementation type, _and_ the `Class` is located in
285-
the same package (and/or outer class) as the return type of the controller method for the
286-
union or interface. In addition, if `ClassNameTypeResolver` is configured as a
283+
By default, corresponding Java classes can be detected out-of-the-box in the following cases:
284+
285+
- The ``Class``'s simple name matches the GraphQL union member of interface implementation
286+
type name, _and_ the `Class` is located in the same package as the return type of the
287+
controller method, or controller class, mapped to the union or interface field.
288+
- The `Class` is inspected in other parts of the schema where the mapped field is of a
289+
concrete union member or interface implementation type.
290+
- You have registered a
287291
xref:request-execution.adoc#execution.graphqlsource.default-type-resolver[TypeResolver]
288-
with explicit class mapping registrations, those are also checked.
292+
that has explicit `Class` to GraphQL type mappings .
289293

290-
If a union member or an interface implementation type is listed as skipped, you have
291-
the following additional options:
294+
In none the above help, and GraphQL types are reported as skipped in the schema inspection
295+
report, you can make the following customizations:
292296

293-
- Register a function to resolve the `Class` name for a given GraphQL type to account
294-
for class naming conventions.
295-
- Register a `ClassResolver` with any custom resolution logic.
297+
- Explicitly map a GraphQL type name to a Java class or classes.
298+
- Configure a function that customizes how a GraphQL type name is adapted to a simple
299+
`Class` name. This can help with a specific Java class naming conventions.
300+
- Provide a `ClassNameTypeResolver` to map a GraphQL type a Java classes.
296301

297-
Use the following for such customizations:
302+
For example:
298303

299304
[source,java,indent=0,subs="verbatim,quotes"]
300305
----
301306
GraphQlSource.Builder builder = ...
302307
303308
builder.schemaResources(..)
304309
.inspectSchemaMappings(
305-
initializer -> initializer.classNameFunction(type -> type.getName() + "Impl")
306-
report -> {
307-
logger.debug(report);
308-
})
310+
initializer -> initializer.classMapping("Author", Author.class)
311+
logger::debug);
309312
----
310313

311314

spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultSchemaResourceGraphQlSourceBuilder.java

+1-4
Original file line numberDiff line numberDiff line change
@@ -238,10 +238,7 @@ private SchemaReport createSchemaReport(GraphQLSchema schema, RuntimeWiring runt
238238
// Add explicit mappings from ClassNameTypeResolver's
239239
runtimeWiring.getTypeResolvers().values().stream().distinct().forEach((resolver) -> {
240240
if (resolver instanceof ClassNameTypeResolver cntr) {
241-
Map<Class<?>, String> mappings = cntr.getMappings();
242-
if (!mappings.isEmpty()) {
243-
initializer.classResolver(SchemaMappingInspector.ClassResolver.create(mappings));
244-
}
241+
cntr.getMappings().forEach((aClass, name) -> initializer.classMapping(name, aClass));
245242
}
246243
});
247244

spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingInspector.java

+48-25
Original file line numberDiff line numberDiff line change
@@ -302,20 +302,43 @@ public static Initializer initializer() {
302302
public interface Initializer {
303303

304304
/**
305-
* Provide a function to derive the simple class name that corresponds to a
306-
* GraphQL union member type, or a GraphQL interface implementation type.
307-
* This is then used to find a Java class in the same package as that of
308-
* the return type of the controller method for the interface or union.
309-
* <p>The default, {@link GraphQLObjectType#getName()} is used
305+
* Provide an explicit mapping between a GraphQL type name and the Java
306+
* class(es) that represent it at runtime to help inspect union member
307+
* and interface implementation types when those associations cannot be
308+
* discovered otherwise.
309+
* <p>Out of the box, there a several ways through which schema inspection
310+
* can locate such types automatically:
311+
* <ul>
312+
* <li>Java class representations are located in the same package as the
313+
* type returned from the controller method for a union or interface field,
314+
* and their {@link Class#getSimpleName() simple class names} match GraphQL
315+
* type names, possibly with the help of a {@link #classNameFunction}.
316+
* <li>Java class representations are located in the same package as the
317+
* declaring class of the controller method for a union or interface field.
318+
* <li>Controller methods return the Java class representations of schema
319+
* fields for concrete union member or interface implementation types.
320+
* </ul>
321+
* @param graphQlTypeName the name of a GraphQL Object type
322+
* @param aClass one or more Java class representations
323+
* @return the same initializer instance
324+
*/
325+
Initializer classMapping(String graphQlTypeName, Class<?>... aClass);
326+
327+
/**
328+
* Help to derive the {@link Class#getSimpleName() simple class name} for
329+
* the Java representation of a GraphQL union member or interface implementing
330+
* type. For more details, see {@link #classMapping(String, Class[])}.
331+
* <p>By default, {@link GraphQLObjectType#getName()} is used.
310332
* @param function the function to use
311333
* @return the same initializer instance
312334
*/
313335
Initializer classNameFunction(Function<GraphQLObjectType, String> function);
314336

315337
/**
316-
* Add a custom {@link ClassResolver} to use to find the Java class for a
317-
* GraphQL union member type, or a GraphQL interface implementation type.
318-
* @param resolver the resolver to add
338+
* Alternative to {@link #classMapping(String, Class[])} with a custom
339+
* {@link ClassResolver} to find the Java class(es) for a GraphQL union
340+
* member or interface implementation type.
341+
* @param resolver the resolver to use to find associated Java classes
319342
* @return the same initializer instance
320343
*/
321344
Initializer classResolver(ClassResolver resolver);
@@ -345,14 +368,6 @@ public interface ClassResolver {
345368
*/
346369
List<Class<?>> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnionType);
347370

348-
349-
/**
350-
* Create a resolver from the given mappings.
351-
* @param mappings from Class to GraphQL type name
352-
*/
353-
static ClassResolver create(Map<Class<?>, String> mappings) {
354-
return new MappingClassResolver(mappings);
355-
}
356371
}
357372

358373

@@ -365,6 +380,8 @@ private static final class DefaultInitializer implements Initializer {
365380

366381
private final List<ClassResolver> classResolvers = new ArrayList<>();
367382

383+
private final MultiValueMap<String, Class<?>> classMappings = new LinkedMultiValueMap<>();
384+
368385
@Override
369386
public Initializer classNameFunction(Function<GraphQLObjectType, String> function) {
370387
this.classNameFunction = function;
@@ -378,13 +395,19 @@ public Initializer classResolver(ClassResolver resolver) {
378395
}
379396

380397
@Override
381-
public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers) {
398+
public Initializer classMapping(String graphQlTypeName, Class<?>... classes) {
399+
for (Class<?> aClass : classes) {
400+
this.classMappings.add(graphQlTypeName, aClass);
401+
}
402+
return this;
403+
}
382404

383-
ReflectionClassResolver reflectionResolver =
384-
ReflectionClassResolver.create(schema, fetchers, this.classNameFunction);
405+
@Override
406+
public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers) {
385407

386408
List<ClassResolver> resolvers = new ArrayList<>(this.classResolvers);
387-
resolvers.add(reflectionResolver);
409+
resolvers.add(new MappingClassResolver(this.classMappings));
410+
resolvers.add(ReflectionClassResolver.create(schema, fetchers, this.classNameFunction));
388411

389412
InterfaceUnionLookup lookup = InterfaceUnionLookup.create(schema, resolvers);
390413

@@ -399,15 +422,15 @@ public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFe
399422
*/
400423
private static final class MappingClassResolver implements ClassResolver {
401424

402-
private final MultiValueMap<String, Class<?>> map = new LinkedMultiValueMap<>();
425+
private final MultiValueMap<String, Class<?>> mappings = new LinkedMultiValueMap<>();
403426

404-
MappingClassResolver(Map<Class<?>, String> mappings) {
405-
mappings.forEach((key, value) -> this.map.add(value, key));
427+
MappingClassResolver(MultiValueMap<String, Class<?>> mappings) {
428+
this.mappings.putAll(mappings);
406429
}
407430

408431
@Override
409432
public List<Class<?>> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnionType) {
410-
return this.map.getOrDefault(objectType.getName(), Collections.emptyList());
433+
return this.mappings.getOrDefault(objectType.getName(), Collections.emptyList());
411434
}
412435
}
413436

@@ -478,7 +501,7 @@ public static ReflectionClassResolver create(
478501
if (PACKAGE_PREDICATE.test(clazz.getPackageName())) {
479502
addClassPrefix(outputTypeName, clazz, classPrefixes);
480503
}
481-
else if (dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribing) {
504+
if (dataFetcher instanceof SelfDescribingDataFetcher<?> selfDescribing) {
482505
if (selfDescribing.getReturnType().getSource() instanceof MethodParameter param) {
483506
addClassPrefix(outputTypeName, param.getDeclaringClass(), classPrefixes);
484507
}

spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorInterfaceTests.java

+1-5
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,11 @@
1717
package org.springframework.graphql.execution;
1818

1919
import java.util.List;
20-
import java.util.Map;
2120

2221
import org.junit.jupiter.api.Nested;
2322
import org.junit.jupiter.api.Test;
2423

2524
import org.springframework.graphql.data.method.annotation.QueryMapping;
26-
import org.springframework.graphql.execution.SchemaMappingInspector.ClassResolver;
2725
import org.springframework.stereotype.Controller;
2826

2927
/**
@@ -103,10 +101,8 @@ void classNameFunction() {
103101
@Test
104102
void classNameTypeResolver() {
105103

106-
Map<Class<?>, String> mappings = Map.of(CarImpl.class, "Car");
107-
108104
SchemaReport report = inspectSchema(schema,
109-
initializer -> initializer.classResolver(ClassResolver.create(mappings)),
105+
initializer -> initializer.classMapping("Car", CarImpl.class),
110106
VehicleController.class);
111107

112108
assertThatReport(report)

spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorUnionTests.java

+1-5
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@
1717
package org.springframework.graphql.execution;
1818

1919
import java.util.List;
20-
import java.util.Map;
2120

2221
import org.junit.jupiter.api.Nested;
2322
import org.junit.jupiter.api.Test;
2423

2524
import org.springframework.graphql.data.method.annotation.Argument;
2625
import org.springframework.graphql.data.method.annotation.QueryMapping;
27-
import org.springframework.graphql.execution.SchemaMappingInspector.ClassResolver;
2826
import org.springframework.stereotype.Controller;
2927

3028
/**
@@ -129,10 +127,8 @@ void classNameFunction() {
129127
@Test
130128
void classNameTypeResolver() {
131129

132-
Map<Class<?>, String> mappings = Map.of(PhotoImpl.class, "Photo");
133-
134130
SchemaReport report = inspectSchema(schema,
135-
initializer -> initializer.classResolver(ClassResolver.create(mappings)),
131+
initializer -> initializer.classMapping("Photo", PhotoImpl.class),
136132
SearchController.class);
137133

138134
assertThatReport(report)

0 commit comments

Comments
 (0)