Skip to content

Commit dceb3af

Browse files
committed
Correctly report skipped concrete union/interface types
Closes gh-962
1 parent 9bcd824 commit dceb3af

File tree

2 files changed

+102
-24
lines changed

2 files changed

+102
-24
lines changed

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

+41-12
Original file line numberDiff line numberDiff line change
@@ -220,14 +220,15 @@ else if (typePair.outputType() instanceof GraphQLInterfaceType interfaceType) {
220220
// Can we inspect GraphQL type?
221221
if (!(graphQlType instanceof GraphQLFieldsContainer fieldContainer)) {
222222
if (isNotScalarOrEnumType(graphQlType)) {
223-
this.reportBuilder.skippedType(graphQlType, parent, field, "Unsupported schema type");
223+
this.reportBuilder.skippedType(graphQlType, parent, field, "Unsupported schema type", false);
224224
}
225225
continue;
226226
}
227227

228228
// Can we inspect the Class?
229229
if (currentResolvableType.resolve(Object.class) == Object.class) {
230-
this.reportBuilder.skippedType(graphQlType, parent, field, "No class information");
230+
boolean isDerived = !graphQlType.equals(typePair.outputType());
231+
this.reportBuilder.skippedType(graphQlType, parent, field, "No class information", isDerived);
231232
continue;
232233
}
233234

@@ -723,7 +724,9 @@ private final class ReportBuilder {
723724

724725
private final MultiValueMap<DataFetcher<?>, String> unmappedArguments = new LinkedMultiValueMap<>();
725726

726-
private final List<SchemaReport.SkippedType> skippedTypes = new ArrayList<>();
727+
private final List<DefaultSkippedType> skippedTypes = new ArrayList<>();
728+
729+
private final List<DefaultSkippedType> candidateSkippedTypes = new ArrayList<>();
727730

728731
void unmappedField(FieldCoordinates coordinates) {
729732
this.unmappedFields.add(coordinates);
@@ -737,19 +740,44 @@ void unmappedArgument(DataFetcher<?> dataFetcher, List<String> arguments) {
737740
this.unmappedArguments.put(dataFetcher, arguments);
738741
}
739742

740-
void skippedType(GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field, String reason) {
741-
DefaultSkippedType skippedType = DefaultSkippedType.create(type, parent, field);
743+
void skippedType(
744+
GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field,
745+
String reason, boolean isDerivedType) {
746+
747+
DefaultSkippedType skippedType = DefaultSkippedType.create(type, parent, field, reason);
748+
749+
if (!isDerivedType) {
750+
skippedType(skippedType);
751+
return;
752+
}
753+
754+
// Keep skipped union member or interface implementing types aside to the end.
755+
// Use of concrete types elsewhere may provide more information.
756+
757+
this.candidateSkippedTypes.add(skippedType);
758+
}
759+
760+
private void skippedType(DefaultSkippedType skippedType) {
742761
if (logger.isDebugEnabled()) {
743-
logger.debug("Skipping '" + skippedType + "': " + reason);
762+
logger.debug("Skipping '" + skippedType + "': " + skippedType.reason());
744763
}
745764
this.skippedTypes.add(skippedType);
746765
}
747766

748767
SchemaReport build() {
768+
769+
this.candidateSkippedTypes.forEach((skippedType) -> {
770+
if (skippedType.type() instanceof GraphQLFieldsContainer fieldsContainer) {
771+
if (SchemaMappingInspector.this.inspectedTypes.contains(fieldsContainer.getName())) {
772+
return;
773+
}
774+
}
775+
skippedType(skippedType);
776+
});
777+
749778
return new DefaultSchemaReport(
750779
this.unmappedFields, this.unmappedRegistrations, this.unmappedArguments, this.skippedTypes);
751780
}
752-
753781
}
754782

755783

@@ -768,7 +796,7 @@ private final class DefaultSchemaReport implements SchemaReport {
768796

769797
DefaultSchemaReport(
770798
List<FieldCoordinates> unmappedFields, Map<FieldCoordinates, DataFetcher<?>> unmappedRegistrations,
771-
MultiValueMap<DataFetcher<?>, String> unmappedArguments, List<SkippedType> skippedTypes) {
799+
MultiValueMap<DataFetcher<?>, String> unmappedArguments, List<DefaultSkippedType> skippedTypes) {
772800

773801
this.unmappedFields = Collections.unmodifiableList(unmappedFields);
774802
this.unmappedRegistrations = Collections.unmodifiableMap(unmappedRegistrations);
@@ -834,17 +862,18 @@ private String formatUnmappedFields() {
834862
* Default implementation of a {@link SchemaReport.SkippedType}.
835863
*/
836864
private record DefaultSkippedType(
837-
GraphQLType type, FieldCoordinates fieldCoordinates) implements SchemaReport.SkippedType {
865+
GraphQLType type, FieldCoordinates fieldCoordinates, String reason)
866+
implements SchemaReport.SkippedType {
838867

839868
@Override
840869
public String toString() {
841-
return (type instanceof GraphQLNamedType namedType) ? namedType.getName() : type.toString();
870+
return (this.type instanceof GraphQLNamedType named) ? named.getName() : this.type.toString();
842871
}
843872

844873
public static DefaultSkippedType create(
845-
GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field) {
874+
GraphQLType type, GraphQLFieldsContainer parent, GraphQLFieldDefinition field, String reason) {
846875

847-
return new DefaultSkippedType(type, FieldCoordinates.coordinates(parent, field));
876+
return new DefaultSkippedType(type, FieldCoordinates.coordinates(parent, field), reason);
848877
}
849878
}
850879

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

+61-12
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.junit.jupiter.api.Nested;
2323
import org.junit.jupiter.api.Test;
2424

25+
import org.springframework.graphql.data.method.annotation.Argument;
2526
import org.springframework.graphql.data.method.annotation.QueryMapping;
2627
import org.springframework.graphql.execution.SchemaMappingInspector.ClassResolver;
2728
import org.springframework.stereotype.Controller;
@@ -35,16 +36,20 @@ public class SchemaMappingInspectorUnionTests extends SchemaMappingInspectorTest
3536

3637
private static final String schema = """
3738
type Query {
38-
search: [SearchResult!]!
39+
search: [SearchResult]
40+
article(id: ID): Article
3941
}
40-
union SearchResult = Photo | Video
42+
union SearchResult = Article | Photo | Video
4143
type Photo {
4244
height: Int
4345
width: Int
4446
}
4547
type Video {
4648
title: String
4749
}
50+
type Article {
51+
content: String
52+
}
4853
""";
4954

5055

@@ -55,8 +60,10 @@ class UnmappedFields {
5560
void reportUnmappedFieldsByCheckingReturnTypePackage() {
5661
SchemaReport report = inspectSchema(schema, SearchController.class);
5762
assertThatReport(report)
58-
.hasSkippedTypeCount(0)
59-
.hasUnmappedFieldCount(3)
63+
.hasSkippedTypeCount(1)
64+
.containsSkippedTypes("Article")
65+
.hasUnmappedFieldCount(4)
66+
.containsUnmappedFields("Query", "article")
6067
.containsUnmappedFields("Photo", "height", "width")
6168
.containsUnmappedFields("Video", "title");
6269
}
@@ -65,17 +72,20 @@ void reportUnmappedFieldsByCheckingReturnTypePackage() {
6572
void reportUnmappedFieldsByCheckingControllerTypePackage() {
6673
SchemaReport report = inspectSchema(schema, ObjectSearchController.class);
6774
assertThatReport(report)
68-
.hasSkippedTypeCount(0)
69-
.hasUnmappedFieldCount(3)
75+
.hasSkippedTypeCount(1)
76+
.containsSkippedTypes("Article")
77+
.hasUnmappedFieldCount(4)
78+
.containsUnmappedFields("Query", "article")
7079
.containsUnmappedFields("Photo", "height", "width")
7180
.containsUnmappedFields("Video", "title");
7281
}
7382

7483

75-
sealed interface ResultItem permits Photo, Video { }
7684
record Photo() implements ResultItem { }
7785
record Video() implements ResultItem { }
7886

87+
sealed interface ResultItem permits Photo, Video { }
88+
7989
@Controller
8090
static class SearchController {
8191

@@ -108,8 +118,10 @@ void classNameFunction() {
108118
SearchController.class);
109119

110120
assertThatReport(report)
111-
.hasSkippedTypeCount(0)
112-
.hasUnmappedFieldCount(3)
121+
.hasSkippedTypeCount(1)
122+
.containsSkippedTypes("Article")
123+
.hasUnmappedFieldCount(4)
124+
.containsUnmappedFields("Query", "article")
113125
.containsUnmappedFields("Photo", "height", "width")
114126
.containsUnmappedFields("Video", "title");
115127
}
@@ -124,8 +136,11 @@ void classNameTypeResolver() {
124136
SearchController.class);
125137

126138
assertThatReport(report)
127-
.hasUnmappedFieldCount(2).containsUnmappedFields("Photo", "height", "width")
128-
.hasSkippedTypeCount(1).containsSkippedTypes("Video");
139+
.hasSkippedTypeCount(2)
140+
.containsSkippedTypes("Article", "Video")
141+
.hasUnmappedFieldCount(3)
142+
.containsUnmappedFields("Query", "article")
143+
.containsUnmappedFields("Photo", "height", "width");
129144
}
130145

131146
sealed interface ResultItem permits PhotoImpl, VideoImpl { }
@@ -149,7 +164,7 @@ class SkippedTypes {
149164
@Test
150165
void reportSkippedImplementations() {
151166
SchemaReport report = inspectSchema(schema, SearchController.class);
152-
assertThatReport(report).hasSkippedTypeCount(2).containsSkippedTypes("Photo", "Video");
167+
assertThatReport(report).hasSkippedTypeCount(3).containsSkippedTypes("Article", "Photo", "Video");
153168
}
154169

155170
interface ResultItem { }
@@ -164,4 +179,38 @@ List<ResultItem> search() {
164179
}
165180
}
166181

182+
183+
@Nested
184+
class CandidateSkippedTypes {
185+
186+
// A union member type is only a candidate to be skipped until the inspection is done.
187+
// Use of the concrete type elsewhere may provide more information.
188+
189+
@Test
190+
void candidateNotSkippedIfConcreteUseElsewhere() {
191+
SchemaReport report = inspectSchema(schema, SearchController.class);
192+
assertThatReport(report).hasSkippedTypeCount(2).containsSkippedTypes("Photo", "Video");
193+
}
194+
195+
@Controller
196+
static class SearchController {
197+
198+
@QueryMapping
199+
List<Object> search() {
200+
throw new UnsupportedOperationException();
201+
}
202+
203+
@QueryMapping
204+
Article article(@Argument Long id) {
205+
throw new UnsupportedOperationException();
206+
}
207+
}
208+
}
209+
210+
211+
/**
212+
* Declared outside {@link CandidateSkippedTypes}, so the union lookup won't find it.
213+
*/
214+
private record Article(String content) { }
215+
167216
}

0 commit comments

Comments
 (0)