Skip to content

Commit

Permalink
Improve class mapping in SchemaMappingInspector
Browse files Browse the repository at this point in the history
  • Loading branch information
rstoyanchev committed May 3, 2024
1 parent dceb3af commit 0ab80ee
Show file tree
Hide file tree
Showing 5 changed files with 69 additions and 54 deletions.
33 changes: 18 additions & 15 deletions spring-graphql-docs/modules/ROOT/pages/request-execution.adoc
Expand Up @@ -280,32 +280,35 @@ 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"]
----
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);
----


Expand Down
Expand Up @@ -238,10 +238,7 @@ private SchemaReport createSchemaReport(GraphQLSchema schema, RuntimeWiring runt
// 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));
}
});

Expand Down
Expand Up @@ -302,20 +302,43 @@ public static Initializer initializer() {
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);
Expand Down Expand Up @@ -345,14 +368,6 @@ public interface ClassResolver {
*/
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);
}
}


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

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;
Expand All @@ -378,13 +395,19 @@ public Initializer classResolver(ClassResolver resolver) {
}

@Override
public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers) {
public Initializer classMapping(String graphQlTypeName, Class<?>... classes) {
for (Class<?> aClass : classes) {
this.classMappings.add(graphQlTypeName, aClass);
}
return this;
}

ReflectionClassResolver reflectionResolver =
ReflectionClassResolver.create(schema, fetchers, this.classNameFunction);
@Override
public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFetcher>> fetchers) {

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);

Expand All @@ -399,15 +422,15 @@ public SchemaReport inspect(GraphQLSchema schema, Map<String, Map<String, DataFe
*/
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());
}
}

Expand Down Expand Up @@ -478,7 +501,7 @@ public static ReflectionClassResolver create(
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);
}
Expand Down
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -103,10 +101,8 @@ void classNameFunction() {
@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)
Expand Down
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -129,10 +127,8 @@ void classNameFunction() {
@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)
Expand Down

0 comments on commit 0ab80ee

Please sign in to comment.