From 7db96fd77fc1ee540a88fa67f7fd12ae632aaf22 Mon Sep 17 00:00:00 2001 From: rstoyanchev Date: Fri, 28 Apr 2023 12:14:00 +0100 Subject: [PATCH] Enhance SchemaReport API See gh-672 --- ...ultSchemaResourceGraphQlSourceBuilder.java | 2 +- .../execution/SchemaMappingInspector.java | 214 +++++++++++++----- .../execution/SchemaMappingReport.java | 50 ---- .../graphql/execution/SchemaReport.java | 99 ++++++++ .../SchemaMappingInspectorTests.java | 92 ++++---- 5 files changed, 309 insertions(+), 148 deletions(-) delete mode 100644 spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingReport.java create mode 100644 spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaReport.java 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 4520d663..cd5b75a2 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 @@ -138,7 +138,7 @@ final class DefaultSchemaResourceGraphQlSourceBuilder configureGraphQl(builder -> { GraphQLSchema schema = builder.build().getGraphQLSchema(); - SchemaMappingReport report = SchemaMappingInspector.inspect(schema, runtimeWiring); + SchemaReport report = SchemaMappingInspector.inspect(schema, runtimeWiring); logger.info(report); }); 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 5b2bb571..8e25819c 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 @@ -16,12 +16,13 @@ package org.springframework.graphql.execution; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashMap; -import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Supplier; import graphql.schema.DataFetcher; import graphql.schema.FieldCoordinates; @@ -52,14 +53,10 @@ import org.springframework.util.MultiValueMap; /** * Declares an {@link #inspect(GraphQLSchema, RuntimeWiring)} method that checks - * if schema fields are covered either by a {@link DataFetcher} registration, - * or match a Java object property. Fields that have neither are reported as - * "unmapped" in the resulting {@link SchemaMappingReport}. The inspection also - * performs a reverse check for {@code DataFetcher} registrations against schema - * fields that don't exist. + * if schema mappings. * - *

The schema field inspection depends on {@code DataFetcher}s to be - * {@link SelfDescribingDataFetcher} to be able to compare schema type and Java + *

Schema mapping checks depend on {@code DataFetcher}s to be + * {@link SelfDescribingDataFetcher} in order to compare schema type and Java * object type structure. If a {@code DataFetcher} does not implement this * interface, then the Java type remains unknown, and the field type is reported * as "skipped". @@ -95,14 +92,10 @@ final class SchemaMappingInspector { private final ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); - private final MultiValueMap unmappedFields = new LinkedMultiValueMap<>(); - - private final Map> unmappedDataFetchers = new LinkedHashMap<>(); - - private final Set skippedTypes = new LinkedHashSet<>(); + private final ReportBuilder reportBuilder = new ReportBuilder(); @Nullable - private SchemaMappingReport report; + private SchemaReport report; private SchemaMappingInspector(GraphQLSchema schema, RuntimeWiring runtimeWiring) { @@ -114,20 +107,19 @@ final class SchemaMappingInspector { /** - * Perform an inspection and create a {@link SchemaMappingReport}. + * Perform an inspection and create a {@link SchemaReport}. * The inspection is one once only, during the first call to this method. */ - public SchemaMappingReport getOrCreateReport() { + public SchemaReport getOrCreateReport() { if (this.report == null) { - checkSchema(); + checkSchemaFields(); checkDataFetcherRegistrations(); - this.report = new SchemaMappingReport( - this.unmappedFields, this.unmappedDataFetchers, this.skippedTypes); + this.report = this.reportBuilder.build(); } return this.report; } - private void checkSchema() { + private void checkSchemaFields() { checkFieldsContainer(this.schema.getQueryType(), null); @@ -143,52 +135,50 @@ final class SchemaMappingInspector { /** * Check the given {@code GraphQLFieldsContainer} against {@code DataFetcher} * registrations, or Java properties of the given {@code ResolvableType}. - * @param fields the GraphQL interface or object type to check + * @param fieldContainer the GraphQL interface or object type to check * @param resolvableType the Java type to match against, or {@code null} if * not applicable such as for Query, Mutation, or Subscription */ @SuppressWarnings("rawtypes") - private void checkFieldsContainer(GraphQLFieldsContainer fields, @Nullable ResolvableType resolvableType) { + private void checkFieldsContainer(GraphQLFieldsContainer fieldContainer, @Nullable ResolvableType resolvableType) { - Map dataFetcherMap = this.runtimeWiring.getDataFetcherForType(fields.getName()); + String typeName = fieldContainer.getName(); + Map dataFetcherMap = this.runtimeWiring.getDataFetcherForType(typeName); - for (GraphQLFieldDefinition field : fields.getFieldDefinitions()) { + for (GraphQLFieldDefinition field : fieldContainer.getFieldDefinitions()) { String fieldName = field.getName(); - if (dataFetcherMap.containsKey(fieldName)) { - DataFetcher fetcher = dataFetcherMap.get(fieldName); - if (fetcher instanceof SelfDescribingDataFetcher selfDescribingDataFetcher) { - checkFieldType( - field.getType(), selfDescribingDataFetcher.getReturnType(), - (fields == this.schema.getSubscriptionType())); - } - else if (isNotScalarOrEnumType(field.getType())) { - addSkippedType(field.getType(), () -> - fetcher.getClass().getName() + " does not implement SelfDescribingDataFetcher."); - } + DataFetcher dataFetcher = dataFetcherMap.get(fieldName); + if (dataFetcher != null) { + checkField(fieldContainer, field, dataFetcher); } else if (resolvableType == null || !hasProperty(resolvableType, fieldName)) { - this.unmappedFields.add(fields.getName(), fieldName); + this.reportBuilder.unmappedField(FieldCoordinates.coordinates(typeName, fieldName)); } } } /** * Check the output {@link GraphQLType} of a field against the given DataFetcher return type. - * @param outputType the field type to inspect - * @param resolvableType the expected Java return type - * @param isSubscriptionField whether this is for a subscription field + * @param parent the parent of the field + * @param field the field to inspect + * @param dataFetcher the registered DataFetcher */ - private void checkFieldType(GraphQLType outputType, ResolvableType resolvableType, boolean isSubscriptionField) { + private void checkField(GraphQLFieldsContainer parent, GraphQLFieldDefinition field, DataFetcher dataFetcher) { + + ResolvableType resolvableType = ResolvableType.NONE; + if (dataFetcher instanceof SelfDescribingDataFetcher selfDescribingDataFetcher) { + resolvableType = selfDescribingDataFetcher.getReturnType(); + } // Remove GraphQL type wrappers, and nest within Java generic types - outputType = unwrapIfNonNull(outputType); + GraphQLType outputType = unwrapIfNonNull(field.getType()); if (isPaginatedType(outputType)) { outputType = getPaginatedType((GraphQLObjectType) outputType); resolvableType = nestForConnection(resolvableType); } else if (outputType instanceof GraphQLList listType) { outputType = unwrapIfNonNull(listType.getWrappedType()); - resolvableType = nestForList(resolvableType, isSubscriptionField); + resolvableType = nestForList(resolvableType, (parent == this.schema.getSubscriptionType())); } else { resolvableType = nestIfReactive(resolvableType); @@ -202,15 +192,16 @@ final class SchemaMappingInspector { // Can we inspect GraphQL type? if (!(outputType instanceof GraphQLFieldsContainer fieldContainer)) { if (isNotScalarOrEnumType(outputType)) { - String schemaTypeName = outputType.getClass().getSimpleName(); - addSkippedType(outputType, () -> "inspection does not support " + schemaTypeName + "."); + FieldCoordinates coordinates = FieldCoordinates.coordinates(parent.getName(), field.getName()); + addSkippedType(outputType, coordinates, "Unsupported schema type"); } return; } // Can we inspect Java type? if (resolvableType.resolve(Object.class) == Object.class) { - addSkippedType(outputType, () -> "inspection could not determine the Java object return type."); + FieldCoordinates coordinates = FieldCoordinates.coordinates(parent.getName(), field.getName()); + addSkippedType(outputType, coordinates, "No Java type information"); return; } @@ -236,6 +227,9 @@ final class SchemaMappingInspector { } private ResolvableType nestForConnection(ResolvableType type) { + if (type == ResolvableType.NONE) { + return type; + } type = nestIfReactive(type); if (logger.isDebugEnabled() && type.getGenerics().length != 1) { logger.debug("Expected Connection type to have a generic parameter: " + type); @@ -256,6 +250,9 @@ final class SchemaMappingInspector { } private ResolvableType nestForList(ResolvableType type, boolean subscription) { + if (type == ResolvableType.NONE) { + return type; + } ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(type.resolve(Object.class)); if (adapter != null) { if (logger.isDebugEnabled() && adapter.isNoValue()) { @@ -266,7 +263,7 @@ final class SchemaMappingInspector { return type; } } - if (logger.isDebugEnabled() && (!type.isArray() && type.getGenerics().length != 1)) { + if (logger.isDebugEnabled() && !type.isArray() && type.getGenerics().length != 1) { logger.debug("Expected List compatible type: " + type); } return type.getNested(2); @@ -295,21 +292,21 @@ final class SchemaMappingInspector { } } - private void addSkippedType(GraphQLType type, Supplier reason) { + private void addSkippedType(GraphQLType type, FieldCoordinates coordinates, String reason) { String typeName = typeNameToString(type); - this.skippedTypes.add(typeName); + this.reportBuilder.skippedType(type, coordinates); if (logger.isDebugEnabled()) { - logger.debug("Skipped '" + typeName + "': " + reason.get()); + logger.debug("Skipped '" + typeName + "': " + reason); } } @SuppressWarnings("rawtypes") private void checkDataFetcherRegistrations() { this.runtimeWiring.getDataFetchers().forEach((typeName, registrations) -> - registrations.forEach((fieldName, fetcher) -> { + registrations.forEach((fieldName, dataFetcher) -> { FieldCoordinates coordinates = FieldCoordinates.coordinates(typeName, fieldName); if (this.schema.getFieldDefinition(coordinates) == null) { - this.unmappedDataFetchers.put(coordinates, fetcher); + this.reportBuilder.unmappedRegistration(coordinates, dataFetcher); } })); } @@ -321,9 +318,120 @@ final class SchemaMappingInspector { * @param runtimeWiring for {@code DataFetcher} registrations * @return the created report */ - public static SchemaMappingReport inspect(GraphQLSchema schema, RuntimeWiring runtimeWiring) { + public static SchemaReport inspect(GraphQLSchema schema, RuntimeWiring runtimeWiring) { return new SchemaMappingInspector(schema, runtimeWiring).getOrCreateReport(); } + /** + * Helps to build a {@link SchemaReport}. + */ + private class ReportBuilder { + + private final List unmappedFields = new ArrayList<>(); + + private final Map> unmappedRegistrations = new LinkedHashMap<>(); + + private final List skippedTypes = new ArrayList<>(); + + public void unmappedField(FieldCoordinates coordinates) { + this.unmappedFields.add(coordinates); + } + + public void unmappedRegistration(FieldCoordinates coordinates, DataFetcher dataFetcher) { + this.unmappedRegistrations.put(coordinates, dataFetcher); + } + + public void skippedType(GraphQLType type, FieldCoordinates coordinates) { + this.skippedTypes.add(new DefaultSkippedType(type, coordinates)); + } + + public SchemaReport build() { + return new DefaultSchemaReport(this.unmappedFields, this.unmappedRegistrations, this.skippedTypes); + } + + } + + + /** + * Default implementation of {@link SchemaReport}. + */ + private class DefaultSchemaReport implements SchemaReport { + + private final List unmappedFields; + + private final Map> unmappedRegistrations; + + private final List skippedTypes; + + public DefaultSchemaReport( + List unmappedFields, Map> unmappedRegistrations, + List skippedTypes) { + + this.unmappedFields = Collections.unmodifiableList(unmappedFields); + this.unmappedRegistrations = Collections.unmodifiableMap(unmappedRegistrations); + this.skippedTypes = Collections.unmodifiableList(skippedTypes); + } + + @Override + public List unmappedFields() { + return this.unmappedFields; + } + + @Override + public Map> unmappedRegistrations() { + return this.unmappedRegistrations; + } + + @Override + public List skippedTypes() { + return this.skippedTypes; + } + + @Override + public GraphQLSchema schema() { + return SchemaMappingInspector.this.schema; + } + + @Override + @Nullable + public DataFetcher dataFetcher(FieldCoordinates coordinates) { + return SchemaMappingInspector.this.runtimeWiring + .getDataFetcherForType(coordinates.getTypeName()) + .get(coordinates.getFieldName()); + } + + @Override + public String toString() { + return "GraphQL schema inspection:\n" + + "\tUnmapped fields: " + formatUnmappedFields() + "\n" + + "\tUnmapped registrations: " + this.unmappedRegistrations + "\n" + + "\tSkipped types: " + this.skippedTypes; + } + + private String formatUnmappedFields() { + MultiValueMap map = new LinkedMultiValueMap<>(); + this.unmappedFields.forEach(coordinates -> { + List fields = map.computeIfAbsent(coordinates.getTypeName(), s -> new ArrayList<>()); + fields.add(coordinates.getFieldName()); + }); + return map.toString(); + } + + } + + + /** + * Default implementation of a {@link SchemaReport.SkippedType}. + */ + private record DefaultSkippedType( + GraphQLType type, FieldCoordinates fieldCoordinates) implements SchemaReport.SkippedType { + + @Override + public String toString() { + return typeNameToString(this.type); + } + + } + } diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingReport.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingReport.java deleted file mode 100644 index a2c54d2a..00000000 --- a/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingReport.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright 2020-2023 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.graphql.execution; - - -import java.util.Map; -import java.util.Set; - -import graphql.schema.DataFetcher; -import graphql.schema.FieldCoordinates; - -import org.springframework.util.MultiValueMap; - - -/** - * The report produced as a result of schema mappings inspection. - * @param unmappedFields map with type names as keys, and unmapped field names as values - * @param unmappedDataFetchers map with unmapped {@code DataFetcher}s and their field coordinates - * @param skippedTypes the names of types skipped by the inspection - * - * @since 1.2.0 - */ -public record SchemaMappingReport( - MultiValueMap unmappedFields, - Map> unmappedDataFetchers, - Set skippedTypes) { - - @Override - public String toString() { - return "GraphQL schema inspection:\n" + - "\tUnmapped fields: " + this.unmappedFields + "\n" + - "\tUnmapped DataFetcher registrations: " + this.unmappedDataFetchers + "\n" + - "\tSkipped types: " + this.skippedTypes; - } - -} diff --git a/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaReport.java b/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaReport.java new file mode 100644 index 00000000..5a8ee775 --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaReport.java @@ -0,0 +1,99 @@ +/* + * Copyright 2020-2023 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.graphql.execution; + +import java.util.List; +import java.util.Map; + +import graphql.language.ListType; +import graphql.language.NonNullType; +import graphql.schema.DataFetcher; +import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; + +import org.springframework.lang.Nullable; + +/** + * Report produced as a result of inspecting schema mappings. + * + *

The inspection checks if schema fields are covered either by a + * {@link DataFetcher} registration, or match a Java object property. Fields + * that have neither are reported as {@link #unmappedFields()}. + * The inspection also checks if any {@code DataFetcher} are registered against + * schema fields that don't exist and reports those as {@link #unmappedRegistrations()}. + * + * @author Rossen Stoyanchev + * @since 1.2.0 + */ +public interface SchemaReport { + + /** + * Return the inspected schema with type and field definitions. + */ + GraphQLSchema schema(); + + /** + * Return the coordinates of unmapped fields. Such fields have neither a + * {@link DataFetcher} registration, such as a {@code @SchemaMapping} + * method, nor a matching Java property in the return type from the parent + * {@code DataFetcher}. + */ + List unmappedFields(); + + /** + * Return the coordinates for invalid {@link DataFetcher} registrations + * referring to fields that don't exist in the schema. + */ + Map> unmappedRegistrations(); + + /** + * Return types skipped during the inspection, either because the schema type + * is not supported, e.g. union, or because there is insufficient Java type + * information, e.g. controller method that returns {@code Object} or wrapper + * type (collection, reactive, asynchronous) with wildcard generics. + */ + List skippedTypes(); + + /** + * Return the {@code DataFetcher} for the given field coordinates, if registered. + */ + @Nullable + DataFetcher dataFetcher(FieldCoordinates coordinates); + + + /** + * Information about a schema type skipped during the inspection. + */ + interface SkippedType { + + /** + * Return the type that was skipped. This corresponds to the output type + * of the {@link #fieldCoordinates() field} where the type was + * encountered, possibly with {@link NonNullType} and {@link ListType} + * wrapper types removed. + */ + GraphQLType type(); + + /** + * Return the coordinates of the field where the type was encountered. + */ + FieldCoordinates fieldCoordinates(); + + } + +} diff --git a/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorTests.java index 5cecf44e..160d1d5d 100644 --- a/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorTests.java +++ b/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorTests.java @@ -20,10 +20,10 @@ import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.concurrent.CompletableFuture; import graphql.schema.FieldCoordinates; +import graphql.schema.GraphQLNamedType; import graphql.schema.GraphQLSchema; import graphql.schema.idl.RuntimeWiring; import graphql.schema.idl.SchemaGenerator; @@ -68,7 +68,7 @@ class SchemaMappingInspectorTests { greeting: String } """; - SchemaMappingReport report = inspectSchema(schema, EmptyController.class); + SchemaReport report = inspectSchema(schema, EmptyController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Query", "greeting"); } @@ -79,7 +79,7 @@ class SchemaMappingInspectorTests { greeting: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).isEmpty(); } @@ -96,7 +96,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -128,7 +128,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -141,7 +141,7 @@ class SchemaMappingInspectorTests { greeting: String } """; - SchemaMappingReport report = inspectSchema(schema, EmptyController.class); + SchemaReport report = inspectSchema(schema, EmptyController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Query", "greeting"); } @@ -166,7 +166,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Mutation", "createBook"); } @@ -185,7 +185,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class, BookController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -205,7 +205,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Mutation", "createBook"); } @@ -230,7 +230,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Subscription", "bookSearch"); } @@ -249,7 +249,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class, BookController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -269,7 +269,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Subscription", "bookSearch"); } @@ -291,7 +291,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -308,7 +308,7 @@ class SchemaMappingInspectorTests { fetcher: String } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -331,7 +331,7 @@ class SchemaMappingInspectorTests { lastName: String } """; - SchemaMappingReport report = inspectSchema(schema, BatchMappingBookController.class); + SchemaReport report = inspectSchema(schema, BatchMappingBookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -348,7 +348,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -359,7 +359,7 @@ class SchemaMappingInspectorTests { anything: String } """; - SchemaMappingReport report = inspectSchema(schema, GreetingController.class); + SchemaReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedDataFetcherCount(1).containsUnmappedDataFetchersFor("Query", "greeting"); } @@ -382,7 +382,7 @@ class SchemaMappingInspectorTests { missing: String } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Author", "missing"); } @@ -404,7 +404,7 @@ class SchemaMappingInspectorTests { missing: String } """; - SchemaMappingReport report = inspectSchema(schema, TeamController.class); + SchemaReport report = inspectSchema(schema, TeamController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("TeamMember", "missing"); } @@ -423,7 +423,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -444,7 +444,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schema, UnionController.class); + SchemaReport report = inspectSchema(schema, UnionController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(1).containsSkippedTypes("FooBar"); } @@ -466,7 +466,7 @@ class SchemaMappingInspectorTests { .type("Query", builder -> builder.dataFetcher("bookById", environment -> null)) .build(); - SchemaMappingReport report = SchemaMappingInspector.inspect(schema, wiring); + SchemaReport report = SchemaMappingInspector.inspect(schema, wiring); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(1).containsSkippedTypes("Book"); } @@ -482,7 +482,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingReport report = inspectSchema(schemaContent, BookController.class); + SchemaReport report = inspectSchema(schemaContent, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(1).containsSkippedTypes("Book"); } @@ -499,7 +499,7 @@ class SchemaMappingInspectorTests { .type("Query", builder -> builder.dataFetcher("greeting", environment -> null)) .build(); - SchemaMappingReport report = SchemaMappingInspector.inspect(schema, wiring); + SchemaReport report = SchemaMappingInspector.inspect(schema, wiring); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -531,18 +531,18 @@ class SchemaMappingInspectorTests { id: ID } """; - SchemaMappingReport report = inspectSchema(schema, BookController.class); + SchemaReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).hasSkippedTypeCount(0); assertThat(report.toString()).isEqualTo(""" GraphQL schema inspection: Unmapped fields: {Book=[missing]} - Unmapped DataFetcher registrations: {Book.fetcher=BookController#fetcher[1 args], Query.paginatedBooks=BookController#paginatedBooks[0 args], Query.bookObject=BookController#bookObject[1 args], Query.bookById=BookController#bookById[1 args]} + Unmapped registrations: {Book.fetcher=BookController#fetcher[1 args], Query.paginatedBooks=BookController#paginatedBooks[0 args], Query.bookObject=BookController#bookObject[1 args], Query.bookById=BookController#bookById[1 args]} Skipped types: []"""); } } - private SchemaMappingReport inspectSchema(String schemaContent, Class... controllers) { + private SchemaReport inspectSchema(String schemaContent, Class... controllers) { GraphQLSchema schema = SchemaGenerator.createdMockedSchema(schemaContent); RuntimeWiring runtimeWiring = createRuntimeWiring(controllers); return SchemaMappingInspector.inspect(schema, runtimeWiring); @@ -565,7 +565,7 @@ class SchemaMappingInspectorTests { return wiringBuilder.build(); } - static SchemaInspectionReportAssert assertThatReport(SchemaMappingReport actual) { + static SchemaInspectionReportAssert assertThatReport(SchemaReport actual) { return new SchemaInspectionReportAssert(actual); } @@ -687,9 +687,9 @@ class SchemaMappingInspectorTests { private static class SchemaInspectionReportAssert - extends AbstractAssert { + extends AbstractAssert { - public SchemaInspectionReportAssert(SchemaMappingReport actual) { + public SchemaInspectionReportAssert(SchemaReport actual) { super(actual, SchemaInspectionReportAssert.class); } @@ -698,8 +698,8 @@ class SchemaMappingInspectorTests { if (!this.actual.unmappedFields().isEmpty()) { failWithMessage("Report contains missing fields: %s", this.actual.unmappedFields()); } - if (!this.actual.unmappedDataFetchers().isEmpty()) { - failWithMessage("Report contains missing DataFetcher registrations for %s", this.actual.unmappedDataFetchers()); + if (!this.actual.unmappedRegistrations().isEmpty()) { + failWithMessage("Report contains missing DataFetcher registrations for %s", this.actual.unmappedRegistrations()); } if (!this.actual.skippedTypes().isEmpty()) { failWithMessage("Report contains skipped types: %s", this.actual.skippedTypes()); @@ -708,8 +708,7 @@ class SchemaMappingInspectorTests { public SchemaInspectionReportAssert hasUnmappedFieldCount(int expected) { isNotNull(); - Integer actual = this.actual.unmappedFields().values().stream().map(List::size).reduce(0, Integer::sum); - if (actual != expected) { + if (this.actual.unmappedFields().size() != expected) { failWithMessage("Expected %s unmapped fields, found %s.", expected, this.actual.unmappedFields()); } return this; @@ -717,7 +716,7 @@ class SchemaMappingInspectorTests { public SchemaInspectionReportAssert hasUnmappedDataFetcherCount(int expected) { isNotNull(); - if (this.actual.unmappedDataFetchers().size() != expected) { + if (this.actual.unmappedRegistrations().size() != expected) { failWithMessage("Expected %s unmapped fields, found %s.", expected, this.actual.unmappedFields()); } return this; @@ -734,8 +733,11 @@ class SchemaMappingInspectorTests { public SchemaInspectionReportAssert containsUnmappedFields(String typeName, String... fieldNames) { isNotNull(); List expected = Arrays.asList(fieldNames); - List actual = this.actual.unmappedFields().get(typeName); - if (actual == null || !actual.containsAll(expected)) { + List actual = this.actual.unmappedFields().stream() + .filter(coordinates -> coordinates.getTypeName().equals(typeName)) + .map(FieldCoordinates::getFieldName) + .toList(); + if (!actual.containsAll(expected)) { failWithMessage("Expected unmapped fields for %s: %s, found %s", typeName, expected, actual); } return this; @@ -744,18 +746,20 @@ class SchemaMappingInspectorTests { public SchemaInspectionReportAssert containsUnmappedDataFetchersFor(String typeName, String... fieldNames) { isNotNull(); List expected = Arrays.stream(fieldNames) - .map(field -> FieldCoordinates.coordinates(typeName, field)).toList(); - if (!this.actual.unmappedDataFetchers().keySet().containsAll(expected)) { - failWithMessage("Expected unmapped DataFetchers for %s, found %s", - expected, this.actual.unmappedDataFetchers()); + .map(field -> FieldCoordinates.coordinates(typeName, field)) + .toList(); + if (!this.actual.unmappedRegistrations().keySet().containsAll(expected)) { + failWithMessage("Expected unmapped DataFetchers for %s, found %s", expected, this.actual.unmappedRegistrations()); } return this; } - public SchemaInspectionReportAssert containsSkippedTypes(String... typeNames) { + public SchemaInspectionReportAssert containsSkippedTypes(String... fieldCoordinates) { isNotNull(); - List expected = Arrays.asList(typeNames); - Set actual = this.actual.skippedTypes(); + List expected = Arrays.asList(fieldCoordinates); + List actual = this.actual.skippedTypes().stream() + .map(skippedType -> ((GraphQLNamedType) skippedType.type()).getName()) + .toList(); if (!actual.containsAll(expected)) { failWithMessage("Expected skipped types: %s, found %s", expected, actual); }