Correctly report skipped concrete union/interface types

Closes gh-962
This commit is contained in:
rstoyanchev
2024-05-03 15:12:21 +01:00
parent 9bcd8242ef
commit dceb3aff94
2 changed files with 102 additions and 24 deletions

View File

@@ -220,14 +220,15 @@ public final class SchemaMappingInspector {
// Can we inspect GraphQL type?
if (!(graphQlType instanceof GraphQLFieldsContainer fieldContainer)) {
if (isNotScalarOrEnumType(graphQlType)) {
this.reportBuilder.skippedType(graphQlType, parent, field, "Unsupported schema type");
this.reportBuilder.skippedType(graphQlType, parent, field, "Unsupported schema type", false);
}
continue;
}
// Can we inspect the Class?
if (currentResolvableType.resolve(Object.class) == Object.class) {
this.reportBuilder.skippedType(graphQlType, parent, field, "No class information");
boolean isDerived = !graphQlType.equals(typePair.outputType());
this.reportBuilder.skippedType(graphQlType, parent, field, "No class information", isDerived);
continue;
}
@@ -723,7 +724,9 @@ public final class SchemaMappingInspector {
private final MultiValueMap<DataFetcher<?>, String> unmappedArguments = new LinkedMultiValueMap<>();
private final List<SchemaReport.SkippedType> skippedTypes = new ArrayList<>();
private final List<DefaultSkippedType> skippedTypes = new ArrayList<>();
private final List<DefaultSkippedType> candidateSkippedTypes = new ArrayList<>();
void unmappedField(FieldCoordinates coordinates) {
this.unmappedFields.add(coordinates);
@@ -737,19 +740,44 @@ public final class SchemaMappingInspector {
this.unmappedArguments.put(dataFetcher, arguments);
}
void skippedType(GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field, String reason) {
DefaultSkippedType skippedType = DefaultSkippedType.create(type, parent, field);
void skippedType(
GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field,
String reason, boolean isDerivedType) {
DefaultSkippedType skippedType = DefaultSkippedType.create(type, parent, field, reason);
if (!isDerivedType) {
skippedType(skippedType);
return;
}
// Keep skipped union member or interface implementing types aside to the end.
// Use of concrete types elsewhere may provide more information.
this.candidateSkippedTypes.add(skippedType);
}
private void skippedType(DefaultSkippedType skippedType) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping '" + skippedType + "': " + reason);
logger.debug("Skipping '" + skippedType + "': " + skippedType.reason());
}
this.skippedTypes.add(skippedType);
}
SchemaReport build() {
this.candidateSkippedTypes.forEach((skippedType) -> {
if (skippedType.type() instanceof GraphQLFieldsContainer fieldsContainer) {
if (SchemaMappingInspector.this.inspectedTypes.contains(fieldsContainer.getName())) {
return;
}
}
skippedType(skippedType);
});
return new DefaultSchemaReport(
this.unmappedFields, this.unmappedRegistrations, this.unmappedArguments, this.skippedTypes);
}
}
@@ -768,7 +796,7 @@ public final class SchemaMappingInspector {
DefaultSchemaReport(
List<FieldCoordinates> unmappedFields, Map<FieldCoordinates, DataFetcher<?>> unmappedRegistrations,
MultiValueMap<DataFetcher<?>, String> unmappedArguments, List<SkippedType> skippedTypes) {
MultiValueMap<DataFetcher<?>, String> unmappedArguments, List<DefaultSkippedType> skippedTypes) {
this.unmappedFields = Collections.unmodifiableList(unmappedFields);
this.unmappedRegistrations = Collections.unmodifiableMap(unmappedRegistrations);
@@ -834,17 +862,18 @@ public final class SchemaMappingInspector {
* Default implementation of a {@link SchemaReport.SkippedType}.
*/
private record DefaultSkippedType(
GraphQLType type, FieldCoordinates fieldCoordinates) implements SchemaReport.SkippedType {
GraphQLType type, FieldCoordinates fieldCoordinates, String reason)
implements SchemaReport.SkippedType {
@Override
public String toString() {
return (type instanceof GraphQLNamedType namedType) ? namedType.getName() : type.toString();
return (this.type instanceof GraphQLNamedType named) ? named.getName() : this.type.toString();
}
public static DefaultSkippedType create(
GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field) {
GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field, String reason) {
return new DefaultSkippedType(type, FieldCoordinates.coordinates(parent, field));
return new DefaultSkippedType(type, FieldCoordinates.coordinates(parent, field), reason);
}
}

View File

@@ -22,6 +22,7 @@ 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;
@@ -35,9 +36,10 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
private static final String schema = """
type Query {
search: [SearchResult!]!
search: [SearchResult]
article(id: ID): Article
}
union SearchResult = Photo | Video
union SearchResult = Article | Photo | Video
type Photo {
height: Int
width: Int
@@ -45,6 +47,9 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
type Video {
title: String
}
type Article {
content: String
}
""";
@@ -55,8 +60,10 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
void reportUnmappedFieldsByCheckingReturnTypePackage() {
SchemaReport report = inspectSchema(schema, SearchController.class);
assertThatReport(report)
.hasSkippedTypeCount(0)
.hasUnmappedFieldCount(3)
.hasSkippedTypeCount(1)
.containsSkippedTypes("Article")
.hasUnmappedFieldCount(4)
.containsUnmappedFields("Query", "article")
.containsUnmappedFields("Photo", "height", "width")
.containsUnmappedFields("Video", "title");
}
@@ -65,17 +72,20 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
void reportUnmappedFieldsByCheckingControllerTypePackage() {
SchemaReport report = inspectSchema(schema, ObjectSearchController.class);
assertThatReport(report)
.hasSkippedTypeCount(0)
.hasUnmappedFieldCount(3)
.hasSkippedTypeCount(1)
.containsSkippedTypes("Article")
.hasUnmappedFieldCount(4)
.containsUnmappedFields("Query", "article")
.containsUnmappedFields("Photo", "height", "width")
.containsUnmappedFields("Video", "title");
}
sealed interface ResultItem permits Photo, Video { }
record Photo() implements ResultItem { }
record Video() implements ResultItem { }
sealed interface ResultItem permits Photo, Video { }
@Controller
static class SearchController {
@@ -108,8 +118,10 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
SearchController.class);
assertThatReport(report)
.hasSkippedTypeCount(0)
.hasUnmappedFieldCount(3)
.hasSkippedTypeCount(1)
.containsSkippedTypes("Article")
.hasUnmappedFieldCount(4)
.containsUnmappedFields("Query", "article")
.containsUnmappedFields("Photo", "height", "width")
.containsUnmappedFields("Video", "title");
}
@@ -124,8 +136,11 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
SearchController.class);
assertThatReport(report)
.hasUnmappedFieldCount(2).containsUnmappedFields("Photo", "height", "width")
.hasSkippedTypeCount(1).containsSkippedTypes("Video");
.hasSkippedTypeCount(2)
.containsSkippedTypes("Article", "Video")
.hasUnmappedFieldCount(3)
.containsUnmappedFields("Query", "article")
.containsUnmappedFields("Photo", "height", "width");
}
sealed interface ResultItem permits PhotoImpl, VideoImpl { }
@@ -149,7 +164,7 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
@Test
void reportSkippedImplementations() {
SchemaReport report = inspectSchema(schema, SearchController.class);
assertThatReport(report).hasSkippedTypeCount(2).containsSkippedTypes("Photo", "Video");
assertThatReport(report).hasSkippedTypeCount(3).containsSkippedTypes("Article", "Photo", "Video");
}
interface ResultItem { }
@@ -164,4 +179,38 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
}
}
@Nested
class CandidateSkippedTypes {
// A union member type is only a candidate to be skipped until the inspection is done.
// Use of the concrete type elsewhere may provide more information.
@Test
void candidateNotSkippedIfConcreteUseElsewhere() {
SchemaReport report = inspectSchema(schema, SearchController.class);
assertThatReport(report).hasSkippedTypeCount(2).containsSkippedTypes("Photo", "Video");
}
@Controller
static class SearchController {
@QueryMapping
List<Object> search() {
throw new UnsupportedOperationException();
}
@QueryMapping
Article article(@Argument Long id) {
throw new UnsupportedOperationException();
}
}
}
/**
* Declared outside {@link CandidateSkippedTypes}, so the union lookup won't find it.
*/
private record Article(String content) { }
}