diff --git a/src/jmh/java/benchmark/InputObjectHasUnbreakableCycleBenchmark.java b/src/jmh/java/benchmark/InputObjectHasUnbreakableCycleBenchmark.java new file mode 100644 index 000000000..8d1320972 --- /dev/null +++ b/src/jmh/java/benchmark/InputObjectHasUnbreakableCycleBenchmark.java @@ -0,0 +1,112 @@ +package benchmark; + +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLTypeVisitor; +import graphql.schema.SchemaTraverser; +import graphql.schema.idl.FastSchemaGenerator; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import graphql.schema.validation.InputObjectHasUnbreakableCycle; +import graphql.schema.validation.SchemaValidationError; +import graphql.schema.validation.SchemaValidationErrorCollector; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.BenchmarkMode; +import org.openjdk.jmh.annotations.Fork; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Measurement; +import org.openjdk.jmh.annotations.Mode; +import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jmh.infra.Blackhole; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** Measures full-schema traversal with only the input object cycle validator. */ +@State(Scope.Thread) +@BenchmarkMode(Mode.AverageTime) +@Warmup(iterations = 2, time = 5) +@Measurement(iterations = 3) +@Fork(2) +@OutputTimeUnit(TimeUnit.MICROSECONDS) +public class InputObjectHasUnbreakableCycleBenchmark { + + private GraphQLSchema twoTypeMixedInputCycle; + private GraphQLSchema tenTypeMixedInputCycle; + private GraphQLSchema tenTypeTenFieldsInputCycle; + private GraphQLSchema finiteValuePropagation; + private SchemaTraverser schemaTraverser; + private List cycleValidators; + private SchemaValidationErrorCollector errorCollector; + private Map, Object> rootVariables; + + @Setup(Level.Trial) + public void setUpSchemas() { + twoTypeMixedInputCycle = loadSchema( + "InputObjectHasUnbreakableCycleBenchmark/twoTypeMixedInputCycle.graphqls" + ); + tenTypeMixedInputCycle = loadSchema( + "InputObjectHasUnbreakableCycleBenchmark/tenTypeMixedInputCycle.graphqls" + ); + tenTypeTenFieldsInputCycle = loadSchema( + "InputObjectHasUnbreakableCycleBenchmark/tenTypeTenFieldsInputCycle.graphqls" + ); + finiteValuePropagation = loadSchema( + "InputObjectHasUnbreakableCycleBenchmark/finiteValuePropagation.graphqls" + ); + schemaTraverser = new SchemaTraverser(); + } + + @Setup(Level.Invocation) + public void setUpInvocation() { + cycleValidators = Collections.singletonList(new InputObjectHasUnbreakableCycle()); + errorCollector = new SchemaValidationErrorCollector(); + rootVariables = new LinkedHashMap<>(); + rootVariables.put(SchemaValidationErrorCollector.class, errorCollector); + } + + @Benchmark + public void twoTypeMixedInputCycle(Blackhole blackhole) { + validate(twoTypeMixedInputCycle, blackhole); + } + + @Benchmark + public void tenTypeMixedInputCycle(Blackhole blackhole) { + validate(tenTypeMixedInputCycle, blackhole); + } + + @Benchmark + public void tenTypeTenFieldsInputCycle(Blackhole blackhole) { + validate(tenTypeTenFieldsInputCycle, blackhole); + } + + @Benchmark + public void finiteValuePropagation(Blackhole blackhole) { + validate(finiteValuePropagation, blackhole); + } + + private void validate(GraphQLSchema schema, Blackhole blackhole) { + schemaTraverser.depthFirstFullSchema(cycleValidators, schema, rootVariables); + for (SchemaValidationError error : errorCollector.getErrors()) { + blackhole.consume(error); + } + } + + private GraphQLSchema loadSchema(String schemaPath) { + String schemaString = BenchmarkUtils.loadResource(schemaPath); + TypeDefinitionRegistry typeRegistry = new SchemaParser().parse(schemaString); + return new FastSchemaGenerator().makeExecutableSchema( + SchemaGenerator.Options.defaultOptions().withValidation(false), + typeRegistry, + RuntimeWiring.MOCKED_WIRING + ); + } +} diff --git a/src/main/java/graphql/schema/validation/InputObjectHasUnbreakableCycle.java b/src/main/java/graphql/schema/validation/InputObjectHasUnbreakableCycle.java new file mode 100644 index 000000000..073869819 --- /dev/null +++ b/src/main/java/graphql/schema/validation/InputObjectHasUnbreakableCycle.java @@ -0,0 +1,336 @@ +package graphql.schema.validation; + +import graphql.Internal; +import graphql.collect.ImmutableKit; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.lang.String.format; + +/** + * Validates that every input object type can be provided a finite value. + */ +@Internal +public class InputObjectHasUnbreakableCycle extends GraphQLTypeVisitorStub { + + /** + * The fields on each input object that lead to another input object and can keep a cycle going. + * For example, given these types: + * input A @oneOf { b: B, value: Int } + * input B { value: Int } + * This contains only A.b for A. + */ + private final Map> targetFields = new LinkedHashMap<>(); + + /** + * The input object reached by each target field. + * For example, given these types: + * input A @oneOf { b: B } + * input B { value: Int } + * A.b maps to B. + */ + private final Map fieldTargets = new LinkedHashMap<>(); + + /** + * The input objects that point to a given input object. + * For example, given these types: + * input A { b: B! } + * input B { value: Int } + * B lists A as a dependent. + */ + private final Map> dependents = new LinkedHashMap<>(); + + /** The total number of fields on each input object, including scalar, enum, and list fields. */ + private final Map fieldCounts = new LinkedHashMap<>(); + + /** + * For each regular input object, the number of required input object fields + * that are not yet known to have a finite value. + */ + private final Map unresolvedTargetCounts = new LinkedHashMap<>(); + + /** The input objects for which a finite value is known to exist. */ + private final Set typesWithFiniteValues = new LinkedHashSet<>(); + + /** Input objects newly known to have a finite value, but whose dependents have not yet been updated. */ + private final Deque typesToPropagate = new ArrayDeque<>(); + + /** + * True after this validator has checked the whole schema. + * The schema visitor calls this validator once for each input object, but the first call checks every input + * object in the schema. This prevents the same work from running again on later calls. + */ + private boolean complete; + + @Override + public TraversalControl visitGraphQLInputObjectType( + GraphQLInputObjectType inputObjectType, + TraverserContext context + ) { + // This validator runs once per schema. Continue if we've already run. + if (complete) { + return TraversalControl.CONTINUE; + } + complete = true; + + GraphQLSchema schema = context.getVarFromParents(GraphQLSchema.class); + SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); + List inputObjectTypes = getInputObjectTypes(schema); + detectInputObjectNonFiniteValues(inputObjectTypes, errorCollector); + return TraversalControl.CONTINUE; + } + + private List getInputObjectTypes(GraphQLSchema schema) { + return ImmutableKit.filterAndMap( + schema.getTypeMap().values(), + GraphQLInputObjectType.class::isInstance, + GraphQLInputObjectType.class::cast + ); + } + + private void detectInputObjectNonFiniteValues( + List inputObjectTypes, + SchemaValidationErrorCollector errorCollector + ) { + /* + * Finds input objects that cannot be given a finite value. + * + * Start with types known to have a finite value, then repeatedly mark types that become finite because of them. + * Once no more types can be marked, report cycles among the remaining types. + */ + + // Prepare a place to store the state of each input object. + initializeInputObjectStates(inputObjectTypes); + + // Record the input object fields that can keep a cycle going. + recordFiniteValueTargets(inputObjectTypes); + + // Start with the input objects that clearly have a finite value. + initializeFiniteValueWorklist(inputObjectTypes); + + // Use those finite values to find other input objects with finite values. + propagateFiniteValues(); + + // Report cycles among the input objects that are still not known to have a finite value. + reportUnbreakableCycles(inputObjectTypes, errorCollector); + } + + private void initializeInputObjectStates(List inputObjectTypes) { + for (GraphQLInputObjectType inputObjectType : inputObjectTypes) { + targetFields.put(inputObjectType, new ArrayList<>()); + dependents.put(inputObjectType, new ArrayList<>()); + unresolvedTargetCounts.put(inputObjectType, 0); + } + } + + private void recordFiniteValueTargets(List inputObjectTypes) { + for (GraphQLInputObjectType inputObjectType : inputObjectTypes) { + List fields = inputObjectType.getFieldDefinitions(); + fieldCounts.put(inputObjectType, fields.size()); + for (GraphQLInputObjectField field : fields) { + recordFiniteValueTarget(inputObjectType, field); + } + } + } + + private void recordFiniteValueTarget( + GraphQLInputObjectType inputObjectType, + GraphQLInputObjectField field + ) { + GraphQLInputObjectType target = getFiniteValueTarget(inputObjectType, field.getType()); + if (target == null) { + return; + } + + targetFields.get(inputObjectType).add(field); + fieldTargets.put(field, target); + List targetDependents = dependents.get(target); + if (targetDependents == null) { + return; + } + targetDependents.add(inputObjectType); + } + + private void initializeFiniteValueWorklist(List inputObjectTypes) { + for (GraphQLInputObjectType inputObjectType : inputObjectTypes) { + List targets = targetFields.get(inputObjectType); + if (inputObjectType.isOneOf()) { + initializeOneOfFiniteValue(inputObjectType, targets); + continue; + } + + unresolvedTargetCounts.put(inputObjectType, targets.size()); + if (targets.isEmpty()) { + markInputObjectHasFiniteValue(inputObjectType); + } + } + } + + private void initializeOneOfFiniteValue( + GraphQLInputObjectType inputObjectType, + List targets + ) { + int fieldCount = fieldCounts.get(inputObjectType); + if (fieldCount == 0 || targets.size() < fieldCount) { + markInputObjectHasFiniteValue(inputObjectType); + } + } + + private void propagateFiniteValues() { + while (!typesToPropagate.isEmpty()) { + GraphQLInputObjectType finiteType = typesToPropagate.removeLast(); + for (GraphQLInputObjectType dependent : dependents.get(finiteType)) { + propagateFiniteValueToDependent(dependent); + } + } + } + + private void propagateFiniteValueToDependent(GraphQLInputObjectType dependent) { + if (typesWithFiniteValues.contains(dependent)) { + return; + } + if (dependent.isOneOf()) { + markInputObjectHasFiniteValue(dependent); + return; + } + + int unresolvedTargetCount = unresolvedTargetCounts.get(dependent) - 1; + unresolvedTargetCounts.put(dependent, unresolvedTargetCount); + if (unresolvedTargetCount == 0) { + markInputObjectHasFiniteValue(dependent); + } + } + + private void markInputObjectHasFiniteValue(GraphQLInputObjectType inputObjectType) { + if (!typesWithFiniteValues.add(inputObjectType)) { + return; + } + typesToPropagate.add(inputObjectType); + } + + @Nullable + private GraphQLInputObjectType getFiniteValueTarget( + GraphQLInputObjectType inputObjectType, + GraphQLInputType fieldType + ) { + if (inputObjectType.isOneOf()) { + if (fieldType instanceof GraphQLInputObjectType) { + return (GraphQLInputObjectType) fieldType; + } + return null; + } + if (!(fieldType instanceof GraphQLNonNull)) { + return null; + } + + GraphQLType nullableType = ((GraphQLNonNull) fieldType).getWrappedType(); + if (nullableType instanceof GraphQLInputObjectType) { + return (GraphQLInputObjectType) nullableType; + } + return null; + } + + private void reportUnbreakableCycles( + List inputObjectTypes, + SchemaValidationErrorCollector errorCollector + ) { + Set visitedTypes = new LinkedHashSet<>(); + List fieldPath = new ArrayList<>(); + Map fieldPathIndexByType = new LinkedHashMap<>(); + for (GraphQLInputObjectType inputObjectType : inputObjectTypes) { + if (typesWithFiniteValues.contains(inputObjectType)) { + continue; + } + reportCycleRecursive( + inputObjectType, + visitedTypes, + fieldPath, + fieldPathIndexByType, + errorCollector + ); + } + } + + private void reportCycleRecursive( + GraphQLInputObjectType inputObjectType, + Set visitedTypes, + List fieldPath, + Map fieldPathIndexByType, + SchemaValidationErrorCollector errorCollector + ) { + if (!visitedTypes.add(inputObjectType)) { + return; + } + fieldPathIndexByType.put(inputObjectType, fieldPath.size()); + + for (GraphQLInputObjectField field : targetFields.get(inputObjectType)) { + reportTargetCycle( + inputObjectType, + field, + visitedTypes, + fieldPath, + fieldPathIndexByType, + errorCollector + ); + } + fieldPathIndexByType.remove(inputObjectType); + } + + private void reportTargetCycle( + GraphQLInputObjectType inputObjectType, + GraphQLInputObjectField field, + Set visitedTypes, + List fieldPath, + Map fieldPathIndexByType, + SchemaValidationErrorCollector errorCollector + ) { + GraphQLInputObjectType target = fieldTargets.get(field); + if (!unresolvedTargetCounts.containsKey(target) || typesWithFiniteValues.contains(target)) { + return; + } + + Integer cycleIndex = fieldPathIndexByType.get(target); + fieldPath.add(inputObjectType.getName() + "." + field.getName()); + if (cycleIndex == null) { + reportCycleRecursive(target, visitedTypes, fieldPath, fieldPathIndexByType, errorCollector); + } else { + addCycleError(target, cycleIndex, fieldPath, errorCollector); + } + fieldPath.remove(fieldPath.size() - 1); + } + + private void addCycleError( + GraphQLInputObjectType inputObjectType, + int cycleIndex, + List fieldPath, + SchemaValidationErrorCollector errorCollector + ) { + List cyclePath = fieldPath.subList(cycleIndex, fieldPath.size()); + String message = format( + "Input Object %s cannot be provided a finite value because it references itself through fields: %s.", + inputObjectType.getName(), + String.join(", ", cyclePath) + ); + errorCollector.addError( + new SchemaValidationError(SchemaValidationErrorType.UnbrokenInputCycle, message) + ); + } +} diff --git a/src/main/java/graphql/schema/validation/NoUnbrokenInputCycles.java b/src/main/java/graphql/schema/validation/NoUnbrokenInputCycles.java deleted file mode 100644 index 9eb061d07..000000000 --- a/src/main/java/graphql/schema/validation/NoUnbrokenInputCycles.java +++ /dev/null @@ -1,84 +0,0 @@ -package graphql.schema.validation; - -import graphql.Internal; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLNonNull; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeVisitorStub; -import graphql.util.TraversalControl; -import graphql.util.TraverserContext; - -import java.util.ArrayList; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import static graphql.schema.GraphQLTypeUtil.isList; -import static graphql.schema.GraphQLTypeUtil.isNonNull; -import static graphql.schema.GraphQLTypeUtil.unwrapAll; - -/** - * Schema validation rule ensuring no input type forms an unbroken non-nullable recursion, - * as such a type would be impossible to satisfy - */ -@Internal -public class NoUnbrokenInputCycles extends GraphQLTypeVisitorStub { - - @Override - public TraversalControl visitGraphQLFieldDefinition(GraphQLFieldDefinition fieldDef, TraverserContext context) { - SchemaValidationErrorCollector validationErrorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); - for (GraphQLArgument argument : fieldDef.getArguments()) { - GraphQLInputType argumentType = argument.getType(); - if (argumentType instanceof GraphQLInputObjectType) { - List path = new ArrayList<>(); - check((GraphQLInputObjectType) argumentType, new LinkedHashSet<>(), path, validationErrorCollector); - } - } - return TraversalControl.CONTINUE; - } - - private void check(GraphQLInputObjectType type, Set seen, List path, SchemaValidationErrorCollector validationErrorCollector) { - if (seen.contains(type)) { - validationErrorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.UnbrokenInputCycle, getErrorMessage(path))); - return; - } - seen.add(type); - - for (GraphQLInputObjectField field : type.getFieldDefinitions()) { - if (isNonNull(field.getType())) { - GraphQLType unwrapped = unwrapNonNull((GraphQLNonNull) field.getType()); - if (unwrapped instanceof GraphQLInputObjectType) { - path = new ArrayList<>(path); - path.add(field.getName() + "!"); - check((GraphQLInputObjectType) unwrapped, new LinkedHashSet<>(seen), path, validationErrorCollector); - } - } - } - } - - private GraphQLType unwrapNonNull(GraphQLNonNull type) { - GraphQLType wrappedType = type.getWrappedType(); - if (isList(wrappedType)) { - return wrappedType; - } - return unwrapAll(wrappedType); - } - - private String getErrorMessage(List path) { - StringBuilder message = new StringBuilder(); - message.append("["); - for (int i = 0; i < path.size(); i++) { - if (i != 0) { - message.append("."); - } - message.append(path.get(i)); - } - message.append("] forms an unsatisfiable cycle"); - return message.toString(); - } -} diff --git a/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java b/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java index b41a7e570..87e39fc32 100644 --- a/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java +++ b/src/main/java/graphql/schema/validation/OneOfInputObjectRules.java @@ -4,16 +4,11 @@ import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; import graphql.schema.GraphQLTypeVisitorStub; -import graphql.schema.GraphQLUnmodifiedType; import graphql.util.TraversalControl; import graphql.util.TraverserContext; -import java.util.LinkedHashSet; -import java.util.Set; - import static java.lang.String.format; /* @@ -24,45 +19,6 @@ @ExperimentalApi public class OneOfInputObjectRules extends GraphQLTypeVisitorStub { - @Override - public TraversalControl visitGraphQLInputObjectType(GraphQLInputObjectType inputObjectType, TraverserContext context) { - if (!inputObjectType.isOneOf()) { - return TraversalControl.CONTINUE; - } - SchemaValidationErrorCollector errorCollector = context.getVarFromParents(SchemaValidationErrorCollector.class); - if (!canBeProvidedAFiniteValue(inputObjectType, new LinkedHashSet<>())) { - String message = format("OneOf Input Object %s must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle.", inputObjectType.getName()); - errorCollector.addError(new SchemaValidationError(SchemaValidationErrorType.OneOfNotInhabited, message)); - } - return TraversalControl.CONTINUE; - } - - private boolean canBeProvidedAFiniteValue(GraphQLInputObjectType oneOfInputObject, Set visited) { - if (visited.contains(oneOfInputObject)) { - return false; - } - Set nextVisited = new LinkedHashSet<>(visited); - nextVisited.add(oneOfInputObject); - for (GraphQLInputObjectField field : oneOfInputObject.getFieldDefinitions()) { - GraphQLType fieldType = field.getType(); - if (GraphQLTypeUtil.isList(fieldType)) { - return true; - } - GraphQLUnmodifiedType namedFieldType = GraphQLTypeUtil.unwrapAll(fieldType); - if (!(namedFieldType instanceof GraphQLInputObjectType)) { - return true; - } - GraphQLInputObjectType inputFieldType = (GraphQLInputObjectType) namedFieldType; - if (!inputFieldType.isOneOf()) { - return true; - } - if (canBeProvidedAFiniteValue(inputFieldType, nextVisited)) { - return true; - } - } - return false; - } - @Override public TraversalControl visitGraphQLInputObjectField(GraphQLInputObjectField inputObjectField, TraverserContext context) { GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) context.getParentNode(); diff --git a/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java b/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java index b7ce1cc63..d8b473928 100644 --- a/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java +++ b/src/main/java/graphql/schema/validation/SchemaValidationErrorType.java @@ -22,7 +22,6 @@ public enum SchemaValidationErrorType implements SchemaValidationErrorClassifica InputTypeUsedInOutputTypeContext, OneOfDefaultValueOnField, OneOfNonNullableField, - OneOfNotInhabited, RequiredInputFieldCannotBeDeprecated, RequiredFieldArgumentCannotBeDeprecated, RequiredDirectiveArgumentCannotBeDeprecated, diff --git a/src/main/java/graphql/schema/validation/SchemaValidator.java b/src/main/java/graphql/schema/validation/SchemaValidator.java index fac409378..e869a7c1a 100644 --- a/src/main/java/graphql/schema/validation/SchemaValidator.java +++ b/src/main/java/graphql/schema/validation/SchemaValidator.java @@ -18,7 +18,7 @@ public class SchemaValidator { private final List rules = new ArrayList<>(); public SchemaValidator() { - rules.add(new NoUnbrokenInputCycles()); + rules.add(new InputObjectHasUnbreakableCycle()); rules.add(new NoDefaultValueCircularRefs()); rules.add(new TypesImplementInterfaces()); rules.add(new TypeAndFieldRule()); diff --git a/src/test/groovy/graphql/schema/validation/InputObjectHasUnbreakableCycleTest.groovy b/src/test/groovy/graphql/schema/validation/InputObjectHasUnbreakableCycleTest.groovy new file mode 100644 index 000000000..bff1155d3 --- /dev/null +++ b/src/test/groovy/graphql/schema/validation/InputObjectHasUnbreakableCycleTest.groovy @@ -0,0 +1,563 @@ +package graphql.schema.validation + +import graphql.TestUtil +import graphql.schema.idl.SchemaGenerator +import graphql.schema.idl.SchemaParser +import spock.lang.Specification + +/** + * Tests for the unified InputObjectHasUnbreakableCycle validation rule, + * which implements the spec's InputObjectHasUnbreakableCycle algorithm. + * + * Ported from graphql-js PR #4564 and the existing graphql-java tests. + * + * @see graphql-spec PR #1211 + * @see graphql-js PR #4564 + */ +class InputObjectHasUnbreakableCycleTest extends Specification { + + // ------------------------------------------------------------------ + // Valid schemas: breakable cycles and inhabited types + // ------------------------------------------------------------------ + + def "accepts an Input Object with breakable circular reference"() { + def sdl = """ + type Query { f(arg: SomeInputObject): String } + input SomeInputObject { + self: SomeInputObject + arrayOfSelf: [SomeInputObject] + nonNullArrayOfSelf: [SomeInputObject]! + nonNullArrayOfNonNullSelf: [SomeInputObject!]! + intermediateSelf: AnotherInputObject + } + input AnotherInputObject { + parent: SomeInputObject + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf Input Object with a scalar field"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { a: Int } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf Input Object with a recursive list field"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { a: [A] } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf Input Object referencing a non-OneOf input object"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B } + input B { x: Int } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf Input Object referencing an already checked input object"() { + def sdl = """ + type Query { f(arg: A): String } + input B { value: Int } + input A @oneOf { b: B } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf Input Object with multiple acyclic input object fields"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B, c: C } + input B { value: Int } + input C { value: Int } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf/OneOf cycle with a scalar escape"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B, escape: Int } + input B @oneOf { a: A } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf/non-OneOf cycle with a nullable escape"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B } + input B { a: A } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a non-OneOf/non-OneOf cycle with a nullable escape"() { + def sdl = """ + type Query { f(arg: A): String } + input A { b: B! } + input B { a: A } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a non-OneOf/non-OneOf cycle with a list escape"() { + def sdl = """ + type Query { f(arg: A): String } + input A { b: [B!]! } + input B { a: A! } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf/non-OneOf with scalar escape"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B, escape: Int } + input B { a: A! } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts multiple fields with chained oneOf escape"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B, c: C } + input B @oneOf { a: A } + input C @oneOf { a: A, escape: String } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf with enum field"() { + def sdl = """ + type Query { f(arg: A): String } + enum Color { RED GREEN BLUE } + input A @oneOf { a: Color } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts oneOf with scalar fields"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { a: String, b: Int } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts mutually referencing oneOf types with scalar escape"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B } + input B @oneOf { a: A, escape: Int } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts oneOf referencing non-oneOf with back-reference"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: RegularInput } + input RegularInput { back: A } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + def "accepts a OneOf Input Object with a non-null recursive list field"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { a: [A!] } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + noExceptionThrown() + } + + // ------------------------------------------------------------------ + // Invalid schemas: unbreakable cycles + // ------------------------------------------------------------------ + + def "rejects an Input Object with non-breakable self-reference"() { + def sdl = """ + type Query { f(arg: SomeInputObject): String } + input SomeInputObject { + nonNullSelf: SomeInputObject! + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object SomeInputObject cannot be provided a finite value because it references itself through fields: SomeInputObject.nonNullSelf." + } + + def "rejects Input Objects with non-breakable circular reference spread across them"() { + def sdl = """ + type Query { f(arg: SomeInputObject): String } + input SomeInputObject { + startLoop: AnotherInputObject! + } + input AnotherInputObject { + nextInLoop: YetAnotherInputObject! + } + input YetAnotherInputObject { + closeLoop: SomeInputObject! + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object AnotherInputObject cannot be provided a finite value because it references itself through fields: AnotherInputObject.nextInLoop, YetAnotherInputObject.closeLoop, SomeInputObject.startLoop." + } + + def "rejects Input Objects with multiple non-breakable circular references"() { + def sdl = """ + type Query { f(arg: SomeInputObject): String } + input SomeInputObject { + startLoop: AnotherInputObject! + } + input AnotherInputObject { + closeLoop: SomeInputObject! + startSecondLoop: YetAnotherInputObject! + } + input YetAnotherInputObject { + closeSecondLoop: AnotherInputObject! + nonNullSelf: YetAnotherInputObject! + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.every { + it.classification == SchemaValidationErrorType.UnbrokenInputCycle + } + schemaProblem.errors.size() == 3 + schemaProblem.errors*.description as Set == [ + "Input Object AnotherInputObject cannot be provided a finite value because it references itself through fields: AnotherInputObject.closeLoop, SomeInputObject.startLoop.", + "Input Object AnotherInputObject cannot be provided a finite value because it references itself through fields: AnotherInputObject.startSecondLoop, YetAnotherInputObject.closeSecondLoop.", + "Input Object YetAnotherInputObject cannot be provided a finite value because it references itself through fields: YetAnotherInputObject.nonNullSelf.", + ] as Set + } + + def "rejects a self-referencing OneOf type with no escapes"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { self: A } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object A cannot be provided a finite value because it references itself through fields: A.self." + } + + def "rejects a mixed OneOf/non-OneOf cycle with no escapes"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B } + input B { a: A! } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object A cannot be provided a finite value because it references itself through fields: A.b, B.a." + } + + def "rejects a larger mixed OneOf/non-OneOf cycle with no escapes"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B } + input B { c: C! } + input C @oneOf { a: A } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object A cannot be provided a finite value because it references itself through fields: A.b, B.c, C.a." + } + + def "rejects multiple oneOf types forming a cycle with no escapes"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B } + input B @oneOf { c: C } + input C @oneOf { a: A } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object A cannot be provided a finite value because it references itself through fields: A.b, B.c, C.a." + } + + def "reports only the underlying cycle when a regular Input Object requires an unbreakable OneOf"() { + def sdl = """ + type Query { f(arg: A): String } + input T @oneOf { self: T } + input A { t: T! } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object T cannot be provided a finite value because it references itself through fields: T.self." + } + + def "reports each distinct cycle through multiple OneOf branches"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { b: B, c: C } + input B { a: A! } + input C { a: A! } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 2 + schemaProblem.errors.every { + it.classification == SchemaValidationErrorType.UnbrokenInputCycle + } + schemaProblem.errors*.description as Set == [ + "Input Object A cannot be provided a finite value because it references itself through fields: A.b, B.a.", + "Input Object A cannot be provided a finite value because it references itself through fields: A.c, C.a.", + ] as Set + } + + def "ignores required scalar list and finite input fields when reporting a cycle"() { + def sdl = """ + type Query { f(arg: A): String } + input A { + list: [B]! + finite: Finite! + b: B! + } + input B { + value: Int! + a: A! + } + input Finite { + value: Int! + } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object A cannot be provided a finite value because it references itself through fields: A.b, B.a." + } + + def "reports a shared unbreakable OneOf subgraph once"() { + def chainLength = 20 + def inputTypes = (1..chainLength) + .collect { index -> "input T${index} @oneOf { a: T${index - 1}, b: T${index - 1} }" } + .join("\n") + def sdl = """ + type Query { f(arg: T${chainLength}): String } + input T0 @oneOf { self: T0 } + ${inputTypes} + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.UnbrokenInputCycle + schemaProblem.errors[0].description == "Input Object T0 cannot be provided a finite value because it references itself through fields: T0.self." + } + + def "does not add an unbreakable cycle error for an empty OneOf"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.InputObjectTypeLackOfFieldError + schemaProblem.errors[0].description == '"A" must define one or more fields.' + } + + def "does not add an unbreakable cycle error for a non-null OneOf field"() { + def sdl = """ + type Query { f(arg: A): String } + input A @oneOf { self: A! } + """ + + when: + def registry = new SchemaParser().parse(sdl) + new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) + + then: + def schemaProblem = thrown(InvalidSchemaException) + schemaProblem.errors.size() == 1 + schemaProblem.errors[0].classification == SchemaValidationErrorType.OneOfNonNullableField + schemaProblem.errors[0].description == "OneOf input field A.self must be nullable." + } +} diff --git a/src/test/groovy/graphql/schema/validation/NoUnbrokenInputCyclesTest.groovy b/src/test/groovy/graphql/schema/validation/NoUnbrokenInputCyclesTest.groovy deleted file mode 100644 index a9f0889e3..000000000 --- a/src/test/groovy/graphql/schema/validation/NoUnbrokenInputCyclesTest.groovy +++ /dev/null @@ -1,96 +0,0 @@ -package graphql.schema.validation - -import graphql.TestUtil -import graphql.schema.GraphQLArgument -import graphql.schema.GraphQLFieldDefinition -import graphql.schema.GraphQLInputObjectField -import graphql.schema.GraphQLInputObjectType -import graphql.schema.idl.SchemaGenerator -import graphql.schema.idl.SchemaParser -import graphql.util.TraverserContext -import spock.lang.Specification - -import static graphql.Scalars.GraphQLBoolean -import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition -import static graphql.schema.GraphQLInputObjectType.newInputObject -import static graphql.schema.GraphQLNonNull.nonNull -import static graphql.schema.GraphQLTypeReference.typeRef - -class NoUnbrokenInputCyclesTest extends Specification { - - SchemaValidationErrorCollector errorCollector = new SchemaValidationErrorCollector() - - def "infinitely recursive input type results in error"() { - given: - GraphQLInputObjectType PersonInputType = newInputObject() - .name("Person") - .field(GraphQLInputObjectField.newInputObjectField() - .name("friend") - .type(typeRef("Person")) - .build()) - .build() - - GraphQLFieldDefinition field = newFieldDefinition() - .name("exists") - .type(GraphQLBoolean) - .argument(GraphQLArgument.newArgument() - .name("person") - .type(PersonInputType)) - .build() - - PersonInputType.getFieldDefinition("friend").replacedType = nonNull(PersonInputType) - def context = Mock(TraverserContext) - context.getVarFromParents(SchemaValidationErrorCollector) >> errorCollector - when: - new NoUnbrokenInputCycles().visitGraphQLFieldDefinition(field, context) - then: - errorCollector.containsValidationError(SchemaValidationErrorType.UnbrokenInputCycle) - } - - def "input object cycles through a non-null list are allowed"() { - def sdl = """ - input Example { - self: [Example!]! - value: String - } - - type Query { - example(example: Example): String - } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "longer input object cycles through a non-null list are allowed"() { - def sdl = """ - input Foo { - bar: Bar! - } - - input Bar { - baz: Baz! - } - - input Baz { - foos: [Foo!]! - } - - type Query { - foo(foo: Foo): String - } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } -} diff --git a/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy b/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy index f91dca44e..776c4cbda 100644 --- a/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy +++ b/src/test/groovy/graphql/schema/validation/OneOfInputObjectRulesTest.groovy @@ -34,157 +34,4 @@ class OneOfInputObjectRulesTest extends Specification { schemaProblem.errors[1].classification == SchemaValidationErrorType.OneOfDefaultValueOnField } - def "oneOf with scalar fields is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { a: String, b: Int } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "oneOf with enum field is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - enum Color { RED GREEN BLUE } - input A @oneOf { a: Color } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "oneOf with list field is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { a: [A] } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "oneOf referencing non-oneOf input is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { a: RegularInput } - input RegularInput { x: String } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "oneOf with escape field is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { b: B, escape: String } - input B @oneOf { a: A } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "mutually referencing oneOf types with scalar escape is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { b: B } - input B @oneOf { a: A, escape: Int } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "oneOf referencing non-oneOf with back-reference is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { b: RegularInput } - input RegularInput { back: A } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "multiple fields with chained oneOf escape is inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { b: B, c: C } - input B @oneOf { a: A } - input C @oneOf { a: A, escape: String } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - noExceptionThrown() - } - - def "single oneOf self-reference cycle is not inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { self: A } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - def schemaProblem = thrown(InvalidSchemaException) - schemaProblem.errors.size() == 1 - schemaProblem.errors[0].description == "OneOf Input Object A must be inhabited but all fields recursively reference only other OneOf Input Objects forming an unresolvable cycle." - schemaProblem.errors[0].classification == SchemaValidationErrorType.OneOfNotInhabited - } - - def "multiple oneOf types forming cycle are not inhabited"() { - def sdl = """ - type Query { f(arg: A): String } - input A @oneOf { b: B } - input B @oneOf { c: C } - input C @oneOf { a: A } - """ - - when: - def registry = new SchemaParser().parse(sdl) - new SchemaGenerator().makeExecutableSchema(registry, TestUtil.getMockRuntimeWiring()) - - then: - def schemaProblem = thrown(InvalidSchemaException) - schemaProblem.errors.size() == 3 - schemaProblem.errors.every { it.classification == SchemaValidationErrorType.OneOfNotInhabited } - } } diff --git a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy index a854129d4..689b62a05 100644 --- a/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy +++ b/src/test/groovy/graphql/schema/validation/SchemaValidatorTest.groovy @@ -12,7 +12,7 @@ class SchemaValidatorTest extends Specification { def rules = validator.rules then: rules.size() == 10 - rules[0] instanceof NoUnbrokenInputCycles + rules[0] instanceof InputObjectHasUnbreakableCycle rules[1] instanceof NoDefaultValueCircularRefs rules[2] instanceof TypesImplementInterfaces rules[3] instanceof TypeAndFieldRule diff --git a/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/finiteValuePropagation.graphqls b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/finiteValuePropagation.graphqls new file mode 100644 index 000000000..c7456f0be --- /dev/null +++ b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/finiteValuePropagation.graphqls @@ -0,0 +1,46 @@ +type Query { + check(input: Finite7): Boolean +} + +input Seed { + value: Int! +} + +input Finite1 { + previous: Seed! +} + +input Finite2 @oneOf { + previous: Finite1 + trap: TrapA +} + +input Finite3 { + previous: Finite2! +} + +input Finite4 @oneOf { + previous: Finite3 + trap: TrapA +} + +input Finite5 { + previous: Finite4! +} + +input Finite6 @oneOf { + previous: Finite5 + trap: TrapA +} + +input Finite7 { + previous: Finite6! +} + +input TrapA @oneOf { + next: TrapB +} + +input TrapB { + next: TrapA! +} diff --git a/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/tenTypeMixedInputCycle.graphqls b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/tenTypeMixedInputCycle.graphqls new file mode 100644 index 000000000..647c288bf --- /dev/null +++ b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/tenTypeMixedInputCycle.graphqls @@ -0,0 +1,43 @@ +type Query { + check(input: Cycle0): Boolean +} + +input Cycle0 @oneOf { + next: Cycle1 +} + +input Cycle1 { + next: Cycle2! +} + +input Cycle2 @oneOf { + next: Cycle3 +} + +input Cycle3 { + next: Cycle4! +} + +input Cycle4 @oneOf { + next: Cycle5 +} + +input Cycle5 { + next: Cycle6! +} + +input Cycle6 @oneOf { + next: Cycle7 +} + +input Cycle7 { + next: Cycle8! +} + +input Cycle8 @oneOf { + next: Cycle9 +} + +input Cycle9 { + next: Cycle0! +} diff --git a/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/tenTypeTenFieldsInputCycle.graphqls b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/tenTypeTenFieldsInputCycle.graphqls new file mode 100644 index 000000000..aec629776 --- /dev/null +++ b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/tenTypeTenFieldsInputCycle.graphqls @@ -0,0 +1,133 @@ +type Query { + check(input: Wide0): Boolean +} + +input Wide0 { + requiredNext: Wide1! + reference1: Wide1 + reference2: Wide2 + reference3: Wide3 + reference4: Wide4 + reference5: Wide5 + reference6: Wide6 + reference7: Wide7 + reference8: Wide8 + reference9: Wide9 +} + +input Wide1 { + requiredNext: Wide2! + reference1: Wide2 + reference2: Wide3 + reference3: Wide4 + reference4: Wide5 + reference5: Wide6 + reference6: Wide7 + reference7: Wide8 + reference8: Wide9 + reference9: Wide0 +} + +input Wide2 { + requiredNext: Wide3! + reference1: Wide3 + reference2: Wide4 + reference3: Wide5 + reference4: Wide6 + reference5: Wide7 + reference6: Wide8 + reference7: Wide9 + reference8: Wide0 + reference9: Wide1 +} + +input Wide3 { + requiredNext: Wide4! + reference1: Wide4 + reference2: Wide5 + reference3: Wide6 + reference4: Wide7 + reference5: Wide8 + reference6: Wide9 + reference7: Wide0 + reference8: Wide1 + reference9: Wide2 +} + +input Wide4 { + requiredNext: Wide5! + reference1: Wide5 + reference2: Wide6 + reference3: Wide7 + reference4: Wide8 + reference5: Wide9 + reference6: Wide0 + reference7: Wide1 + reference8: Wide2 + reference9: Wide3 +} + +input Wide5 { + requiredNext: Wide6! + reference1: Wide6 + reference2: Wide7 + reference3: Wide8 + reference4: Wide9 + reference5: Wide0 + reference6: Wide1 + reference7: Wide2 + reference8: Wide3 + reference9: Wide4 +} + +input Wide6 { + requiredNext: Wide7! + reference1: Wide7 + reference2: Wide8 + reference3: Wide9 + reference4: Wide0 + reference5: Wide1 + reference6: Wide2 + reference7: Wide3 + reference8: Wide4 + reference9: Wide5 +} + +input Wide7 { + requiredNext: Wide8! + reference1: Wide8 + reference2: Wide9 + reference3: Wide0 + reference4: Wide1 + reference5: Wide2 + reference6: Wide3 + reference7: Wide4 + reference8: Wide5 + reference9: Wide6 +} + +input Wide8 { + requiredNext: Wide9! + reference1: Wide9 + reference2: Wide0 + reference3: Wide1 + reference4: Wide2 + reference5: Wide3 + reference6: Wide4 + reference7: Wide5 + reference8: Wide6 + reference9: Wide7 +} + +input Wide9 { + requiredNext: Wide0! + reference1: Wide0 + reference2: Wide1 + reference3: Wide2 + reference4: Wide3 + reference5: Wide4 + reference6: Wide5 + reference7: Wide6 + reference8: Wide7 + reference9: Wide8 +} diff --git a/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/twoTypeMixedInputCycle.graphqls b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/twoTypeMixedInputCycle.graphqls new file mode 100644 index 000000000..232f67fa3 --- /dev/null +++ b/src/test/resources/InputObjectHasUnbreakableCycleBenchmark/twoTypeMixedInputCycle.graphqls @@ -0,0 +1,11 @@ +type Query { + check(input: Cycle0): Boolean +} + +input Cycle0 @oneOf { + next: Cycle1 +} + +input Cycle1 { + next: Cycle0! +}