From 0ab80ee5ce47f657d6569a9ec213a818e969bac2 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 3 May 2024 20:01:56 +0100 Subject: [PATCH] Improve class mapping in SchemaMappingInspector --- .../modules/ROOT/pages/request-execution.adoc | 33 +++++---- ...ultSchemaResourceGraphQlSourceBuilder.java | 5 +- .../execution/SchemaMappingInspector.java | 73 ++++++++++++------- .../SchemaMappingInspectorInterfaceTests.java | 6 +- .../SchemaMappingInspectorUnionTests.java | 6 +- 5 files changed, 69 insertions(+), 54 deletions(-) diff --git a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc index 527ad0c8..23c3cf2c 100644 --- a/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc +++ b/spring-graphql-docs/modules/ROOT/pages/request-execution.adoc @@ -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); ---- diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultSchemaResourceGraphQlSourceBuilder.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultSchemaResourceGraphQlSourceBuilder.java index 99d79115..be163f48 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultSchemaResourceGraphQlSourceBuilder.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/DefaultSchemaResourceGraphQlSourceBuilder.java @@ -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, String> mappings = cntr.getMappings(); - if (!mappings.isEmpty()) { - initializer.classResolver(SchemaMappingInspector.ClassResolver.create(mappings)); - } + cntr.getMappings().forEach((aClass, name) -> initializer.classMapping(name, aClass)); } }); diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingInspector.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingInspector.java index 505e6109..cea57752 100644 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingInspector.java +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingInspector.java @@ -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. - *

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. + *

Out of the box, there a several ways through which schema inspection + * can locate such types automatically: + *

+ * @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[])}. + *

By default, {@link GraphQLObjectType#getName()} is used. * @param function the function to use * @return the same initializer instance */ Initializer classNameFunction(Function 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> resolveClass(GraphQLObjectType objectType, GraphQLNamedOutputType interfaceOrUnionType); - - /** - * Create a resolver from the given mappings. - * @param mappings from Class to GraphQL type name - */ - static ClassResolver create(Map, String> mappings) { - return new MappingClassResolver(mappings); - } } @@ -365,6 +380,8 @@ public final class SchemaMappingInspector { private final List classResolvers = new ArrayList<>(); + private final MultiValueMap> classMappings = new LinkedMultiValueMap<>(); + @Override public Initializer classNameFunction(Function 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> fetchers) { - ReflectionClassResolver reflectionResolver = - ReflectionClassResolver.create(schema, fetchers, this.classNameFunction); - List 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> map = new LinkedMultiValueMap<>(); + private final MultiValueMap> mappings = new LinkedMultiValueMap<>(); - MappingClassResolver(Map, String> mappings) { - mappings.forEach((key, value) -> this.map.add(value, key)); + MappingClassResolver(MultiValueMap> mappings) { + this.mappings.putAll(mappings); } @Override public List> 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); } diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorInterfaceTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorInterfaceTests.java index 0acafc2c..ff60e286 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorInterfaceTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorInterfaceTests.java @@ -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, 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) diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorUnionTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorUnionTests.java index 44066fd0..b60a2ff0 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorUnionTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorUnionTests.java @@ -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, 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)