Enhance SchemaReport API

See gh-672
This commit is contained in:
rstoyanchev
2023-04-28 12:14:00 +01:00
parent 7dd4ac0786
commit 7db96fd77f
5 changed files with 309 additions and 148 deletions

View File

@@ -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);
});

View File

@@ -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.
*
* <p>The schema field inspection depends on {@code DataFetcher}s to be
* {@link SelfDescribingDataFetcher} to be able to compare schema type and Java
* <p>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<String, String> unmappedFields = new LinkedMultiValueMap<>();
private final Map<FieldCoordinates, DataFetcher<?>> unmappedDataFetchers = new LinkedHashMap<>();
private final Set<String> 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<String, DataFetcher> dataFetcherMap = this.runtimeWiring.getDataFetcherForType(fields.getName());
String typeName = fieldContainer.getName();
Map<String, DataFetcher> 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<String> 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<FieldCoordinates> unmappedFields = new ArrayList<>();
private final Map<FieldCoordinates, DataFetcher<?>> unmappedRegistrations = new LinkedHashMap<>();
private final List<SchemaReport.SkippedType> 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<FieldCoordinates> unmappedFields;
private final Map<FieldCoordinates, DataFetcher<?>> unmappedRegistrations;
private final List<SchemaReport.SkippedType> skippedTypes;
public DefaultSchemaReport(
List<FieldCoordinates> unmappedFields, Map<FieldCoordinates, DataFetcher<?>> unmappedRegistrations,
List<SkippedType> skippedTypes) {
this.unmappedFields = Collections.unmodifiableList(unmappedFields);
this.unmappedRegistrations = Collections.unmodifiableMap(unmappedRegistrations);
this.skippedTypes = Collections.unmodifiableList(skippedTypes);
}
@Override
public List<FieldCoordinates> unmappedFields() {
return this.unmappedFields;
}
@Override
public Map<FieldCoordinates, DataFetcher<?>> unmappedRegistrations() {
return this.unmappedRegistrations;
}
@Override
public List<SkippedType> 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<String, String> map = new LinkedMultiValueMap<>();
this.unmappedFields.forEach(coordinates -> {
List<String> 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);
}
}
}

View File

@@ -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<String, String> unmappedFields,
Map<FieldCoordinates, DataFetcher<?>> unmappedDataFetchers,
Set<String> 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;
}
}

View File

@@ -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.
*
* <p>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<FieldCoordinates> unmappedFields();
/**
* Return the coordinates for invalid {@link DataFetcher} registrations
* referring to fields that don't exist in the schema.
*/
Map<FieldCoordinates, DataFetcher<?>> 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<SkippedType> 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();
}
}

View File

@@ -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<SchemaInspectionReportAssert, SchemaMappingReport> {
extends AbstractAssert<SchemaInspectionReportAssert, SchemaReport> {
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<String> expected = Arrays.asList(fieldNames);
List<String> actual = this.actual.unmappedFields().get(typeName);
if (actual == null || !actual.containsAll(expected)) {
List<String> 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<FieldCoordinates> 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<String> expected = Arrays.asList(typeNames);
Set<String> actual = this.actual.skippedTypes();
List<String> expected = Arrays.asList(fieldCoordinates);
List<String> 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);
}