16
16
17
17
package org .springframework .graphql .execution ;
18
18
19
+ import java .util .ArrayList ;
20
+ import java .util .Collections ;
19
21
import java .util .HashSet ;
20
22
import java .util .LinkedHashMap ;
21
- import java .util .LinkedHashSet ;
23
+ import java .util .List ;
22
24
import java .util .Map ;
23
25
import java .util .Set ;
24
- import java .util .function .Supplier ;
25
26
26
27
import graphql .schema .DataFetcher ;
27
28
import graphql .schema .FieldCoordinates ;
52
53
53
54
/**
54
55
* Declares an {@link #inspect(GraphQLSchema, RuntimeWiring)} method that checks
55
- * if schema fields are covered either by a {@link DataFetcher} registration,
56
- * or match a Java object property. Fields that have neither are reported as
57
- * "unmapped" in the resulting {@link SchemaMappingReport}. The inspection also
58
- * performs a reverse check for {@code DataFetcher} registrations against schema
59
- * fields that don't exist.
56
+ * if schema mappings.
60
57
*
61
- * <p>The schema field inspection depends on {@code DataFetcher}s to be
62
- * {@link SelfDescribingDataFetcher} to be able to compare schema type and Java
58
+ * <p>Schema mapping checks depend on {@code DataFetcher}s to be
59
+ * {@link SelfDescribingDataFetcher} in order to compare schema type and Java
63
60
* object type structure. If a {@code DataFetcher} does not implement this
64
61
* interface, then the Java type remains unknown, and the field type is reported
65
62
* as "skipped".
@@ -95,14 +92,10 @@ final class SchemaMappingInspector {
95
92
96
93
private final ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry .getSharedInstance ();
97
94
98
- private final MultiValueMap <String , String > unmappedFields = new LinkedMultiValueMap <>();
99
-
100
- private final Map <FieldCoordinates , DataFetcher <?>> unmappedDataFetchers = new LinkedHashMap <>();
101
-
102
- private final Set <String > skippedTypes = new LinkedHashSet <>();
95
+ private final ReportBuilder reportBuilder = new ReportBuilder ();
103
96
104
97
@ Nullable
105
- private SchemaMappingReport report ;
98
+ private SchemaReport report ;
106
99
107
100
108
101
private SchemaMappingInspector (GraphQLSchema schema , RuntimeWiring runtimeWiring ) {
@@ -114,20 +107,19 @@ private SchemaMappingInspector(GraphQLSchema schema, RuntimeWiring runtimeWiring
114
107
115
108
116
109
/**
117
- * Perform an inspection and create a {@link SchemaMappingReport }.
110
+ * Perform an inspection and create a {@link SchemaReport }.
118
111
* The inspection is one once only, during the first call to this method.
119
112
*/
120
- public SchemaMappingReport getOrCreateReport () {
113
+ public SchemaReport getOrCreateReport () {
121
114
if (this .report == null ) {
122
- checkSchema ();
115
+ checkSchemaFields ();
123
116
checkDataFetcherRegistrations ();
124
- this .report = new SchemaMappingReport (
125
- this .unmappedFields , this .unmappedDataFetchers , this .skippedTypes );
117
+ this .report = this .reportBuilder .build ();
126
118
}
127
119
return this .report ;
128
120
}
129
121
130
- private void checkSchema () {
122
+ private void checkSchemaFields () {
131
123
132
124
checkFieldsContainer (this .schema .getQueryType (), null );
133
125
@@ -143,52 +135,50 @@ private void checkSchema() {
143
135
/**
144
136
* Check the given {@code GraphQLFieldsContainer} against {@code DataFetcher}
145
137
* registrations, or Java properties of the given {@code ResolvableType}.
146
- * @param fields the GraphQL interface or object type to check
138
+ * @param fieldContainer the GraphQL interface or object type to check
147
139
* @param resolvableType the Java type to match against, or {@code null} if
148
140
* not applicable such as for Query, Mutation, or Subscription
149
141
*/
150
142
@ SuppressWarnings ("rawtypes" )
151
- private void checkFieldsContainer (GraphQLFieldsContainer fields , @ Nullable ResolvableType resolvableType ) {
143
+ private void checkFieldsContainer (GraphQLFieldsContainer fieldContainer , @ Nullable ResolvableType resolvableType ) {
152
144
153
- Map <String , DataFetcher > dataFetcherMap = this .runtimeWiring .getDataFetcherForType (fields .getName ());
145
+ String typeName = fieldContainer .getName ();
146
+ Map <String , DataFetcher > dataFetcherMap = this .runtimeWiring .getDataFetcherForType (typeName );
154
147
155
- for (GraphQLFieldDefinition field : fields .getFieldDefinitions ()) {
148
+ for (GraphQLFieldDefinition field : fieldContainer .getFieldDefinitions ()) {
156
149
String fieldName = field .getName ();
157
- if (dataFetcherMap .containsKey (fieldName )) {
158
- DataFetcher <?> fetcher = dataFetcherMap .get (fieldName );
159
- if (fetcher instanceof SelfDescribingDataFetcher <?> selfDescribingDataFetcher ) {
160
- checkFieldType (
161
- field .getType (), selfDescribingDataFetcher .getReturnType (),
162
- (fields == this .schema .getSubscriptionType ()));
163
- }
164
- else if (isNotScalarOrEnumType (field .getType ())) {
165
- addSkippedType (field .getType (), () ->
166
- fetcher .getClass ().getName () + " does not implement SelfDescribingDataFetcher." );
167
- }
150
+ DataFetcher <?> dataFetcher = dataFetcherMap .get (fieldName );
151
+ if (dataFetcher != null ) {
152
+ checkField (fieldContainer , field , dataFetcher );
168
153
}
169
154
else if (resolvableType == null || !hasProperty (resolvableType , fieldName )) {
170
- this .unmappedFields . add ( fields . getName () , fieldName );
155
+ this .reportBuilder . unmappedField ( FieldCoordinates . coordinates ( typeName , fieldName ) );
171
156
}
172
157
}
173
158
}
174
159
175
160
/**
176
161
* Check the output {@link GraphQLType} of a field against the given DataFetcher return type.
177
- * @param outputType the field type to inspect
178
- * @param resolvableType the expected Java return type
179
- * @param isSubscriptionField whether this is for a subscription field
162
+ * @param parent the parent of the field
163
+ * @param field the field to inspect
164
+ * @param dataFetcher the registered DataFetcher
180
165
*/
181
- private void checkFieldType (GraphQLType outputType , ResolvableType resolvableType , boolean isSubscriptionField ) {
166
+ private void checkField (GraphQLFieldsContainer parent , GraphQLFieldDefinition field , DataFetcher <?> dataFetcher ) {
167
+
168
+ ResolvableType resolvableType = ResolvableType .NONE ;
169
+ if (dataFetcher instanceof SelfDescribingDataFetcher <?> selfDescribingDataFetcher ) {
170
+ resolvableType = selfDescribingDataFetcher .getReturnType ();
171
+ }
182
172
183
173
// Remove GraphQL type wrappers, and nest within Java generic types
184
- outputType = unwrapIfNonNull (outputType );
174
+ GraphQLType outputType = unwrapIfNonNull (field . getType () );
185
175
if (isPaginatedType (outputType )) {
186
176
outputType = getPaginatedType ((GraphQLObjectType ) outputType );
187
177
resolvableType = nestForConnection (resolvableType );
188
178
}
189
179
else if (outputType instanceof GraphQLList listType ) {
190
180
outputType = unwrapIfNonNull (listType .getWrappedType ());
191
- resolvableType = nestForList (resolvableType , isSubscriptionField );
181
+ resolvableType = nestForList (resolvableType , ( parent == this . schema . getSubscriptionType ()) );
192
182
}
193
183
else {
194
184
resolvableType = nestIfReactive (resolvableType );
@@ -202,15 +192,16 @@ else if (outputType instanceof GraphQLList listType) {
202
192
// Can we inspect GraphQL type?
203
193
if (!(outputType instanceof GraphQLFieldsContainer fieldContainer )) {
204
194
if (isNotScalarOrEnumType (outputType )) {
205
- String schemaTypeName = outputType . getClass (). getSimpleName ( );
206
- addSkippedType (outputType , () -> "inspection does not support " + schemaTypeName + ". " );
195
+ FieldCoordinates coordinates = FieldCoordinates . coordinates ( parent . getName (), field . getName () );
196
+ addSkippedType (outputType , coordinates , "Unsupported schema type " );
207
197
}
208
198
return ;
209
199
}
210
200
211
201
// Can we inspect Java type?
212
202
if (resolvableType .resolve (Object .class ) == Object .class ) {
213
- addSkippedType (outputType , () -> "inspection could not determine the Java object return type." );
203
+ FieldCoordinates coordinates = FieldCoordinates .coordinates (parent .getName (), field .getName ());
204
+ addSkippedType (outputType , coordinates , "No Java type information" );
214
205
return ;
215
206
}
216
207
@@ -236,6 +227,9 @@ private GraphQLType getPaginatedType(GraphQLObjectType type) {
236
227
}
237
228
238
229
private ResolvableType nestForConnection (ResolvableType type ) {
230
+ if (type == ResolvableType .NONE ) {
231
+ return type ;
232
+ }
239
233
type = nestIfReactive (type );
240
234
if (logger .isDebugEnabled () && type .getGenerics ().length != 1 ) {
241
235
logger .debug ("Expected Connection type to have a generic parameter: " + type );
@@ -256,6 +250,9 @@ private ResolvableType nestIfReactive(ResolvableType type) {
256
250
}
257
251
258
252
private ResolvableType nestForList (ResolvableType type , boolean subscription ) {
253
+ if (type == ResolvableType .NONE ) {
254
+ return type ;
255
+ }
259
256
ReactiveAdapter adapter = this .reactiveAdapterRegistry .getAdapter (type .resolve (Object .class ));
260
257
if (adapter != null ) {
261
258
if (logger .isDebugEnabled () && adapter .isNoValue ()) {
@@ -266,7 +263,7 @@ private ResolvableType nestForList(ResolvableType type, boolean subscription) {
266
263
return type ;
267
264
}
268
265
}
269
- if (logger .isDebugEnabled () && ( !type .isArray () && type .getGenerics ().length != 1 ) ) {
266
+ if (logger .isDebugEnabled () && !type .isArray () && type .getGenerics ().length != 1 ) {
270
267
logger .debug ("Expected List compatible type: " + type );
271
268
}
272
269
return type .getNested (2 );
@@ -295,21 +292,21 @@ private boolean hasProperty(ResolvableType resolvableType, String fieldName) {
295
292
}
296
293
}
297
294
298
- private void addSkippedType (GraphQLType type , Supplier < String > reason ) {
295
+ private void addSkippedType (GraphQLType type , FieldCoordinates coordinates , String reason ) {
299
296
String typeName = typeNameToString (type );
300
- this .skippedTypes . add ( typeName );
297
+ this .reportBuilder . skippedType ( type , coordinates );
301
298
if (logger .isDebugEnabled ()) {
302
- logger .debug ("Skipped '" + typeName + "': " + reason . get () );
299
+ logger .debug ("Skipped '" + typeName + "': " + reason );
303
300
}
304
301
}
305
302
306
303
@ SuppressWarnings ("rawtypes" )
307
304
private void checkDataFetcherRegistrations () {
308
305
this .runtimeWiring .getDataFetchers ().forEach ((typeName , registrations ) ->
309
- registrations .forEach ((fieldName , fetcher ) -> {
306
+ registrations .forEach ((fieldName , dataFetcher ) -> {
310
307
FieldCoordinates coordinates = FieldCoordinates .coordinates (typeName , fieldName );
311
308
if (this .schema .getFieldDefinition (coordinates ) == null ) {
312
- this .unmappedDataFetchers . put (coordinates , fetcher );
309
+ this .reportBuilder . unmappedRegistration (coordinates , dataFetcher );
313
310
}
314
311
}));
315
312
}
@@ -321,9 +318,120 @@ private void checkDataFetcherRegistrations() {
321
318
* @param runtimeWiring for {@code DataFetcher} registrations
322
319
* @return the created report
323
320
*/
324
- public static SchemaMappingReport inspect (GraphQLSchema schema , RuntimeWiring runtimeWiring ) {
321
+ public static SchemaReport inspect (GraphQLSchema schema , RuntimeWiring runtimeWiring ) {
325
322
return new SchemaMappingInspector (schema , runtimeWiring ).getOrCreateReport ();
326
323
}
327
324
328
325
326
+ /**
327
+ * Helps to build a {@link SchemaReport}.
328
+ */
329
+ private class ReportBuilder {
330
+
331
+ private final List <FieldCoordinates > unmappedFields = new ArrayList <>();
332
+
333
+ private final Map <FieldCoordinates , DataFetcher <?>> unmappedRegistrations = new LinkedHashMap <>();
334
+
335
+ private final List <SchemaReport .SkippedType > skippedTypes = new ArrayList <>();
336
+
337
+ public void unmappedField (FieldCoordinates coordinates ) {
338
+ this .unmappedFields .add (coordinates );
339
+ }
340
+
341
+ public void unmappedRegistration (FieldCoordinates coordinates , DataFetcher <?> dataFetcher ) {
342
+ this .unmappedRegistrations .put (coordinates , dataFetcher );
343
+ }
344
+
345
+ public void skippedType (GraphQLType type , FieldCoordinates coordinates ) {
346
+ this .skippedTypes .add (new DefaultSkippedType (type , coordinates ));
347
+ }
348
+
349
+ public SchemaReport build () {
350
+ return new DefaultSchemaReport (this .unmappedFields , this .unmappedRegistrations , this .skippedTypes );
351
+ }
352
+
353
+ }
354
+
355
+
356
+ /**
357
+ * Default implementation of {@link SchemaReport}.
358
+ */
359
+ private class DefaultSchemaReport implements SchemaReport {
360
+
361
+ private final List <FieldCoordinates > unmappedFields ;
362
+
363
+ private final Map <FieldCoordinates , DataFetcher <?>> unmappedRegistrations ;
364
+
365
+ private final List <SchemaReport .SkippedType > skippedTypes ;
366
+
367
+ public DefaultSchemaReport (
368
+ List <FieldCoordinates > unmappedFields , Map <FieldCoordinates , DataFetcher <?>> unmappedRegistrations ,
369
+ List <SkippedType > skippedTypes ) {
370
+
371
+ this .unmappedFields = Collections .unmodifiableList (unmappedFields );
372
+ this .unmappedRegistrations = Collections .unmodifiableMap (unmappedRegistrations );
373
+ this .skippedTypes = Collections .unmodifiableList (skippedTypes );
374
+ }
375
+
376
+ @ Override
377
+ public List <FieldCoordinates > unmappedFields () {
378
+ return this .unmappedFields ;
379
+ }
380
+
381
+ @ Override
382
+ public Map <FieldCoordinates , DataFetcher <?>> unmappedRegistrations () {
383
+ return this .unmappedRegistrations ;
384
+ }
385
+
386
+ @ Override
387
+ public List <SkippedType > skippedTypes () {
388
+ return this .skippedTypes ;
389
+ }
390
+
391
+ @ Override
392
+ public GraphQLSchema schema () {
393
+ return SchemaMappingInspector .this .schema ;
394
+ }
395
+
396
+ @ Override
397
+ @ Nullable
398
+ public DataFetcher <?> dataFetcher (FieldCoordinates coordinates ) {
399
+ return SchemaMappingInspector .this .runtimeWiring
400
+ .getDataFetcherForType (coordinates .getTypeName ())
401
+ .get (coordinates .getFieldName ());
402
+ }
403
+
404
+ @ Override
405
+ public String toString () {
406
+ return "GraphQL schema inspection:\n " +
407
+ "\t Unmapped fields: " + formatUnmappedFields () + "\n " +
408
+ "\t Unmapped registrations: " + this .unmappedRegistrations + "\n " +
409
+ "\t Skipped types: " + this .skippedTypes ;
410
+ }
411
+
412
+ private String formatUnmappedFields () {
413
+ MultiValueMap <String , String > map = new LinkedMultiValueMap <>();
414
+ this .unmappedFields .forEach (coordinates -> {
415
+ List <String > fields = map .computeIfAbsent (coordinates .getTypeName (), s -> new ArrayList <>());
416
+ fields .add (coordinates .getFieldName ());
417
+ });
418
+ return map .toString ();
419
+ }
420
+
421
+ }
422
+
423
+
424
+ /**
425
+ * Default implementation of a {@link SchemaReport.SkippedType}.
426
+ */
427
+ private record DefaultSkippedType (
428
+ GraphQLType type , FieldCoordinates fieldCoordinates ) implements SchemaReport .SkippedType {
429
+
430
+ @ Override
431
+ public String toString () {
432
+ return typeNameToString (this .type );
433
+ }
434
+
435
+ }
436
+
329
437
}
0 commit comments