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 ffb94dee..4520d663 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(); - SchemaMappingInspector.Report report = SchemaMappingInspector.inspect(schema, runtimeWiring); + SchemaMappingReport 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 37eb6b99..5b2bb571 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 @@ -54,7 +54,7 @@ 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 Report Resport}. The inspection also + * "unmapped" in the resulting {@link SchemaMappingReport}. The inspection also * performs a reverse check for {@code DataFetcher} registrations against schema * fields that don't exist. * @@ -82,7 +82,7 @@ import org.springframework.util.MultiValueMap; * @author Rossen Stoyanchev * @since 1.2.0 */ -class SchemaMappingInspector { +final class SchemaMappingInspector { private static final Log logger = LogFactory.getLog(SchemaMappingInspector.class); @@ -91,12 +91,19 @@ class SchemaMappingInspector { private final RuntimeWiring runtimeWiring; - private final ReportBuilder reportBuilder = new ReportBuilder(); - private final Set inspectedTypes = new HashSet<>(); private final ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); + private final MultiValueMap unmappedFields = new LinkedMultiValueMap<>(); + + private final Map> unmappedDataFetchers = new LinkedHashMap<>(); + + private final Set skippedTypes = new LinkedHashSet<>(); + + @Nullable + private SchemaMappingReport report; + private SchemaMappingInspector(GraphQLSchema schema, RuntimeWiring runtimeWiring) { Assert.notNull(schema, "GraphQLSchema is required"); @@ -107,34 +114,41 @@ class SchemaMappingInspector { /** - * Inspect all fields, starting from Query, Mutation, and Subscription, and - * working recursively down through the types they return. - * @return a report with unmapped fields and skipped types. + * Perform an inspection and create a {@link SchemaMappingReport}. + * The inspection is one once only, during the first call to this method. */ - public Report inspect() { + public SchemaMappingReport getOrCreateReport() { + if (this.report == null) { + checkSchema(); + checkDataFetcherRegistrations(); + this.report = new SchemaMappingReport( + this.unmappedFields, this.unmappedDataFetchers, this.skippedTypes); + } + return this.report; + } + + private void checkSchema() { + + checkFieldsContainer(this.schema.getQueryType(), null); - inspectFields(this.schema.getQueryType(), null); if (this.schema.isSupportingMutations()) { - inspectFields(this.schema.getMutationType(), null); + checkFieldsContainer(this.schema.getMutationType(), null); } + if (this.schema.isSupportingSubscriptions()) { - inspectFields(this.schema.getSubscriptionType(), null); + checkFieldsContainer(this.schema.getSubscriptionType(), null); } - - inspectDataFetcherRegistrations(); - - return this.reportBuilder.build(); } /** - * Inspect the given {@code GraphQLFieldsContainer} check against {@code DataFetcher} - * registrations, or Java properties in the given {@code ResolvableType}. - * @param fields the GraphQL schema type to inspect + * 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 resolvableType the Java type to match against, or {@code null} if * not applicable such as for Query, Mutation, or Subscription */ @SuppressWarnings("rawtypes") - private void inspectFields(GraphQLFieldsContainer fields, @Nullable ResolvableType resolvableType) { + private void checkFieldsContainer(GraphQLFieldsContainer fields, @Nullable ResolvableType resolvableType) { Map dataFetcherMap = this.runtimeWiring.getDataFetcherForType(fields.getName()); @@ -143,7 +157,7 @@ class SchemaMappingInspector { if (dataFetcherMap.containsKey(fieldName)) { DataFetcher fetcher = dataFetcherMap.get(fieldName); if (fetcher instanceof SelfDescribingDataFetcher selfDescribingDataFetcher) { - inspectFieldType( + checkFieldType( field.getType(), selfDescribingDataFetcher.getReturnType(), (fields == this.schema.getSubscriptionType())); } @@ -153,18 +167,18 @@ class SchemaMappingInspector { } } else if (resolvableType == null || !hasProperty(resolvableType, fieldName)) { - this.reportBuilder.addUnmappedField(fields.getName(), fieldName); + this.unmappedFields.add(fields.getName(), fieldName); } } } /** - * Inspect the output {@link GraphQLType} of a field. + * 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 */ - private void inspectFieldType(GraphQLType outputType, ResolvableType resolvableType, boolean isSubscriptionField) { + private void checkFieldType(GraphQLType outputType, ResolvableType resolvableType, boolean isSubscriptionField) { // Remove GraphQL type wrappers, and nest within Java generic types outputType = unwrapIfNonNull(outputType); @@ -201,7 +215,7 @@ class SchemaMappingInspector { } // Nest within the - inspectFields(fieldContainer, resolvableType); + checkFieldsContainer(fieldContainer, resolvableType); } private GraphQLType unwrapIfNonNull(GraphQLType type) { @@ -283,19 +297,19 @@ class SchemaMappingInspector { private void addSkippedType(GraphQLType type, Supplier reason) { String typeName = typeNameToString(type); - this.reportBuilder.addSkippedType(typeName); + this.skippedTypes.add(typeName); if (logger.isDebugEnabled()) { logger.debug("Skipped '" + typeName + "': " + reason.get()); } } @SuppressWarnings("rawtypes") - private void inspectDataFetcherRegistrations() { + private void checkDataFetcherRegistrations() { this.runtimeWiring.getDataFetchers().forEach((typeName, registrations) -> registrations.forEach((fieldName, fetcher) -> { FieldCoordinates coordinates = FieldCoordinates.coordinates(typeName, fieldName); if (this.schema.getFieldDefinition(coordinates) == null) { - this.reportBuilder.addUnmappedDataFetcher(coordinates, fetcher); + this.unmappedDataFetchers.put(coordinates, fetcher); } })); } @@ -307,73 +321,9 @@ class SchemaMappingInspector { * @param runtimeWiring for {@code DataFetcher} registrations * @return the created report */ - public static Report inspect(GraphQLSchema schema, RuntimeWiring runtimeWiring) { - SchemaMappingInspector inspector = new SchemaMappingInspector(schema, runtimeWiring); - return inspector.inspect(); + public static SchemaMappingReport inspect(GraphQLSchema schema, RuntimeWiring runtimeWiring) { + return new SchemaMappingInspector(schema, runtimeWiring).getOrCreateReport(); } - - /** - * 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 - */ - public record Report( - 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; - } - } - - - /** - * Builder for a {@link Report}. - */ - private static class ReportBuilder { - - private final MultiValueMap unmappedFields = new LinkedMultiValueMap<>(); - - private final Map> unmappedDataFetchers = new LinkedHashMap<>(); - - private final Set skippedTypes = new LinkedHashSet<>(); - - /** - * Add an unmapped field. - */ - public void addUnmappedField(String typeName, String fieldName) { - this.unmappedFields.add(typeName, fieldName); - } - - /** - * Add an unmapped {@code DataFetcher} registration. - */ - public void addUnmappedDataFetcher(FieldCoordinates coordinates, DataFetcher dataFetcher) { - this.unmappedDataFetchers.put(coordinates, dataFetcher); - } - - /** - * Add a skipped type name. - */ - public void addSkippedType(String typeName) { - this.skippedTypes.add(typeName); - } - - public Report build() { - return new Report( - new LinkedMultiValueMap<>(this.unmappedFields), - new LinkedHashMap<>(this.unmappedDataFetchers), - new LinkedHashSet<>(this.skippedTypes)); - } - - } - } 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 new file mode 100644 index 00000000..a2c54d2a --- /dev/null +++ b/spring-graphql/src/main/java/org/springframework/graphql/execution/SchemaMappingReport.java @@ -0,0 +1,50 @@ +/* + * 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/test/java/org/springframework/graphql/execution/SchemaMappingInspectorTests.java b/spring-graphql/src/test/java/org/springframework/graphql/execution/SchemaMappingInspectorTests.java index 781d4a61..5cecf44e 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 @@ -68,7 +68,7 @@ class SchemaMappingInspectorTests { greeting: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, EmptyController.class); + SchemaMappingReport report = inspectSchema(schema, EmptyController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Query", "greeting"); } @@ -79,7 +79,7 @@ class SchemaMappingInspectorTests { greeting: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).isEmpty(); } @@ -96,7 +96,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -128,7 +128,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -141,7 +141,7 @@ class SchemaMappingInspectorTests { greeting: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, EmptyController.class); + SchemaMappingReport report = inspectSchema(schema, EmptyController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Query", "greeting"); } @@ -166,7 +166,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Mutation", "createBook"); } @@ -185,7 +185,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class, BookController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -205,7 +205,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Mutation", "createBook"); } @@ -230,7 +230,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Subscription", "bookSearch"); } @@ -249,7 +249,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class, BookController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -269,7 +269,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Subscription", "bookSearch"); } @@ -291,7 +291,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -308,7 +308,7 @@ class SchemaMappingInspectorTests { fetcher: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -331,7 +331,7 @@ class SchemaMappingInspectorTests { lastName: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BatchMappingBookController.class); + SchemaMappingReport report = inspectSchema(schema, BatchMappingBookController.class); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -348,7 +348,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -359,7 +359,7 @@ class SchemaMappingInspectorTests { anything: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, GreetingController.class); + SchemaMappingReport report = inspectSchema(schema, GreetingController.class); assertThatReport(report).hasUnmappedDataFetcherCount(1).containsUnmappedDataFetchersFor("Query", "greeting"); } @@ -382,7 +382,7 @@ class SchemaMappingInspectorTests { missing: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Author", "missing"); } @@ -404,7 +404,7 @@ class SchemaMappingInspectorTests { missing: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, TeamController.class); + SchemaMappingReport report = inspectSchema(schema, TeamController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("TeamMember", "missing"); } @@ -423,7 +423,7 @@ class SchemaMappingInspectorTests { missing: Boolean } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).containsUnmappedFields("Book", "missing"); } @@ -444,7 +444,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schema, UnionController.class); + SchemaMappingReport 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(); - SchemaMappingInspector.Report report = SchemaMappingInspector.inspect(schema, wiring); + SchemaMappingReport report = SchemaMappingInspector.inspect(schema, wiring); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(1).containsSkippedTypes("Book"); } @@ -482,7 +482,7 @@ class SchemaMappingInspectorTests { name: String } """; - SchemaMappingInspector.Report report = inspectSchema(schemaContent, BookController.class); + SchemaMappingReport 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(); - SchemaMappingInspector.Report report = SchemaMappingInspector.inspect(schema, wiring); + SchemaMappingReport report = SchemaMappingInspector.inspect(schema, wiring); assertThatReport(report).hasUnmappedFieldCount(0).hasSkippedTypeCount(0); } @@ -531,7 +531,7 @@ class SchemaMappingInspectorTests { id: ID } """; - SchemaMappingInspector.Report report = inspectSchema(schema, BookController.class); + SchemaMappingReport report = inspectSchema(schema, BookController.class); assertThatReport(report).hasUnmappedFieldCount(1).hasSkippedTypeCount(0); assertThat(report.toString()).isEqualTo(""" GraphQL schema inspection: @@ -542,7 +542,7 @@ class SchemaMappingInspectorTests { } - private SchemaMappingInspector.Report inspectSchema(String schemaContent, Class... controllers) { + private SchemaMappingReport 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(SchemaMappingInspector.Report actual) { + static SchemaInspectionReportAssert assertThatReport(SchemaMappingReport actual) { return new SchemaInspectionReportAssert(actual); } @@ -687,9 +687,9 @@ class SchemaMappingInspectorTests { private static class SchemaInspectionReportAssert - extends AbstractAssert { + extends AbstractAssert { - public SchemaInspectionReportAssert(SchemaMappingInspector.Report actual) { + public SchemaInspectionReportAssert(SchemaMappingReport actual) { super(actual, SchemaInspectionReportAssert.class); }