From 27fa59b9619a46a6c6f946aef020fae5fd5bddce Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sat, 1 Aug 2026 00:20:46 +0000 Subject: [PATCH 01/32] Add schema universe --- build.gradle | 8 +- docs/schema-universe.md | 206 ++++ .../benchmark/SchemaUniverseBenchmark.java | 958 ++++++++++++++++++ src/main/java/graphql/Directives.java | 2 +- .../schema/idl/GraphQLSchemaPrintAccess.java | 631 ++++++++++++ .../schema/idl/SUSchemaPrintAccess.java | 457 +++++++++ .../graphql/schema/idl/SchemaPrintAccess.java | 78 ++ .../schema/idl/SchemaPrintChildKind.java | 18 + .../schema/idl/SchemaPrintElementKind.java | 25 + .../graphql/schema/idl/SchemaPrinter.java | 27 + .../schema/idl/SchemaPrinterWriter.java | 509 ++++++++++ .../schema/universe/EdgeMapBranch.java | 143 +++ .../graphql/schema/universe/EdgeMapLeaf.java | 62 ++ .../graphql/schema/universe/EdgeMapNode.java | 56 + .../schema/universe/MutablePackedEdgeSet.java | 158 +++ .../schema/universe/PackedEdgeSet.java | 241 +++++ .../schema/universe/PersistentEdgeMap.java | 106 ++ .../schema/universe/SUAppliedDirective.java | 55 + .../universe/SUAppliedDirectiveArgument.java | 62 ++ .../universe/SUAppliedDirectiveContainer.java | 12 + .../graphql/schema/universe/SUArgument.java | 39 + .../graphql/schema/universe/SUDirective.java | 66 ++ .../graphql/schema/universe/SUEdgeKind.java | 97 ++ .../graphql/schema/universe/SUEnumType.java | 16 + .../graphql/schema/universe/SUEnumValue.java | 16 + .../graphql/schema/universe/SUExporter.java | 416 ++++++++ .../java/graphql/schema/universe/SUField.java | 16 + .../graphql/schema/universe/SUImporter.java | 503 +++++++++ .../graphql/schema/universe/SUInputField.java | 39 + .../schema/universe/SUInputObjectType.java | 16 + .../schema/universe/SUInterfaceType.java | 16 + .../graphql/schema/universe/SUListType.java | 15 + .../graphql/schema/universe/SUNamedType.java | 24 + .../schema/universe/SUNonNullType.java | 15 + .../graphql/schema/universe/SUObjectType.java | 16 + .../graphql/schema/universe/SUScalarType.java | 16 + .../graphql/schema/universe/SUSchema.java | 416 ++++++++ .../schema/universe/SUSchemaBuilder.java | 578 +++++++++++ .../schema/universe/SUSchemaGenerator.java | 631 ++++++++++++ .../schema/universe/SUSchemaPrinter.java | 33 + .../graphql/schema/universe/SUSchemaRoot.java | 16 + .../java/graphql/schema/universe/SUType.java | 24 + .../graphql/schema/universe/SUUnionType.java | 16 + .../graphql/schema/universe/SUVertex.java | 65 ++ .../graphql/schema/universe/SUVertexKind.java | 27 + .../schema/universe/SchemaUniverse.java | 439 ++++++++ .../universe/SUSchemaGeneratorTest.groovy | 201 ++++ .../universe/SUSchemaPrinterTest.groovy | 161 +++ .../universe/SUSchemaRoundTripTest.groovy | 372 +++++++ .../graphql/schema/universe/SUTest.groovy | 887 ++++++++++++++++ 50 files changed, 9002 insertions(+), 4 deletions(-) create mode 100644 docs/schema-universe.md create mode 100644 src/jmh/java/benchmark/SchemaUniverseBenchmark.java create mode 100644 src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java create mode 100644 src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java create mode 100644 src/main/java/graphql/schema/idl/SchemaPrintAccess.java create mode 100644 src/main/java/graphql/schema/idl/SchemaPrintChildKind.java create mode 100644 src/main/java/graphql/schema/idl/SchemaPrintElementKind.java create mode 100644 src/main/java/graphql/schema/idl/SchemaPrinterWriter.java create mode 100644 src/main/java/graphql/schema/universe/EdgeMapBranch.java create mode 100644 src/main/java/graphql/schema/universe/EdgeMapLeaf.java create mode 100644 src/main/java/graphql/schema/universe/EdgeMapNode.java create mode 100644 src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java create mode 100644 src/main/java/graphql/schema/universe/PackedEdgeSet.java create mode 100644 src/main/java/graphql/schema/universe/PersistentEdgeMap.java create mode 100644 src/main/java/graphql/schema/universe/SUAppliedDirective.java create mode 100644 src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java create mode 100644 src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java create mode 100644 src/main/java/graphql/schema/universe/SUArgument.java create mode 100644 src/main/java/graphql/schema/universe/SUDirective.java create mode 100644 src/main/java/graphql/schema/universe/SUEdgeKind.java create mode 100644 src/main/java/graphql/schema/universe/SUEnumType.java create mode 100644 src/main/java/graphql/schema/universe/SUEnumValue.java create mode 100644 src/main/java/graphql/schema/universe/SUExporter.java create mode 100644 src/main/java/graphql/schema/universe/SUField.java create mode 100644 src/main/java/graphql/schema/universe/SUImporter.java create mode 100644 src/main/java/graphql/schema/universe/SUInputField.java create mode 100644 src/main/java/graphql/schema/universe/SUInputObjectType.java create mode 100644 src/main/java/graphql/schema/universe/SUInterfaceType.java create mode 100644 src/main/java/graphql/schema/universe/SUListType.java create mode 100644 src/main/java/graphql/schema/universe/SUNamedType.java create mode 100644 src/main/java/graphql/schema/universe/SUNonNullType.java create mode 100644 src/main/java/graphql/schema/universe/SUObjectType.java create mode 100644 src/main/java/graphql/schema/universe/SUScalarType.java create mode 100644 src/main/java/graphql/schema/universe/SUSchema.java create mode 100644 src/main/java/graphql/schema/universe/SUSchemaBuilder.java create mode 100644 src/main/java/graphql/schema/universe/SUSchemaGenerator.java create mode 100644 src/main/java/graphql/schema/universe/SUSchemaPrinter.java create mode 100644 src/main/java/graphql/schema/universe/SUSchemaRoot.java create mode 100644 src/main/java/graphql/schema/universe/SUType.java create mode 100644 src/main/java/graphql/schema/universe/SUUnionType.java create mode 100644 src/main/java/graphql/schema/universe/SUVertex.java create mode 100644 src/main/java/graphql/schema/universe/SUVertexKind.java create mode 100644 src/main/java/graphql/schema/universe/SchemaUniverse.java create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy create mode 100644 src/test/groovy/graphql/schema/universe/SUTest.groovy diff --git a/build.gradle b/build.gradle index 404bc5e254..48a7cb5c1d 100644 --- a/build.gradle +++ b/build.gradle @@ -166,6 +166,7 @@ dependencies { // this is needed for the idea jmh plugin to work correctly jmh 'org.openjdk.jmh:jmh-core:1.37' jmh 'org.openjdk.jmh:jmh-generator-annprocess:1.37' + jmh 'org.openjdk.jol:jol-core:0.17' jmh 'me.bechberger:ap-loader-all:4.4-13' // comment this in if you want to run JMH benchmarks from idea @@ -482,7 +483,10 @@ tasks.withType(Test) { if (!name.startsWith('testng')) { useJUnitPlatform() } - maxHeapSize = "1g" + maxHeapSize = project.findProperty("testMaxHeapSize") ?: "1g" + if (project.hasProperty("largeSchemaSdlTest")) { + systemProperty "graphql.largeSchemaSdlTest", "true" + } testLogging { events "FAILED", "SKIPPED" exceptionFormat = "FULL" @@ -801,5 +805,3 @@ tasks.withType(PublishToMavenRepository) { tasks.withType(GenerateModuleMetadata) { enabled = false } - - diff --git a/docs/schema-universe.md b/docs/schema-universe.md new file mode 100644 index 0000000000..7a096c3213 --- /dev/null +++ b/docs/schema-universe.md @@ -0,0 +1,206 @@ +# Schema universe design + +## Status + +This is an experimental type-system core. It establishes storage semantics and a transformation +model; it does not yet replace `GraphQLSchema` in execution. + +## Goals + +- Hold hundreds or thousands of related schemas in one universe. +- Reuse unchanged schema elements and adjacency between derived schemas. +- Make a small transformation proportional to the changed sources, not the whole schema. +- Keep edges outside vertices. +- Avoid an object allocation per edge and avoid boxed integers in steady-state storage. +- Publish immutable snapshots that are safe for concurrent reads. + +The key workload assumption is lineage: schemas are derived from an initial schema, usually through +small edits. This is materially different from reconciling unrelated schemas. + +## Model + +`SchemaUniverse` is an append-only vertex arena. Every vertex receives a dense integer ID. There is +one Java vertex type for each supported GraphQL schema-element kind. A vertex contains intrinsic +scalar properties such as kind, name, and description, but no child references. + +`SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex. The +effective schema is the graph reachable from that root. Root edges identify operation roots, +non-root named types, and directive definitions. + +Every successfully built snapshot registers itself with its universe under its unique name. +`SchemaUniverse.getSchema(name)` selects one snapshot directly, while `getSchemas()` and +`getSchemasByName()` return immutable views in registration order. Imports and transformations use +the same registration path. Reusing a schema name in one universe is rejected. + +The snapshot can retain adjacency for a currently unreachable vertex. This dormant adjacency is not +part of the effective rooted graph; it allows a later derived schema to reattach an unchanged +subgraph at constant edit scope. Consequently, `getStoredEdgeCount()` is a storage count rather than +a reachable-edge count. + +The current API assumes callers traverse from the root or another known-reachable vertex. + +## Typed traversal + +`SUSchema` is the primary traversal API. Callers normally do not need to inspect edge kinds +directly. It provides typed helpers for: + +- operation roots, additional types, all named types, and lookup by type name; +- directive definitions and schema-applied directives; +- object and interface fields; +- field and directive arguments; +- declared types and list/non-null wrapped types; +- implemented interfaces, union members, enum values, and input fields; +- applied directives, including repeatable applications filtered by name; +- applied-directive argument payloads and their exact input types. + +For example, `getFields(objectType)`, `getField(objectType, name)`, +`getAppliedDirectives(field)`, and `getType(field)` all use the selected schema snapshot's +adjacency. The same shared vertex can therefore return different children when accessed through +different `SUSchema` instances. `getChildren`, `getChild`, and `containsEdge` remain available as +generic escape hatches. + +## Edge representation + +An edge is packed into one primitive `long`: + +``` + 63 56 55 32 31 0 ++-----------+------------------------+-------------------------+ +| kind (8) | target name id (24) | target vertex id (32) | ++-----------+------------------------+-------------------------+ +``` + +The source ID is the key of the adjacency map, so it is not repeated per edge. Immutable outgoing +arrays are sorted by kind and target-name ID. This provides: + +- 8 bytes per stored edge in an adjacency array; +- binary search by relationship and GraphQL name; +- deterministic iteration; +- no edge objects or boxed endpoint IDs. + +The 24-bit name ID limits one universe to 16,777,215 distinct names. Vertex IDs are non-negative +Java integers. + +## Persistent snapshots + +Outgoing arrays are stored in a specialized persistent hash-array mapped trie keyed by source +vertex ID. A transform batches changes by source, freezes each changed outgoing array once, and +path-copies only the affected trie branches. All unchanged branches and outgoing arrays are shared +by identity with the parent schema. + +Approximate operation costs, where `d` is source out-degree and `V` is the number of sources with +adjacency: + +| Operation | Time | New retained storage | +| --- | --- | --- | +| Read outgoing adjacency | `O(log32 V)` | none | +| Child lookup by name | `O(log32 V + log d)` | none | +| Add/remove edge | `O(d)` transient, then sort at build | one changed primitive array | +| Publish `k` changed sources | `O(k log32 V)` | copied trie paths plus `k` arrays | +| Derive with no type edits | effectively root replacement | one root array and two copied trie paths | + +The append-only vertex arena uses chunked arrays. Vertex creation is serialized; published vertex +and schema reads are lock-free. + +## Why this representation + +The existing [`graphql.schema.diffing.SchemaGraph`](../src/main/java/graphql/schema/diffing/SchemaGraph.java) +already demonstrates an external-vertex/external-edge model and supplies a useful element taxonomy. +It is intentionally schema-local and uses Java edge objects, property maps, lists, Guava tables, and +multimaps. That is appropriate for pairwise graph-edit analysis, but it duplicates storage for every +schema and is not a multi-version store. + +Research considered these alternatives: + +- [JGraphT `AsSubgraph`](https://github.com/jgrapht/jgrapht/blob/master/jgrapht-core/src/main/java/org/jgrapht/graph/AsSubgraph.java) + represents a subset with per-view vertex and edge sets. Its own documentation notes that adjacent + iteration can be proportional to base-graph degree, even for a small subset. Hundreds of views + would also duplicate set entries. +- [JGraphT sparse CSR graphs](https://github.com/jgrapht/jgrapht/blob/master/jgrapht-opt/src/main/java/org/jgrapht/opt/graph/sparse/SparseIntDirectedGraph.java) + are compact and fast for write-once/read-many graphs, but explicitly unmodifiable. A small schema + edit requires rebuilding a complete representation. +- [JGraphT Sux4J graphs](https://github.com/jgrapht/jgrapht/tree/master/jgrapht-unimi-dsi/src/main/java/org/jgrapht/sux4j) + and [WebGraph](https://github.com/vigna/webgraph) approach information-theoretic storage bounds for + frozen graphs. They are candidates for archival compaction, not the primary mutable-lineage form. +- [RoaringBitmap](https://github.com/RoaringBitmap/RoaringBitmap) can efficiently represent the + schema IDs containing each global edge. It is attractive for cross-schema set queries, but + creating a derived schema requires touching every inherited edge or adding a parent/delta layer; + direct outgoing lookup also needs a separate index. It remains a possible secondary membership + index after profiling. +- Persistent collections such as [PCollections](https://github.com/hrldcpr/pcollections) avoid full + copies, while [Capsule](https://github.com/usethesource/capsule) documents the CHAMP/HAMT design and + its JVM memory benefits. graphql-java has a no-new-dependencies policy, so the prototype uses a + small specialized integer-to-adjacency HAMT rather than a general collection library. +- Primitive libraries such as [fastutil](https://github.com/vigna/fastutil) and + [Eclipse Collections](https://github.com/eclipse-collections/eclipse-collections) reduce boxing + in mutable graphs, but do not by themselves provide persistent schema snapshots. + +## Vertex identity + +Vertex sharing is explicit rather than structural. Calling `newField("foo")` twice creates two +vertices even when their current properties match. A transformation reuses a vertex only when it +means to preserve that schema element. + +Default values are intrinsic argument or input-field vertex data. Applied-directive arguments are +compact immutable values embedded in their `SUAppliedDirective` occurrence rather than independent +vertices. They retain graphql-java's `InputValueWithState` representation and exact input type +vertex ID, so not-set, literal, external, internal, and explicit-null states remain distinguishable +and programmatic schemas round trip without deriving types from directive definitions. + +Changing an applied argument requires creating a new applied-directive occurrence and replacing the +container's `APPLIED_DIRECTIVE` edge. Unchanged argument values can be reused in the replacement +payload. External and internal values are retained by reference; snapshot immutability is shallow +with respect to a mutable value supplied by an application. + +Directive repeatability and valid locations are also intrinsic. Locations are stored as a compact +bit mask. Directive-definition and applied-directive AST nodes are retained when available. + +The unresolved policy question is whether a field's `TYPE` edge is part of field identity. The +prototype permits a schema snapshot to select a different single `TYPE` edge for the same field +vertex. If "specific element" includes its type, this operation should instead be rejected globally +and the caller should create a new field vertex. The same decision applies to argument types and +wrapper targets. + +## Import coverage + +`SUImporter` seeds a universe from an existing `GraphQLSchema`. It currently imports: + +- object, interface, union, enum, scalar, and input-object types; +- fields, arguments, enum values, and input fields; +- list and non-null wrappers; +- operation roots, non-root named types, implementations, and union membership; +- argument and input-field defaults, preserving all `InputValueWithState` states; +- directive repeatability, valid locations, definitions, and modern applied-directive topology; +- applied-directive argument values, exact input types, and application AST nodes. + +It does not yet preserve `GraphQLCodeRegistry`, coercing implementations, deprecation metadata, +general non-directive AST definitions/extensions, comparators, or source locations. These need typed +intrinsic payloads before round-trip conversion can be lossless. + +## Lifecycle and compaction + +The prototype is append-only and retains history. Production lifecycle support should use one of +these policies: + +1. Retain all versions for a bounded universe. +2. Reference-count snapshots and periodically mark vertices/adjacency reachable from live roots. +3. Compact live schemas into a new universe, optionally using CSR or a succinct frozen format. + +Compaction is preferable to per-edit reference counting unless schema deletion is frequent, because +reference counts add writes and contention to every transformation. + +## Required measurements + +Before integrating with execution, benchmarks should compare: + +- retained bytes for 1, 100, and 1,000 schemas at several edit percentages; +- initial import time; +- one-field add/remove/type-change latency; +- type and field lookup throughput; +- rooted full traversal; +- materialization back to `GraphQLSchema`; +- persistent HAMT against page-based copy-on-write and edge-membership bitmaps. + +Measurements should use JMH for throughput/allocation and JOL or a heap histogram for retained size. +The result should determine whether to add lazy per-schema name/reverse indexes and whether packed +adjacency needs a small-degree linear-search specialization. diff --git a/src/jmh/java/benchmark/SchemaUniverseBenchmark.java b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java new file mode 100644 index 0000000000..0cc029d9c7 --- /dev/null +++ b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java @@ -0,0 +1,958 @@ +package benchmark; + +import graphql.Scalars; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLTypeVisitorStub; +import graphql.schema.GraphqlTypeComparatorRegistry; +import graphql.schema.InputValueWithState; +import graphql.schema.SchemaTransformer; +import graphql.schema.idl.SchemaGenerator; +import graphql.schema.idl.SchemaPrinter; +import graphql.schema.universe.SUAppliedDirective; +import graphql.schema.universe.SUAppliedDirectiveArgument; +import graphql.schema.universe.SUField; +import graphql.schema.universe.SUObjectType; +import graphql.schema.universe.SUScalarType; +import graphql.schema.universe.SUSchema; +import graphql.schema.universe.SchemaUniverse; +import graphql.util.TraversalControl; +import graphql.util.TraverserContext; +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.TearDown; +import org.openjdk.jmh.annotations.Timeout; +import org.openjdk.jmh.annotations.Warmup; +import org.openjdk.jol.info.GraphLayout; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static java.util.Objects.requireNonNull; + +/** + * Compares 100 related large-schema-5 versions represented by GraphQLSchema and SchemaUniverse. + * + *

Every version replaces a versioned applied directive on 1,000 object types distributed across + * the source schema. Each five-step cycle additionally adds a detached object type, adds and removes + * a field, adds a replacement field, and removes the type. The {@code retain100...} methods print + * exact JOL object-graph footprints. Run the {@code profileRetained100...} methods separately with + * async-profiler's sampled live-allocation mode to attribute surviving allocations; use the JOL + * values, not the sampled profile totals, for exact retained byte counts:

+ * + *
+ * ./gradlew jmh \
+ *   -PjmhInclude='SchemaUniverseBenchmark.profileRetained100(GraphQLSchema|SUSchema)Versions' \
+ *   -PjmhProfilers='async:event=alloc;rawCommand=live;output=text;dir=build/reports/jmh/schema-universe'
+ * 
+ */ +@OutputTimeUnit(TimeUnit.MILLISECONDS) +@Timeout(time = 30, timeUnit = TimeUnit.MINUTES) +public class SchemaUniverseBenchmark { + + private static final int CYCLE_COUNT = 20; + private static final int VERSION_COUNT = CYCLE_COUNT * EditKind.values().length; + private static final int DIRECTIVE_TARGET_COUNT = 1_000; + private static final double BYTES_PER_MIB = 1024.0 * 1024.0; + private static final String BASE_SCHEMA_NAME = "large_schema_5_base"; + private static final String SYNTHETIC_PREFIX = "BenchmarkVersion"; + private static final String BENCHMARK_DIRECTIVE_NAME = "benchmarkEdit"; + private static final String BENCHMARK_DIRECTIVE_VERSION_ARGUMENT = "version"; + + /** + * Runs the expensive pairwise correctness check independently of JMH measurements. Pass a + * version count to verify a smaller prefix of the mutation plan. + */ + public static void main(String[] args) { + GraphQLSchema baseSchema = loadBaseSchema(); + MutationPlan plan = MutationPlan.create(baseSchema) + .firstVersions(verificationVersionCount(args)); + GraphQLVersionSet graphQLVersions = + createGraphQLSchemaVersions(baseSchema, plan); + + SchemaUniverse universe = new SchemaUniverse(); + SUSchema suBase = universe.importSchema(BASE_SCHEMA_NAME, baseSchema); + createSUSchemaVersions(universe, suBase, plan); + + verifyEquivalentVersions(graphQLVersions, universe.getSchemas()); + } + + private static int verificationVersionCount(String[] args) { + if (args.length == 0) { + return VERSION_COUNT; + } + if (args.length != 1) { + throw new IllegalArgumentException("Expected at most one version-count argument"); + } + int versionCount = Integer.parseInt(args[0]); + if (versionCount < 1 || versionCount > VERSION_COUNT) { + throw new IllegalArgumentException(String.format( + Locale.ROOT, + "Version count must be between 1 and %d", + VERSION_COUNT)); + } + return versionCount; + } + + @State(Scope.Benchmark) + public static class GraphQLTimingState { + GraphQLSchema baseSchema; + MutationPlan plan; + + @Setup(Level.Trial) + public void setup() { + baseSchema = loadBaseSchema(); + plan = MutationPlan.create(baseSchema); + } + } + + @State(Scope.Benchmark) + public static class SUTimingState { + GraphQLSchema sourceSchema; + MutationPlan plan; + SchemaUniverse universe; + SUSchema baseSchema; + + @Setup(Level.Trial) + public void setupTrial() { + sourceSchema = loadBaseSchema(); + plan = MutationPlan.create(sourceSchema); + } + + @Setup(Level.Iteration) + public void setupIteration() { + universe = new SchemaUniverse(); + baseSchema = universe.importSchema(BASE_SCHEMA_NAME, sourceSchema); + } + } + + @State(Scope.Benchmark) + public static class GraphQLMemoryState { + GraphQLSchema baseSchema; + MutationPlan plan; + long baseRetainedBytes; + GraphQLVersionSet retainedVersions; + + @Setup(Level.Trial) + public void setup() { + baseSchema = loadBaseSchema(); + plan = MutationPlan.create(baseSchema); + baseRetainedBytes = retainedSize(baseSchema); + } + + @TearDown(Level.Trial) + public void reportRetainedMemory() { + requireNonNull(retainedVersions, "retained GraphQLSchema versions"); + reportFootprint( + "GraphQLSchema", + baseRetainedBytes, + retainedSize(retainedVersions)); + } + } + + @State(Scope.Benchmark) + public static class SUMemoryState { + MutationPlan plan; + SchemaUniverse universe; + SUSchema baseSchema; + long baseRetainedBytes; + SchemaUniverse retainedUniverse; + + @Setup(Level.Trial) + public void setup() { + GraphQLSchema sourceSchema = loadBaseSchema(); + plan = MutationPlan.create(sourceSchema); + universe = new SchemaUniverse(); + baseSchema = universe.importSchema(BASE_SCHEMA_NAME, sourceSchema); + baseRetainedBytes = retainedSize(universe); + } + + @TearDown(Level.Trial) + public void reportRetainedMemory() { + requireNonNull(retainedUniverse, "retained schema universe"); + reportFootprint( + "SUSchema", + baseRetainedBytes, + retainedSize(retainedUniverse)); + } + } + + @State(Scope.Benchmark) + public static class GraphQLProfileState { + GraphQLSchema baseSchema; + MutationPlan plan; + GraphQLVersionSet retainedVersions; + + @Setup(Level.Trial) + public void setup() { + baseSchema = loadBaseSchema(); + plan = MutationPlan.create(baseSchema); + } + } + + @State(Scope.Benchmark) + public static class SUProfileState { + MutationPlan plan; + SchemaUniverse universe; + SUSchema baseSchema; + SchemaUniverse retainedUniverse; + + @Setup(Level.Trial) + public void setup() { + GraphQLSchema sourceSchema = loadBaseSchema(); + plan = MutationPlan.create(sourceSchema); + universe = new SchemaUniverse(); + baseSchema = universe.importSchema(BASE_SCHEMA_NAME, sourceSchema); + } + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Warmup(iterations = 0) + @Measurement(iterations = 1) + @Fork( + value = 1, + jvmArgsAppend = { + "-Xms8g", + "-Xmx30g", + "-Djdk.attach.allowAttachSelf=true", + "-Djol.magicFieldOffset=true", + "-XX:+EnableDynamicAgentLoading" + }) + public GraphQLVersionSet create100GraphQLSchemaVersions(GraphQLTimingState state) { + return createGraphQLSchemaVersions(state.baseSchema, state.plan); + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Warmup(iterations = 0) + @Measurement(iterations = 1) + @Fork( + value = 1, + jvmArgsAppend = { + "-Xms8g", + "-Xmx30g", + "-Djdk.attach.allowAttachSelf=true", + "-Djol.magicFieldOffset=true", + "-XX:+EnableDynamicAgentLoading" + }) + public SchemaUniverse create100SUSchemaVersions(SUTimingState state) { + createSUSchemaVersions(state.universe, state.baseSchema, state.plan); + return state.universe; + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Warmup(iterations = 0) + @Measurement(iterations = 1) + @Fork( + value = 1, + jvmArgsAppend = { + "-Xms8g", + "-Xmx30g", + "-Djdk.attach.allowAttachSelf=true", + "-Djol.magicFieldOffset=true", + "-XX:+EnableDynamicAgentLoading" + }) + public GraphQLVersionSet retain100GraphQLSchemaVersions(GraphQLMemoryState state) { + state.retainedVersions = createGraphQLSchemaVersions(state.baseSchema, state.plan); + System.gc(); + return state.retainedVersions; + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Warmup(iterations = 0) + @Measurement(iterations = 1) + @Fork( + value = 1, + jvmArgsAppend = { + "-Xms8g", + "-Xmx30g", + "-Djdk.attach.allowAttachSelf=true", + "-Djol.magicFieldOffset=true", + "-XX:+EnableDynamicAgentLoading" + }) + public SchemaUniverse retain100SUSchemaVersions(SUMemoryState state) { + createSUSchemaVersions(state.universe, state.baseSchema, state.plan); + state.retainedUniverse = state.universe; + System.gc(); + return state.retainedUniverse; + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Warmup(iterations = 0) + @Measurement(iterations = 1) + @Fork( + value = 1, + jvmArgsAppend = { + "-Xms8g", + "-Xmx30g", + "-Djdk.attach.allowAttachSelf=true", + "-Djol.magicFieldOffset=true", + "-XX:+EnableDynamicAgentLoading" + }) + public GraphQLVersionSet profileRetained100GraphQLSchemaVersions( + GraphQLProfileState state) { + state.retainedVersions = + createGraphQLSchemaVersions(state.baseSchema, state.plan); + System.gc(); + return state.retainedVersions; + } + + @Benchmark + @BenchmarkMode(Mode.SingleShotTime) + @Warmup(iterations = 0) + @Measurement(iterations = 1) + @Fork( + value = 1, + jvmArgsAppend = { + "-Xms8g", + "-Xmx30g", + "-Djdk.attach.allowAttachSelf=true", + "-Djol.magicFieldOffset=true", + "-XX:+EnableDynamicAgentLoading" + }) + public SchemaUniverse profileRetained100SUSchemaVersions( + SUProfileState state) { + createSUSchemaVersions(state.universe, state.baseSchema, state.plan); + state.retainedUniverse = state.universe; + System.gc(); + return state.retainedUniverse; + } + + private static GraphQLVersionSet createGraphQLSchemaVersions( + GraphQLSchema baseSchema, + MutationPlan plan) { + List schemas = new ArrayList<>(VERSION_COUNT + 1); + schemas.add(baseSchema); + GraphQLSchema current = baseSchema; + for (int i = 0; i < plan.mutations.size(); i++) { + Mutation mutation = plan.mutations.get(i); + current = applyGraphQLMutation(current, mutation, plan, i + 1); + schemas.add(current); + } + verifyFinalGraphQLSchema(baseSchema, current, schemas, plan); + return new GraphQLVersionSet(schemas); + } + + private static GraphQLSchema applyGraphQLMutation( + GraphQLSchema current, + Mutation mutation, + MutationPlan plan, + int version) { + int expectedExistingDirectives = version == 1 ? 0 : 1; + int[] touchedTargetCount = {0}; + GraphQLTypeVisitorStub visitor = new GraphQLTypeVisitorStub() { + @Override + public TraversalControl visitGraphQLObjectType( + GraphQLObjectType objectType, + TraverserContext context) { + GraphQLObjectType changedObject = objectType; + boolean changed = false; + if (plan.directiveTargetTypeNameSet.contains(objectType.getName())) { + List retainedDirectives = new ArrayList<>(); + int existingDirectiveCount = 0; + for (GraphQLAppliedDirective directive : objectType.getAppliedDirectives()) { + if (BENCHMARK_DIRECTIVE_NAME.equals(directive.getName())) { + existingDirectiveCount++; + } else { + retainedDirectives.add(directive); + } + } + if (existingDirectiveCount != expectedExistingDirectives) { + throw new IllegalStateException(String.format( + Locale.ROOT, + "Expected %d @%s directives on %s before version %d, found %d", + expectedExistingDirectives, + BENCHMARK_DIRECTIVE_NAME, + objectType.getName(), + version, + existingDirectiveCount)); + } + retainedDirectives.add(newBenchmarkAppliedDirective(version)); + changedObject = changedObject.transform(builder -> + builder.replaceAppliedDirectives(retainedDirectives)); + touchedTargetCount[0]++; + changed = true; + } + + if (mutation.syntheticTypeName.equals(objectType.getName())) { + switch (mutation.kind) { + case ADD_SYNTHETIC_FIELD: + changedObject = withAddedField( + changedObject, + newStringField(current, mutation.syntheticFieldName)); + changed = true; + break; + case REMOVE_SYNTHETIC_FIELD: + changedObject = withRemovedField( + changedObject, + mutation.syntheticFieldName); + changed = true; + break; + case ADD_REPLACEMENT_FIELD: + changedObject = withAddedField( + changedObject, + newStringField(current, mutation.replacementFieldName)); + changed = true; + break; + case REMOVE_SYNTHETIC_TYPE: + return deleteNode(context); + default: + break; + } + } + return changed + ? changeNode(context, changedObject) + : TraversalControl.CONTINUE; + } + }; + + GraphQLSchema transformed; + if (mutation.kind == EditKind.ADD_SYNTHETIC_TYPE) { + GraphQLObjectType syntheticType = GraphQLObjectType.newObject() + .name(mutation.syntheticTypeName) + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("value") + .type((GraphQLOutputType) requireNonNull(current.getType("String")))) + .build(); + transformed = SchemaTransformer.transformSchema( + current, + visitor, + builder -> builder.additionalType(syntheticType)); + } else if (mutation.kind == EditKind.REMOVE_SYNTHETIC_TYPE) { + transformed = SchemaTransformer.transformSchemaWithDeletes(current, visitor); + } else { + transformed = SchemaTransformer.transformSchema(current, visitor); + } + assertTouchedDirectiveTargets("GraphQLSchema", version, touchedTargetCount[0], plan); + return transformed; + } + + private static GraphQLAppliedDirective newBenchmarkAppliedDirective(int version) { + return GraphQLAppliedDirective.newDirective() + .name(BENCHMARK_DIRECTIVE_NAME) + .argument(GraphQLAppliedDirectiveArgument.newArgument() + .name(BENCHMARK_DIRECTIVE_VERSION_ARGUMENT) + .type(Scalars.GraphQLInt) + .valueProgrammatic(version)) + .build(); + } + + private static GraphQLFieldDefinition newStringField( + GraphQLSchema schema, + String name) { + GraphQLOutputType stringType = + (GraphQLOutputType) requireNonNull(schema.getType("String")); + return GraphQLFieldDefinition.newFieldDefinition() + .name(name) + .type(stringType) + .build(); + } + + private static GraphQLObjectType withAddedField( + GraphQLObjectType objectType, + GraphQLFieldDefinition field) { + if (objectType.getFieldDefinition(field.getName()) != null) { + throw new IllegalStateException("Field already exists: " + field.getName()); + } + return objectType.transform(builder -> builder.field(field)); + } + + private static GraphQLObjectType withRemovedField( + GraphQLObjectType objectType, + String fieldName) { + List fields = + new ArrayList<>(objectType.getFieldDefinitions()); + boolean removed = fields.removeIf(field -> fieldName.equals(field.getName())); + if (!removed) { + throw new IllegalStateException("Field does not exist: " + fieldName); + } + return objectType.transform(builder -> builder.replaceFields(fields)); + } + + private static void createSUSchemaVersions( + SchemaUniverse universe, + SUSchema baseSchema, + MutationPlan plan) { + SUSchema current = baseSchema; + SUScalarType stringType = + requireNonNull(baseSchema.getScalarType("String")); + SUScalarType intType = + requireNonNull(baseSchema.getScalarType("Int")); + for (int i = 0; i < plan.mutations.size(); i++) { + Mutation mutation = plan.mutations.get(i); + current = applySUMutation( + universe, + current, + stringType, + intType, + mutation, + plan, + i + 1); + } + verifyFinalSUSchema(universe, baseSchema, current, plan); + } + + private static SUSchema applySUMutation( + SchemaUniverse universe, + SUSchema current, + SUScalarType stringType, + SUScalarType intType, + Mutation mutation, + MutationPlan plan, + int version) { + String versionName = String.format(Locale.ROOT, "version_%03d", version); + int expectedExistingDirectives = version == 1 ? 0 : 1; + return current.transform(versionName, builder -> { + switch (mutation.kind) { + case ADD_SYNTHETIC_TYPE: + SUObjectType syntheticType = + universe.newObjectType(mutation.syntheticTypeName); + SUField valueField = universe.newField("value"); + builder.addAdditionalType(syntheticType) + .addField(syntheticType, valueField) + .setFieldType(valueField, stringType); + break; + case ADD_SYNTHETIC_FIELD: + SUObjectType typeForAddition = + requireNonNull(current.getObjectType(mutation.syntheticTypeName)); + SUField syntheticField = + universe.newField(mutation.syntheticFieldName); + builder.addField(typeForAddition, syntheticField) + .setFieldType(syntheticField, stringType); + break; + case REMOVE_SYNTHETIC_FIELD: + SUObjectType typeForRemoval = + requireNonNull(current.getObjectType(mutation.syntheticTypeName)); + builder.removeField(typeForRemoval, mutation.syntheticFieldName); + break; + case ADD_REPLACEMENT_FIELD: + SUObjectType typeForReplacement = + requireNonNull(current.getObjectType(mutation.syntheticTypeName)); + SUField replacementField = + universe.newField(mutation.replacementFieldName); + builder.addField(typeForReplacement, replacementField) + .setFieldType(replacementField, stringType); + break; + case REMOVE_SYNTHETIC_TYPE: + builder.removeAdditionalType(mutation.syntheticTypeName); + break; + default: + throw new IllegalStateException("Unhandled mutation " + mutation.kind); + } + + int touchedTargetCount = 0; + for (String targetTypeName : plan.directiveTargetTypeNames) { + SUObjectType target = + requireNonNull(current.getObjectType(targetTypeName)); + List existingDirectives = + current.getAppliedDirectives(target, BENCHMARK_DIRECTIVE_NAME); + if (existingDirectives.size() != expectedExistingDirectives) { + throw new IllegalStateException(String.format( + Locale.ROOT, + "Expected %d @%s directives on %s before version %d, found %d", + expectedExistingDirectives, + BENCHMARK_DIRECTIVE_NAME, + targetTypeName, + version, + existingDirectives.size())); + } + for (SUAppliedDirective existingDirective : existingDirectives) { + builder.removeAppliedDirective(target, existingDirective); + } + + SUAppliedDirectiveArgument versionArgument = + universe.newAppliedDirectiveArgument( + BENCHMARK_DIRECTIVE_VERSION_ARGUMENT, + intType, + InputValueWithState.newExternalValue(version)); + SUAppliedDirective appliedDirective = + universe.newAppliedDirective( + BENCHMARK_DIRECTIVE_NAME, + List.of(versionArgument)); + builder.addAppliedDirective(target, appliedDirective); + touchedTargetCount++; + } + assertTouchedDirectiveTargets("SUSchema", version, touchedTargetCount, plan); + }); + } + + private static void verifyFinalGraphQLSchema( + GraphQLSchema baseSchema, + GraphQLSchema finalSchema, + List schemas, + MutationPlan plan) { + if (schemas.size() != plan.mutations.size() + 1) { + throw new IllegalStateException(String.format( + Locale.ROOT, + "Expected %d modified GraphQLSchema versions", + plan.mutations.size())); + } + if (!fieldNames(baseSchema.getQueryType()) + .equals(fieldNames(finalSchema.getQueryType()))) { + throw new IllegalStateException("Final GraphQLSchema query differs from the base"); + } + verifyFinalSyntheticType(finalSchema, plan); + verifyFinalGraphQLDirectives(finalSchema, plan); + } + + private static void verifyFinalSUSchema( + SchemaUniverse universe, + SUSchema baseSchema, + SUSchema finalSchema, + MutationPlan plan) { + if (universe.getSchemas().size() != plan.mutations.size() + 1) { + throw new IllegalStateException(String.format( + Locale.ROOT, + "Expected %d modified SUSchema versions", + plan.mutations.size())); + } + if (!fieldNames(baseSchema).equals(fieldNames(finalSchema))) { + throw new IllegalStateException("Final SUSchema query differs from the base"); + } + verifyFinalSyntheticType(finalSchema, plan); + verifyFinalSUDirectives(finalSchema, plan); + } + + private static void verifyFinalSyntheticType( + GraphQLSchema schema, + MutationPlan plan) { + int mutationCount = plan.mutations.size(); + int finalCycle = (mutationCount - 1) / EditKind.values().length; + boolean shouldBePresent = mutationCount % EditKind.values().length != 0; + boolean isPresent = schema.getType(SYNTHETIC_PREFIX + "Type" + finalCycle) != null; + if (isPresent != shouldBePresent) { + throw new IllegalStateException("Final synthetic GraphQLSchema type state differs"); + } + } + + private static void verifyFinalSyntheticType( + SUSchema schema, + MutationPlan plan) { + int mutationCount = plan.mutations.size(); + int finalCycle = (mutationCount - 1) / EditKind.values().length; + boolean shouldBePresent = mutationCount % EditKind.values().length != 0; + boolean isPresent = schema.getType(SYNTHETIC_PREFIX + "Type" + finalCycle) != null; + if (isPresent != shouldBePresent) { + throw new IllegalStateException("Final synthetic SUSchema type state differs"); + } + } + + private static void verifyFinalGraphQLDirectives( + GraphQLSchema schema, + MutationPlan plan) { + int expectedVersion = plan.mutations.size(); + for (String targetTypeName : plan.directiveTargetTypeNames) { + GraphQLObjectType target = requireNonNull(schema.getObjectType(targetTypeName)); + List directives = + target.getAppliedDirectives(BENCHMARK_DIRECTIVE_NAME); + if (directives.size() != 1 + || !Integer.valueOf(expectedVersion).equals(requireNonNull( + directives.get(0).getArgument(BENCHMARK_DIRECTIVE_VERSION_ARGUMENT)) + .getValue())) { + throw new IllegalStateException( + "Final GraphQLSchema directive differs on " + targetTypeName); + } + } + } + + private static void verifyFinalSUDirectives( + SUSchema schema, + MutationPlan plan) { + int expectedVersion = plan.mutations.size(); + for (String targetTypeName : plan.directiveTargetTypeNames) { + SUObjectType target = requireNonNull(schema.getObjectType(targetTypeName)); + List directives = + schema.getAppliedDirectives(target, BENCHMARK_DIRECTIVE_NAME); + if (directives.size() != 1) { + throw new IllegalStateException( + "Final SUSchema directive differs on " + targetTypeName); + } + SUAppliedDirectiveArgument argument = requireNonNull(schema.getArgument( + directives.get(0), + BENCHMARK_DIRECTIVE_VERSION_ARGUMENT)); + if (!Integer.valueOf(expectedVersion).equals(argument.getArgumentValue().getValue())) { + throw new IllegalStateException( + "Final SUSchema directive argument differs on " + targetTypeName); + } + } + } + + private static void assertTouchedDirectiveTargets( + String model, + int version, + int touchedTargetCount, + MutationPlan plan) { + if (touchedTargetCount != plan.directiveTargetTypeNames.size() + || touchedTargetCount < DIRECTIVE_TARGET_COUNT) { + throw new IllegalStateException(String.format( + Locale.ROOT, + "%s version %d touched %d directive targets; expected at least %d", + model, + version, + touchedTargetCount, + DIRECTIVE_TARGET_COUNT)); + } + } + + private static Set fieldNames(GraphQLObjectType query) { + Set result = new LinkedHashSet<>(); + for (GraphQLFieldDefinition field : query.getFieldDefinitions()) { + result.add(field.getName()); + } + return result; + } + + private static Set fieldNames(SUSchema schema) { + Set result = new LinkedHashSet<>(); + for (SUField field : schema.getFields(schema.getQueryType())) { + result.add(requireNonNull(field.getName())); + } + return result; + } + + private static void verifyEquivalentVersions( + GraphQLVersionSet graphQLVersions, + List suVersions) { + List expectedVersions = graphQLVersions.getSchemas(); + if (expectedVersions.size() != suVersions.size()) { + throw new IllegalStateException(String.format( + Locale.ROOT, + "Version count differs: GraphQLSchema=%d SUSchema=%d", + expectedVersions.size(), + suVersions.size())); + } + + SchemaPrinter.Options options = SchemaPrinter.Options.defaultOptions() + .includeSchemaDefinition(true) + .includeScalarTypes(true) + .setComparators(GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY); + SchemaPrinter printer = new SchemaPrinter(options); + for (int i = 0; i < expectedVersions.size(); i++) { + String expected = printer.print(expectedVersions.get(i)); + String actual = printer.print(suVersions.get(i).toGraphQLSchema()); + if (!expected.equals(actual)) { + throw new IllegalStateException(schemaDifference(i, expected, actual)); + } + System.out.printf( + Locale.ROOT, + "SCHEMA_EQUIVALENCE version=%d/%d matched%n", + i, + expectedVersions.size() - 1); + } + System.out.printf( + Locale.ROOT, + "SCHEMA_EQUIVALENCE allVersions=%d matched%n", + expectedVersions.size()); + } + + private static String schemaDifference( + int version, + String expected, + String actual) { + int commonLength = Math.min(expected.length(), actual.length()); + int offset = 0; + while (offset < commonLength + && expected.charAt(offset) == actual.charAt(offset)) { + offset++; + } + int start = Math.max(0, offset - 80); + int expectedEnd = Math.min(expected.length(), offset + 80); + int actualEnd = Math.min(actual.length(), offset + 80); + return String.format( + Locale.ROOT, + "Schema version %d differs at character %d:%nexpected: %s%nactual: %s", + version, + offset, + printable(expected.substring(start, expectedEnd)), + printable(actual.substring(start, actualEnd))); + } + + private static String printable(String value) { + return value.replace("\r", "\\r").replace("\n", "\\n"); + } + + private static GraphQLSchema loadBaseSchema() { + String schema = BenchmarkUtils.loadResource("large-schema-5.graphqls.part1") + + BenchmarkUtils.loadResource("large-schema-5.graphqls.part2"); + GraphQLSchema generatedSchema = SchemaGenerator.createdMockedSchema(schema); + if (generatedSchema.getDirective(BENCHMARK_DIRECTIVE_NAME) != null) { + throw new IllegalStateException( + "Benchmark directive already exists: " + BENCHMARK_DIRECTIVE_NAME); + } + GraphQLDirective benchmarkDirective = GraphQLDirective.newDirective() + .name(BENCHMARK_DIRECTIVE_NAME) + .argument(GraphQLArgument.newArgument() + .name(BENCHMARK_DIRECTIVE_VERSION_ARGUMENT) + .type(Scalars.GraphQLInt)) + .validLocation(DirectiveLocation.OBJECT) + .build(); + return generatedSchema.transform(builder -> + builder.additionalDirective(benchmarkDirective)); + } + + private static long retainedSize(Object root) { + return GraphLayout.parseInstance(root).totalSize(); + } + + private static void reportFootprint( + String model, + long baseBytes, + long totalBytes) { + long incrementalBytes = totalBytes - baseBytes; + System.out.printf( + Locale.ROOT, + "RETAINED_MEMORY model=%s modifiedVersions=%d baseBytes=%d " + + "totalBytes=%d incrementalBytes=%d incrementalBytesPerVersion=%.2f " + + "baseMiB=%.2f totalMiB=%.2f incrementalMiB=%.2f%n", + model, + VERSION_COUNT, + baseBytes, + totalBytes, + incrementalBytes, + incrementalBytes / (double) VERSION_COUNT, + baseBytes / BYTES_PER_MIB, + totalBytes / BYTES_PER_MIB, + incrementalBytes / BYTES_PER_MIB); + } + + public static final class GraphQLVersionSet { + private final List schemas; + + GraphQLVersionSet(List schemas) { + this.schemas = List.copyOf(schemas); + } + + public List getSchemas() { + return schemas; + } + } + + private enum EditKind { + ADD_SYNTHETIC_TYPE, + ADD_SYNTHETIC_FIELD, + REMOVE_SYNTHETIC_FIELD, + ADD_REPLACEMENT_FIELD, + REMOVE_SYNTHETIC_TYPE + } + + private static final class Mutation { + private final EditKind kind; + private final String syntheticFieldName; + private final String replacementFieldName; + private final String syntheticTypeName; + + private Mutation( + EditKind kind, + String syntheticFieldName, + String replacementFieldName, + String syntheticTypeName) { + this.kind = kind; + this.syntheticFieldName = syntheticFieldName; + this.replacementFieldName = replacementFieldName; + this.syntheticTypeName = syntheticTypeName; + } + } + + private static final class MutationPlan { + private final List mutations; + private final List directiveTargetTypeNames; + private final Set directiveTargetTypeNameSet; + + private MutationPlan( + List mutations, + List directiveTargetTypeNames) { + this.mutations = List.copyOf(mutations); + this.directiveTargetTypeNames = List.copyOf(directiveTargetTypeNames); + this.directiveTargetTypeNameSet = Set.copyOf(directiveTargetTypeNames); + } + + private static MutationPlan create(GraphQLSchema schema) { + List mutations = new ArrayList<>(VERSION_COUNT); + for (int cycle = 0; cycle < CYCLE_COUNT; cycle++) { + String syntheticFieldName = + SYNTHETIC_PREFIX + "Field" + cycle; + String replacementFieldName = + SYNTHETIC_PREFIX + "Replacement" + cycle; + String syntheticTypeName = + SYNTHETIC_PREFIX + "Type" + cycle; + if (schema.getType(syntheticTypeName) != null) { + throw new IllegalStateException( + "Synthetic benchmark type already exists: " + syntheticTypeName); + } + for (EditKind kind : EditKind.values()) { + mutations.add(new Mutation( + kind, + syntheticFieldName, + replacementFieldName, + syntheticTypeName)); + } + } + List directiveTargetTypeNames = + selectDirectiveTargetTypeNames(schema); + return new MutationPlan(mutations, directiveTargetTypeNames); + } + + private MutationPlan firstVersions(int versionCount) { + if (versionCount == mutations.size()) { + return this; + } + return new MutationPlan( + mutations.subList(0, versionCount), + directiveTargetTypeNames); + } + + private static List selectDirectiveTargetTypeNames(GraphQLSchema schema) { + List objectTypeNames = new ArrayList<>(); + for (GraphQLNamedType type : schema.getAllTypesAsList()) { + if (type instanceof GraphQLObjectType + && !type.getName().startsWith("__")) { + objectTypeNames.add(type.getName()); + } + } + objectTypeNames.sort(String::compareTo); + if (objectTypeNames.size() < DIRECTIVE_TARGET_COUNT) { + throw new IllegalStateException(String.format( + Locale.ROOT, + "Schema has only %d eligible directive targets; expected at least %d", + objectTypeNames.size(), + DIRECTIVE_TARGET_COUNT)); + } + + List selected = new ArrayList<>(DIRECTIVE_TARGET_COUNT); + for (int i = 0; i < DIRECTIVE_TARGET_COUNT; i++) { + int index = (int) ((long) i * objectTypeNames.size() + / DIRECTIVE_TARGET_COUNT); + selected.add(objectTypeNames.get(index)); + } + if (new LinkedHashSet<>(selected).size() != DIRECTIVE_TARGET_COUNT) { + throw new IllegalStateException("Directive target selection contains duplicates"); + } + return selected; + } + } +} diff --git a/src/main/java/graphql/Directives.java b/src/main/java/graphql/Directives.java index 2835867e05..0d81b08122 100644 --- a/src/main/java/graphql/Directives.java +++ b/src/main/java/graphql/Directives.java @@ -153,7 +153,7 @@ public class Directives { .directiveLocation(newDirectiveLocation().name(QUERY.name()).build()) .directiveLocation(newDirectiveLocation().name(MUTATION.name()).build()) .directiveLocation(newDirectiveLocation().name(SUBSCRIPTION.name()).build()) - .description(createDescription("This directive allows returning null in non-null positions that have an associated error")) + .description(createDescription("This directive disables error propagation when a non nullable field returns null for the given operation.")) .build(); } diff --git a/src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java b/src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java new file mode 100644 index 0000000000..7db136c930 --- /dev/null +++ b/src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java @@ -0,0 +1,631 @@ +package graphql.schema.idl; + +import graphql.Directives; +import graphql.DirectivesUtil; +import graphql.GraphQLContext; +import graphql.Internal; +import graphql.execution.ValuesResolver; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.Comment; +import graphql.language.Description; +import graphql.language.EnumTypeDefinition; +import graphql.language.EnumValueDefinition; +import graphql.language.FieldDefinition; +import graphql.language.InputObjectTypeDefinition; +import graphql.language.InputValueDefinition; +import graphql.language.InterfaceTypeDefinition; +import graphql.language.Node; +import graphql.language.ObjectTypeDefinition; +import graphql.language.ScalarTypeDefinition; +import graphql.language.SchemaDefinition; +import graphql.language.UnionTypeDefinition; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLDirectiveContainer; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeUtil; +import graphql.schema.GraphQLUnionType; +import graphql.schema.GraphqlTypeComparatorEnvironment; +import graphql.schema.InputValueWithState; +import graphql.schema.visibility.GraphqlFieldVisibility; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.stream.Collectors; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; + +@Internal +@NullMarked +public final class GraphQLSchemaPrintAccess implements SchemaPrintAccess { + + private final GraphQLSchema schema; + private final SchemaPrinter.Options options; + private final GraphqlFieldVisibility visibility; + + public GraphQLSchemaPrintAccess( + GraphQLSchema schema, + SchemaPrinter.Options options) { + this.schema = assertNotNull(schema); + this.options = assertNotNull(options); + this.visibility = schema.getCodeRegistry().getFieldVisibility(); + } + + @Override + public Object getSchema() { + return schema; + } + + @Override + public Object getQueryType() { + return schema.getQueryType(); + } + + @Override + public @Nullable Object getMutationType() { + return schema.getMutationType(); + } + + @Override + public @Nullable Object getSubscriptionType() { + return schema.getSubscriptionType(); + } + + @Override + public List getTypes() { + return new ArrayList<>(schema.getAllTypesAsList()); + } + + @Override + public List getDirectiveDefinitions() { + return new ArrayList<>(schema.getDirectives()); + } + + @Override + public SchemaPrintElementKind getKind(Object element) { + if (element instanceof GraphQLSchema) { + return SchemaPrintElementKind.SCHEMA; + } + if (element instanceof GraphQLObjectType) { + return SchemaPrintElementKind.OBJECT; + } + if (element instanceof GraphQLFieldDefinition) { + return SchemaPrintElementKind.FIELD; + } + if (element instanceof GraphQLInterfaceType) { + return SchemaPrintElementKind.INTERFACE; + } + if (element instanceof GraphQLUnionType) { + return SchemaPrintElementKind.UNION; + } + if (element instanceof GraphQLEnumType) { + return SchemaPrintElementKind.ENUM; + } + if (element instanceof GraphQLEnumValueDefinition) { + return SchemaPrintElementKind.ENUM_VALUE; + } + if (element instanceof GraphQLScalarType) { + return SchemaPrintElementKind.SCALAR; + } + if (element instanceof GraphQLInputObjectType) { + return SchemaPrintElementKind.INPUT_OBJECT; + } + if (element instanceof GraphQLInputObjectField) { + return SchemaPrintElementKind.INPUT_FIELD; + } + if (element instanceof GraphQLArgument) { + return SchemaPrintElementKind.ARGUMENT; + } + if (element instanceof GraphQLDirective) { + return SchemaPrintElementKind.DIRECTIVE; + } + if (element instanceof GraphQLAppliedDirective) { + return SchemaPrintElementKind.APPLIED_DIRECTIVE; + } + if (element instanceof GraphQLAppliedDirectiveArgument) { + return SchemaPrintElementKind.APPLIED_DIRECTIVE_ARGUMENT; + } + if (element instanceof GraphQLList) { + return SchemaPrintElementKind.LIST; + } + if (element instanceof GraphQLNonNull) { + return SchemaPrintElementKind.NON_NULL; + } + return assertShouldNeverHappen( + "Unsupported GraphQL schema print element %s", + element.getClass().getName()); + } + + @Override + public String getName(Object element) { + if (element instanceof GraphQLNamedType) { + return ((GraphQLNamedType) element).getName(); + } + if (element instanceof GraphQLFieldDefinition) { + return ((GraphQLFieldDefinition) element).getName(); + } + if (element instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) element).getName(); + } + if (element instanceof GraphQLEnumValueDefinition) { + return ((GraphQLEnumValueDefinition) element).getName(); + } + if (element instanceof GraphQLArgument) { + return ((GraphQLArgument) element).getName(); + } + if (element instanceof GraphQLDirective) { + return ((GraphQLDirective) element).getName(); + } + if (element instanceof GraphQLAppliedDirective) { + return ((GraphQLAppliedDirective) element).getName(); + } + if (element instanceof GraphQLAppliedDirectiveArgument) { + return ((GraphQLAppliedDirectiveArgument) element).getName(); + } + return assertShouldNeverHappen( + "Schema print element has no name: %s", + element.getClass().getName()); + } + + @Override + public @Nullable String getDescription(Object element) { + if (element instanceof GraphQLSchema) { + GraphQLSchema graphQLSchema = (GraphQLSchema) element; + SchemaDefinition definition = graphQLSchema.getDefinition(); + return description( + graphQLSchema.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLObjectType) { + GraphQLObjectType type = (GraphQLObjectType) element; + ObjectTypeDefinition definition = type.getDefinition(); + return description( + type.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLFieldDefinition) { + GraphQLFieldDefinition field = (GraphQLFieldDefinition) element; + FieldDefinition definition = field.getDefinition(); + return description( + field.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLInterfaceType) { + GraphQLInterfaceType type = (GraphQLInterfaceType) element; + InterfaceTypeDefinition definition = type.getDefinition(); + return description( + type.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLUnionType) { + GraphQLUnionType type = (GraphQLUnionType) element; + UnionTypeDefinition definition = type.getDefinition(); + return description( + type.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLEnumType) { + GraphQLEnumType type = (GraphQLEnumType) element; + EnumTypeDefinition definition = type.getDefinition(); + return description( + type.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLEnumValueDefinition) { + GraphQLEnumValueDefinition value = + (GraphQLEnumValueDefinition) element; + EnumValueDefinition definition = value.getDefinition(); + return description( + value.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLScalarType) { + GraphQLScalarType type = (GraphQLScalarType) element; + ScalarTypeDefinition definition = type.getDefinition(); + return description( + type.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLInputObjectType) { + GraphQLInputObjectType type = (GraphQLInputObjectType) element; + InputObjectTypeDefinition definition = type.getDefinition(); + return description( + type.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLInputObjectField) { + GraphQLInputObjectField field = (GraphQLInputObjectField) element; + InputValueDefinition definition = field.getDefinition(); + return description( + field.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLArgument) { + GraphQLArgument argument = (GraphQLArgument) element; + InputValueDefinition definition = argument.getDefinition(); + return description( + argument.getDescription(), + definition == null ? null : definition.getDescription()); + } + if (element instanceof GraphQLDirective) { + return ((GraphQLDirective) element).getDescription(); + } + return null; + } + + private @Nullable String description( + @Nullable String runtimeDescription, + @Nullable Description astDescription) { + if (runtimeDescription != null && !runtimeDescription.isEmpty()) { + return runtimeDescription; + } + return astDescription == null ? runtimeDescription : astDescription.getContent(); + } + + @Override + public @Nullable String getAstDefinitionComments(Object element) { + Node definition = getDefinition(element); + if (definition == null || definition.getComments().isEmpty()) { + return null; + } + return definition.getComments().stream() + .map(Comment::getContent) + .collect(Collectors.joining("\n", "", "\n")); + } + + @Override + public boolean isIncluded(Object element) { + return options.getIncludeSchemaElement().test((GraphQLSchemaElement) element); + } + + @Override + public boolean isIntrospectionType(Object type) { + return !options.isIncludeIntrospectionTypes() + && getName(type).startsWith("__"); + } + + @Override + public boolean isSpecifiedScalar(Object type) { + return ScalarInfo.isGraphqlSpecifiedScalar((GraphQLScalarType) type); + } + + @Override + public List getFields(Object type) { + if (type instanceof GraphQLObjectType) { + return new ArrayList<>( + visibility.getFieldDefinitions((GraphQLObjectType) type)); + } + return new ArrayList<>( + visibility.getFieldDefinitions((GraphQLInterfaceType) type)); + } + + @Override + public List getArguments(Object fieldOrDirective) { + if (fieldOrDirective instanceof GraphQLFieldDefinition) { + return new ArrayList<>( + ((GraphQLFieldDefinition) fieldOrDirective).getArguments()); + } + return new ArrayList<>(((GraphQLDirective) fieldOrDirective).getArguments()); + } + + @Override + public Object getType(Object typedElement) { + if (typedElement instanceof GraphQLFieldDefinition) { + return ((GraphQLFieldDefinition) typedElement).getType(); + } + if (typedElement instanceof GraphQLArgument) { + return ((GraphQLArgument) typedElement).getType(); + } + if (typedElement instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) typedElement).getType(); + } + return ((GraphQLAppliedDirectiveArgument) typedElement).getType(); + } + + @Override + public List getInterfaces(Object type) { + if (type instanceof GraphQLObjectType) { + return new ArrayList<>(((GraphQLObjectType) type).getInterfaces()); + } + return new ArrayList<>(((GraphQLInterfaceType) type).getInterfaces()); + } + + @Override + public List getUnionMembers(Object unionType) { + return new ArrayList<>(((GraphQLUnionType) unionType).getTypes()); + } + + @Override + public List getEnumValues(Object enumType) { + return new ArrayList<>(((GraphQLEnumType) enumType).getValues()); + } + + @Override + public List getInputFields(Object inputObjectType) { + return new ArrayList<>( + visibility.getFieldDefinitions((GraphQLInputObjectType) inputObjectType)); + } + + @Override + public List getAppliedDirectives(Object container) { + if (container instanceof GraphQLSchema) { + GraphQLSchema graphQLSchema = (GraphQLSchema) container; + return new ArrayList<>(DirectivesUtil.toAppliedDirectives( + graphQLSchema.getSchemaAppliedDirectives(), + graphQLSchema.getSchemaDirectives())); + } + GraphQLDirectiveContainer directiveContainer = + (GraphQLDirectiveContainer) container; + List directives = + DirectivesUtil.toAppliedDirectives(directiveContainer); + if (!isDeprecated(container)) { + return new ArrayList<>(directives); + } + return new ArrayList<>(withDeprecationReason( + directives, + assertNotNull(getDeprecationReason(container)))); + } + + @Override + public List getAppliedDirectiveArguments(Object directive) { + return new ArrayList<>( + ((GraphQLAppliedDirective) directive).getArguments()); + } + + @Override + public InputValueWithState getDefaultValue(Object argumentOrInputField) { + if (argumentOrInputField instanceof GraphQLArgument) { + return ((GraphQLArgument) argumentOrInputField).getArgumentDefaultValue(); + } + return ((GraphQLInputObjectField) argumentOrInputField) + .getInputFieldDefaultValue(); + } + + @Override + public InputValueWithState getAppliedDirectiveArgumentValue(Object argument) { + return ((GraphQLAppliedDirectiveArgument) argument).getArgumentValue(); + } + + @Override + public boolean isRepeatable(Object directive) { + return ((GraphQLDirective) directive).isRepeatable(); + } + + @Override + public Set getDirectiveLocations(Object directive) { + return ((GraphQLDirective) directive).validLocations(); + } + + @Override + public @Nullable String getSpecifiedByUrl(Object scalar) { + return ((GraphQLScalarType) scalar).getSpecifiedByUrl(); + } + + @Override + public String getTypeString(Object type) { + return GraphQLTypeUtil.simplePrint((GraphQLType) type); + } + + @Override + public String printValue(InputValueWithState value, Object type) { + return graphql.language.AstPrinter.printAst(ValuesResolver.valueToLiteral( + value, + (GraphQLInputType) type, + GraphQLContext.getDefault(), + Locale.getDefault())); + } + + @Override + @SuppressWarnings({"rawtypes", "unchecked"}) + public List sort( + @Nullable Object parent, + SchemaPrintChildKind childKind, + List elements) { + Class parentType = + parent == null ? GraphQLSchemaElement.class : elementClass(parent); + Class elementType = childClass(childKind); + GraphqlTypeComparatorEnvironment environment = + GraphqlTypeComparatorEnvironment.newEnvironment() + .parentType(parentType) + .elementType(elementType) + .build(); + Comparator comparator = options.getComparatorRegistry().getComparator(environment); + List result = new ArrayList<>(elements); + result.sort(comparator); + return result; + } + + private @Nullable Node getDefinition(Object element) { + if (element instanceof GraphQLSchema) { + return ((GraphQLSchema) element).getDefinition(); + } + if (element instanceof GraphQLObjectType) { + return ((GraphQLObjectType) element).getDefinition(); + } + if (element instanceof GraphQLFieldDefinition) { + return ((GraphQLFieldDefinition) element).getDefinition(); + } + if (element instanceof GraphQLInterfaceType) { + return ((GraphQLInterfaceType) element).getDefinition(); + } + if (element instanceof GraphQLUnionType) { + return ((GraphQLUnionType) element).getDefinition(); + } + if (element instanceof GraphQLEnumType) { + return ((GraphQLEnumType) element).getDefinition(); + } + if (element instanceof GraphQLEnumValueDefinition) { + return ((GraphQLEnumValueDefinition) element).getDefinition(); + } + if (element instanceof GraphQLScalarType) { + return ((GraphQLScalarType) element).getDefinition(); + } + if (element instanceof GraphQLInputObjectType) { + return ((GraphQLInputObjectType) element).getDefinition(); + } + if (element instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) element).getDefinition(); + } + if (element instanceof GraphQLArgument) { + return ((GraphQLArgument) element).getDefinition(); + } + if (element instanceof GraphQLDirective) { + return ((GraphQLDirective) element).getDefinition(); + } + return null; + } + + private boolean isDeprecated(Object element) { + if (element instanceof GraphQLFieldDefinition) { + return ((GraphQLFieldDefinition) element).isDeprecated(); + } + if (element instanceof GraphQLEnumValueDefinition) { + return ((GraphQLEnumValueDefinition) element).isDeprecated(); + } + if (element instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) element).isDeprecated(); + } + if (element instanceof GraphQLArgument) { + return ((GraphQLArgument) element).isDeprecated(); + } + return false; + } + + private @Nullable String getDeprecationReason(Object element) { + if (element instanceof GraphQLFieldDefinition) { + return ((GraphQLFieldDefinition) element).getDeprecationReason(); + } + if (element instanceof GraphQLEnumValueDefinition) { + return ((GraphQLEnumValueDefinition) element).getDeprecationReason(); + } + if (element instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) element).getDeprecationReason(); + } + if (element instanceof GraphQLArgument) { + return ((GraphQLArgument) element).getDeprecationReason(); + } + return null; + } + + private List withDeprecationReason( + List directives, + String reason) { + List result = new ArrayList<>(directives.size() + 1); + boolean found = false; + for (GraphQLAppliedDirective directive : directives) { + if (!Directives.DeprecatedDirective.getName().equals(directive.getName())) { + result.add(directive); + continue; + } + found = true; + GraphQLAppliedDirectiveArgument existing = directive.getArgument("reason"); + if (existing != null + && existing.getArgumentValue() == InputValueWithState.NOT_SET) { + result.add(directive); + continue; + } + result.add(directive.transform(builder -> + builder.argument(deprecationReasonArgument(reason)))); + } + if (!found) { + result.add(GraphQLAppliedDirective.newDirective() + .name(Directives.DeprecatedDirective.getName()) + .argument(deprecationReasonArgument(reason)) + .build()); + } + return result; + } + + private GraphQLAppliedDirectiveArgument deprecationReasonArgument(String reason) { + return GraphQLAppliedDirectiveArgument.newArgument() + .name("reason") + .valueProgrammatic(reason) + .type(graphql.Scalars.GraphQLString) + .build(); + } + + private Class elementClass(Object element) { + switch (getKind(element)) { + case SCHEMA: + return GraphQLSchemaElement.class; + case OBJECT: + return GraphQLObjectType.class; + case FIELD: + return GraphQLFieldDefinition.class; + case INTERFACE: + return GraphQLInterfaceType.class; + case UNION: + return GraphQLUnionType.class; + case ENUM: + return GraphQLEnumType.class; + case ENUM_VALUE: + return GraphQLEnumValueDefinition.class; + case SCALAR: + return GraphQLScalarType.class; + case INPUT_OBJECT: + return GraphQLInputObjectType.class; + case INPUT_FIELD: + return GraphQLInputObjectField.class; + case ARGUMENT: + return GraphQLArgument.class; + case DIRECTIVE: + return GraphQLDirective.class; + case APPLIED_DIRECTIVE: + return GraphQLAppliedDirective.class; + case APPLIED_DIRECTIVE_ARGUMENT: + return GraphQLAppliedDirectiveArgument.class; + default: + return assertShouldNeverHappen( + "No comparator class for %s", + element.getClass().getName()); + } + } + + private @Nullable Class childClass( + SchemaPrintChildKind childKind) { + switch (childKind) { + case TOP_LEVEL: + return null; + case FIELD: + return GraphQLFieldDefinition.class; + case ARGUMENT: + return GraphQLArgument.class; + case INTERFACE: + case UNION_MEMBER: + return GraphQLOutputType.class; + case ENUM_VALUE: + return GraphQLEnumValueDefinition.class; + case INPUT_FIELD: + return GraphQLInputObjectField.class; + case APPLIED_DIRECTIVE: + return GraphQLAppliedDirective.class; + case APPLIED_DIRECTIVE_ARGUMENT: + return GraphQLAppliedDirectiveArgument.class; + default: + return assertShouldNeverHappen("Unsupported schema print child kind"); + } + } +} diff --git a/src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java b/src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java new file mode 100644 index 0000000000..94d17a022b --- /dev/null +++ b/src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java @@ -0,0 +1,457 @@ +package graphql.schema.idl; + +import graphql.Internal; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.ArrayValue; +import graphql.language.AstPrinter; +import graphql.language.BooleanValue; +import graphql.language.Comment; +import graphql.language.EnumValue; +import graphql.language.FloatValue; +import graphql.language.IntValue; +import graphql.language.Node; +import graphql.language.NullValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.schema.GraphqlTypeComparatorRegistry; +import graphql.schema.InputValueWithState; +import graphql.schema.universe.SUAppliedDirective; +import graphql.schema.universe.SUAppliedDirectiveArgument; +import graphql.schema.universe.SUArgument; +import graphql.schema.universe.SUDirective; +import graphql.schema.universe.SUEnumType; +import graphql.schema.universe.SUInputField; +import graphql.schema.universe.SUInputObjectType; +import graphql.schema.universe.SUInterfaceType; +import graphql.schema.universe.SUListType; +import graphql.schema.universe.SUNonNullType; +import graphql.schema.universe.SUObjectType; +import graphql.schema.universe.SUScalarType; +import graphql.schema.universe.SUSchema; +import graphql.schema.universe.SUUnionType; +import graphql.schema.universe.SUVertex; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.lang.reflect.Array; +import java.math.BigDecimal; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; + +@Internal +@NullMarked +public final class SUSchemaPrintAccess implements SchemaPrintAccess { + + private final SUSchema schema; + private final SchemaPrinter.Options options; + + public SUSchemaPrintAccess( + SUSchema schema, + SchemaPrinter.Options options) { + this.schema = assertNotNull(schema); + this.options = assertNotNull(options); + } + + @Override + public Object getSchema() { + return schema.getRoot(); + } + + @Override + public Object getQueryType() { + return schema.getQueryType(); + } + + @Override + public @Nullable Object getMutationType() { + return schema.getMutationType(); + } + + @Override + public @Nullable Object getSubscriptionType() { + return schema.getSubscriptionType(); + } + + @Override + public List getTypes() { + return new ArrayList<>(schema.getTypes()); + } + + @Override + public List getDirectiveDefinitions() { + return new ArrayList<>(schema.getDirectiveDefinitions()); + } + + @Override + public SchemaPrintElementKind getKind(Object element) { + if (element instanceof SUAppliedDirectiveArgument) { + return SchemaPrintElementKind.APPLIED_DIRECTIVE_ARGUMENT; + } + SUVertex vertex = (SUVertex) element; + switch (vertex.getKind()) { + case SCHEMA: + return SchemaPrintElementKind.SCHEMA; + case OBJECT: + return SchemaPrintElementKind.OBJECT; + case FIELD: + return SchemaPrintElementKind.FIELD; + case INTERFACE: + return SchemaPrintElementKind.INTERFACE; + case UNION: + return SchemaPrintElementKind.UNION; + case ENUM: + return SchemaPrintElementKind.ENUM; + case ENUM_VALUE: + return SchemaPrintElementKind.ENUM_VALUE; + case SCALAR: + return SchemaPrintElementKind.SCALAR; + case INPUT_OBJECT: + return SchemaPrintElementKind.INPUT_OBJECT; + case INPUT_FIELD: + return SchemaPrintElementKind.INPUT_FIELD; + case ARGUMENT: + return SchemaPrintElementKind.ARGUMENT; + case DIRECTIVE: + return SchemaPrintElementKind.DIRECTIVE; + case APPLIED_DIRECTIVE: + return SchemaPrintElementKind.APPLIED_DIRECTIVE; + case LIST: + return SchemaPrintElementKind.LIST; + case NON_NULL: + return SchemaPrintElementKind.NON_NULL; + default: + return assertShouldNeverHappen( + "Unsupported schema universe print vertex %s", + vertex); + } + } + + @Override + public String getName(Object element) { + if (element instanceof SUAppliedDirectiveArgument) { + return ((SUAppliedDirectiveArgument) element).getName(); + } + return assertNotNull(((SUVertex) element).getName()); + } + + @Override + public @Nullable String getDescription(Object element) { + if (element instanceof SUAppliedDirectiveArgument) { + return null; + } + return ((SUVertex) element).getDescription(); + } + + @Override + public @Nullable String getAstDefinitionComments(Object element) { + Node definition = definition(element); + if (definition == null || definition.getComments().isEmpty()) { + return null; + } + return definition.getComments().stream() + .map(Comment::getContent) + .collect(Collectors.joining("\n", "", "\n")); + } + + @Override + public boolean isIncluded(Object element) { + return true; + } + + @Override + public boolean isIntrospectionType(Object type) { + return !options.isIncludeIntrospectionTypes() + && getName(type).startsWith("__"); + } + + @Override + public boolean isSpecifiedScalar(Object type) { + return ScalarInfo.isGraphqlSpecifiedScalar(getName(type)); + } + + @Override + public List getFields(Object type) { + if (type instanceof SUObjectType) { + return new ArrayList<>(schema.getFields((SUObjectType) type)); + } + return new ArrayList<>(schema.getFields((SUInterfaceType) type)); + } + + @Override + public List getArguments(Object fieldOrDirective) { + if (fieldOrDirective instanceof SUDirective) { + return new ArrayList<>( + schema.getArguments((SUDirective) fieldOrDirective)); + } + return new ArrayList<>( + schema.getArguments((graphql.schema.universe.SUField) fieldOrDirective)); + } + + @Override + public Object getType(Object typedElement) { + if (typedElement instanceof SUAppliedDirectiveArgument) { + return schema.getType((SUAppliedDirectiveArgument) typedElement); + } + if (typedElement instanceof SUArgument) { + return assertNotNull(schema.getType((SUArgument) typedElement)); + } + if (typedElement instanceof SUInputField) { + return assertNotNull(schema.getType((SUInputField) typedElement)); + } + return assertNotNull( + schema.getType((graphql.schema.universe.SUField) typedElement)); + } + + @Override + public List getInterfaces(Object type) { + if (type instanceof SUObjectType) { + return new ArrayList<>(schema.getInterfaces((SUObjectType) type)); + } + return new ArrayList<>(schema.getInterfaces((SUInterfaceType) type)); + } + + @Override + public List getUnionMembers(Object unionType) { + return new ArrayList<>( + schema.getUnionMembers((SUUnionType) unionType)); + } + + @Override + public List getEnumValues(Object enumType) { + return new ArrayList<>( + schema.getEnumValues((SUEnumType) enumType)); + } + + @Override + public List getInputFields(Object inputObjectType) { + return new ArrayList<>( + schema.getInputFields((SUInputObjectType) inputObjectType)); + } + + @Override + public List getAppliedDirectives(Object container) { + return new ArrayList<>( + schema.getAppliedDirectives((SUVertex) container)); + } + + @Override + public List getAppliedDirectiveArguments(Object directive) { + return new ArrayList<>( + schema.getArguments((SUAppliedDirective) directive)); + } + + @Override + public InputValueWithState getDefaultValue(Object argumentOrInputField) { + if (argumentOrInputField instanceof SUArgument) { + return ((SUArgument) argumentOrInputField).getArgumentDefaultValue(); + } + return ((SUInputField) argumentOrInputField).getInputFieldDefaultValue(); + } + + @Override + public InputValueWithState getAppliedDirectiveArgumentValue(Object argument) { + return ((SUAppliedDirectiveArgument) argument).getArgumentValue(); + } + + @Override + public boolean isRepeatable(Object directive) { + return ((SUDirective) directive).isRepeatable(); + } + + @Override + public Set getDirectiveLocations(Object directive) { + return ((SUDirective) directive).validLocations(); + } + + @Override + public @Nullable String getSpecifiedByUrl(Object scalar) { + for (SUAppliedDirective directive : + schema.getAppliedDirectives( + (SUScalarType) scalar, + "specifiedBy")) { + SUAppliedDirectiveArgument url = schema.getArgument(directive, "url"); + if (url == null || url.getArgumentValue().isNotSet()) { + continue; + } + Object value = url.getArgumentValue().getValue(); + if (value instanceof StringValue) { + return ((StringValue) value).getValue(); + } + return value == null ? null : String.valueOf(value); + } + return null; + } + + @Override + public String getTypeString(Object type) { + SUVertex vertex = (SUVertex) type; + if (vertex instanceof SUListType) { + return "[" + getTypeString(assertNotNull( + schema.getWrappedType((SUListType) vertex))) + "]"; + } + if (vertex instanceof SUNonNullType) { + return getTypeString(assertNotNull( + schema.getWrappedType((SUNonNullType) vertex))) + "!"; + } + return assertNotNull(vertex.getName()); + } + + @Override + public String printValue(InputValueWithState value, Object type) { + if (value.isLiteral()) { + return AstPrinter.printAst((Value) assertNotNull(value.getValue())); + } + return AstPrinter.printAst(toLiteral(value.getValue(), (SUVertex) type)); + } + + @Override + public List sort( + @Nullable Object parent, + SchemaPrintChildKind childKind, + List elements) { + List result = new ArrayList<>(elements); + if (options.getComparatorRegistry() == GraphqlTypeComparatorRegistry.AS_IS_REGISTRY) { + return result; + } + Comparator comparator = Comparator.comparing(this::getName); + if (childKind == SchemaPrintChildKind.TOP_LEVEL + && options.getComparatorRegistry() + != GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) { + comparator = Comparator.comparingInt(this::topLevelRank) + .thenComparing(this::getName); + } + result.sort(comparator); + return result; + } + + private @Nullable Node definition(Object element) { + if (element instanceof SUArgument) { + return ((SUArgument) element).getDefinition(); + } + if (element instanceof SUInputField) { + return ((SUInputField) element).getDefinition(); + } + if (element instanceof SUDirective) { + return ((SUDirective) element).getDefinition(); + } + if (element instanceof SUAppliedDirective) { + return ((SUAppliedDirective) element).getDefinition(); + } + if (element instanceof SUAppliedDirectiveArgument) { + return ((SUAppliedDirectiveArgument) element).getDefinition(); + } + return null; + } + + private int topLevelRank(Object element) { + switch (getKind(element)) { + case DIRECTIVE: + return 1; + case INTERFACE: + return 2; + case UNION: + return 3; + case OBJECT: + return 4; + case ENUM: + return 5; + case SCALAR: + return 6; + case INPUT_OBJECT: + return 7; + default: + return 0; + } + } + + private Value toLiteral(@Nullable Object value, SUVertex type) { + if (type instanceof SUNonNullType) { + return toLiteral( + value, + assertNotNull(schema.getWrappedType((SUNonNullType) type))); + } + if (value == null) { + return NullValue.of(); + } + if (type instanceof SUListType) { + return listLiteral(value, (SUListType) type); + } + if (type instanceof SUInputObjectType) { + return objectLiteral(value, (SUInputObjectType) type); + } + if (type instanceof SUEnumType) { + return EnumValue.newEnumValue(String.valueOf(value)).build(); + } + return scalarLiteral(value, assertNotNull(type.getName())); + } + + private Value listLiteral(Object value, SUListType type) { + SUVertex wrappedType = assertNotNull(schema.getWrappedType(type)); + List values = new ArrayList<>(); + if (value instanceof Iterable) { + for (Object item : (Iterable) value) { + values.add(toLiteral(item, wrappedType)); + } + } else if (value.getClass().isArray()) { + for (int i = 0; i < Array.getLength(value); i++) { + values.add(toLiteral(Array.get(value, i), wrappedType)); + } + } else { + values.add(toLiteral(value, wrappedType)); + } + return ArrayValue.newArrayValue().values(values).build(); + } + + private Value objectLiteral(Object value, SUInputObjectType type) { + if (!(value instanceof Map)) { + return assertShouldNeverHappen( + "Cannot print value '%s' for input object '%s' without a map", + value, + getName(type)); + } + Map map = (Map) value; + List fields = new ArrayList<>(); + for (SUInputField field : schema.getInputFields(type)) { + String name = assertNotNull(field.getName()); + if (!map.containsKey(name)) { + continue; + } + fields.add(ObjectField.newObjectField() + .name(name) + .value(toLiteral( + map.get(name), + assertNotNull(schema.getType(field)))) + .build()); + } + return ObjectValue.newObjectValue().objectFields(fields).build(); + } + + private Value scalarLiteral(Object value, String scalarName) { + if ("String".equals(scalarName) || "ID".equals(scalarName)) { + return StringValue.newStringValue(String.valueOf(value)).build(); + } + if ("Boolean".equals(scalarName) && value instanceof Boolean) { + return BooleanValue.newBooleanValue((Boolean) value).build(); + } + if ("Int".equals(scalarName) && value instanceof Number) { + return IntValue.newIntValue(new BigInteger(String.valueOf(value))).build(); + } + if ("Float".equals(scalarName) && value instanceof Number) { + return FloatValue.newFloatValue(new BigDecimal(String.valueOf(value))).build(); + } + return assertShouldNeverHappen( + "Cannot print a non-literal value for custom scalar '%s' " + + "because SUSchema does not retain its coercing", + scalarName); + } +} diff --git a/src/main/java/graphql/schema/idl/SchemaPrintAccess.java b/src/main/java/graphql/schema/idl/SchemaPrintAccess.java new file mode 100644 index 0000000000..681769a36e --- /dev/null +++ b/src/main/java/graphql/schema/idl/SchemaPrintAccess.java @@ -0,0 +1,78 @@ +package graphql.schema.idl; + +import graphql.Internal; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.schema.InputValueWithState; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +@Internal +@NullMarked +public interface SchemaPrintAccess { + + Object getSchema(); + + Object getQueryType(); + + @Nullable Object getMutationType(); + + @Nullable Object getSubscriptionType(); + + List getTypes(); + + List getDirectiveDefinitions(); + + SchemaPrintElementKind getKind(Object element); + + String getName(Object element); + + @Nullable String getDescription(Object element); + + @Nullable String getAstDefinitionComments(Object element); + + boolean isIncluded(Object element); + + boolean isIntrospectionType(Object type); + + boolean isSpecifiedScalar(Object type); + + List getFields(Object type); + + List getArguments(Object fieldOrDirective); + + Object getType(Object typedElement); + + List getInterfaces(Object type); + + List getUnionMembers(Object unionType); + + List getEnumValues(Object enumType); + + List getInputFields(Object inputObjectType); + + List getAppliedDirectives(Object container); + + List getAppliedDirectiveArguments(Object directive); + + InputValueWithState getDefaultValue(Object argumentOrInputField); + + InputValueWithState getAppliedDirectiveArgumentValue(Object argument); + + boolean isRepeatable(Object directive); + + Set getDirectiveLocations(Object directive); + + @Nullable String getSpecifiedByUrl(Object scalar); + + String getTypeString(Object type); + + String printValue(InputValueWithState value, Object type); + + List sort( + @Nullable Object parent, + SchemaPrintChildKind childKind, + List elements); +} diff --git a/src/main/java/graphql/schema/idl/SchemaPrintChildKind.java b/src/main/java/graphql/schema/idl/SchemaPrintChildKind.java new file mode 100644 index 0000000000..8e1cc5f3c7 --- /dev/null +++ b/src/main/java/graphql/schema/idl/SchemaPrintChildKind.java @@ -0,0 +1,18 @@ +package graphql.schema.idl; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +@Internal +@NullMarked +public enum SchemaPrintChildKind { + TOP_LEVEL, + FIELD, + ARGUMENT, + INTERFACE, + UNION_MEMBER, + ENUM_VALUE, + INPUT_FIELD, + APPLIED_DIRECTIVE, + APPLIED_DIRECTIVE_ARGUMENT +} diff --git a/src/main/java/graphql/schema/idl/SchemaPrintElementKind.java b/src/main/java/graphql/schema/idl/SchemaPrintElementKind.java new file mode 100644 index 0000000000..febfe729d3 --- /dev/null +++ b/src/main/java/graphql/schema/idl/SchemaPrintElementKind.java @@ -0,0 +1,25 @@ +package graphql.schema.idl; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +@Internal +@NullMarked +public enum SchemaPrintElementKind { + SCHEMA, + OBJECT, + FIELD, + INTERFACE, + UNION, + ENUM, + ENUM_VALUE, + SCALAR, + INPUT_OBJECT, + INPUT_FIELD, + ARGUMENT, + DIRECTIVE, + APPLIED_DIRECTIVE, + APPLIED_DIRECTIVE_ARGUMENT, + LIST, + NON_NULL +} diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index b199926588..c777c89795 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -3,6 +3,7 @@ import graphql.Assert; import graphql.Directives; import graphql.DirectivesUtil; +import graphql.ExperimentalApi; import graphql.GraphQLContext; import graphql.PublicApi; import graphql.execution.ValuesResolver; @@ -49,6 +50,7 @@ import graphql.schema.GraphqlTypeComparatorEnvironment; import graphql.schema.GraphqlTypeComparatorRegistry; import graphql.schema.InputValueWithState; +import graphql.schema.universe.SUSchema; import graphql.schema.visibility.GraphqlFieldVisibility; import java.io.PrintWriter; @@ -506,6 +508,12 @@ public String print(Document schemaIDL) { * @return the logical schema definition */ public String print(GraphQLSchema schema) { + if (!options.isUseAstDefinitions()) { + return new SchemaPrinterWriter( + new GraphQLSchemaPrintAccess(schema, options), + options) + .print(); + } StringWriter sw = new StringWriter(); PrintWriter out = new PrintWriter(sw); @@ -531,6 +539,25 @@ public String print(GraphQLSchema schema) { return trimNewLineChars(sw.toString()); } + /** + * Prints a schema-universe snapshot directly without materializing a {@link GraphQLSchema}. + * + *

Schema-universe extensions are flattened into their semantic topology, so this always + * produces canonical semantic SDL even when {@link Options#useAstDefinitions(boolean)} is + * enabled.

+ * + * @param schema the schema-universe snapshot + * + * @return the logical schema definition + */ + @ExperimentalApi + public String print(SUSchema schema) { + return new SchemaPrinterWriter( + new SUSchemaPrintAccess(schema, options), + options) + .print(); + } + private interface SchemaElementPrinter { void print(PrintWriter out, T schemaElement, GraphqlFieldVisibility visibility); diff --git a/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java b/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java new file mode 100644 index 0000000000..e24dd34ca2 --- /dev/null +++ b/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java @@ -0,0 +1,509 @@ +package graphql.schema.idl; + +import graphql.Internal; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.schema.InputValueWithState; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.util.EscapeUtil.escapeJsonString; + +@Internal +@NullMarked +public final class SchemaPrinterWriter { + + private final SchemaPrintAccess access; + private final SchemaPrinter.Options options; + + public SchemaPrinterWriter( + SchemaPrintAccess access, + SchemaPrinter.Options options) { + this.access = assertNotNull(access); + this.options = assertNotNull(options); + } + + public String print() { + StringWriter stringWriter = new StringWriter(); + PrintWriter out = new PrintWriter(stringWriter); + printSchema(out); + + List elements = new ArrayList<>(); + elements.addAll(access.getTypes()); + elements.addAll(access.getDirectiveDefinitions()); + elements.removeIf(element -> !access.isIncluded(element)); + elements = access.sort(null, SchemaPrintChildKind.TOP_LEVEL, elements); + for (Object element : elements) { + printElement(out, element); + } + return trimNewLineChars(stringWriter.toString()); + } + + private void printSchema(PrintWriter out) { + Object queryType = access.getQueryType(); + Object mutationType = access.getMutationType(); + Object subscriptionType = access.getSubscriptionType(); + boolean needsSchemaPrinted = options.isIncludeSchemaDefinition() + || !"Query".equals(access.getName(queryType)); + if (mutationType != null + && !"Mutation".equals(access.getName(mutationType))) { + needsSchemaPrinted = true; + } + if (subscriptionType != null + && !"Subscription".equals(access.getName(subscriptionType))) { + needsSchemaPrinted = true; + } + if (!needsSchemaPrinted) { + return; + } + + Object schema = access.getSchema(); + printComments(out, schema, ""); + out.format( + "schema %s{\n", + directivesString(schema, access.getAppliedDirectives(schema), true)); + out.format(" query: %s\n", access.getName(queryType)); + if (mutationType != null) { + out.format(" mutation: %s\n", access.getName(mutationType)); + } + if (subscriptionType != null) { + out.format(" subscription: %s\n", access.getName(subscriptionType)); + } + out.format("}\n\n"); + } + + private void printElement(PrintWriter out, Object element) { + switch (access.getKind(element)) { + case OBJECT: + printObject(out, element); + return; + case INTERFACE: + printInterface(out, element); + return; + case UNION: + printUnion(out, element); + return; + case ENUM: + printEnum(out, element); + return; + case SCALAR: + printScalar(out, element); + return; + case INPUT_OBJECT: + printInputObject(out, element); + return; + case DIRECTIVE: + printDirectiveDefinition(out, element); + return; + default: + assertShouldNeverHappen( + "Unsupported top-level schema print element %s", + element); + } + } + + private void printObject(PrintWriter out, Object type) { + if (access.isIntrospectionType(type)) { + return; + } + printComments(out, type, ""); + out.format("type %s", access.getName(type)); + printInterfaces(out, type); + out.print(directivesString( + type, + access.getAppliedDirectives(type), + false)); + printFields(out, type); + out.format("\n\n"); + } + + private void printInterface(PrintWriter out, Object type) { + if (access.isIntrospectionType(type)) { + return; + } + printComments(out, type, ""); + out.format("interface %s", access.getName(type)); + printInterfaces(out, type); + out.print(directivesString( + type, + access.getAppliedDirectives(type), + false)); + printFields(out, type); + out.format("\n\n"); + } + + private void printInterfaces(PrintWriter out, Object type) { + List interfaces = access.sort( + type, + SchemaPrintChildKind.INTERFACE, + access.getInterfaces(type)); + if (interfaces.isEmpty()) { + return; + } + String names = interfaces.stream() + .map(access::getName) + .collect(Collectors.joining(" & ")); + out.format(" implements %s", names); + } + + private void printFields(PrintWriter out, Object type) { + List fields = access.sort( + type, + SchemaPrintChildKind.FIELD, + access.getFields(type)); + fields.removeIf(field -> !access.isIncluded(field)); + if (fields.isEmpty()) { + return; + } + out.format(" {\n"); + for (Object field : fields) { + printComments(out, field, " "); + out.format( + " %s%s: %s%s\n", + access.getName(field), + argsString(field), + access.getTypeString(access.getType(field)), + directivesString( + field, + access.getAppliedDirectives(field), + false)); + } + out.format("}"); + } + + private void printUnion(PrintWriter out, Object type) { + if (access.isIntrospectionType(type)) { + return; + } + printComments(out, type, ""); + out.format( + "union %s%s = ", + access.getName(type), + directivesString( + type, + access.getAppliedDirectives(type), + false)); + List members = access.sort( + type, + SchemaPrintChildKind.UNION_MEMBER, + access.getUnionMembers(type)); + out.print(members.stream() + .map(access::getName) + .collect(Collectors.joining(" | "))); + out.format("\n\n"); + } + + private void printEnum(PrintWriter out, Object type) { + if (access.isIntrospectionType(type)) { + return; + } + printComments(out, type, ""); + out.format( + "enum %s%s", + access.getName(type), + directivesString( + type, + access.getAppliedDirectives(type), + false)); + List values = access.sort( + type, + SchemaPrintChildKind.ENUM_VALUE, + access.getEnumValues(type)); + if (!values.isEmpty()) { + out.format(" {\n"); + for (Object value : values) { + printComments(out, value, " "); + out.format( + " %s%s\n", + access.getName(value), + directivesString( + value, + access.getAppliedDirectives(value), + false)); + } + out.format("}"); + } + out.format("\n\n"); + } + + private void printScalar(PrintWriter out, Object type) { + if (!options.isIncludeScalars() || access.isSpecifiedScalar(type)) { + return; + } + printComments(out, type, ""); + List directives = access.getAppliedDirectives(type).stream() + .filter(directive -> !"specifiedBy".equals(access.getName(directive))) + .collect(Collectors.toList()); + out.format( + "scalar %s%s%s\n\n", + access.getName(type), + directivesString(type, directives, false), + specifiedByUrlString(type)); + } + + private String specifiedByUrlString(Object scalar) { + String url = access.getSpecifiedByUrl(scalar); + if (url == null || !options.getIncludeDirective().test("specifiedBy")) { + return ""; + } + return " @specifiedBy(url : \"" + escapeJsonString(url) + "\")"; + } + + private void printInputObject(PrintWriter out, Object type) { + if (access.isIntrospectionType(type)) { + return; + } + printComments(out, type, ""); + out.format( + "input %s%s", + access.getName(type), + directivesString( + type, + access.getAppliedDirectives(type), + false)); + List fields = access.sort( + type, + SchemaPrintChildKind.INPUT_FIELD, + access.getInputFields(type)); + fields.removeIf(field -> !access.isIncluded(field)); + if (!fields.isEmpty()) { + out.format(" {\n"); + for (Object field : fields) { + printInputField(out, field); + } + out.format("}"); + } + out.format("\n\n"); + } + + private void printInputField(PrintWriter out, Object field) { + printComments(out, field, " "); + Object type = access.getType(field); + out.format( + " %s: %s", + access.getName(field), + access.getTypeString(type)); + InputValueWithState defaultValue = access.getDefaultValue(field); + if (defaultValue.isSet()) { + out.format(" = %s", access.printValue(defaultValue, type)); + } + out.print(directivesString( + field, + access.getAppliedDirectives(field), + false)); + out.format("\n"); + } + + private void printDirectiveDefinition(PrintWriter out, Object directive) { + if (!options.isIncludeDirectiveDefinitions() + || !options.getIncludeDirective().test(access.getName(directive)) + || !options.getIncludeDirectiveDefinition() + .test(access.getName(directive))) { + return; + } + printComments(out, directive, ""); + out.format( + "directive @%s%s", + access.getName(directive), + argsString(directive)); + if (access.isRepeatable(directive)) { + out.print(" repeatable"); + } + String locations = access.getDirectiveLocations(directive).stream() + .map(DirectiveLocation::name) + .collect(Collectors.joining(" | ")); + out.format(" on %s\n\n", locations); + } + + private String argsString(Object parent) { + List arguments = access.getArguments(parent); + boolean hasComments = arguments.stream() + .anyMatch(this::hasAstDefinitionComments); + boolean hasDescriptions = arguments.stream() + .anyMatch(this::hasDescription); + String halfPrefix = hasComments || hasDescriptions ? " " : ""; + String prefix = hasComments || hasDescriptions ? " " : ""; + arguments = access.sort( + parent, + SchemaPrintChildKind.ARGUMENT, + arguments); + arguments.removeIf(argument -> !access.isIncluded(argument)); + if (arguments.isEmpty()) { + return ""; + } + + StringBuilder result = new StringBuilder("("); + for (int i = 0; i < arguments.size(); i++) { + Object argument = arguments.get(i); + if (i > 0) { + result.append(","); + if (!hasComments && !hasDescriptions) { + result.append(" "); + } + } + if (hasComments || hasDescriptions) { + result.append("\n"); + } + result.append(commentsString(argument, prefix)); + Object type = access.getType(argument); + result.append(prefix) + .append(access.getName(argument)) + .append(": ") + .append(access.getTypeString(type)); + InputValueWithState defaultValue = access.getDefaultValue(argument); + if (defaultValue.isSet()) { + result.append(" = ") + .append(access.printValue(defaultValue, type)); + } + result.append(directivesString( + argument, + access.getAppliedDirectives(argument), + false)); + } + if (hasComments || hasDescriptions) { + result.append("\n"); + } + return result.append(halfPrefix).append(")").toString(); + } + + private String directivesString( + Object parent, + List directives, + boolean schemaContainer) { + directives = directives.stream() + .filter(access::isIncluded) + .filter(directive -> options.getIncludeDirective() + .test(access.getName(directive))) + .collect(Collectors.toList()); + if (directives.isEmpty()) { + return ""; + } + directives = access.sort( + parent, + SchemaPrintChildKind.APPLIED_DIRECTIVE, + directives); + String result = directives.stream() + .map(this::directiveString) + .collect(Collectors.joining(" ")); + return schemaContainer ? result : " " + result; + } + + private String directiveString(Object directive) { + StringBuilder result = new StringBuilder("@") + .append(access.getName(directive)); + List arguments = access.getAppliedDirectiveArguments(directive) + .stream() + .filter(argument -> access.getAppliedDirectiveArgumentValue(argument) + .isSet()) + .collect(Collectors.toList()); + arguments = access.sort( + directive, + SchemaPrintChildKind.APPLIED_DIRECTIVE_ARGUMENT, + arguments); + if (arguments.isEmpty()) { + return result.toString(); + } + result.append("("); + for (int i = 0; i < arguments.size(); i++) { + Object argument = arguments.get(i); + if (i > 0) { + result.append(", "); + } + result.append(access.getName(argument)) + .append(" : ") + .append(access.printValue( + access.getAppliedDirectiveArgumentValue(argument), + access.getType(argument))); + } + return result.append(")").toString(); + } + + private void printComments( + PrintWriter out, + Object element, + String prefix) { + out.print(commentsString(element, prefix)); + } + + private String commentsString(Object element, String prefix) { + StringWriter stringWriter = new StringWriter(); + PrintWriter out = new PrintWriter(stringWriter); + String description = access.getDescription(element); + if (description != null && !description.isEmpty()) { + printDescription(out, prefix, description); + } + if (options.isIncludeAstDefinitionComments()) { + String comments = access.getAstDefinitionComments(element); + if (comments != null && !comments.isEmpty()) { + printHashDescription( + out, + prefix, + Arrays.asList(comments.split("\n"))); + } + } + return stringWriter.toString(); + } + + private void printDescription( + PrintWriter out, + String prefix, + String description) { + List lines = Arrays.asList(description.split("\n")); + if (lines.isEmpty()) { + return; + } + if (options.isDescriptionsAsHashComments()) { + printHashDescription(out, prefix, lines); + return; + } + if (lines.size() > 1) { + out.printf("%s\"\"\"\n", prefix); + for (String line : lines) { + out.printf( + "%s%s\n", + prefix, + line.replaceAll("\"\"\"", "\\\\\"\"\"")); + } + out.printf("%s\"\"\"\n", prefix); + return; + } + out.printf("%s\"%s\"\n", prefix, escapeJsonString(lines.get(0))); + } + + private void printHashDescription( + PrintWriter out, + String prefix, + List lines) { + for (String line : lines) { + out.printf("%s#%s\n", prefix, line); + } + } + + private boolean hasDescription(Object element) { + String description = access.getDescription(element); + return description != null && !description.isEmpty(); + } + + private boolean hasAstDefinitionComments(Object element) { + if (!options.isIncludeAstDefinitionComments()) { + return false; + } + String comments = access.getAstDefinitionComments(element); + return comments != null && !comments.isEmpty(); + } + + private String trimNewLineChars(String value) { + if (value.endsWith("\n\n")) { + return value.substring(0, value.length() - 1); + } + return value; + } +} diff --git a/src/main/java/graphql/schema/universe/EdgeMapBranch.java b/src/main/java/graphql/schema/universe/EdgeMapBranch.java new file mode 100644 index 0000000000..3d2a5b3710 --- /dev/null +++ b/src/main/java/graphql/schema/universe/EdgeMapBranch.java @@ -0,0 +1,143 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; + +/** + * An immutable, bitmap-compressed 32-way branch in the persistent edge map. + * + *

Each bit in {@code bitmap} denotes an occupied logical slot selected by five hash bits. + * {@code children} is a dense array containing only occupied slots, in ascending slot order. The + * array index for a slot is the number of set bits preceding it.

+ * + *

Updates copy only this branch's dense child array and replace the affected child. All other + * child nodes remain shared with the previous map version.

+ */ +@Internal +@NullMarked +public final class EdgeMapBranch implements EdgeMapNode { + + private final int bitmap; + private final EdgeMapNode[] children; + + /** + * Creates a branch from a bitmap and its dense, slot-ordered child array. + * + * @param bitmap the occupied logical slots + * @param children children for the occupied slots, in ascending slot order + */ + public EdgeMapBranch(int bitmap, EdgeMapNode[] children) { + this.bitmap = bitmap; + this.children = children; + } + + /** + * Builds the minimum branch path needed to distinguish two leaves. + * + *

If both hashes select the same slot at this depth, another single-child branch is created + * and the next five hash bits are examined. {@link PersistentEdgeMap}'s hash mixer is + * one-to-one for integer keys, so distinct source IDs eventually select different slots and no + * separate full-hash collision node is needed.

+ * + * @param first the first leaf + * @param second the second leaf + * @param shift the number of hash bits already consumed + * + * @return a branch containing both leaves + */ + public static EdgeMapNode merge(EdgeMapLeaf first, EdgeMapLeaf second, int shift) { + int firstSlot = slot(first.getHash(), shift); + int secondSlot = slot(second.getHash(), shift); + if (firstSlot == secondSlot) { + EdgeMapNode child = merge(first, second, shift + 5); + return new EdgeMapBranch(bit(firstSlot), new EdgeMapNode[]{child}); + } + int combinedBitmap = bit(firstSlot) | bit(secondSlot); + EdgeMapNode[] children = firstSlot < secondSlot + ? new EdgeMapNode[]{first, second} + : new EdgeMapNode[]{second, first}; + return new EdgeMapBranch(combinedBitmap, children); + } + + @Override + public @Nullable PackedEdgeSet get(int key, int hash, int shift) { + int bit = bit(slot(hash, shift)); + if ((bitmap & bit) == 0) { + return null; + } + return children[index(bit)].get(key, hash, shift + 5); + } + + @Override + public EdgeMapNode put(int key, int hash, PackedEdgeSet value, int shift) { + int bit = bit(slot(hash, shift)); + int index = index(bit); + if ((bitmap & bit) == 0) { + return insert(bit, index, new EdgeMapLeaf(key, hash, value)); + } + EdgeMapNode oldChild = children[index]; + EdgeMapNode newChild = oldChild.put(key, hash, value, shift + 5); + if (oldChild == newChild) { + return this; + } + EdgeMapNode[] newChildren = Arrays.copyOf(children, children.length); + newChildren[index] = newChild; + return new EdgeMapBranch(bitmap, newChildren); + } + + @Override + public @Nullable EdgeMapNode remove(int key, int hash, int shift) { + int bit = bit(slot(hash, shift)); + if ((bitmap & bit) == 0) { + return this; + } + int index = index(bit); + EdgeMapNode oldChild = children[index]; + EdgeMapNode newChild = oldChild.remove(key, hash, shift + 5); + if (oldChild == newChild) { + return this; + } + if (newChild != null) { + EdgeMapNode[] newChildren = Arrays.copyOf(children, children.length); + newChildren[index] = newChild; + return new EdgeMapBranch(bitmap, newChildren); + } + return removeChild(bit, index); + } + + private EdgeMapNode insert(int bit, int index, EdgeMapNode child) { + EdgeMapNode[] newChildren = new EdgeMapNode[children.length + 1]; + System.arraycopy(children, 0, newChildren, 0, index); + newChildren[index] = child; + System.arraycopy(children, index, newChildren, index + 1, children.length - index); + return new EdgeMapBranch(bitmap | bit, newChildren); + } + + private @Nullable EdgeMapNode removeChild(int bit, int index) { + if (children.length == 1) { + return null; + } + EdgeMapNode[] newChildren = new EdgeMapNode[children.length - 1]; + System.arraycopy(children, 0, newChildren, 0, index); + System.arraycopy(children, index + 1, newChildren, index, children.length - index - 1); + if (newChildren.length == 1 && newChildren[0] instanceof EdgeMapLeaf) { + return newChildren[0]; + } + return new EdgeMapBranch(bitmap ^ bit, newChildren); + } + + private int index(int bit) { + return Integer.bitCount(bitmap & (bit - 1)); + } + + private static int slot(int hash, int shift) { + return (hash >>> shift) & 31; + } + + private static int bit(int slot) { + return 1 << slot; + } +} diff --git a/src/main/java/graphql/schema/universe/EdgeMapLeaf.java b/src/main/java/graphql/schema/universe/EdgeMapLeaf.java new file mode 100644 index 0000000000..6346269be3 --- /dev/null +++ b/src/main/java/graphql/schema/universe/EdgeMapLeaf.java @@ -0,0 +1,62 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * An immutable terminal mapping from one source vertex ID to its outgoing edges. + * + *

The mixed hash is retained so that two leaves can be separated into the required branch + * levels without hashing either key again. Leaves compare the original key on lookup, making the + * key authoritative after trie navigation has selected a candidate leaf.

+ */ +@Internal +@NullMarked +public final class EdgeMapLeaf implements EdgeMapNode { + + private final int key; + private final int hash; + private final PackedEdgeSet value; + + /** + * Creates a leaf. + * + * @param key the source vertex ID + * @param hash the mixed key hash + * @param value the complete outgoing edge set for the source + */ + public EdgeMapLeaf(int key, int hash, PackedEdgeSet value) { + this.key = key; + this.hash = hash; + this.value = value; + } + + @Override + public @Nullable PackedEdgeSet get(int requestedKey, int requestedHash, int shift) { + return key == requestedKey ? value : null; + } + + @Override + public EdgeMapNode put(int requestedKey, int requestedHash, PackedEdgeSet requestedValue, int shift) { + if (key == requestedKey) { + return value == requestedValue ? this : new EdgeMapLeaf(key, hash, requestedValue); + } + EdgeMapLeaf other = new EdgeMapLeaf(requestedKey, requestedHash, requestedValue); + return EdgeMapBranch.merge(this, other, shift); + } + + @Override + public @Nullable EdgeMapNode remove(int requestedKey, int requestedHash, int shift) { + return key == requestedKey ? null : this; + } + + /** + * Returns the mixed hash used to place this leaf in the trie. + * + * @return the mixed key hash + */ + public int getHash() { + return hash; + } +} diff --git a/src/main/java/graphql/schema/universe/EdgeMapNode.java b/src/main/java/graphql/schema/universe/EdgeMapNode.java new file mode 100644 index 0000000000..30129bdaa5 --- /dev/null +++ b/src/main/java/graphql/schema/universe/EdgeMapNode.java @@ -0,0 +1,56 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * A node in the persistent map from source vertex IDs to their complete outgoing edge sets. + * + *

The map is a bitmap-compressed hash-array mapped trie. Each branch consumes five bits of the + * mixed key hash, so {@code shift} advances by five on every recursive call. Implementations are + * immutable: an update returns either the same node for a no-op or a replacement node that shares + * all unaffected descendants.

+ * + *

A nullable node result represents an empty subtree. A missing value is represented by + * {@code null} inside the trie and normalized to {@link PackedEdgeSet#empty()} by + * {@link PersistentEdgeMap}.

+ */ +@Internal +@NullMarked +public interface EdgeMapNode { + + /** + * Finds the outgoing edges for {@code key}. + * + * @param key the source vertex ID + * @param hash the mixed key hash + * @param shift the number of hash bits already consumed + * + * @return the stored edge set, or {@code null} when the key is absent + */ + @Nullable PackedEdgeSet get(int key, int hash, int shift); + + /** + * Associates {@code key} with {@code value}. + * + * @param key the source vertex ID + * @param hash the mixed key hash + * @param value the complete non-empty outgoing edge set + * @param shift the number of hash bits already consumed + * + * @return this node for an identity no-op, otherwise a structurally shared replacement + */ + EdgeMapNode put(int key, int hash, PackedEdgeSet value, int shift); + + /** + * Removes {@code key}. + * + * @param key the source vertex ID + * @param hash the mixed key hash + * @param shift the number of hash bits already consumed + * + * @return this node when the key is absent, a replacement node, or {@code null} if empty + */ + @Nullable EdgeMapNode remove(int key, int hash, int shift); +} diff --git a/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java b/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java new file mode 100644 index 0000000000..d5f6d802ab --- /dev/null +++ b/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java @@ -0,0 +1,158 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; + +import static graphql.Assert.assertTrue; + +/** + * A transient mutable copy of one source vertex's outgoing edges. + * + *

{@link SUSchemaBuilder} creates this object lazily when a source vertex is first changed. + * Additions may be unsorted and may contain exact duplicates while editing. {@link #freeze()} + * canonicalizes the data into an immutable {@link PackedEdgeSet}, folds exact duplicates, and + * validates the cardinality rules declared by {@link SUEdgeKind}.

+ */ +@Internal +@NullMarked +public final class MutablePackedEdgeSet { + + private long[] edges; + private int size; + + /** + * Creates a mutable copy of an immutable edge set. + * + * @param original the source edge set + */ + public MutablePackedEdgeSet(PackedEdgeSet original) { + this.edges = original.copyEdges(); + this.size = edges.length; + } + + /** + * Appends a packed edge. + * + *

Ordering and duplicate handling are deferred until {@link #freeze()}.

+ * + * @param edge the packed edge + */ + public void add(long edge) { + ensureCapacity(size + 1); + edges[size++] = edge; + } + + /** + * Removes the first exact occurrence of a packed edge. + * + * @param edge the packed edge + */ + public void remove(long edge) { + for (int i = 0; i < size; i++) { + if (edges[i] != edge) { + continue; + } + removeAt(i); + return; + } + } + + /** + * Removes every edge with the given kind and target name. + * + * @param kind the relationship kind + * @param nameId the target name ID + */ + public void removeByName(SUEdgeKind kind, int nameId) { + int i = 0; + while (i < size) { + long edge = edges[i]; + boolean matches = PackedEdgeSet.edgeKind(edge) == kind + && PackedEdgeSet.edgeNameId(edge) == nameId; + if (!matches) { + i++; + continue; + } + removeAt(i); + } + } + + /** + * Removes every edge of a relationship kind. + * + * @param kind the relationship kind + */ + public void removeKind(SUEdgeKind kind) { + int i = 0; + while (i < size) { + if (PackedEdgeSet.edgeKind(edges[i]) != kind) { + i++; + continue; + } + removeAt(i); + } + } + + /** + * Produces a canonical immutable edge set. + * + *

The result is sorted, exact duplicates are removed, single-valued edge kinds are checked, + * and kinds requiring unique target names are validated.

+ * + * @return the immutable edge set + */ + public PackedEdgeSet freeze() { + if (size == 0) { + return PackedEdgeSet.empty(); + } + long[] sorted = Arrays.copyOf(edges, size); + Arrays.sort(sorted); + int uniqueSize = removeExactDuplicates(sorted); + validateCardinality(sorted, uniqueSize); + return new PackedEdgeSet(Arrays.copyOf(sorted, uniqueSize)); + } + + private int removeExactDuplicates(long[] sorted) { + int writeIndex = 1; + for (int readIndex = 1; readIndex < sorted.length; readIndex++) { + if (sorted[readIndex] == sorted[writeIndex - 1]) { + continue; + } + sorted[writeIndex++] = sorted[readIndex]; + } + return writeIndex; + } + + private void validateCardinality(long[] sorted, int uniqueSize) { + for (int i = 1; i < uniqueSize; i++) { + long previous = sorted[i - 1]; + long current = sorted[i]; + SUEdgeKind kind = PackedEdgeSet.edgeKind(current); + if (kind != PackedEdgeSet.edgeKind(previous)) { + continue; + } + assertTrue(!kind.isSingle(), "Edge kind %s only allows one target", kind); + int nameId = PackedEdgeSet.edgeNameId(current); + boolean duplicateName = nameId == PackedEdgeSet.edgeNameId(previous); + assertTrue(!kind.isUniqueName() || !duplicateName, "Edge kind %s requires unique target names", kind); + } + } + + private void ensureCapacity(int requiredSize) { + if (requiredSize <= edges.length) { + return; + } + int newSize = Math.max(requiredSize, Math.max(4, edges.length * 2)); + edges = Arrays.copyOf(edges, newSize); + } + + private void removeAt(int index) { + int moved = size - index - 1; + if (moved > 0) { + System.arraycopy(edges, index + 1, edges, index, moved); + } + size--; + } +} diff --git a/src/main/java/graphql/schema/universe/PackedEdgeSet.java b/src/main/java/graphql/schema/universe/PackedEdgeSet.java new file mode 100644 index 0000000000..f7090fc7f6 --- /dev/null +++ b/src/main/java/graphql/schema/universe/PackedEdgeSet.java @@ -0,0 +1,241 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +import java.util.Arrays; + +import static graphql.Assert.assertTrue; + +/** + * The immutable, sorted outgoing adjacency of one source vertex. + * + *

Each edge occupies one {@code long} with this layout:

+ * + *
+ *  63          56 55                  32 31                    0
+ * +--------------+----------------------+-----------------------+
+ * | edge kind    | target name ID       | target vertex ID      |
+ * | 8 bits       | 24 bits              | 32 bits               |
+ * +--------------+----------------------+-----------------------+
+ * 
+ * + *

The packed values are sorted by kind, name ID, and target ID. Edges of one kind are therefore + * contiguous, while exact and name-based lookups can use binary search. Instances are values in + * {@link PersistentEdgeMap}; unchanged source vertices share the same instance across schema + * snapshots.

+ */ +@Internal +@NullMarked +public final class PackedEdgeSet { + + private static final int MAX_NAME_ID = 0x00ff_ffff; + private static final long TARGET_MASK = 0xffff_ffffL; + private static final long NAME_MASK = 0x00ff_ffffL; + private static final PackedEdgeSet EMPTY = new PackedEdgeSet(new long[0]); + + private final long[] edges; + + /** + * Creates a set that takes ownership of an already sorted and validated edge array. + * + *

The array is not copied and must not be modified after construction. General callers + * should build through {@link MutablePackedEdgeSet#freeze()}.

+ * + * @param edges sorted, unique packed edges + */ + public PackedEdgeSet(long[] edges) { + this.edges = edges; + } + + /** + * Returns the shared empty edge set. + * + * @return the empty edge set + */ + public static PackedEdgeSet empty() { + return EMPTY; + } + + /** + * Packs an edge into its compact long representation. + * + * @param kind the relationship kind + * @param nameId the universe-interned target name ID, or zero for an unnamed target + * @param targetId the target vertex ID + * + * @return the packed edge + */ + public static long pack(SUEdgeKind kind, int nameId, int targetId) { + assertTrue(nameId >= 0 && nameId <= MAX_NAME_ID, "Schema universe name id is too large: %s", nameId); + assertTrue(targetId >= 0, "Schema universe target id must be positive: %s", targetId); + return ((long) kind.getCode() << 56) + | ((long) nameId << 32) + | (targetId & TARGET_MASK); + } + + /** + * Returns the number of edges. + * + * @return the edge count + */ + public int size() { + return edges.length; + } + + /** + * Reports whether this set contains no edges. + * + * @return {@code true} when empty + */ + public boolean isEmpty() { + return edges.length == 0; + } + + /** + * Tests for one exact edge. + * + * @param kind the relationship kind + * @param nameId the target name ID + * @param targetId the target vertex ID + * + * @return {@code true} when the exact edge exists + */ + public boolean contains(SUEdgeKind kind, int nameId, int targetId) { + return Arrays.binarySearch(edges, pack(kind, nameId, targetId)) >= 0; + } + + /** + * Finds a target by relationship kind and target name. + * + *

For edge kinds that permit duplicate names, this returns the first target in target-ID + * order.

+ * + * @param kind the relationship kind + * @param nameId the target name ID + * + * @return the target vertex ID, or {@code -1} when absent + */ + public int targetByName(SUEdgeKind kind, int nameId) { + long prefix = prefix(kind, nameId); + int index = lowerBound(prefix); + if (index == edges.length || edgePrefix(edges[index]) != prefix) { + return -1; + } + return targetId(edges[index]); + } + + /** + * Returns the first target for an edge kind. + * + * @param kind the relationship kind + * + * @return the first target vertex ID, or {@code -1} when absent + */ + public int firstTarget(SUEdgeKind kind) { + int index = firstIndex(kind); + if (index == edges.length || edgeKind(edges[index]) != kind) { + return -1; + } + return targetId(edges[index]); + } + + /** + * Returns the inclusive array index at which an edge kind begins. + * + * @param kind the relationship kind + * + * @return the first possible index for the kind + */ + public int firstIndex(SUEdgeKind kind) { + return lowerBound(prefix(kind, 0)); + } + + /** + * Returns the exclusive array index at which an edge kind ends. + * + * @param kind the relationship kind + * + * @return the index immediately after the kind's edges + */ + public int endIndex(SUEdgeKind kind) { + long nextKindPrefix = (long) (kind.getCode() + 1) << 56; + return lowerBound(nextKindPrefix); + } + + /** + * Extracts the target vertex ID at an array index. + * + * @param index an index between {@link #firstIndex(SUEdgeKind)} and + * {@link #endIndex(SUEdgeKind)} + * + * @return the target vertex ID + */ + public int targetIdAt(int index) { + return targetId(edges[index]); + } + + /** + * Copies the packed edge array for mutable editing. + * + * @return a new array containing all packed edges + */ + public long[] copyEdges() { + return Arrays.copyOf(edges, edges.length); + } + + /** + * Extracts the relationship kind from a packed edge. + * + * @param edge a packed edge + * + * @return the relationship kind + */ + public static SUEdgeKind edgeKind(long edge) { + return SUEdgeKind.fromCode((int) (edge >>> 56)); + } + + /** + * Extracts the target name ID from a packed edge. + * + * @param edge a packed edge + * + * @return the target name ID + */ + public static int edgeNameId(long edge) { + return (int) ((edge >>> 32) & NAME_MASK); + } + + /** + * Extracts the target vertex ID from a packed edge. + * + * @param edge a packed edge + * + * @return the target vertex ID + */ + public static int targetId(long edge) { + return (int) edge; + } + + private static long prefix(SUEdgeKind kind, int nameId) { + return ((long) kind.getCode() << 56) | ((long) nameId << 32); + } + + private static long edgePrefix(long edge) { + return edge & 0xffff_ffff_0000_0000L; + } + + private int lowerBound(long value) { + int low = 0; + int high = edges.length; + while (low < high) { + int middle = (low + high) >>> 1; + if (edges[middle] < value) { + low = middle + 1; + continue; + } + high = middle; + } + return low; + } +} diff --git a/src/main/java/graphql/schema/universe/PersistentEdgeMap.java b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java new file mode 100644 index 0000000000..ce3e88dd0b --- /dev/null +++ b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java @@ -0,0 +1,106 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * An immutable map from source vertex IDs to complete outgoing {@link PackedEdgeSet}s. + * + *

The map is implemented as a bitmap-compressed hash-array mapped trie with a logical branching + * factor of 32. Updating one source copies only the nodes on its trie path; unaffected branches and + * edge sets remain shared between {@link SUSchema} snapshots.

+ * + *

Only non-empty edge sets are stored. Looking up an absent source returns the shared empty edge + * set, and putting an empty set removes the source binding. The integer mixer is a permutation: + * XOR-shifts are reversible and multiplication by an odd integer is invertible modulo + * {@code 2^32}. Distinct source IDs therefore cannot produce a full-hash collision.

+ */ +@Internal +@NullMarked +public final class PersistentEdgeMap { + + private static final PersistentEdgeMap EMPTY = new PersistentEdgeMap(null); + + private final @Nullable EdgeMapNode root; + + /** + * Creates a map around an immutable trie root. + * + * @param root the trie root, or {@code null} for an empty map + */ + public PersistentEdgeMap(@Nullable EdgeMapNode root) { + this.root = root; + } + + /** + * Returns the shared empty map. + * + * @return the empty map + */ + public static PersistentEdgeMap empty() { + return EMPTY; + } + + /** + * Returns all outgoing edges stored for a source vertex. + * + * @param key the source vertex ID + * + * @return the stored edge set, or the shared empty set when absent + */ + public PackedEdgeSet get(int key) { + if (root == null) { + return PackedEdgeSet.empty(); + } + PackedEdgeSet result = root.get(key, mix(key), 0); + return result == null ? PackedEdgeSet.empty() : result; + } + + /** + * Returns a map associating a source vertex with its complete outgoing edge set. + * + *

An empty value removes the binding. Supplying the same value instance for an existing key + * is an identity no-op and returns this map.

+ * + * @param key the source vertex ID + * @param value the complete outgoing edge set + * + * @return this map for a no-op, otherwise a structurally shared replacement + */ + public PersistentEdgeMap put(int key, PackedEdgeSet value) { + if (value.isEmpty()) { + return remove(key); + } + int hash = mix(key); + EdgeMapNode newRoot = root == null + ? new EdgeMapLeaf(key, hash, value) + : root.put(key, hash, value, 0); + return root == newRoot ? this : new PersistentEdgeMap(newRoot); + } + + /** + * Returns a map without the source vertex binding. + * + * @param key the source vertex ID + * + * @return this map when absent, otherwise a structurally shared replacement + */ + public PersistentEdgeMap remove(int key) { + if (root == null) { + return this; + } + EdgeMapNode newRoot = root.remove(key, mix(key), 0); + return root == newRoot ? this : new PersistentEdgeMap(newRoot); + } + + private static int mix(int value) { + int result = value; + result ^= result >>> 16; + result *= 0x7feb352d; + result ^= result >>> 15; + result *= 0x846ca68b; + result ^= result >>> 16; + return result; + } +} diff --git a/src/main/java/graphql/schema/universe/SUAppliedDirective.java b/src/main/java/graphql/schema/universe/SUAppliedDirective.java new file mode 100644 index 0000000000..9bf94407f7 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUAppliedDirective.java @@ -0,0 +1,55 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.language.Directive; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static graphql.Assert.assertNotNull; + +@ExperimentalApi +@NullMarked +public final class SUAppliedDirective extends SUVertex { + + private final @Nullable Directive definition; + private final SUAppliedDirectiveArgument[] arguments; + + @Internal + public SUAppliedDirective( + int id, + int nameId, + String name, + @Nullable Directive definition, + List arguments) { + super(id, nameId, SUVertexKind.APPLIED_DIRECTIVE, name, null); + this.definition = definition; + this.arguments = assertNotNull(arguments) + .toArray(new SUAppliedDirectiveArgument[0]); + } + + public @Nullable Directive getDefinition() { + return definition; + } + + public List getArguments() { + if (arguments.length == 0) { + return Collections.emptyList(); + } + return Collections.unmodifiableList(Arrays.asList(arguments)); + } + + public @Nullable SUAppliedDirectiveArgument getArgument(String name) { + assertNotNull(name); + for (SUAppliedDirectiveArgument argument : arguments) { + if (name.equals(argument.getName())) { + return argument; + } + } + return null; + } +} diff --git a/src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java b/src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java new file mode 100644 index 0000000000..6f7a84d971 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java @@ -0,0 +1,62 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.language.Argument; +import graphql.schema.InputValueWithState; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import static graphql.Assert.assertNotNull; + +@ExperimentalApi +@NullMarked +public final class SUAppliedDirectiveArgument { + + private final SchemaUniverse universe; + private final String name; + private final int typeId; + private final InputValueWithState value; + private final @Nullable Argument definition; + + @Internal + public SUAppliedDirectiveArgument( + SchemaUniverse universe, + String name, + int typeId, + InputValueWithState value, + @Nullable Argument definition) { + this.universe = assertNotNull(universe); + this.name = assertNotNull(name); + this.typeId = typeId; + this.value = assertNotNull(value); + this.definition = definition; + } + + public String getName() { + return name; + } + + public InputValueWithState getArgumentValue() { + return value; + } + + public @Nullable Argument getDefinition() { + return definition; + } + + @Internal + public SchemaUniverse getUniverse() { + return universe; + } + + @Internal + public int getTypeId() { + return typeId; + } + + @Override + public String toString() { + return "APPLIED_DIRECTIVE_ARGUMENT{" + name + "}"; + } +} diff --git a/src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java b/src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java new file mode 100644 index 0000000000..46e1685fd4 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java @@ -0,0 +1,12 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * Marks a schema-universe vertex that can carry applied directives. + */ +@ExperimentalApi +@NullMarked +public interface SUAppliedDirectiveContainer { +} diff --git a/src/main/java/graphql/schema/universe/SUArgument.java b/src/main/java/graphql/schema/universe/SUArgument.java new file mode 100644 index 0000000000..554ef046e1 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUArgument.java @@ -0,0 +1,39 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.language.InputValueDefinition; +import graphql.schema.InputValueWithState; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import static graphql.Assert.assertNotNull; + +@ExperimentalApi +@NullMarked +public final class SUArgument extends SUVertex implements SUAppliedDirectiveContainer { + + private final InputValueWithState defaultValue; + private final @Nullable InputValueDefinition definition; + + @Internal + public SUArgument( + int id, + int nameId, + String name, + @Nullable String description, + InputValueWithState defaultValue, + @Nullable InputValueDefinition definition) { + super(id, nameId, SUVertexKind.ARGUMENT, name, description); + this.defaultValue = assertNotNull(defaultValue); + this.definition = definition; + } + + public InputValueWithState getArgumentDefaultValue() { + return defaultValue; + } + + public @Nullable InputValueDefinition getDefinition() { + return definition; + } +} diff --git a/src/main/java/graphql/schema/universe/SUDirective.java b/src/main/java/graphql/schema/universe/SUDirective.java new file mode 100644 index 0000000000..badda6312e --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUDirective.java @@ -0,0 +1,66 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.DirectiveDefinition; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.EnumSet; +import java.util.Set; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertTrue; + +@ExperimentalApi +@NullMarked +public final class SUDirective extends SUVertex { + + private final boolean repeatable; + private final long locationMask; + private final @Nullable DirectiveDefinition definition; + + @Internal + public SUDirective( + int id, + int nameId, + String name, + @Nullable String description, + boolean repeatable, + Set validLocations, + @Nullable DirectiveDefinition definition) { + super(id, nameId, SUVertexKind.DIRECTIVE, name, description); + this.repeatable = repeatable; + this.locationMask = packLocations(assertNotNull(validLocations)); + this.definition = definition; + } + + public boolean isRepeatable() { + return repeatable; + } + + public EnumSet validLocations() { + EnumSet result = EnumSet.noneOf(DirectiveLocation.class); + for (DirectiveLocation location : DirectiveLocation.values()) { + if ((locationMask & (1L << location.ordinal())) != 0) { + result.add(location); + } + } + return result; + } + + public @Nullable DirectiveDefinition getDefinition() { + return definition; + } + + private static long packLocations(Set validLocations) { + assertTrue(DirectiveLocation.values().length <= Long.SIZE, + "Too many directive locations to pack into a long"); + long result = 0; + for (DirectiveLocation location : validLocations) { + result |= 1L << assertNotNull(location).ordinal(); + } + return result; + } +} diff --git a/src/main/java/graphql/schema/universe/SUEdgeKind.java b/src/main/java/graphql/schema/universe/SUEdgeKind.java new file mode 100644 index 0000000000..a00163ca32 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUEdgeKind.java @@ -0,0 +1,97 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +import static graphql.Assert.assertTrue; + +/** + * The relationship kinds between schema-universe vertices. + * + *

Each kind has a stable compact code stored in the high eight bits of a packed edge. + * {@code single} limits the kind to one target per source. {@code uniqueName} allows multiple + * targets but rejects two targets with the same universe-interned name. Applied directives permit + * duplicate names because repeatable directive occurrences must remain distinct.

+ */ +@Internal +@NullMarked +public enum SUEdgeKind { + QUERY_TYPE(1, true, true), + MUTATION_TYPE(2, true, true), + SUBSCRIPTION_TYPE(3, true, true), + ADDITIONAL_TYPE(4, false, true), + DIRECTIVE_DEFINITION(5, false, true), + FIELD(6, false, true), + ARGUMENT(7, false, true), + TYPE(8, true, true), + IMPLEMENTS(9, false, true), + UNION_MEMBER(10, false, true), + ENUM_VALUE(11, false, true), + INPUT_FIELD(12, false, true), + APPLIED_DIRECTIVE(13, false, false), + WRAPPED_TYPE(15, true, true); + + private static final SUEdgeKind[] BY_CODE = new SUEdgeKind[16]; + + static { + for (SUEdgeKind kind : values()) { + BY_CODE[kind.code] = kind; + } + } + + private final int code; + private final boolean single; + private final boolean uniqueName; + + SUEdgeKind(int code, boolean single, boolean uniqueName) { + this.code = code; + this.single = single; + this.uniqueName = uniqueName; + } + + /** + * Returns the stable packed representation of this kind. + * + * @return the packed edge code + */ + @Internal + public int getCode() { + return code; + } + + /** + * Reports whether a source may have at most one edge of this kind. + * + * @return {@code true} for a single-valued edge kind + */ + @Internal + public boolean isSingle() { + return single; + } + + /** + * Reports whether targets of this kind must have distinct names for one source. + * + * @return {@code true} when duplicate target names are forbidden + */ + @Internal + public boolean isUniqueName() { + return uniqueName; + } + + /** + * Resolves a packed edge code. + * + * @param code the packed code + * + * @return the corresponding edge kind + */ + @Internal + public static SUEdgeKind fromCode(int code) { + assertTrue( + code > 0 && code < BY_CODE.length && BY_CODE[code] != null, + "Invalid schema universe edge code %s", + code); + return BY_CODE[code]; + } +} diff --git a/src/main/java/graphql/schema/universe/SUEnumType.java b/src/main/java/graphql/schema/universe/SUEnumType.java new file mode 100644 index 0000000000..dd1639dc8b --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUEnumType.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUEnumType extends SUNamedType { + + @Internal + public SUEnumType(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.ENUM, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUEnumValue.java b/src/main/java/graphql/schema/universe/SUEnumValue.java new file mode 100644 index 0000000000..8bf4eb20b6 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUEnumValue.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUEnumValue extends SUVertex implements SUAppliedDirectiveContainer { + + @Internal + public SUEnumValue(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.ENUM_VALUE, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUExporter.java b/src/main/java/graphql/schema/universe/SUExporter.java new file mode 100644 index 0000000000..2e273a0b63 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUExporter.java @@ -0,0 +1,416 @@ +package graphql.schema.universe; + +import graphql.Directives; +import graphql.ExperimentalApi; +import graphql.introspection.Introspection; +import graphql.language.StringValue; +import graphql.language.Value; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLCodeRegistry; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeReference; +import graphql.schema.GraphQLUnionType; +import graphql.schema.InputValueWithState; +import graphql.schema.TypeResolver; +import graphql.schema.idl.EchoingWiringFactory; +import graphql.schema.idl.ScalarInfo; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static graphql.Assert.assertNotNull; +import static java.util.stream.Collectors.toList; + +/** + * Exports the type-system topology of an {@link SUSchema} as a {@link GraphQLSchema}. + * + *

The exported schema is suitable for inspection and structural comparison. It is not intended + * for execution because a schema universe does not retain data fetchers, type resolvers, custom + * scalar coercing implementations, or other runtime wiring.

+ */ +@ExperimentalApi +@NullMarked +public final class SUExporter { + + private static final TypeResolver UNEXECUTABLE_TYPE_RESOLVER = environment -> null; + + private final SUSchema schema; + private final Map namedTypes = new LinkedHashMap<>(); + + public SUExporter(SUSchema schema) { + this.schema = assertNotNull(schema); + } + + public GraphQLSchema exportSchema() { + for (SUVertex type : schema.getTypes()) { + if (!Introspection.isIntrospectionTypes(requiredName(type))) { + namedTypes.put(type.getId(), exportNamedType(type)); + } + } + + GraphQLSchema.Builder builder = GraphQLSchema.newSchema() + .query(objectType(schema.getQueryType())) + .description(schema.getRoot().getDescription()) + .codeRegistry(exportCodeRegistry()); + + SUObjectType mutationType = schema.getMutationType(); + if (mutationType != null) { + builder.mutation(objectType(mutationType)); + } + SUObjectType subscriptionType = schema.getSubscriptionType(); + if (subscriptionType != null) { + builder.subscription(objectType(subscriptionType)); + } + for (SUVertex additionalType : schema.getAdditionalTypes()) { + if (!Introspection.isIntrospectionTypes(requiredName(additionalType))) { + builder.additionalType(namedType(additionalType)); + } + } + for (SUDirective directive : schema.getDirectiveDefinitions()) { + builder.additionalDirective(exportDirectiveDefinition(directive)); + } + builder.withSchemaAppliedDirectives(exportAppliedDirectives(schema.getRoot())); + return builder.build(); + } + + private GraphQLCodeRegistry exportCodeRegistry() { + GraphQLCodeRegistry.Builder builder = GraphQLCodeRegistry.newCodeRegistry(); + for (GraphQLNamedType type : namedTypes.values()) { + if (type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType) { + builder.typeResolver(type.getName(), UNEXECUTABLE_TYPE_RESOLVER); + } + } + return builder.build(); + } + + private GraphQLNamedType exportNamedType(SUVertex type) { + switch (type.getKind()) { + case OBJECT: + return exportObject((SUObjectType) type); + case INTERFACE: + return exportInterface((SUInterfaceType) type); + case UNION: + return exportUnion((SUUnionType) type); + case ENUM: + return exportEnum((SUEnumType) type); + case SCALAR: + return exportScalar((SUScalarType) type); + case INPUT_OBJECT: + return exportInputObject((SUInputObjectType) type); + default: + throw new IllegalStateException("Not a named type: " + type); + } + } + + private GraphQLObjectType exportObject(SUObjectType type) { + GraphQLObjectType.Builder builder = GraphQLObjectType.newObject() + .name(requiredName(type)) + .description(type.getDescription()) + .replaceAppliedDirectives(exportAppliedDirectives(type)); + for (SUField field : schema.getFields(type)) { + builder.field(exportField(field)); + } + for (SUInterfaceType interfaceType : schema.getInterfaces(type)) { + builder.withInterface(typeReference(interfaceType)); + } + return builder.build(); + } + + private GraphQLInterfaceType exportInterface(SUInterfaceType type) { + GraphQLInterfaceType.Builder builder = GraphQLInterfaceType.newInterface() + .name(requiredName(type)) + .description(type.getDescription()) + .replaceAppliedDirectives(exportAppliedDirectives(type)); + for (SUField field : schema.getFields(type)) { + builder.field(exportField(field)); + } + for (SUInterfaceType interfaceType : schema.getInterfaces(type)) { + builder.withInterface(typeReference(interfaceType)); + } + return builder.build(); + } + + private GraphQLUnionType exportUnion(SUUnionType type) { + GraphQLUnionType.Builder builder = GraphQLUnionType.newUnionType() + .name(requiredName(type)) + .description(type.getDescription()) + .replaceAppliedDirectives(exportAppliedDirectives(type)); + for (SUObjectType member : schema.getUnionMembers(type)) { + builder.possibleType(typeReference(member)); + } + return builder.build(); + } + + private GraphQLEnumType exportEnum(SUEnumType type) { + GraphQLEnumType.Builder builder = GraphQLEnumType.newEnum() + .name(requiredName(type)) + .description(type.getDescription()) + .replaceAppliedDirectives(exportAppliedDirectives(type)); + for (SUEnumValue value : schema.getEnumValues(type)) { + GraphQLEnumValueDefinition.Builder valueBuilder = + GraphQLEnumValueDefinition.newEnumValueDefinition() + .name(requiredName(value)) + .description(value.getDescription()) + .value(requiredName(value)) + .replaceAppliedDirectives(exportAppliedDirectives(value)); + String deprecationReason = deprecationReason(value); + if (deprecationReason != null) { + valueBuilder.deprecationReason(deprecationReason); + } + builder.value(valueBuilder.build()); + } + return builder.build(); + } + + private GraphQLScalarType exportScalar(SUScalarType type) { + GraphQLScalarType source = specifiedScalar(requiredName(type)); + if (source != null) { + return source; + } + return GraphQLScalarType.newScalar(EchoingWiringFactory.fakeScalar(requiredName(type))) + .description(type.getDescription()) + .replaceAppliedDirectives(exportAppliedDirectives(type)) + .build(); + } + + private GraphQLInputObjectType exportInputObject(SUInputObjectType type) { + GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject() + .name(requiredName(type)) + .description(type.getDescription()) + .replaceAppliedDirectives(exportAppliedDirectives(type)); + for (SUInputField field : schema.getInputFields(type)) { + builder.field(exportInputField(field)); + } + return builder.build(); + } + + private GraphQLFieldDefinition exportField(SUField field) { + GraphQLFieldDefinition.Builder builder = GraphQLFieldDefinition.newFieldDefinition() + .name(requiredName(field)) + .description(field.getDescription()) + .type(outputType(requiredType(field))) + .replaceAppliedDirectives(exportAppliedDirectives(field)); + for (SUArgument argument : schema.getArguments(field)) { + builder.argument(exportArgument(argument)); + } + String deprecationReason = deprecationReason(field); + if (deprecationReason != null) { + builder.deprecate(deprecationReason); + } + return builder.build(); + } + + private GraphQLArgument exportArgument(SUArgument argument) { + GraphQLArgument.Builder builder = GraphQLArgument.newArgument() + .name(requiredName(argument)) + .description(argument.getDescription()) + .type(inputType(requiredType(argument))) + .replaceAppliedDirectives(exportAppliedDirectives(argument)); + applyArgumentDefault(builder, argument.getArgumentDefaultValue()); + if (argument.getDefinition() != null) { + builder.definition(argument.getDefinition()); + } + String deprecationReason = deprecationReason(argument); + if (deprecationReason != null) { + builder.deprecate(deprecationReason); + } + return builder.build(); + } + + private GraphQLInputObjectField exportInputField(SUInputField field) { + GraphQLInputObjectField.Builder builder = GraphQLInputObjectField.newInputObjectField() + .name(requiredName(field)) + .description(field.getDescription()) + .type(inputType(requiredType(field))) + .replaceAppliedDirectives(exportAppliedDirectives(field)); + applyInputFieldDefault(builder, field.getInputFieldDefaultValue()); + if (field.getDefinition() != null) { + builder.definition(field.getDefinition()); + } + String deprecationReason = deprecationReason(field); + if (deprecationReason != null) { + builder.deprecate(deprecationReason); + } + return builder.build(); + } + + private GraphQLDirective exportDirectiveDefinition(SUDirective directive) { + GraphQLDirective.Builder builder = GraphQLDirective.newDirective() + .name(requiredName(directive)) + .description(directive.getDescription()) + .repeatable(directive.isRepeatable()); + directive.validLocations().forEach(builder::validLocation); + for (SUArgument argument : schema.getArguments(directive)) { + builder.argument(exportArgument(argument)); + } + if (directive.getDefinition() != null) { + builder.definition(directive.getDefinition()); + } + return builder.build(); + } + + private List exportAppliedDirectives(SUVertex container) { + return schema.getAppliedDirectives(container).stream() + .map(this::exportAppliedDirective) + .collect(toList()); + } + + private GraphQLAppliedDirective exportAppliedDirective(SUAppliedDirective directive) { + GraphQLAppliedDirective.Builder builder = GraphQLAppliedDirective.newDirective() + .name(requiredName(directive)); + if (directive.getDefinition() != null) { + builder.definition(directive.getDefinition()); + } + for (SUAppliedDirectiveArgument argument : schema.getArguments(directive)) { + GraphQLAppliedDirectiveArgument.Builder argumentBuilder = + GraphQLAppliedDirectiveArgument.newArgument() + .name(argument.getName()) + .type(inputType(requiredType(argument))) + .inputValueWithState(argument.getArgumentValue()); + if (argument.getDefinition() != null) { + argumentBuilder.definition(argument.getDefinition()); + } + builder.argument(argumentBuilder.build()); + } + return builder.build(); + } + + private GraphQLType exportType(SUVertex type) { + if (type instanceof SUListType) { + return GraphQLList.list(exportType(requiredWrappedType((SUListType) type))); + } + if (type instanceof SUNonNullType) { + return GraphQLNonNull.nonNull(exportType(requiredWrappedType((SUNonNullType) type))); + } + return typeReference(type); + } + + private GraphQLOutputType outputType(SUVertex type) { + return (GraphQLOutputType) exportType(type); + } + + private GraphQLInputType inputType(SUVertex type) { + return (GraphQLInputType) exportType(type); + } + + private GraphQLTypeReference typeReference(SUVertex type) { + return GraphQLTypeReference.typeRef(requiredName(type)); + } + + private SUVertex requiredType(SUField field) { + return assertNotNull(schema.getType(field), "Field '%s' has no type", requiredName(field)); + } + + private SUVertex requiredType(SUArgument argument) { + return assertNotNull(schema.getType(argument), "Argument '%s' has no type", requiredName(argument)); + } + + private SUVertex requiredType(SUInputField field) { + return assertNotNull(schema.getType(field), "Input field '%s' has no type", requiredName(field)); + } + + private SUVertex requiredType(SUAppliedDirectiveArgument argument) { + return assertNotNull( + schema.getType(argument), + "Applied directive argument '%s' has no type", + argument.getName()); + } + + private SUVertex requiredWrappedType(SUListType type) { + return assertNotNull(schema.getWrappedType(type), "List type '%s' has no wrapped type", type.getId()); + } + + private SUVertex requiredWrappedType(SUNonNullType type) { + return assertNotNull( + schema.getWrappedType(type), + "Non-null type '%s' has no wrapped type", + type.getId()); + } + + private GraphQLNamedType namedType(SUVertex type) { + return assertNotNull(namedTypes.get(type.getId()), "Type '%s' was not exported", type); + } + + private GraphQLObjectType objectType(SUObjectType type) { + return (GraphQLObjectType) namedType(type); + } + + private String requiredName(SUVertex vertex) { + return assertNotNull(vertex.getName(), "Vertex '%s' has no name", vertex.getId()); + } + + private @Nullable GraphQLScalarType specifiedScalar(String name) { + for (GraphQLScalarType scalar : ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS) { + if (name.equals(scalar.getName())) { + return scalar; + } + } + return null; + } + + private @Nullable String deprecationReason(SUVertex vertex) { + for (SUAppliedDirective directive : + schema.getAppliedDirectives(vertex, Directives.DeprecatedDirective.getName())) { + SUAppliedDirectiveArgument reason = schema.getArgument(directive, "reason"); + if (reason == null || reason.getArgumentValue().isNotSet()) { + return Directives.NO_LONGER_SUPPORTED; + } + Object value = reason.getArgumentValue().getValue(); + if (value instanceof StringValue) { + return ((StringValue) value).getValue(); + } + return value == null ? null : String.valueOf(value); + } + return null; + } + + @SuppressWarnings("deprecation") + private void applyArgumentDefault( + GraphQLArgument.Builder builder, + InputValueWithState defaultValue) { + if (defaultValue.isLiteral()) { + builder.defaultValueLiteral((Value) assertNotNull(defaultValue.getValue())); + } else if (defaultValue.isExternal()) { + builder.defaultValueProgrammatic(defaultValue.getValue()); + } else if (defaultValue.isInternal()) { + builder.defaultValue(defaultValue.getValue()); + } else { + builder.clearDefaultValue(); + } + } + + @SuppressWarnings("deprecation") + private void applyInputFieldDefault( + GraphQLInputObjectField.Builder builder, + InputValueWithState defaultValue) { + if (defaultValue.isLiteral()) { + builder.defaultValueLiteral((Value) assertNotNull(defaultValue.getValue())); + } else if (defaultValue.isExternal()) { + builder.defaultValueProgrammatic(defaultValue.getValue()); + } else if (defaultValue.isInternal()) { + builder.defaultValue(defaultValue.getValue()); + } else { + builder.clearDefaultValue(); + } + } +} diff --git a/src/main/java/graphql/schema/universe/SUField.java b/src/main/java/graphql/schema/universe/SUField.java new file mode 100644 index 0000000000..184cfdc44f --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUField.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUField extends SUVertex implements SUAppliedDirectiveContainer { + + @Internal + public SUField(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.FIELD, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUImporter.java b/src/main/java/graphql/schema/universe/SUImporter.java new file mode 100644 index 0000000000..8e3071636e --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUImporter.java @@ -0,0 +1,503 @@ +package graphql.schema.universe; + +import graphql.Directives; +import graphql.DirectivesUtil; +import graphql.ExperimentalApi; +import graphql.language.StringValue; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLDirectiveContainer; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLNamedOutputType; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLTypeReference; +import graphql.schema.GraphQLUnionType; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; + +/** + * Imports the type-system topology of an existing {@link GraphQLSchema} into a universe. + * + *

The importer preserves schema-element object identity during one import. Runtime wiring in + * {@code GraphQLCodeRegistry}, AST definitions, coercing implementations, and input values are not + * part of this topology import.

+ */ +@ExperimentalApi +@NullMarked +public final class SUImporter { + + private final SchemaUniverse universe; + private final Map namedTypes = new LinkedHashMap<>(); + private final Map elementVertices = new IdentityHashMap<>(); + private final Map + appliedArgumentValues = new IdentityHashMap<>(); + private final Map directiveDefinitions = new LinkedHashMap<>(); + private @Nullable SUSchemaBuilder schemaBuilder; + + public SUImporter(SchemaUniverse universe) { + this.universe = assertNotNull(universe); + } + + public SUSchema importSchema(String name, GraphQLSchema schema) { + schemaBuilder = universe.newSchema(name, schema.getDescription()); + createNamedTypeVertices(schema.getAllTypesAsList()); + addRootTypes(schema); + addDirectiveDefinitions(schema.getDirectives()); + expandNamedTypes(schema.getAllTypesAsList()); + addAppliedDirectives( + DirectivesUtil.toAppliedDirectives( + schema.getSchemaAppliedDirectives(), + schema.getSchemaDirectives()), + schemaBuilderRoot()); + return builder().build(); + } + + private void createNamedTypeVertices(List types) { + for (GraphQLNamedType type : types) { + SUNamedType vertex = createNamedTypeVertex(type); + namedTypes.put(type.getName(), vertex); + elementVertices.put(type, vertex); + } + } + + private SUNamedType createNamedTypeVertex(GraphQLNamedType type) { + if (type instanceof GraphQLObjectType) { + GraphQLObjectType objectType = (GraphQLObjectType) type; + return universe.newObjectType(objectType.getName(), objectType.getDescription()); + } + if (type instanceof GraphQLInterfaceType) { + GraphQLInterfaceType interfaceType = (GraphQLInterfaceType) type; + return universe.newInterfaceType(interfaceType.getName(), interfaceType.getDescription()); + } + if (type instanceof GraphQLUnionType) { + GraphQLUnionType unionType = (GraphQLUnionType) type; + return universe.newUnionType(unionType.getName(), unionType.getDescription()); + } + if (type instanceof GraphQLEnumType) { + GraphQLEnumType enumType = (GraphQLEnumType) type; + return universe.newEnumType(enumType.getName(), enumType.getDescription()); + } + if (type instanceof GraphQLScalarType) { + GraphQLScalarType scalarType = (GraphQLScalarType) type; + return universe.newScalarType(scalarType.getName(), scalarType.getDescription()); + } + if (type instanceof GraphQLInputObjectType) { + GraphQLInputObjectType inputType = (GraphQLInputObjectType) type; + return universe.newInputObjectType(inputType.getName(), inputType.getDescription()); + } + return assertShouldNeverHappen("Unsupported schema universe named type %s", type.getClass().getName()); + } + + private void addRootTypes(GraphQLSchema schema) { + SUObjectType query = (SUObjectType) namedType(schema.getQueryType()); + builder().queryType(query); + GraphQLObjectType mutationType = schema.getMutationType(); + if (mutationType != null) { + builder().mutationType((SUObjectType) namedType(mutationType)); + } + GraphQLObjectType subscriptionType = schema.getSubscriptionType(); + if (subscriptionType != null) { + builder().subscriptionType((SUObjectType) namedType(subscriptionType)); + } + addNonRootTypes(schema, query); + } + + private void addNonRootTypes(GraphQLSchema schema, SUObjectType query) { + SUNamedType mutation = nullableNamedType(schema.getMutationType()); + SUNamedType subscription = nullableNamedType(schema.getSubscriptionType()); + for (SUNamedType type : namedTypes.values()) { + if (type == query || type == mutation || type == subscription) { + continue; + } + builder().addAdditionalType(type); + } + } + + private void addDirectiveDefinitions(List directives) { + for (GraphQLDirective directive : directives) { + directiveDefinitions.put(directive.getName(), directive); + SUDirective vertex = universe.newDirective( + directive.getName(), + directive.getDescription(), + directive.isRepeatable(), + directive.validLocations(), + directive.getDefinition()); + elementVertices.put(directive, vertex); + builder().addDirectiveDefinition(vertex); + for (GraphQLArgument argument : directive.getArguments()) { + addArgument(vertex, argument); + } + } + } + + private void expandNamedTypes(List types) { + for (GraphQLNamedType type : types) { + SUNamedType vertex = namedType(type); + if (type instanceof GraphQLObjectType) { + expandObject((GraphQLObjectType) type, (SUObjectType) vertex); + continue; + } + if (type instanceof GraphQLInterfaceType) { + expandInterface((GraphQLInterfaceType) type, (SUInterfaceType) vertex); + continue; + } + if (type instanceof GraphQLUnionType) { + expandUnion((GraphQLUnionType) type, (SUUnionType) vertex); + continue; + } + if (type instanceof GraphQLEnumType) { + expandEnum((GraphQLEnumType) type, (SUEnumType) vertex); + continue; + } + if (type instanceof GraphQLInputObjectType) { + expandInputObject((GraphQLInputObjectType) type, (SUInputObjectType) vertex); + continue; + } + if (type instanceof GraphQLDirectiveContainer) { + addAppliedDirectives((GraphQLDirectiveContainer) type, vertex); + addSpecifiedByDirective(type, vertex); + } + } + } + + private void expandObject(GraphQLObjectType type, SUObjectType vertex) { + for (GraphQLFieldDefinition field : type.getFieldDefinitions()) { + addField(vertex, field); + } + for (GraphQLNamedOutputType interfaceType : type.getInterfaces()) { + builder().addInterface(vertex, (SUInterfaceType) typeVertex(interfaceType)); + } + addAppliedDirectives(type, vertex); + } + + private void expandInterface(GraphQLInterfaceType type, SUInterfaceType vertex) { + for (GraphQLFieldDefinition field : type.getFieldDefinitions()) { + addField(vertex, field); + } + for (GraphQLNamedOutputType interfaceType : type.getInterfaces()) { + builder().addInterface(vertex, (SUInterfaceType) typeVertex(interfaceType)); + } + addAppliedDirectives(type, vertex); + } + + private void expandUnion(GraphQLUnionType type, SUUnionType vertex) { + for (GraphQLNamedOutputType memberType : type.getTypes()) { + builder().addUnionMember(vertex, (SUObjectType) typeVertex(memberType)); + } + addAppliedDirectives(type, vertex); + } + + private void expandEnum(GraphQLEnumType type, SUEnumType vertex) { + for (GraphQLEnumValueDefinition value : type.getValues()) { + SUEnumValue valueVertex = enumValue(value); + builder().addEnumValue(vertex, valueVertex); + addAppliedDirectives(value, valueVertex); + } + addAppliedDirectives(type, vertex); + } + + private void expandInputObject(GraphQLInputObjectType type, SUInputObjectType vertex) { + for (GraphQLInputObjectField field : type.getFields()) { + SUInputField fieldVertex = inputField(field); + builder().addInputField(vertex, fieldVertex); + builder().setInputFieldType(fieldVertex, typeVertex(field.getType())); + addAppliedDirectives(field, fieldVertex); + } + addAppliedDirectives(type, vertex); + } + + private void addField(SUObjectType parent, GraphQLFieldDefinition field) { + SUField fieldVertex = field(field); + builder().addField(parent, fieldVertex); + builder().setFieldType(fieldVertex, typeVertex(field.getType())); + for (GraphQLArgument argument : field.getArguments()) { + addArgument(fieldVertex, argument); + } + addAppliedDirectives(field, fieldVertex); + } + + private void addField(SUInterfaceType parent, GraphQLFieldDefinition field) { + SUField fieldVertex = field(field); + builder().addField(parent, fieldVertex); + builder().setFieldType(fieldVertex, typeVertex(field.getType())); + for (GraphQLArgument argument : field.getArguments()) { + addArgument(fieldVertex, argument); + } + addAppliedDirectives(field, fieldVertex); + } + + private void addArgument(SUField parent, GraphQLArgument argument) { + SUArgument argumentVertex = argument(argument); + builder().addArgument(parent, argumentVertex); + builder().setArgumentType(argumentVertex, typeVertex(argument.getType())); + addAppliedDirectives(argument, argumentVertex); + } + + private void addArgument(SUDirective parent, GraphQLArgument argument) { + SUArgument argumentVertex = argument(argument); + builder().addArgument(parent, argumentVertex); + builder().setArgumentType(argumentVertex, typeVertex(argument.getType())); + addAppliedDirectives(argument, argumentVertex); + } + + private void addAppliedDirectives( + GraphQLDirectiveContainer container, + SUAppliedDirectiveContainer containerVertex) { + List directives = + DirectivesUtil.toAppliedDirectives(container); + addAppliedDirectives(directives, containerVertex); + addDeprecatedDirective(container, directives, containerVertex); + } + + private void addAppliedDirectives( + List directives, + SUAppliedDirectiveContainer containerVertex) { + for (GraphQLAppliedDirective directive : directives) { + SUAppliedDirective directiveVertex = appliedDirective(directive); + builder().addAppliedDirective(containerVertex, directiveVertex); + } + } + + private void addDeprecatedDirective( + GraphQLDirectiveContainer container, + List directives, + SUAppliedDirectiveContainer containerVertex) { + String reason = deprecationReason(container); + if (reason == null || hasDirective(directives, Directives.DeprecatedDirective.getName())) { + return; + } + addStringDirective( + Directives.DeprecatedDirective.getName(), + "reason", + reason, + containerVertex); + } + + private void addSpecifiedByDirective( + GraphQLNamedType type, + SUNamedType typeVertex) { + if (!(type instanceof GraphQLScalarType)) { + return; + } + GraphQLScalarType scalarType = (GraphQLScalarType) type; + String url = scalarType.getSpecifiedByUrl(); + if (url == null || hasDirective( + DirectivesUtil.toAppliedDirectives(scalarType), + Directives.SpecifiedByDirective.getName())) { + return; + } + addStringDirective( + Directives.SpecifiedByDirective.getName(), + "url", + url, + typeVertex); + } + + private void addStringDirective( + String directiveName, + String argumentName, + String value, + SUAppliedDirectiveContainer containerVertex) { + GraphQLDirective definition = assertNotNull( + directiveDefinitions.get(directiveName), + "Directive '%s' is not part of the imported schema", + directiveName); + GraphQLArgument argumentDefinition = assertNotNull( + definition.getArgument(argumentName), + "Directive '%s' has no argument '%s'", + directiveName, + argumentName); + SUAppliedDirectiveArgument argument = + universe.newAppliedDirectiveArgument( + argumentName, + typeVertex(argumentDefinition.getType()), + graphql.schema.InputValueWithState.newLiteralValue( + StringValue.newStringValue(value).build())); + SUAppliedDirective directive = universe.newAppliedDirective( + directiveName, + List.of(argument)); + builder().addAppliedDirective(containerVertex, directive); + } + + private boolean hasDirective( + List directives, + String name) { + for (GraphQLAppliedDirective directive : directives) { + if (name.equals(directive.getName())) { + return true; + } + } + return false; + } + + private @Nullable String deprecationReason( + GraphQLDirectiveContainer container) { + if (container instanceof GraphQLFieldDefinition) { + return ((GraphQLFieldDefinition) container).getDeprecationReason(); + } + if (container instanceof GraphQLEnumValueDefinition) { + return ((GraphQLEnumValueDefinition) container).getDeprecationReason(); + } + if (container instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) container).getDeprecationReason(); + } + if (container instanceof GraphQLArgument) { + return ((GraphQLArgument) container).getDeprecationReason(); + } + return null; + } + + private SUType typeVertex(GraphQLType type) { + if (type instanceof GraphQLList) { + return listType((GraphQLList) type); + } + if (type instanceof GraphQLNonNull) { + return nonNullType((GraphQLNonNull) type); + } + if (type instanceof GraphQLTypeReference) { + return assertNotNull(namedTypes.get(((GraphQLTypeReference) type).getName())); + } + return namedType((GraphQLNamedType) type); + } + + private SUListType listType(GraphQLList type) { + SUVertex existing = elementVertices.get(type); + if (existing != null) { + return (SUListType) existing; + } + SUListType vertex = universe.newListType(); + elementVertices.put(type, vertex); + builder().setWrappedType(vertex, typeVertex(type.getWrappedType())); + return vertex; + } + + private SUNonNullType nonNullType(GraphQLNonNull type) { + SUVertex existing = elementVertices.get(type); + if (existing != null) { + return (SUNonNullType) existing; + } + SUNonNullType vertex = universe.newNonNullType(); + elementVertices.put(type, vertex); + builder().setWrappedType(vertex, typeVertex(type.getWrappedType())); + return vertex; + } + + private SUField field(GraphQLFieldDefinition field) { + SUVertex existing = elementVertices.get(field); + if (existing != null) { + return (SUField) existing; + } + SUField vertex = universe.newField(field.getName(), field.getDescription()); + elementVertices.put(field, vertex); + return vertex; + } + + private SUArgument argument(GraphQLArgument argument) { + SUVertex existing = elementVertices.get(argument); + if (existing != null) { + return (SUArgument) existing; + } + SUArgument vertex = universe.newArgument( + argument.getName(), + argument.getDescription(), + argument.getArgumentDefaultValue(), + argument.getDefinition()); + elementVertices.put(argument, vertex); + return vertex; + } + + private SUInputField inputField(GraphQLInputObjectField field) { + SUVertex existing = elementVertices.get(field); + if (existing != null) { + return (SUInputField) existing; + } + SUInputField vertex = universe.newInputField( + field.getName(), + field.getDescription(), + field.getInputFieldDefaultValue(), + field.getDefinition()); + elementVertices.put(field, vertex); + return vertex; + } + + private SUEnumValue enumValue(GraphQLEnumValueDefinition value) { + SUVertex existing = elementVertices.get(value); + if (existing != null) { + return (SUEnumValue) existing; + } + SUEnumValue vertex = universe.newEnumValue(value.getName(), value.getDescription()); + elementVertices.put(value, vertex); + return vertex; + } + + private SUAppliedDirective appliedDirective(GraphQLAppliedDirective directive) { + SUVertex existing = elementVertices.get(directive); + if (existing != null) { + return (SUAppliedDirective) existing; + } + List arguments = + new ArrayList<>(directive.getArguments().size()); + for (GraphQLAppliedDirectiveArgument argument : directive.getArguments()) { + arguments.add(appliedArgument(argument)); + } + SUAppliedDirective vertex = universe.newAppliedDirective( + directive.getName(), + arguments, + directive.getDefinition()); + elementVertices.put(directive, vertex); + return vertex; + } + + private SUAppliedDirectiveArgument appliedArgument(GraphQLAppliedDirectiveArgument argument) { + SUAppliedDirectiveArgument existing = appliedArgumentValues.get(argument); + if (existing != null) { + return existing; + } + SUAppliedDirectiveArgument value = universe.newAppliedDirectiveArgument( + argument.getName(), + typeVertex(argument.getType()), + argument.getArgumentValue(), + argument.getDefinition()); + appliedArgumentValues.put(argument, value); + return value; + } + + private SUNamedType namedType(GraphQLNamedType type) { + return assertNotNull(namedTypes.get(type.getName()), "Type '%s' is not part of the imported schema", type.getName()); + } + + private @Nullable SUNamedType nullableNamedType(@Nullable GraphQLNamedType type) { + return type == null ? null : namedType(type); + } + + private SUSchemaRoot schemaBuilderRoot() { + return builder().getRoot(); + } + + private SUSchemaBuilder builder() { + return assertNotNull(schemaBuilder, "Schema import has not started"); + } +} diff --git a/src/main/java/graphql/schema/universe/SUInputField.java b/src/main/java/graphql/schema/universe/SUInputField.java new file mode 100644 index 0000000000..5afe287572 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUInputField.java @@ -0,0 +1,39 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.language.InputValueDefinition; +import graphql.schema.InputValueWithState; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import static graphql.Assert.assertNotNull; + +@ExperimentalApi +@NullMarked +public final class SUInputField extends SUVertex implements SUAppliedDirectiveContainer { + + private final InputValueWithState defaultValue; + private final @Nullable InputValueDefinition definition; + + @Internal + public SUInputField( + int id, + int nameId, + String name, + @Nullable String description, + InputValueWithState defaultValue, + @Nullable InputValueDefinition definition) { + super(id, nameId, SUVertexKind.INPUT_FIELD, name, description); + this.defaultValue = assertNotNull(defaultValue); + this.definition = definition; + } + + public InputValueWithState getInputFieldDefaultValue() { + return defaultValue; + } + + public @Nullable InputValueDefinition getDefinition() { + return definition; + } +} diff --git a/src/main/java/graphql/schema/universe/SUInputObjectType.java b/src/main/java/graphql/schema/universe/SUInputObjectType.java new file mode 100644 index 0000000000..23e75d2d6f --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUInputObjectType.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUInputObjectType extends SUNamedType { + + @Internal + public SUInputObjectType(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.INPUT_OBJECT, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUInterfaceType.java b/src/main/java/graphql/schema/universe/SUInterfaceType.java new file mode 100644 index 0000000000..9e6befaeb0 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUInterfaceType.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUInterfaceType extends SUNamedType { + + @Internal + public SUInterfaceType(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.INTERFACE, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUListType.java b/src/main/java/graphql/schema/universe/SUListType.java new file mode 100644 index 0000000000..eba8a342a6 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUListType.java @@ -0,0 +1,15 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +@ExperimentalApi +@NullMarked +public final class SUListType extends SUType { + + @Internal + public SUListType(int id) { + super(id, 0, SUVertexKind.LIST, null, null); + } +} diff --git a/src/main/java/graphql/schema/universe/SUNamedType.java b/src/main/java/graphql/schema/universe/SUNamedType.java new file mode 100644 index 0000000000..396300f552 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUNamedType.java @@ -0,0 +1,24 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * A named GraphQL type vertex. + */ +@ExperimentalApi +@NullMarked +public abstract class SUNamedType extends SUType implements SUAppliedDirectiveContainer { + + @Internal + protected SUNamedType( + int id, + int nameId, + SUVertexKind kind, + String name, + @Nullable String description) { + super(id, nameId, kind, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUNonNullType.java b/src/main/java/graphql/schema/universe/SUNonNullType.java new file mode 100644 index 0000000000..5f5efed62c --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUNonNullType.java @@ -0,0 +1,15 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +@ExperimentalApi +@NullMarked +public final class SUNonNullType extends SUType { + + @Internal + public SUNonNullType(int id) { + super(id, 0, SUVertexKind.NON_NULL, null, null); + } +} diff --git a/src/main/java/graphql/schema/universe/SUObjectType.java b/src/main/java/graphql/schema/universe/SUObjectType.java new file mode 100644 index 0000000000..5b0b91caf5 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUObjectType.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUObjectType extends SUNamedType { + + @Internal + public SUObjectType(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.OBJECT, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUScalarType.java b/src/main/java/graphql/schema/universe/SUScalarType.java new file mode 100644 index 0000000000..99edbfc348 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUScalarType.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUScalarType extends SUNamedType { + + @Internal + public SUScalarType(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.SCALAR, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java new file mode 100644 index 0000000000..cfeb133f70 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -0,0 +1,416 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.schema.GraphQLSchema; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertTrue; + +/** + * An immutable graph view selecting one schema from a {@link SchemaUniverse}. + */ +@ExperimentalApi +@NullMarked +public final class SUSchema { + + private final SchemaUniverse universe; + private final SUSchemaRoot root; + private final PersistentEdgeMap edgeMap; + private final int storedEdgeCount; + + @Internal + public SUSchema( + SchemaUniverse universe, + SUSchemaRoot root, + PersistentEdgeMap edgeMap, + int storedEdgeCount) { + this.universe = assertNotNull(universe); + this.root = assertNotNull(root); + this.edgeMap = assertNotNull(edgeMap); + this.storedEdgeCount = storedEdgeCount; + } + + public String getName() { + return assertNotNull(root.getName()); + } + + public SUSchemaRoot getRoot() { + return root; + } + + /** + * Exports this snapshot as an unexecutable {@link GraphQLSchema}. + * + *

The result preserves the type-system topology and metadata represented by this schema. + * Runtime wiring is not retained by a schema universe and therefore cannot be exported.

+ */ + public GraphQLSchema toGraphQLSchema() { + return new SUExporter(this).exportSchema(); + } + + public SUObjectType getQueryType() { + return (SUObjectType) assertNotNull(getSingleTarget(root, SUEdgeKind.QUERY_TYPE)); + } + + public @Nullable SUObjectType getMutationType() { + return (SUObjectType) getSingleTarget(root, SUEdgeKind.MUTATION_TYPE); + } + + public @Nullable SUObjectType getSubscriptionType() { + return (SUObjectType) getSingleTarget(root, SUEdgeKind.SUBSCRIPTION_TYPE); + } + + public List getRootTypes() { + List result = new ArrayList<>(3); + result.add(getQueryType()); + SUObjectType mutationType = getMutationType(); + if (mutationType != null) { + result.add(mutationType); + } + SUObjectType subscriptionType = getSubscriptionType(); + if (subscriptionType != null) { + result.add(subscriptionType); + } + return Collections.unmodifiableList(result); + } + + public List getAdditionalTypes() { + return getTypedChildren(root, SUEdgeKind.ADDITIONAL_TYPE, SUNamedType.class); + } + + public List getTypes() { + Map result = new LinkedHashMap<>(); + for (SUObjectType rootType : getRootTypes()) { + result.put(rootType.getId(), rootType); + } + for (SUNamedType additionalType : getAdditionalTypes()) { + result.put(additionalType.getId(), additionalType); + } + return Collections.unmodifiableList(new ArrayList<>(result.values())); + } + + public @Nullable SUNamedType getType(String name) { + assertNotNull(name); + for (SUObjectType rootType : getRootTypes()) { + if (name.equals(rootType.getName())) { + return rootType; + } + } + return (SUNamedType) getChild(root, SUEdgeKind.ADDITIONAL_TYPE, name); + } + + public @Nullable SUObjectType getObjectType(String name) { + return getTypeAs(name, SUObjectType.class); + } + + public @Nullable SUInterfaceType getInterfaceType(String name) { + return getTypeAs(name, SUInterfaceType.class); + } + + public @Nullable SUUnionType getUnionType(String name) { + return getTypeAs(name, SUUnionType.class); + } + + public @Nullable SUEnumType getEnumType(String name) { + return getTypeAs(name, SUEnumType.class); + } + + public @Nullable SUScalarType getScalarType(String name) { + return getTypeAs(name, SUScalarType.class); + } + + public @Nullable SUInputObjectType getInputObjectType(String name) { + return getTypeAs(name, SUInputObjectType.class); + } + + public List getDirectiveDefinitions() { + return getTypedChildren(root, SUEdgeKind.DIRECTIVE_DEFINITION, SUDirective.class); + } + + public @Nullable SUDirective getDirectiveDefinition(String name) { + return getTypedChild(root, SUEdgeKind.DIRECTIVE_DEFINITION, name, SUDirective.class); + } + + public List getFields(SUObjectType objectType) { + return getTypedChildren(objectType, SUEdgeKind.FIELD, SUField.class); + } + + public List getFields(SUInterfaceType interfaceType) { + return getTypedChildren(interfaceType, SUEdgeKind.FIELD, SUField.class); + } + + public @Nullable SUField getField(SUObjectType objectType, String name) { + return getTypedChild(objectType, SUEdgeKind.FIELD, name, SUField.class); + } + + public @Nullable SUField getField(SUInterfaceType interfaceType, String name) { + return getTypedChild(interfaceType, SUEdgeKind.FIELD, name, SUField.class); + } + + public List getArguments(SUField field) { + return getTypedChildren(field, SUEdgeKind.ARGUMENT, SUArgument.class); + } + + public List getArguments(SUDirective directive) { + return getTypedChildren(directive, SUEdgeKind.ARGUMENT, SUArgument.class); + } + + public List getArguments(SUAppliedDirective directive) { + assertOwned(directive); + return directive.getArguments(); + } + + public @Nullable SUArgument getArgument(SUField field, String name) { + return getTypedChild(field, SUEdgeKind.ARGUMENT, name, SUArgument.class); + } + + public @Nullable SUArgument getArgument(SUDirective directive, String name) { + return getTypedChild(directive, SUEdgeKind.ARGUMENT, name, SUArgument.class); + } + + public @Nullable SUAppliedDirectiveArgument getArgument( + SUAppliedDirective directive, + String name) { + assertOwned(directive); + return directive.getArgument(name); + } + + public @Nullable SUType getType(SUField field) { + return (SUType) getSingleTarget(field, SUEdgeKind.TYPE); + } + + public @Nullable SUType getType(SUArgument argument) { + return (SUType) getSingleTarget(argument, SUEdgeKind.TYPE); + } + + public @Nullable SUType getType(SUInputField inputField) { + return (SUType) getSingleTarget(inputField, SUEdgeKind.TYPE); + } + + public SUType getType(SUAppliedDirectiveArgument argument) { + assertOwned(argument); + return (SUType) universe.getVertex(argument.getTypeId()); + } + + public List getInterfaces(SUObjectType objectType) { + return getTypedChildren(objectType, SUEdgeKind.IMPLEMENTS, SUInterfaceType.class); + } + + public List getInterfaces(SUInterfaceType interfaceType) { + return getTypedChildren(interfaceType, SUEdgeKind.IMPLEMENTS, SUInterfaceType.class); + } + + public @Nullable SUInterfaceType getInterface(SUObjectType objectType, String name) { + return getTypedChild(objectType, SUEdgeKind.IMPLEMENTS, name, SUInterfaceType.class); + } + + public @Nullable SUInterfaceType getInterface(SUInterfaceType interfaceType, String name) { + return getTypedChild(interfaceType, SUEdgeKind.IMPLEMENTS, name, SUInterfaceType.class); + } + + public List getUnionMembers(SUUnionType unionType) { + return getTypedChildren(unionType, SUEdgeKind.UNION_MEMBER, SUObjectType.class); + } + + public @Nullable SUObjectType getUnionMember(SUUnionType unionType, String name) { + return getTypedChild(unionType, SUEdgeKind.UNION_MEMBER, name, SUObjectType.class); + } + + public List getEnumValues(SUEnumType enumType) { + return getTypedChildren(enumType, SUEdgeKind.ENUM_VALUE, SUEnumValue.class); + } + + public @Nullable SUEnumValue getEnumValue(SUEnumType enumType, String name) { + return getTypedChild(enumType, SUEdgeKind.ENUM_VALUE, name, SUEnumValue.class); + } + + public List getInputFields(SUInputObjectType inputObjectType) { + return getTypedChildren(inputObjectType, SUEdgeKind.INPUT_FIELD, SUInputField.class); + } + + public @Nullable SUInputField getInputField(SUInputObjectType inputObjectType, String name) { + return getTypedChild(inputObjectType, SUEdgeKind.INPUT_FIELD, name, SUInputField.class); + } + + public List getSchemaAppliedDirectives() { + return getAppliedDirectives(root); + } + + public List getSchemaAppliedDirectives(String name) { + return getAppliedDirectives(root, name); + } + + public List getAppliedDirectives(SUVertex container) { + return getTypedChildren( + container, + SUEdgeKind.APPLIED_DIRECTIVE, + SUAppliedDirective.class); + } + + public List getAppliedDirectives(SUVertex container, String name) { + assertNotNull(name); + List directives = getAppliedDirectives(container); + if (directives.isEmpty()) { + return directives; + } + List result = new ArrayList<>(); + for (SUAppliedDirective directive : directives) { + if (name.equals(directive.getName())) { + result.add(directive); + } + } + return result.isEmpty() + ? Collections.emptyList() + : Collections.unmodifiableList(result); + } + + public @Nullable SUType getWrappedType(SUListType listType) { + return (SUType) getSingleTarget(listType, SUEdgeKind.WRAPPED_TYPE); + } + + public @Nullable SUType getWrappedType(SUNonNullType nonNullType) { + return (SUType) getSingleTarget(nonNullType, SUEdgeKind.WRAPPED_TYPE); + } + + /** + * Returns the number of adjacency bindings retained by this snapshot. + * + *

This can include bindings for vertices that are not currently reachable from the schema + * root. Such dormant bindings allow a derived schema to reattach a subgraph cheaply.

+ */ + public int getStoredEdgeCount() { + return storedEdgeCount; + } + + @Internal + public List getChildren( + SUVertex source, + SUEdgeKind kind) { + assertOwned(source); + PackedEdgeSet edges = edgeMap.get(source.getId()); + int start = edges.firstIndex(kind); + int end = edges.endIndex(kind); + if (start == end) { + return Collections.emptyList(); + } + List result = new ArrayList<>(end - start); + for (int i = start; i < end; i++) { + result.add(universe.getVertex(edges.targetIdAt(i))); + } + return Collections.unmodifiableList(result); + } + + @Internal + public @Nullable SUVertex getChild( + SUVertex source, + SUEdgeKind kind, + String name) { + assertOwned(source); + int nameId = universe.getNameId(assertNotNull(name)); + if (nameId < 0) { + return null; + } + int targetId = edgeMap.get(source.getId()).targetByName(kind, nameId); + return targetId < 0 ? null : universe.getVertex(targetId); + } + + @Internal + public boolean containsEdge( + SUVertex source, + SUEdgeKind kind, + SUVertex target) { + assertOwned(source); + assertOwned(target); + return edgeMap.get(source.getId()).contains(kind, target.getNameId(), target.getId()); + } + + public SUSchema transform( + String name, + Consumer builderConsumer) { + SUSchemaBuilder builder = universe.transformBuilder(this, name); + builderConsumer.accept(builder); + return builder.build(); + } + + @Internal + public SchemaUniverse getUniverse() { + return universe; + } + + @Internal + public PersistentEdgeMap getEdgeMap() { + return edgeMap; + } + + @Internal + public PackedEdgeSet getPackedEdges(SUVertex source) { + assertOwned(source); + return edgeMap.get(source.getId()); + } + + @Internal + public boolean sharesOutgoingEdgesWith( + SUSchema other, + SUVertex source) { + assertTrue(universe == other.universe, "Schemas belong to different schema universes"); + return getPackedEdges(source) == other.getPackedEdges(source); + } + + private @Nullable SUVertex getSingleTarget( + SUVertex source, + SUEdgeKind kind) { + assertOwned(source); + int targetId = edgeMap.get(source.getId()).firstTarget(kind); + return targetId < 0 ? null : universe.getVertex(targetId); + } + + private List getTypedChildren( + SUVertex source, + SUEdgeKind kind, + Class targetType) { + List children = getChildren(source, kind); + if (children.isEmpty()) { + return Collections.emptyList(); + } + List result = new ArrayList<>(children.size()); + for (SUVertex child : children) { + result.add(targetType.cast(child)); + } + return Collections.unmodifiableList(result); + } + + private @Nullable T getTypedChild( + SUVertex source, + SUEdgeKind kind, + String name, + Class targetType) { + SUVertex child = getChild(source, kind, name); + return child == null ? null : targetType.cast(child); + } + + private @Nullable T getTypeAs(String name, Class type) { + SUNamedType vertex = getType(name); + return type.isInstance(vertex) ? type.cast(vertex) : null; + } + + private void assertOwned(SUVertex vertex) { + assertTrue(universe.owns(vertex), "Vertex %s belongs to another schema universe", vertex); + } + + private void assertOwned(SUAppliedDirectiveArgument argument) { + assertTrue( + argument.getUniverse() == universe, + "Applied directive argument %s belongs to another schema universe", + argument); + } +} diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java new file mode 100644 index 0000000000..d423a26bb5 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -0,0 +1,578 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.LinkedHashMap; +import java.util.Map; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertTrue; + +/** + * Builds an immutable schema snapshot, optionally by changing an existing snapshot. + */ +@ExperimentalApi +@NullMarked +public final class SUSchemaBuilder { + + private final SchemaUniverse universe; + private final SUSchemaRoot root; + private final @Nullable SUSchema baseSchema; + private final Map changedEdges = new LinkedHashMap<>(); + private boolean built; + + @Internal + public SUSchemaBuilder( + SchemaUniverse universe, + SUSchemaRoot root, + @Nullable SUSchema baseSchema) { + this.universe = assertNotNull(universe); + this.root = assertNotNull(root); + this.baseSchema = baseSchema; + copyRootEdges(baseSchema); + } + + public SUSchemaBuilder queryType(SUObjectType queryType) { + return setQueryType(queryType); + } + + public SUSchemaBuilder setQueryType(SUObjectType queryType) { + return setEdge(root, SUEdgeKind.QUERY_TYPE, queryType); + } + + public SUSchemaRoot getRoot() { + return root; + } + + public SUSchemaBuilder mutationType(@Nullable SUObjectType mutationType) { + return setMutationType(mutationType); + } + + public SUSchemaBuilder setMutationType(@Nullable SUObjectType mutationType) { + return setOptionalEdge(SUEdgeKind.MUTATION_TYPE, mutationType); + } + + public SUSchemaBuilder subscriptionType(@Nullable SUObjectType subscriptionType) { + return setSubscriptionType(subscriptionType); + } + + public SUSchemaBuilder setSubscriptionType(@Nullable SUObjectType subscriptionType) { + return setOptionalEdge(SUEdgeKind.SUBSCRIPTION_TYPE, subscriptionType); + } + + public SUSchemaBuilder additionalType(SUNamedType type) { + return addAdditionalType(type); + } + + public SUSchemaBuilder addAdditionalType(SUNamedType type) { + return addEdge(root, SUEdgeKind.ADDITIONAL_TYPE, type); + } + + public SUSchemaBuilder removeAdditionalType(SUNamedType type) { + return removeEdge(root, SUEdgeKind.ADDITIONAL_TYPE, type); + } + + public SUSchemaBuilder removeAdditionalType(String name) { + return removeEdge(root, SUEdgeKind.ADDITIONAL_TYPE, name); + } + + public SUSchemaBuilder clearAdditionalTypes() { + return removeEdges(root, SUEdgeKind.ADDITIONAL_TYPE); + } + + public SUSchemaBuilder directiveDefinition(SUDirective directive) { + return addDirectiveDefinition(directive); + } + + public SUSchemaBuilder addDirectiveDefinition(SUDirective directive) { + return addEdge(root, SUEdgeKind.DIRECTIVE_DEFINITION, directive); + } + + public SUSchemaBuilder removeDirectiveDefinition(SUDirective directive) { + return removeEdge(root, SUEdgeKind.DIRECTIVE_DEFINITION, directive); + } + + public SUSchemaBuilder removeDirectiveDefinition(String name) { + return removeEdge(root, SUEdgeKind.DIRECTIVE_DEFINITION, name); + } + + public SUSchemaBuilder clearDirectiveDefinitions() { + return removeEdges(root, SUEdgeKind.DIRECTIVE_DEFINITION); + } + + public SUSchemaBuilder addSchemaAppliedDirective(SUAppliedDirective directive) { + return addAppliedDirective(root, directive); + } + + public SUSchemaBuilder removeSchemaAppliedDirective(SUAppliedDirective directive) { + return removeAppliedDirective(root, directive); + } + + public SUSchemaBuilder removeSchemaAppliedDirectives(String name) { + return removeAppliedDirectives(root, name); + } + + public SUSchemaBuilder clearSchemaAppliedDirectives() { + return clearAppliedDirectives(root); + } + + public SUSchemaBuilder addField(SUObjectType objectType, SUField field) { + return addEdge(objectType, SUEdgeKind.FIELD, field); + } + + public SUSchemaBuilder addField(SUInterfaceType interfaceType, SUField field) { + return addEdge(interfaceType, SUEdgeKind.FIELD, field); + } + + public SUSchemaBuilder removeField(SUObjectType objectType, SUField field) { + return removeEdge(objectType, SUEdgeKind.FIELD, field); + } + + public SUSchemaBuilder removeField(SUInterfaceType interfaceType, SUField field) { + return removeEdge(interfaceType, SUEdgeKind.FIELD, field); + } + + public SUSchemaBuilder removeField(SUObjectType objectType, String name) { + return removeEdge(objectType, SUEdgeKind.FIELD, name); + } + + public SUSchemaBuilder removeField(SUInterfaceType interfaceType, String name) { + return removeEdge(interfaceType, SUEdgeKind.FIELD, name); + } + + public SUSchemaBuilder clearFields(SUObjectType objectType) { + return removeEdges(objectType, SUEdgeKind.FIELD); + } + + public SUSchemaBuilder clearFields(SUInterfaceType interfaceType) { + return removeEdges(interfaceType, SUEdgeKind.FIELD); + } + + public SUSchemaBuilder setFieldType(SUField field, SUType type) { + assertOutputType(type); + return setEdge(field, SUEdgeKind.TYPE, type); + } + + public SUSchemaBuilder addArgument(SUField field, SUArgument argument) { + return addEdge(field, SUEdgeKind.ARGUMENT, argument); + } + + public SUSchemaBuilder addArgument(SUDirective directive, SUArgument argument) { + return addEdge(directive, SUEdgeKind.ARGUMENT, argument); + } + + public SUSchemaBuilder removeArgument(SUField field, SUArgument argument) { + return removeEdge(field, SUEdgeKind.ARGUMENT, argument); + } + + public SUSchemaBuilder removeArgument(SUDirective directive, SUArgument argument) { + return removeEdge(directive, SUEdgeKind.ARGUMENT, argument); + } + + public SUSchemaBuilder removeArgument(SUField field, String name) { + return removeEdge(field, SUEdgeKind.ARGUMENT, name); + } + + public SUSchemaBuilder removeArgument(SUDirective directive, String name) { + return removeEdge(directive, SUEdgeKind.ARGUMENT, name); + } + + public SUSchemaBuilder clearArguments(SUField field) { + return removeEdges(field, SUEdgeKind.ARGUMENT); + } + + public SUSchemaBuilder clearArguments(SUDirective directive) { + return removeEdges(directive, SUEdgeKind.ARGUMENT); + } + + public SUSchemaBuilder setArgumentType(SUArgument argument, SUType type) { + assertInputType(type); + return setEdge(argument, SUEdgeKind.TYPE, type); + } + + public SUSchemaBuilder addInterface( + SUObjectType objectType, + SUInterfaceType interfaceType) { + return addEdge(objectType, SUEdgeKind.IMPLEMENTS, interfaceType); + } + + public SUSchemaBuilder addInterface( + SUInterfaceType implementingType, + SUInterfaceType interfaceType) { + return addEdge(implementingType, SUEdgeKind.IMPLEMENTS, interfaceType); + } + + public SUSchemaBuilder removeInterface( + SUObjectType objectType, + SUInterfaceType interfaceType) { + return removeEdge(objectType, SUEdgeKind.IMPLEMENTS, interfaceType); + } + + public SUSchemaBuilder removeInterface( + SUInterfaceType implementingType, + SUInterfaceType interfaceType) { + return removeEdge(implementingType, SUEdgeKind.IMPLEMENTS, interfaceType); + } + + public SUSchemaBuilder removeInterface(SUObjectType objectType, String name) { + return removeEdge(objectType, SUEdgeKind.IMPLEMENTS, name); + } + + public SUSchemaBuilder removeInterface(SUInterfaceType interfaceType, String name) { + return removeEdge(interfaceType, SUEdgeKind.IMPLEMENTS, name); + } + + public SUSchemaBuilder clearInterfaces(SUObjectType objectType) { + return removeEdges(objectType, SUEdgeKind.IMPLEMENTS); + } + + public SUSchemaBuilder clearInterfaces(SUInterfaceType interfaceType) { + return removeEdges(interfaceType, SUEdgeKind.IMPLEMENTS); + } + + public SUSchemaBuilder addUnionMember(SUUnionType unionType, SUObjectType member) { + return addEdge(unionType, SUEdgeKind.UNION_MEMBER, member); + } + + public SUSchemaBuilder removeUnionMember(SUUnionType unionType, SUObjectType member) { + return removeEdge(unionType, SUEdgeKind.UNION_MEMBER, member); + } + + public SUSchemaBuilder removeUnionMember(SUUnionType unionType, String name) { + return removeEdge(unionType, SUEdgeKind.UNION_MEMBER, name); + } + + public SUSchemaBuilder clearUnionMembers(SUUnionType unionType) { + return removeEdges(unionType, SUEdgeKind.UNION_MEMBER); + } + + public SUSchemaBuilder addEnumValue(SUEnumType enumType, SUEnumValue value) { + return addEdge(enumType, SUEdgeKind.ENUM_VALUE, value); + } + + public SUSchemaBuilder removeEnumValue(SUEnumType enumType, SUEnumValue value) { + return removeEdge(enumType, SUEdgeKind.ENUM_VALUE, value); + } + + public SUSchemaBuilder removeEnumValue(SUEnumType enumType, String name) { + return removeEdge(enumType, SUEdgeKind.ENUM_VALUE, name); + } + + public SUSchemaBuilder clearEnumValues(SUEnumType enumType) { + return removeEdges(enumType, SUEdgeKind.ENUM_VALUE); + } + + public SUSchemaBuilder addInputField( + SUInputObjectType inputObjectType, + SUInputField field) { + return addEdge(inputObjectType, SUEdgeKind.INPUT_FIELD, field); + } + + public SUSchemaBuilder removeInputField( + SUInputObjectType inputObjectType, + SUInputField field) { + return removeEdge(inputObjectType, SUEdgeKind.INPUT_FIELD, field); + } + + public SUSchemaBuilder removeInputField(SUInputObjectType inputObjectType, String name) { + return removeEdge(inputObjectType, SUEdgeKind.INPUT_FIELD, name); + } + + public SUSchemaBuilder clearInputFields(SUInputObjectType inputObjectType) { + return removeEdges(inputObjectType, SUEdgeKind.INPUT_FIELD); + } + + public SUSchemaBuilder setInputFieldType(SUInputField field, SUType type) { + assertInputType(type); + return setEdge(field, SUEdgeKind.TYPE, type); + } + + public SUSchemaBuilder addAppliedDirective( + SUAppliedDirectiveContainer container, + SUAppliedDirective directive) { + return addEdge(containerVertex(container), SUEdgeKind.APPLIED_DIRECTIVE, directive); + } + + public SUSchemaBuilder removeAppliedDirective( + SUAppliedDirectiveContainer container, + SUAppliedDirective directive) { + return removeEdge(containerVertex(container), SUEdgeKind.APPLIED_DIRECTIVE, directive); + } + + public SUSchemaBuilder removeAppliedDirectives( + SUAppliedDirectiveContainer container, + String name) { + return removeEdge(containerVertex(container), SUEdgeKind.APPLIED_DIRECTIVE, name); + } + + public SUSchemaBuilder clearAppliedDirectives(SUAppliedDirectiveContainer container) { + return removeEdges(containerVertex(container), SUEdgeKind.APPLIED_DIRECTIVE); + } + + public SUSchemaBuilder setWrappedType(SUListType listType, SUType wrappedType) { + return setEdge(listType, SUEdgeKind.WRAPPED_TYPE, wrappedType); + } + + public SUSchemaBuilder setWrappedType(SUNonNullType nonNullType, SUType wrappedType) { + return setEdge(nonNullType, SUEdgeKind.WRAPPED_TYPE, wrappedType); + } + + private SUSchemaBuilder addEdge( + SUVertex source, + SUEdgeKind kind, + SUVertex target) { + assertCanChange(); + assertEdgeShape(source, kind, target); + mutableEdges(source).add(PackedEdgeSet.pack(kind, target.getNameId(), target.getId())); + return this; + } + + private SUSchemaBuilder setEdge( + SUVertex source, + SUEdgeKind kind, + SUVertex target) { + assertCanChange(); + assertTrue(kind.isSingle(), "Edge kind %s is not single-valued", kind); + assertEdgeShape(source, kind, target); + MutablePackedEdgeSet edges = mutableEdges(source); + edges.removeKind(kind); + edges.add(PackedEdgeSet.pack(kind, target.getNameId(), target.getId())); + return this; + } + + private SUSchemaBuilder removeEdge( + SUVertex source, + SUEdgeKind kind, + SUVertex target) { + assertCanChange(); + assertOwned(source); + assertOwned(target); + mutableEdges(source).remove(PackedEdgeSet.pack(kind, target.getNameId(), target.getId())); + return this; + } + + private SUSchemaBuilder removeEdge( + SUVertex source, + SUEdgeKind kind, + String targetName) { + assertCanChange(); + assertOwned(source); + int nameId = universe.getNameId(targetName); + if (nameId >= 0) { + mutableEdges(source).removeByName(kind, nameId); + } + return this; + } + + private SUSchemaBuilder removeEdges( + SUVertex source, + SUEdgeKind kind) { + assertCanChange(); + assertOwned(source); + mutableEdges(source).removeKind(kind); + return this; + } + + public SUSchema build() { + assertCanChange(); + built = true; + PersistentEdgeMap edgeMap = baseEdgeMap(); + int edgeCount = baseEdgeCount(); + for (Map.Entry entry : changedEdges.entrySet()) { + int sourceId = entry.getKey(); + PackedEdgeSet oldEdges = edgeMap.get(sourceId); + PackedEdgeSet newEdges = entry.getValue().freeze(); + edgeCount += newEdges.size() - oldEdges.size(); + edgeMap = edgeMap.put(sourceId, newEdges); + } + assertTrue(edgeMap.get(root.getId()).firstTarget(SUEdgeKind.QUERY_TYPE) >= 0, + "A schema universe schema requires a query type"); + SUSchema schema = new SUSchema(universe, root, edgeMap, edgeCount); + universe.registerSchema(schema); + return schema; + } + + private SUSchemaBuilder setOptionalEdge( + SUEdgeKind kind, + @Nullable SUObjectType target) { + if (target == null) { + return removeEdges(root, kind); + } + return setEdge(root, kind, target); + } + + private void copyRootEdges(@Nullable SUSchema base) { + if (base == null) { + return; + } + changedEdges.put(base.getRoot().getId(), new MutablePackedEdgeSet(PackedEdgeSet.empty())); + changedEdges.put(root.getId(), new MutablePackedEdgeSet(base.getPackedEdges(base.getRoot()))); + } + + private MutablePackedEdgeSet mutableEdges(SUVertex source) { + assertOwned(source); + MutablePackedEdgeSet changed = changedEdges.get(source.getId()); + if (changed != null) { + return changed; + } + PackedEdgeSet original = baseSchema == null + ? PackedEdgeSet.empty() + : baseSchema.getPackedEdges(source); + MutablePackedEdgeSet mutable = new MutablePackedEdgeSet(original); + changedEdges.put(source.getId(), mutable); + return mutable; + } + + private PersistentEdgeMap baseEdgeMap() { + return baseSchema == null ? PersistentEdgeMap.empty() : baseSchema.getEdgeMap(); + } + + private int baseEdgeCount() { + return baseSchema == null ? 0 : baseSchema.getStoredEdgeCount(); + } + + private void assertCanChange() { + assertTrue(!built, "This schema universe builder has already built a schema"); + } + + private void assertOwned(SUVertex vertex) { + assertTrue(universe.owns(vertex), "Vertex %s belongs to another schema universe", vertex); + } + + private SUVertex containerVertex(SUAppliedDirectiveContainer container) { + assertTrue( + container instanceof SUVertex, + "Applied directive container must be a schema universe vertex"); + return (SUVertex) container; + } + + private void assertOutputType(SUType type) { + assertNotNull(type); + assertTrue( + type.getKind() != SUVertexKind.INPUT_OBJECT, + "Field type must be an output type, found %s", + type.getKind()); + } + + private void assertInputType(SUType type) { + assertNotNull(type); + SUVertexKind kind = type.getKind(); + assertTrue( + kind != SUVertexKind.OBJECT + && kind != SUVertexKind.INTERFACE + && kind != SUVertexKind.UNION, + "Input value type must be an input type, found %s", + kind); + } + + private void assertEdgeShape( + SUVertex source, + SUEdgeKind kind, + SUVertex target) { + assertOwned(source); + assertOwned(target); + if (source.getKind() == SUVertexKind.SCHEMA) { + assertTrue(source == root, "Only the current schema root can have root edges"); + } + assertTrue(isValidEdge(source, kind, target), "Invalid %s edge from %s to %s", + kind, source.getKind(), target.getKind()); + } + + private boolean isValidEdge( + SUVertex source, + SUEdgeKind kind, + SUVertex target) { + SUVertexKind sourceKind = source.getKind(); + SUVertexKind targetKind = target.getKind(); + switch (kind) { + case QUERY_TYPE: + case MUTATION_TYPE: + case SUBSCRIPTION_TYPE: + return sourceKind == SUVertexKind.SCHEMA + && targetKind == SUVertexKind.OBJECT; + case ADDITIONAL_TYPE: + return sourceKind == SUVertexKind.SCHEMA && isNamedType(targetKind); + case DIRECTIVE_DEFINITION: + return sourceKind == SUVertexKind.SCHEMA + && targetKind == SUVertexKind.DIRECTIVE; + case FIELD: + return isFieldsContainer(sourceKind) && targetKind == SUVertexKind.FIELD; + case ARGUMENT: + return isArgumentsContainer(sourceKind) && targetKind == SUVertexKind.ARGUMENT; + case TYPE: + return isTypedElement(sourceKind) && isType(targetKind); + case IMPLEMENTS: + return isImplementingType(sourceKind) && targetKind == SUVertexKind.INTERFACE; + case UNION_MEMBER: + return sourceKind == SUVertexKind.UNION + && targetKind == SUVertexKind.OBJECT; + case ENUM_VALUE: + return sourceKind == SUVertexKind.ENUM + && targetKind == SUVertexKind.ENUM_VALUE; + case INPUT_FIELD: + return sourceKind == SUVertexKind.INPUT_OBJECT + && targetKind == SUVertexKind.INPUT_FIELD; + case APPLIED_DIRECTIVE: + return isDirectiveContainer(sourceKind) + && targetKind == SUVertexKind.APPLIED_DIRECTIVE; + case WRAPPED_TYPE: + return isWrappingType(sourceKind) && isType(targetKind) + && !(sourceKind == SUVertexKind.NON_NULL + && targetKind == SUVertexKind.NON_NULL); + default: + return false; + } + } + + private boolean isFieldsContainer(SUVertexKind kind) { + return kind == SUVertexKind.OBJECT || kind == SUVertexKind.INTERFACE; + } + + private boolean isArgumentsContainer(SUVertexKind kind) { + return kind == SUVertexKind.FIELD || kind == SUVertexKind.DIRECTIVE; + } + + private boolean isTypedElement(SUVertexKind kind) { + return kind == SUVertexKind.FIELD + || kind == SUVertexKind.ARGUMENT + || kind == SUVertexKind.INPUT_FIELD; + } + + private boolean isImplementingType(SUVertexKind kind) { + return kind == SUVertexKind.OBJECT || kind == SUVertexKind.INTERFACE; + } + + private boolean isDirectiveContainer(SUVertexKind kind) { + return kind == SUVertexKind.SCHEMA + || kind == SUVertexKind.OBJECT + || kind == SUVertexKind.FIELD + || kind == SUVertexKind.INTERFACE + || kind == SUVertexKind.UNION + || kind == SUVertexKind.ENUM + || kind == SUVertexKind.ENUM_VALUE + || kind == SUVertexKind.SCALAR + || kind == SUVertexKind.INPUT_OBJECT + || kind == SUVertexKind.INPUT_FIELD + || kind == SUVertexKind.ARGUMENT; + } + + private boolean isWrappingType(SUVertexKind kind) { + return kind == SUVertexKind.LIST || kind == SUVertexKind.NON_NULL; + } + + private boolean isNamedType(SUVertexKind kind) { + return kind == SUVertexKind.OBJECT + || kind == SUVertexKind.INTERFACE + || kind == SUVertexKind.UNION + || kind == SUVertexKind.ENUM + || kind == SUVertexKind.SCALAR + || kind == SUVertexKind.INPUT_OBJECT; + } + + private boolean isType(SUVertexKind kind) { + return isNamedType(kind) || isWrappingType(kind); + } +} diff --git a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java new file mode 100644 index 0000000000..ee34b80758 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java @@ -0,0 +1,631 @@ +package graphql.schema.universe; + +import graphql.Directives; +import graphql.ExperimentalApi; +import graphql.GraphQLError; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.Argument; +import graphql.language.Comment; +import graphql.language.Description; +import graphql.language.Directive; +import graphql.language.DirectiveDefinition; +import graphql.language.EnumTypeDefinition; +import graphql.language.EnumTypeExtensionDefinition; +import graphql.language.EnumValueDefinition; +import graphql.language.FieldDefinition; +import graphql.language.InputObjectTypeDefinition; +import graphql.language.InputObjectTypeExtensionDefinition; +import graphql.language.InputValueDefinition; +import graphql.language.InterfaceTypeDefinition; +import graphql.language.InterfaceTypeExtensionDefinition; +import graphql.language.ListType; +import graphql.language.Node; +import graphql.language.NonNullType; +import graphql.language.ObjectTypeDefinition; +import graphql.language.ObjectTypeExtensionDefinition; +import graphql.language.OperationTypeDefinition; +import graphql.language.ScalarTypeDefinition; +import graphql.language.ScalarTypeExtensionDefinition; +import graphql.language.SchemaDefinition; +import graphql.language.SchemaExtensionDefinition; +import graphql.language.Type; +import graphql.language.TypeDefinition; +import graphql.language.TypeName; +import graphql.language.UnionTypeDefinition; +import graphql.language.UnionTypeExtensionDefinition; +import graphql.language.Value; +import graphql.schema.InputValueWithState; +import graphql.schema.idl.ImmutableTypeDefinitionRegistry; +import graphql.schema.idl.RuntimeWiring; +import graphql.schema.idl.ScalarInfo; +import graphql.schema.idl.SchemaTypeChecker; +import graphql.schema.idl.TypeDefinitionRegistry; +import graphql.schema.idl.errors.SchemaProblem; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.Assert.assertTrue; + +/** + * Compiles parsed type-system definitions directly into a {@link SUSchema}. + */ +@ExperimentalApi +@NullMarked +public final class SUSchemaGenerator { + + private static final List BUILT_IN_DIRECTIVE_DEFINITIONS = + Collections.unmodifiableList(Arrays.asList( + Directives.INCLUDE_DIRECTIVE_DEFINITION, + Directives.SKIP_DIRECTIVE_DEFINITION, + Directives.DEPRECATED_DIRECTIVE_DEFINITION, + Directives.SPECIFIED_BY_DIRECTIVE_DEFINITION, + Directives.ONE_OF_DIRECTIVE_DEFINITION, + Directives.DEFER_DIRECTIVE_DEFINITION, + Directives.EXPERIMENTAL_DISABLE_ERROR_PROPAGATION_DIRECTIVE_DEFINITION)); + + private final SchemaUniverse universe; + private final Map namedTypes = new LinkedHashMap<>(); + private final Map directiveDefinitions = new LinkedHashMap<>(); + private final Map> directiveArguments = new LinkedHashMap<>(); + private final Map directiveArgumentTypes = new LinkedHashMap<>(); + private final Set referencedTypeNames = new HashSet<>(); + private @Nullable SUSchemaBuilder schemaBuilder; + private @Nullable SUObjectType queryType; + private @Nullable SUObjectType mutationType; + private @Nullable SUObjectType subscriptionType; + + public SUSchemaGenerator(SchemaUniverse universe) { + this.universe = assertNotNull(universe); + } + + public SUSchema generate(String name, TypeDefinitionRegistry registry) { + assertTrue(schemaBuilder == null, "This schema universe generator has already been used"); + ImmutableTypeDefinitionRegistry definitions = validatedDefinitions(registry); + schemaBuilder = universe.newSchema(name, schemaDescription(definitions)); + + createNamedTypes(definitions); + createDirectiveDefinitions(definitions); + expandDirectiveDefinitions(definitions); + expandNamedTypes(definitions); + addOperationRoots(definitions); + addSchemaDirectives(definitions); + addAdditionalTypes(); + return builder().build(); + } + + private ImmutableTypeDefinitionRegistry validatedDefinitions(TypeDefinitionRegistry registry) { + TypeDefinitionRegistry copy = new TypeDefinitionRegistry(); + copy.merge(assertNotNull(registry)); + addBuiltInDirectiveDefinitions(copy); + ImmutableTypeDefinitionRegistry definitions = copy.readOnly(); + List errors = + new SchemaTypeChecker().checkTypeRegistry(definitions, RuntimeWiring.MOCKED_WIRING); + if (!errors.isEmpty()) { + throw new SchemaProblem(errors); + } + return definitions; + } + + private void addBuiltInDirectiveDefinitions(TypeDefinitionRegistry registry) { + for (DirectiveDefinition definition : BUILT_IN_DIRECTIVE_DEFINITIONS) { + if (registry.getDirectiveDefinition(definition.getName()).isEmpty()) { + registry.add(definition); + } + } + } + + private void createNamedTypes(ImmutableTypeDefinitionRegistry registry) { + for (TypeDefinition definition : registry.types().values()) { + namedTypes.put(definition.getName(), createNamedType(definition)); + } + for (ScalarTypeDefinition definition : registry.scalars().values()) { + namedTypes.putIfAbsent( + definition.getName(), + universe.newScalarType( + definition.getName(), + description(definition, definition.getDescription()))); + } + } + + private SUNamedType createNamedType(TypeDefinition definition) { + if (definition instanceof ObjectTypeDefinition) { + ObjectTypeDefinition objectType = (ObjectTypeDefinition) definition; + return universe.newObjectType( + objectType.getName(), + description(objectType, objectType.getDescription())); + } + if (definition instanceof InterfaceTypeDefinition) { + InterfaceTypeDefinition interfaceType = (InterfaceTypeDefinition) definition; + return universe.newInterfaceType( + interfaceType.getName(), + description(interfaceType, interfaceType.getDescription())); + } + if (definition instanceof UnionTypeDefinition) { + UnionTypeDefinition unionType = (UnionTypeDefinition) definition; + return universe.newUnionType( + unionType.getName(), + description(unionType, unionType.getDescription())); + } + if (definition instanceof EnumTypeDefinition) { + EnumTypeDefinition enumType = (EnumTypeDefinition) definition; + return universe.newEnumType( + enumType.getName(), + description(enumType, enumType.getDescription())); + } + if (definition instanceof InputObjectTypeDefinition) { + InputObjectTypeDefinition inputObjectType = (InputObjectTypeDefinition) definition; + return universe.newInputObjectType( + inputObjectType.getName(), + description(inputObjectType, inputObjectType.getDescription())); + } + if (definition instanceof ScalarTypeDefinition) { + ScalarTypeDefinition scalarType = (ScalarTypeDefinition) definition; + return universe.newScalarType( + scalarType.getName(), + description(scalarType, scalarType.getDescription())); + } + return assertShouldNeverHappen( + "Unsupported schema universe type definition %s", + definition.getClass().getName()); + } + + private void createDirectiveDefinitions(ImmutableTypeDefinitionRegistry registry) { + for (DirectiveDefinition definition : registry.getDirectiveDefinitions().values()) { + SUDirective directive = universe.newDirective( + definition.getName(), + description(definition, definition.getDescription()), + definition.isRepeatable(), + directiveLocations(definition), + definition); + directiveDefinitions.put(definition.getName(), directive); + builder().addDirectiveDefinition(directive); + } + } + + private EnumSet directiveLocations(DirectiveDefinition definition) { + EnumSet result = EnumSet.noneOf(DirectiveLocation.class); + definition.getDirectiveLocations().forEach(location -> + result.add(DirectiveLocation.valueOf( + location.getName().toUpperCase(Locale.ROOT)))); + return result; + } + + private void expandDirectiveDefinitions(ImmutableTypeDefinitionRegistry registry) { + for (DirectiveDefinition definition : registry.getDirectiveDefinitions().values()) { + SUDirective directive = assertNotNull(directiveDefinitions.get(definition.getName())); + Map arguments = new LinkedHashMap<>(); + directiveArguments.put(definition.getName(), arguments); + for (InputValueDefinition argumentDefinition : + definition.getInputValueDefinitions()) { + SUArgument argument = newArgument(argumentDefinition); + SUType type = typeVertex(argumentDefinition.getType()); + arguments.put(argument.getName(), argument); + directiveArgumentTypes.put(argument, type); + builder().addArgument(directive, argument); + builder().setArgumentType(argument, type); + addAppliedDirectives(argumentDefinition.getDirectives(), argument); + } + } + } + + private void expandNamedTypes(ImmutableTypeDefinitionRegistry registry) { + for (TypeDefinition definition : registry.types().values()) { + SUNamedType type = namedType(definition.getName()); + if (definition instanceof ObjectTypeDefinition) { + expandObject((ObjectTypeDefinition) definition, (SUObjectType) type, registry); + } else if (definition instanceof InterfaceTypeDefinition) { + expandInterface( + (InterfaceTypeDefinition) definition, + (SUInterfaceType) type, + registry); + } else if (definition instanceof UnionTypeDefinition) { + expandUnion((UnionTypeDefinition) definition, (SUUnionType) type, registry); + } else if (definition instanceof EnumTypeDefinition) { + expandEnum((EnumTypeDefinition) definition, (SUEnumType) type, registry); + } else if (definition instanceof InputObjectTypeDefinition) { + expandInputObject( + (InputObjectTypeDefinition) definition, + (SUInputObjectType) type, + registry); + } else if (definition instanceof ScalarTypeDefinition) { + expandScalar((ScalarTypeDefinition) definition, (SUScalarType) type, registry); + } + } + for (ScalarTypeDefinition definition : registry.scalars().values()) { + if (!registry.types().containsKey(definition.getName())) { + expandScalar( + definition, + (SUScalarType) namedType(definition.getName()), + registry); + } + } + } + + private void expandObject( + ObjectTypeDefinition definition, + SUObjectType type, + ImmutableTypeDefinitionRegistry registry) { + addFields(definition.getFieldDefinitions(), type); + addImplementedInterfaces(definition.getImplements(), type); + addAppliedDirectives(definition.getDirectives(), type); + for (ObjectTypeExtensionDefinition extension : + registry.objectTypeExtensions() + .getOrDefault(definition.getName(), Collections.emptyList())) { + addFields(extension.getFieldDefinitions(), type); + addImplementedInterfaces(extension.getImplements(), type); + addAppliedDirectives(extension.getDirectives(), type); + } + } + + private void expandInterface( + InterfaceTypeDefinition definition, + SUInterfaceType type, + ImmutableTypeDefinitionRegistry registry) { + addFields(definition.getFieldDefinitions(), type); + addImplementedInterfaces(definition.getImplements(), type); + addAppliedDirectives(definition.getDirectives(), type); + for (InterfaceTypeExtensionDefinition extension : + registry.interfaceTypeExtensions() + .getOrDefault(definition.getName(), Collections.emptyList())) { + addFields(extension.getFieldDefinitions(), type); + addImplementedInterfaces(extension.getImplements(), type); + addAppliedDirectives(extension.getDirectives(), type); + } + } + + private void addFields(List definitions, SUVertex parent) { + for (FieldDefinition definition : definitions) { + SUField field = universe.newField( + definition.getName(), + description(definition, definition.getDescription())); + addField(parent, field); + builder().setFieldType(field, typeVertex(definition.getType())); + for (InputValueDefinition argumentDefinition : + definition.getInputValueDefinitions()) { + SUArgument argument = newArgument(argumentDefinition); + builder().addArgument(field, argument); + builder().setArgumentType(argument, typeVertex(argumentDefinition.getType())); + addAppliedDirectives(argumentDefinition.getDirectives(), argument); + } + addAppliedDirectives(definition.getDirectives(), field); + } + } + + private void addField(SUVertex parent, SUField field) { + if (parent instanceof SUObjectType) { + builder().addField((SUObjectType) parent, field); + } else { + builder().addField((SUInterfaceType) parent, field); + } + } + + private SUArgument newArgument(InputValueDefinition definition) { + return universe.newArgument( + definition.getName(), + description(definition, definition.getDescription()), + inputValue(definition.getDefaultValue()), + definition); + } + + private void addImplementedInterfaces(List interfaces, SUVertex type) { + for (Type interfaceType : interfaces) { + addInterface(type, (SUInterfaceType) namedType(typeName(interfaceType))); + } + } + + private void addInterface(SUVertex type, SUInterfaceType interfaceType) { + if (type instanceof SUObjectType) { + builder().addInterface((SUObjectType) type, interfaceType); + } else { + builder().addInterface((SUInterfaceType) type, interfaceType); + } + } + + private void expandUnion( + UnionTypeDefinition definition, + SUUnionType type, + ImmutableTypeDefinitionRegistry registry) { + addUnionMembers(definition.getMemberTypes(), type); + addAppliedDirectives(definition.getDirectives(), type); + for (UnionTypeExtensionDefinition extension : + registry.unionTypeExtensions() + .getOrDefault(definition.getName(), Collections.emptyList())) { + addUnionMembers(extension.getMemberTypes(), type); + addAppliedDirectives(extension.getDirectives(), type); + } + } + + private void addUnionMembers(List memberTypes, SUUnionType type) { + for (Type memberType : memberTypes) { + builder().addUnionMember( + type, + (SUObjectType) namedType(typeName(memberType))); + } + } + + private void expandEnum( + EnumTypeDefinition definition, + SUEnumType type, + ImmutableTypeDefinitionRegistry registry) { + addEnumValues(definition.getEnumValueDefinitions(), type); + addAppliedDirectives(definition.getDirectives(), type); + for (EnumTypeExtensionDefinition extension : + registry.enumTypeExtensions() + .getOrDefault(definition.getName(), Collections.emptyList())) { + addEnumValues(extension.getEnumValueDefinitions(), type); + addAppliedDirectives(extension.getDirectives(), type); + } + } + + private void addEnumValues(List definitions, SUEnumType type) { + for (EnumValueDefinition definition : definitions) { + SUEnumValue value = universe.newEnumValue( + definition.getName(), + description(definition, definition.getDescription())); + builder().addEnumValue(type, value); + addAppliedDirectives(definition.getDirectives(), value); + } + } + + private void expandInputObject( + InputObjectTypeDefinition definition, + SUInputObjectType type, + ImmutableTypeDefinitionRegistry registry) { + addInputFields(definition.getInputValueDefinitions(), type); + addAppliedDirectives(definition.getDirectives(), type); + for (InputObjectTypeExtensionDefinition extension : + registry.inputObjectTypeExtensions() + .getOrDefault(definition.getName(), Collections.emptyList())) { + addInputFields(extension.getInputValueDefinitions(), type); + addAppliedDirectives(extension.getDirectives(), type); + } + } + + private void addInputFields( + List definitions, + SUInputObjectType type) { + for (InputValueDefinition definition : definitions) { + SUInputField field = universe.newInputField( + definition.getName(), + description(definition, definition.getDescription()), + inputValue(definition.getDefaultValue()), + definition); + builder().addInputField(type, field); + builder().setInputFieldType(field, typeVertex(definition.getType())); + addAppliedDirectives(definition.getDirectives(), field); + } + } + + private void expandScalar( + ScalarTypeDefinition definition, + SUScalarType type, + ImmutableTypeDefinitionRegistry registry) { + addAppliedDirectives(definition.getDirectives(), type); + for (ScalarTypeExtensionDefinition extension : + registry.scalarTypeExtensions() + .getOrDefault(definition.getName(), Collections.emptyList())) { + addAppliedDirectives(extension.getDirectives(), type); + } + } + + private void addOperationRoots(ImmutableTypeDefinitionRegistry registry) { + Map operations = operationTypes(registry); + OperationTypeDefinition query = operations.get("query"); + queryType = (SUObjectType) namedType( + query == null ? "Query" : query.getTypeName().getName()); + builder().queryType(queryType); + + OperationTypeDefinition mutation = operations.get("mutation"); + if (mutation != null) { + mutationType = (SUObjectType) namedType(mutation.getTypeName().getName()); + builder().mutationType(mutationType); + } else if (registry.schemaDefinition().isEmpty() + && namedTypes.get("Mutation") instanceof SUObjectType) { + mutationType = (SUObjectType) namedType("Mutation"); + builder().mutationType(mutationType); + } + + OperationTypeDefinition subscription = operations.get("subscription"); + if (subscription != null) { + subscriptionType = + (SUObjectType) namedType(subscription.getTypeName().getName()); + builder().subscriptionType(subscriptionType); + } else if (registry.schemaDefinition().isEmpty() + && namedTypes.get("Subscription") instanceof SUObjectType) { + subscriptionType = (SUObjectType) namedType("Subscription"); + builder().subscriptionType(subscriptionType); + } + } + + private Map operationTypes( + ImmutableTypeDefinitionRegistry registry) { + Map result = new LinkedHashMap<>(); + SchemaDefinition schemaDefinition = registry.schemaDefinition().orElse(null); + if (schemaDefinition != null) { + addOperationTypes(result, schemaDefinition.getOperationTypeDefinitions()); + } + for (SchemaExtensionDefinition extension : + registry.getSchemaExtensionDefinitions()) { + addOperationTypes(result, extension.getOperationTypeDefinitions()); + } + return result; + } + + private void addOperationTypes( + Map result, + List definitions) { + for (OperationTypeDefinition definition : definitions) { + result.put(definition.getName(), definition); + } + } + + private void addSchemaDirectives(ImmutableTypeDefinitionRegistry registry) { + SchemaDefinition schemaDefinition = registry.schemaDefinition().orElse(null); + if (schemaDefinition != null) { + addAppliedDirectives(schemaDefinition.getDirectives(), builder().getRoot()); + } + for (SchemaExtensionDefinition extension : + registry.getSchemaExtensionDefinitions()) { + addAppliedDirectives(extension.getDirectives(), builder().getRoot()); + } + } + + private void addAdditionalTypes() { + for (SUNamedType type : namedTypes.values()) { + if (type == queryType || type == mutationType || type == subscriptionType) { + continue; + } + if (type instanceof SUScalarType + && ScalarInfo.isGraphqlSpecifiedScalar(assertNotNull(type.getName())) + && !referencedTypeNames.contains(type.getName())) { + continue; + } + builder().addAdditionalType(type); + } + } + + private void addAppliedDirectives( + List directives, + SUAppliedDirectiveContainer container) { + for (Directive directive : directives) { + SUDirective definition = assertNotNull( + directiveDefinitions.get(directive.getName()), + "Directive '%s' has no definition", + directive.getName()); + List arguments = + appliedArguments(directive, definition); + SUAppliedDirective applied = + universe.newAppliedDirective( + directive.getName(), + arguments, + directive); + builder().addAppliedDirective(container, applied); + } + } + + private List appliedArguments( + Directive directive, + SUDirective definition) { + Map definitions = assertNotNull( + directiveArguments.get(definition.getName())); + Map explicit = new LinkedHashMap<>(); + List result = new ArrayList<>(); + for (Argument argument : directive.getArguments()) { + explicit.put(argument.getName(), argument); + result.add(appliedArgument( + definitions.get(argument.getName()), + inputValue(argument.getValue()), + argument)); + } + for (SUArgument argument : definitions.values()) { + if (explicit.containsKey(argument.getName())) { + continue; + } + result.add(appliedArgument( + argument, + argument.getArgumentDefaultValue(), + null)); + } + return result; + } + + private SUAppliedDirectiveArgument appliedArgument( + @Nullable SUArgument definition, + InputValueWithState value, + @Nullable Argument argumentDefinition) { + SUArgument requiredDefinition = assertNotNull(definition); + return universe.newAppliedDirectiveArgument( + assertNotNull(requiredDefinition.getName()), + assertNotNull(directiveArgumentTypes.get(requiredDefinition)), + value, + argumentDefinition); + } + + private SUType typeVertex(Type type) { + if (type instanceof TypeName) { + String name = ((TypeName) type).getName(); + referencedTypeNames.add(name); + return namedType(name); + } + if (type instanceof ListType) { + SUListType list = universe.newListType(); + builder().setWrappedType(list, typeVertex(((ListType) type).getType())); + return list; + } + if (type instanceof NonNullType) { + SUNonNullType nonNull = universe.newNonNullType(); + builder().setWrappedType(nonNull, typeVertex(((NonNullType) type).getType())); + return nonNull; + } + return assertShouldNeverHappen("Unsupported type AST %s", type.getClass().getName()); + } + + private String typeName(Type type) { + Type current = type; + while (!(current instanceof TypeName)) { + if (current instanceof ListType) { + current = ((ListType) current).getType(); + } else if (current instanceof NonNullType) { + current = ((NonNullType) current).getType(); + } else { + return assertShouldNeverHappen( + "Unsupported type AST %s", + current.getClass().getName()); + } + } + return ((TypeName) current).getName(); + } + + private SUNamedType namedType(String name) { + return assertNotNull(namedTypes.get(name), "Unknown type '%s'", name); + } + + private InputValueWithState inputValue(@Nullable Value value) { + return value == null + ? InputValueWithState.NOT_SET + : InputValueWithState.newLiteralValue(value); + } + + private @Nullable String schemaDescription( + ImmutableTypeDefinitionRegistry registry) { + SchemaDefinition definition = registry.schemaDefinition().orElse(null); + return definition == null + ? null + : description(definition, definition.getDescription()); + } + + private @Nullable String description( + Node node, + @Nullable Description description) { + if (description != null) { + return description.getContent(); + } + List lines = new ArrayList<>(); + for (Comment comment : node.getComments()) { + String line = comment.getContent(); + if (line.trim().isEmpty()) { + lines.clear(); + } else { + lines.add(line); + } + } + return lines.isEmpty() ? null : String.join("\n", lines); + } + + private SUSchemaBuilder builder() { + return assertNotNull(schemaBuilder, "Schema generation has not started"); + } +} diff --git a/src/main/java/graphql/schema/universe/SUSchemaPrinter.java b/src/main/java/graphql/schema/universe/SUSchemaPrinter.java new file mode 100644 index 0000000000..befe272886 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUSchemaPrinter.java @@ -0,0 +1,33 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.schema.idl.SchemaPrinter; +import org.jspecify.annotations.NullMarked; + +import static graphql.Assert.assertNotNull; + +/** + * Prints a {@link SUSchema} directly as canonical semantic SDL. + * + *

Literal values are preserved directly. Non-literal values for GraphQL specified scalars are + * converted without a runtime schema; non-literal custom scalar values cannot be printed because + * a schema universe does not retain scalar coercing implementations.

+ */ +@ExperimentalApi +@NullMarked +public final class SUSchemaPrinter { + + private final SchemaPrinter schemaPrinter; + + public SUSchemaPrinter() { + this(SchemaPrinter.Options.defaultOptions()); + } + + public SUSchemaPrinter(SchemaPrinter.Options options) { + schemaPrinter = new SchemaPrinter(assertNotNull(options)); + } + + public String print(SUSchema schema) { + return schemaPrinter.print(assertNotNull(schema)); + } +} diff --git a/src/main/java/graphql/schema/universe/SUSchemaRoot.java b/src/main/java/graphql/schema/universe/SUSchemaRoot.java new file mode 100644 index 0000000000..744aeb83e9 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUSchemaRoot.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUSchemaRoot extends SUVertex implements SUAppliedDirectiveContainer { + + @Internal + public SUSchemaRoot(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.SCHEMA, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUType.java b/src/main/java/graphql/schema/universe/SUType.java new file mode 100644 index 0000000000..11def16290 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUType.java @@ -0,0 +1,24 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * A GraphQL type vertex, including named types and list/non-null wrappers. + */ +@ExperimentalApi +@NullMarked +public abstract class SUType extends SUVertex { + + @Internal + protected SUType( + int id, + int nameId, + SUVertexKind kind, + @Nullable String name, + @Nullable String description) { + super(id, nameId, kind, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUUnionType.java b/src/main/java/graphql/schema/universe/SUUnionType.java new file mode 100644 index 0000000000..bb1bfdada8 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUUnionType.java @@ -0,0 +1,16 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@ExperimentalApi +@NullMarked +public final class SUUnionType extends SUNamedType { + + @Internal + public SUUnionType(int id, int nameId, String name, @Nullable String description) { + super(id, nameId, SUVertexKind.UNION, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUVertex.java b/src/main/java/graphql/schema/universe/SUVertex.java new file mode 100644 index 0000000000..66258158a7 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUVertex.java @@ -0,0 +1,65 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import static graphql.Assert.assertNotNull; + +/** + * A schema element in a {@link SchemaUniverse}. + * + *

Vertices contain only properties intrinsic to the element. Relationships to other schema + * elements are stored by each {@link SUSchema}.

+ */ +@ExperimentalApi +@NullMarked +public abstract class SUVertex { + + private final int id; + private final int nameId; + private final SUVertexKind kind; + private final @Nullable String name; + private final @Nullable String description; + + @Internal + public SUVertex( + int id, + int nameId, + SUVertexKind kind, + @Nullable String name, + @Nullable String description) { + this.id = id; + this.nameId = nameId; + this.kind = assertNotNull(kind); + this.name = name; + this.description = description; + } + + public final int getId() { + return id; + } + + public final SUVertexKind getKind() { + return kind; + } + + public final @Nullable String getName() { + return name; + } + + public final @Nullable String getDescription() { + return description; + } + + @Internal + public final int getNameId() { + return nameId; + } + + @Override + public String toString() { + return kind + "{" + (name == null ? id : name) + "}"; + } +} diff --git a/src/main/java/graphql/schema/universe/SUVertexKind.java b/src/main/java/graphql/schema/universe/SUVertexKind.java new file mode 100644 index 0000000000..ce566e1b6c --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUVertexKind.java @@ -0,0 +1,27 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * The kinds of vertices stored in a {@link SchemaUniverse}. + */ +@ExperimentalApi +@NullMarked +public enum SUVertexKind { + SCHEMA, + OBJECT, + FIELD, + INTERFACE, + UNION, + ENUM, + ENUM_VALUE, + SCALAR, + INPUT_OBJECT, + INPUT_FIELD, + ARGUMENT, + DIRECTIVE, + APPLIED_DIRECTIVE, + LIST, + NON_NULL +} diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java new file mode 100644 index 0000000000..c945fb34e3 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -0,0 +1,439 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.Argument; +import graphql.language.Directive; +import graphql.language.DirectiveDefinition; +import graphql.language.InputValueDefinition; +import graphql.schema.GraphQLSchema; +import graphql.schema.InputValueWithState; +import graphql.schema.idl.SchemaParser; +import graphql.schema.idl.TypeDefinitionRegistry; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertTrue; +import static graphql.Assert.assertValidName; +import static graphql.util.Interning.intern; + +/** + * An append-only store of schema vertices shared by many immutable schema snapshots. + * + *

A universe is safe for concurrent readers. Vertex creation is serialized, while reading a + * published vertex or schema does not require locking.

+ */ +@ExperimentalApi +@NullMarked +public final class SchemaUniverse { + + private static final int VERTEX_CHUNK_SHIFT = 10; + private static final int VERTEX_CHUNK_SIZE = 1 << VERTEX_CHUNK_SHIFT; + private static final int VERTEX_CHUNK_MASK = VERTEX_CHUNK_SIZE - 1; + private static final int MAX_NAME_ID = 0x00ff_ffff; + + private final Map nameIds = new ConcurrentHashMap<>(); + private final Map schemasByName = new ConcurrentHashMap<>(); + private final ConcurrentLinkedQueue schemas = new ConcurrentLinkedQueue<>(); + private volatile SUVertex[][] vertexChunks = new SUVertex[1][]; + private volatile int vertexCount; + private int nextNameId = 1; + + public SUSchemaBuilder newSchema(String name) { + return new SUSchemaBuilder(this, newSchemaRoot(name, null), null); + } + + public SUSchemaBuilder newSchema(String name, @Nullable String description) { + return new SUSchemaBuilder(this, newSchemaRoot(name, description), null); + } + + public SUSchema importSchema(String name, GraphQLSchema schema) { + return new SUImporter(this).importSchema(name, schema); + } + + public SUSchema importSchema(String name, TypeDefinitionRegistry typeDefinitionRegistry) { + return new SUSchemaGenerator(this).generate(name, typeDefinitionRegistry); + } + + public SUSchema parseSchema(String name, String sdl) { + return importSchema(name, new SchemaParser().parse(sdl)); + } + + public @Nullable SUSchema getSchema(String name) { + return schemasByName.get(assertNotNull(name)); + } + + public List getSchemas() { + return List.copyOf(schemas); + } + + public Map getSchemasByName() { + Map result = new LinkedHashMap<>(); + for (SUSchema schema : schemas) { + result.put(schema.getName(), schema); + } + return Collections.unmodifiableMap(result); + } + + public synchronized SUObjectType newObjectType(String name) { + return newObjectType(name, null); + } + + public synchronized SUObjectType newObjectType(String name, @Nullable String description) { + String validName = named(name); + SUObjectType vertex = new SUObjectType(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUField newField(String name) { + return newField(name, null); + } + + public synchronized SUField newField(String name, @Nullable String description) { + String validName = named(name); + SUField vertex = new SUField(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUInterfaceType newInterfaceType(String name) { + return newInterfaceType(name, null); + } + + public synchronized SUInterfaceType newInterfaceType(String name, @Nullable String description) { + String validName = named(name); + SUInterfaceType vertex = new SUInterfaceType(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUUnionType newUnionType(String name) { + return newUnionType(name, null); + } + + public synchronized SUUnionType newUnionType(String name, @Nullable String description) { + String validName = named(name); + SUUnionType vertex = new SUUnionType(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUEnumType newEnumType(String name) { + return newEnumType(name, null); + } + + public synchronized SUEnumType newEnumType(String name, @Nullable String description) { + String validName = named(name); + SUEnumType vertex = new SUEnumType(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUEnumValue newEnumValue(String name) { + return newEnumValue(name, null); + } + + public synchronized SUEnumValue newEnumValue(String name, @Nullable String description) { + String validName = named(name); + SUEnumValue vertex = new SUEnumValue(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUScalarType newScalarType(String name) { + return newScalarType(name, null); + } + + public synchronized SUScalarType newScalarType(String name, @Nullable String description) { + String validName = named(name); + SUScalarType vertex = new SUScalarType(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUInputObjectType newInputObjectType(String name) { + return newInputObjectType(name, null); + } + + public synchronized SUInputObjectType newInputObjectType(String name, @Nullable String description) { + String validName = named(name); + SUInputObjectType vertex = new SUInputObjectType(vertexCount, nameId(validName), validName, description); + return append(vertex); + } + + public synchronized SUInputField newInputField(String name) { + return newInputField(name, null); + } + + public synchronized SUInputField newInputField(String name, @Nullable String description) { + return newInputField(name, description, InputValueWithState.NOT_SET, null); + } + + public synchronized SUInputField newInputField( + String name, + @Nullable String description, + InputValueWithState defaultValue) { + return newInputField(name, description, defaultValue, null); + } + + public synchronized SUInputField newInputField( + String name, + @Nullable String description, + InputValueWithState defaultValue, + @Nullable InputValueDefinition definition) { + String validName = named(name); + SUInputField vertex = new SUInputField( + vertexCount, + nameId(validName), + validName, + description, + defaultValue, + definition); + return append(vertex); + } + + public synchronized SUArgument newArgument(String name) { + return newArgument(name, null); + } + + public synchronized SUArgument newArgument(String name, @Nullable String description) { + return newArgument(name, description, InputValueWithState.NOT_SET, null); + } + + public synchronized SUArgument newArgument( + String name, + @Nullable String description, + InputValueWithState defaultValue) { + return newArgument(name, description, defaultValue, null); + } + + public synchronized SUArgument newArgument( + String name, + @Nullable String description, + InputValueWithState defaultValue, + @Nullable InputValueDefinition definition) { + String validName = named(name); + SUArgument vertex = new SUArgument( + vertexCount, + nameId(validName), + validName, + description, + defaultValue, + definition); + return append(vertex); + } + + public synchronized SUDirective newDirective(String name) { + return newDirective(name, null); + } + + public synchronized SUDirective newDirective(String name, @Nullable String description) { + return newDirective(name, description, false, Collections.emptySet(), null); + } + + public synchronized SUDirective newDirective( + String name, + @Nullable String description, + boolean repeatable, + Set validLocations) { + return newDirective(name, description, repeatable, validLocations, null); + } + + public synchronized SUDirective newDirective( + String name, + @Nullable String description, + boolean repeatable, + Set validLocations, + @Nullable DirectiveDefinition definition) { + String validName = named(name); + SUDirective vertex = new SUDirective( + vertexCount, + nameId(validName), + validName, + description, + repeatable, + validLocations, + definition); + return append(vertex); + } + + public synchronized SUAppliedDirective newAppliedDirective(String name) { + return newAppliedDirective(name, Collections.emptyList(), null); + } + + public synchronized SUAppliedDirective newAppliedDirective( + String name, + @Nullable Directive definition) { + return newAppliedDirective(name, Collections.emptyList(), definition); + } + + public synchronized SUAppliedDirective newAppliedDirective( + String name, + List arguments) { + return newAppliedDirective(name, arguments, null); + } + + public synchronized SUAppliedDirective newAppliedDirective( + String name, + List arguments, + @Nullable Directive definition) { + String validName = named(name); + Set argumentNames = new HashSet<>(); + for (SUAppliedDirectiveArgument argument : assertNotNull(arguments)) { + assertNotNull(argument); + assertTrue( + argument.getUniverse() == this, + "Applied directive argument '%s' belongs to another schema universe", + argument.getName()); + assertTrue( + argumentNames.add(argument.getName()), + "Applied directive contains duplicate argument '%s'", + argument.getName()); + } + SUAppliedDirective vertex = + new SUAppliedDirective( + vertexCount, + nameId(validName), + validName, + definition, + arguments); + return append(vertex); + } + + public synchronized SUAppliedDirectiveArgument newAppliedDirectiveArgument( + String name, + SUType type) { + return newAppliedDirectiveArgument(name, type, InputValueWithState.NOT_SET, null); + } + + public synchronized SUAppliedDirectiveArgument newAppliedDirectiveArgument( + String name, + SUType type, + InputValueWithState value) { + return newAppliedDirectiveArgument(name, type, value, null); + } + + public synchronized SUAppliedDirectiveArgument newAppliedDirectiveArgument( + String name, + SUType type, + InputValueWithState value, + @Nullable Argument definition) { + assertTrue(owns(assertNotNull(type)), "Type vertex %s belongs to another schema universe", type); + assertTrue( + isInputType(type.getKind()), + "Applied directive argument type must be an input type, found %s", + type.getKind()); + String validName = named(name); + return new SUAppliedDirectiveArgument( + this, + validName, + type.getId(), + value, + definition); + } + + public synchronized SUListType newListType() { + return append(new SUListType(vertexCount)); + } + + public synchronized SUNonNullType newNonNullType() { + return append(new SUNonNullType(vertexCount)); + } + + public int getVertexCount() { + return vertexCount; + } + + public SUVertex getVertex(int id) { + int currentSize = vertexCount; + assertTrue(id >= 0 && id < currentSize, "Unknown schema universe vertex id %s", id); + SUVertex[][] chunks = vertexChunks; + return assertNotNull(chunks[id >>> VERTEX_CHUNK_SHIFT][id & VERTEX_CHUNK_MASK]); + } + + @Internal + public int getNameId(String name) { + Integer nameId = nameIds.get(name); + return nameId == null ? -1 : nameId; + } + + @Internal + public boolean owns(SUVertex vertex) { + int id = vertex.getId(); + return id >= 0 && id < vertexCount && getVertex(id) == vertex; + } + + @Internal + public SUSchemaBuilder transformBuilder(SUSchema schema, String name) { + assertTrue(schema.getUniverse() == this, "Schema belongs to another universe"); + return new SUSchemaBuilder( + this, + newSchemaRoot(name, schema.getRoot().getDescription()), + schema); + } + + @Internal + public synchronized void registerSchema(SUSchema schema) { + assertTrue(schema.getUniverse() == this, "Schema belongs to another universe"); + String name = schema.getName(); + assertTrue(!schemasByName.containsKey(name), + "Schema universe already contains a schema named '%s'", name); + schemas.add(schema); + schemasByName.put(name, schema); + } + + private synchronized SUSchemaRoot newSchemaRoot(String name, @Nullable String description) { + String validName = named(name); + SUSchemaRoot root = new SUSchemaRoot(vertexCount, nameId(validName), validName, description); + return append(root); + } + + private String named(String name) { + return intern(assertValidName(name)); + } + + private int nameId(String name) { + Integer existing = nameIds.get(name); + if (existing != null) { + return existing; + } + assertTrue(nextNameId <= MAX_NAME_ID, "Schema universe has too many distinct names"); + int newNameId = nextNameId++; + nameIds.put(name, newNameId); + return newNameId; + } + + private boolean isInputType(SUVertexKind kind) { + return kind == SUVertexKind.SCALAR + || kind == SUVertexKind.ENUM + || kind == SUVertexKind.INPUT_OBJECT + || kind == SUVertexKind.LIST + || kind == SUVertexKind.NON_NULL; + } + + private T append(T vertex) { + int id = vertex.getId(); + int chunkIndex = id >>> VERTEX_CHUNK_SHIFT; + ensureChunk(chunkIndex); + vertexChunks[chunkIndex][id & VERTEX_CHUNK_MASK] = vertex; + vertexCount = id + 1; + return vertex; + } + + private void ensureChunk(int chunkIndex) { + SUVertex[][] chunks = vertexChunks; + if (chunkIndex >= chunks.length) { + chunks = Arrays.copyOf(chunks, Math.max(chunkIndex + 1, chunks.length * 2)); + vertexChunks = chunks; + } + if (chunks[chunkIndex] == null) { + chunks[chunkIndex] = new SUVertex[VERTEX_CHUNK_SIZE]; + } + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy new file mode 100644 index 0000000000..199ea75556 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy @@ -0,0 +1,201 @@ +package graphql.schema.universe + +import graphql.language.ArrayValue +import graphql.language.BooleanValue +import graphql.language.ObjectValue +import graphql.language.StringValue +import graphql.schema.idl.SchemaParser +import spock.lang.Specification + +class SUSchemaGeneratorTest extends Specification { + + def "compiles roots cyclic types and flattened extensions directly from SDL"() { + given: + def sdl = ''' + directive @tag(value: String = "default") repeatable on SCHEMA | OBJECT | FIELD_DEFINITION + + """The schema description.""" + schema @tag(value: "schema") { + query: Root + mutation: Changes + } + + interface Node { + id: ID! + } + + type Root implements Node @tag { + id: ID! + next: Root + } + + extend type Root { + added: Choice + } + + type Changes { + change(input: Filter): Root + } + + input Filter { + nested: Filter + } + + enum Choice { + FIRST + } + + extend enum Choice { + SECOND + } + ''' + def universe = new SchemaUniverse() + + when: + def schema = universe.parseSchema("native", sdl) + + then: + schema.name == "native" + schema.root.description == "The schema description." + schema.queryType.name == "Root" + schema.mutationType.name == "Changes" + schema.subscriptionType == null + schema.additionalTypes*.name.containsAll(["Node", "Filter", "Choice", "String", "ID", "Boolean"]) + + and: + def root = schema.getObjectType("Root") + schema.getFields(root)*.name == ["id", "next", "added"] + schema.getInterfaces(root)*.name == ["Node"] + schema.getType(schema.getField(root, "next")).is(root) + schema.getEnumValues(schema.getEnumType("Choice"))*.name == ["FIRST", "SECOND"] + + and: + def filter = schema.getInputObjectType("Filter") + schema.getType(schema.getInputField(filter, "nested")).is(filter) + + and: + def schemaTag = schema.schemaAppliedDirectives[0] + schemaTag.name == "tag" + schemaTag.definition != null + schemaTag.arguments*.name == ["value"] + schemaTag.arguments[0].argumentValue.value instanceof StringValue + schemaTag.arguments[0].argumentValue.value.value == "schema" + + and: + def rootTag = schema.getAppliedDirectives(root)[0] + rootTag.arguments*.name == ["value"] + rootTag.arguments[0].argumentValue.value instanceof StringValue + rootTag.arguments[0].argumentValue.value.value == "default" + } + + def "uses conventional operation roots when schema definition is absent"() { + given: + def sdl = ''' + type Query { + query: String + } + + type Mutation { + mutate: Boolean + } + + type Subscription { + changed: Int + } + ''' + + when: + def schema = new SchemaUniverse().parseSchema("defaults", sdl) + + then: + schema.queryType.name == "Query" + schema.mutationType.name == "Mutation" + schema.subscriptionType.name == "Subscription" + !schema.additionalTypes*.name.contains("Query") + !schema.additionalTypes*.name.contains("Mutation") + !schema.additionalTypes*.name.contains("Subscription") + } + + def "preserves input defaults and all applied directive argument states"() { + given: + def sdl = ''' + directive @settings( + explicit: String + defaulted: Int = 42 + omitted: Boolean + values: [String!] = ["a", "b"] + ) on FIELD_DEFINITION + + input Filter { + enabled: Boolean = true + labels: [String!] = ["x"] + nested: Nested = {active: false} + } + + input Nested { + active: Boolean! + } + + type Query { + value( + filter: Filter = {enabled: false} + ): String @settings(explicit: "yes") + } + ''' + + when: + def schema = new SchemaUniverse().importSchema( + "defaults", + new SchemaParser().parse(sdl)) + + then: + def query = schema.queryType + def value = schema.getField(query, "value") + def filterArgument = schema.getArgument(value, "filter") + filterArgument.argumentDefaultValue.isLiteral() + filterArgument.argumentDefaultValue.value instanceof ObjectValue + + and: + def filter = schema.getInputObjectType("Filter") + schema.getInputField(filter, "enabled").inputFieldDefaultValue.value instanceof BooleanValue + schema.getInputField(filter, "labels").inputFieldDefaultValue.value instanceof ArrayValue + schema.getInputField(filter, "nested").inputFieldDefaultValue.value instanceof ObjectValue + + and: + def settings = schema.getAppliedDirectives(value)[0] + settings.arguments*.name == ["explicit", "defaulted", "omitted", "values"] + settings.getArgument("explicit").argumentValue.isLiteral() + settings.getArgument("defaulted").argumentValue.isLiteral() + settings.getArgument("omitted").argumentValue.isNotSet() + settings.getArgument("values").argumentValue.isLiteral() + + and: + def definition = schema.getDirectiveDefinition("settings") + settings.arguments.each { appliedArgument -> + def definitionArgument = schema.getArgument(definition, appliedArgument.name) + schema.getType(appliedArgument).is(schema.getType(definitionArgument)) + } + } + + def "creates fresh wrapper vertices for separate typed occurrences"() { + given: + def sdl = ''' + type Query { + first(values: [String!]!): [String!]! + second(values: [String!]!): [String!]! + } + ''' + + when: + def schema = new SchemaUniverse().parseSchema("wrappers", sdl) + + then: + def first = schema.getField(schema.queryType, "first") + def second = schema.getField(schema.queryType, "second") + def firstArgument = schema.getArgument(first, "values") + def secondArgument = schema.getArgument(second, "values") + !schema.getType(first).is(schema.getType(second)) + !schema.getType(firstArgument).is(schema.getType(secondArgument)) + !schema.getType(first).is(schema.getType(firstArgument)) + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy new file mode 100644 index 0000000000..546b2b1a85 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy @@ -0,0 +1,161 @@ +package graphql.schema.universe + +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.SchemaPrinter +import graphql.schema.idl.TypeDefinitionRegistry +import graphql.schema.idl.UnExecutableSchemaGenerator +import spock.lang.Specification +import spock.lang.Requires + +class SUSchemaPrinterTest extends Specification { + + def "native SDL import and GraphQLSchema import print the same semantic schema"() { + given: + def registry = new SchemaParser().parse(comprehensiveSdl()) + def graphQLSchema = + UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + def direct = new SchemaUniverse().importSchema("direct", registry) + def viaGraphQL = + new SchemaUniverse().importSchema("graphql", graphQLSchema) + def options = SchemaPrinter.Options.defaultOptions() + .includeSchemaDefinition(true) + def printer = new SUSchemaPrinter(options) + + when: + def directSdl = printer.print(direct) + def viaGraphQLSdl = printer.print(viaGraphQL) + def graphQLSdl = new SchemaPrinter(options).print(graphQLSchema) + + then: + directSdl == viaGraphQLSdl + directSdl == graphQLSdl + + and: + def expectedTypeNames = graphQLSchema.allTypesAsList*.name + .findAll { !it.startsWith("__") } as Set + (direct.types*.name as Set) == expectedTypeNames + } + + def "native semantic printing is stable after parsing the printed SDL"() { + given: + def options = SchemaPrinter.Options.defaultOptions() + .includeSchemaDefinition(true) + def printer = new SUSchemaPrinter(options) + def first = new SchemaUniverse().parseSchema("first", comprehensiveSdl()) + + when: + def firstSdl = printer.print(first) + def second = new SchemaUniverse().parseSchema("second", firstSdl) + def secondSdl = printer.print(second) + + then: + firstSdl == secondSdl + firstSdl.contains("type Root implements Node @tag") + firstSdl.contains("SECOND") + !firstSdl.contains("extend type") + !firstSdl.contains("extend enum") + } + + @Requires({ Boolean.getBoolean("graphql.largeSchemaSdlTest") }) + def "large schema native import and printing match GraphQLSchema"() { + given: + def sdl = resource("large-schema-5.graphqls.part1") + + resource("large-schema-5.graphqls.part2") + def options = SchemaPrinter.Options.defaultOptions() + .includeSchemaDefinition(true) + + when: + def canonicalSdl = printNative(sdl, options) + System.gc() + + then: + printGraphQL(sdl, options) == canonicalSdl + + when: + System.gc() + + then: + printNative(canonicalSdl, options) == canonicalSdl + + when: + System.gc() + + then: + printGraphQL(canonicalSdl, options) == canonicalSdl + } + + private static String comprehensiveSdl() { + return ''' + directive @tag( + value: String = "default" + count: Int + ) repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | + ARGUMENT_DEFINITION | INTERFACE | ENUM | ENUM_VALUE | + INPUT_OBJECT | INPUT_FIELD_DEFINITION + + """Schema description.""" + schema @tag(value: "schema") { + query: Root + mutation: Changes + } + + extend schema @tag(value: "extension") + + scalar Date @specifiedBy(url: "https://example.com/date") + + interface Node @tag { + id: ID! + } + + type Root implements Node @tag { + id: ID! + first( + filter: Filter = {enabled: true} + ): [String!]! @tag(count: 2) + } + + extend type Root @tag(value: "extension") { + second: Choice @deprecated(reason: "Use first") + } + + type Changes { + update(input: Filter): Root + } + + input Filter @tag { + enabled: Boolean = false + choice: Choice @deprecated + } + + enum Choice @tag { + FIRST + } + + extend enum Choice { + SECOND @tag + } + ''' + } + + private static String resource(String name) { + def resource = SUSchemaPrinterTest.getResource("/" + name) + assert resource != null + return resource.getText("UTF-8") + } + + private static String printNative( + String sdl, + SchemaPrinter.Options options) { + TypeDefinitionRegistry registry = new SchemaParser().parse(sdl) + def schema = new SchemaUniverse().importSchema("direct_large", registry) + return new SUSchemaPrinter(options).print(schema) + } + + private static String printGraphQL( + String sdl, + SchemaPrinter.Options options) { + TypeDefinitionRegistry registry = new SchemaParser().parse(sdl) + def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + return new SchemaPrinter(options).print(schema) + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy new file mode 100644 index 0000000000..708626364f --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy @@ -0,0 +1,372 @@ +package graphql.schema.universe + +import graphql.schema.GraphQLSchema +import graphql.schema.GraphqlTypeComparatorRegistry +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.SchemaPrinter +import graphql.schema.idl.TypeDefinitionRegistry +import graphql.schema.idl.UnExecutableSchemaGenerator +import spock.lang.Specification +import spock.lang.Unroll + +class SUSchemaRoundTripTest extends Specification { + + @Unroll + def "GraphQLSchema and SUSchema remain identical through bidirectional SDL round trips: #scenario"() { + when: + def nativeSource = nativeSnapshot(sourceSdl) + def graphQLSource = graphQLSnapshot(sourceSdl) + + and: + def nativeFromGraphQL = nativeSnapshot(graphQLSource.sdl) + def graphQLFromNative = graphQLSnapshot(nativeSource.sdl) + + and: + def nativeSecondGeneration = nativeSnapshot(graphQLFromNative.sdl) + def graphQLSecondGeneration = graphQLSnapshot(nativeFromGraphQL.sdl) + + then: + nativeSource == graphQLSource + nativeFromGraphQL == graphQLSource + graphQLFromNative == nativeSource + nativeSecondGeneration == nativeSource + graphQLSecondGeneration == graphQLSource + + where: + scenario | sourceSdl + "operation roots and cyclic types" | operationRootsAndCyclesSdl() + "directives on every schema kind" | directivesEverywhereSdl() + "all input default literal shapes" | inputDefaultsSdl() + "descriptions and built-in metadata"| descriptionsAndMetadataSdl() + "all type and schema extensions" | extensionsSdl() + "default roots and unused types" | defaultRootsAndUnusedTypesSdl() + } + + private static Map nativeSnapshot(String sdl) { + TypeDefinitionRegistry registry = new SchemaParser().parse(sdl) + SUSchema schema = + new SchemaUniverse().importSchema("native_round_trip", registry) + return [ + sdl : new SUSchemaPrinter(printerOptions()).print(schema), + query : schema.queryType.name, + mutation : schema.mutationType?.name, + subscription: schema.subscriptionType?.name, + types : schema.types*.name.toSorted(), + directives: schema.directiveDefinitions*.name.toSorted() + ] + } + + private static Map graphQLSnapshot(String sdl) { + TypeDefinitionRegistry registry = new SchemaParser().parse(sdl) + GraphQLSchema schema = + UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + return [ + sdl : new SchemaPrinter(printerOptions()).print(schema), + query : schema.queryType.name, + mutation : schema.mutationType?.name, + subscription: schema.subscriptionType?.name, + types : schema.allTypesAsList*.name + .findAll { !it.startsWith("__") } + .toSorted(), + directives: schema.directives*.name.toSorted() + ] + } + + private static SchemaPrinter.Options printerOptions() { + return SchemaPrinter.Options.defaultOptions() + .includeSchemaDefinition(true) + .includeScalarTypes(true) + .setComparators(GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) + } + + private static String operationRootsAndCyclesSdl() { + return ''' + schema { + query: RootQuery + mutation: RootMutation + subscription: RootSubscription + } + + interface Node { + id: ID! + owner: Node + } + + interface Resource implements Node { + id: ID! + owner: Node + url: String! + } + + type User implements Node & Resource { + id: ID! + owner: Node + url: String! + friends(limit: Int = 10): [User!]! + } + + type Photo implements Node & Resource { + id: ID! + owner: Node + url: String! + related: [SearchResult!]! + } + + union SearchResult = User | Photo + + type RootQuery { + node(id: ID!): Node + search: [SearchResult!]! + } + + type RootMutation { + update(id: ID!, input: UpdateInput!): User + } + + type RootSubscription { + changed: Node + } + + input UpdateInput { + owner: ID + nested: UpdateInput + } + ''' + } + + private static String directivesEverywhereSdl() { + return ''' + directive @meta( + text: String = "fallback" + count: Int + flags: [Boolean!] = [true, false] + config: MetaInput = {enabled: true, mode: FIRST} + unset: String + ) repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | + ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | + INPUT_OBJECT | INPUT_FIELD_DEFINITION + + schema @meta(text: "schema") @meta(count: 2) { + query: Query + } + + scalar Custom @meta + + interface Named @meta { + name(prefix: String @meta): String @meta + } + + type Item implements Named @meta { + name(prefix: String @meta(count: 3)): String @meta + custom: Custom + } + + union Result @meta = Item + + enum Mode @meta { + FIRST @meta + SECOND @meta(text: "second") + } + + input MetaInput @meta { + enabled: Boolean! @meta + mode: Mode = FIRST + } + + type Query @meta { + result(input: MetaInput @meta): Result @meta(flags: [false]) + } + ''' + } + + private static String inputDefaultsSdl() { + return ''' + enum Color { + RED + GREEN + BLUE + } + + input Nested { + required: Boolean! + note: String + } + + input Defaults { + nullValue: String = null + intValue: Int = -7 + floatValue: Float = 1.25 + stringValue: String = "quoted \\"value\\"" + booleanValue: Boolean = true + enumValue: Color = RED + listValue: [Int!] = [1, 2, 3] + objectValue: Nested = {required: true, note: "nested"} + } + + type Query { + test( + defaults: Defaults = { + nullValue: null + intValue: 9 + floatValue: 2.5 + stringValue: "argument" + booleanValue: false + enumValue: BLUE + listValue: [4, 5] + objectValue: {required: false} + } + nested: [Nested!] = [ + {required: true} + {required: false, note: "two"} + ] + color: Color = GREEN + ): String + } + ''' + } + + private static String descriptionsAndMetadataSdl() { + return ''' + """A directive description.""" + directive @documented( + """An argument description.""" + value: String = "documented" + ) on OBJECT | FIELD_DEFINITION + + """A URL scalar.""" + scalar Url + @specifiedBy(url: "https://example.com/url") + + """A node interface.""" + interface Node { + """The stable identifier.""" + id: ID! + } + + """A result union.""" + union Result = Record + + """A state enum.""" + enum State { + """Still active.""" + ACTIVE + """No longer active.""" + OLD @deprecated(reason: "Use ACTIVE") + } + + """Filter input.""" + input Filter { + """An old input.""" + old: String @deprecated(reason: "Use current") + current: String + } + + """A record.""" + type Record implements Node @documented { + id: ID! + """An old field.""" + old( + """An old argument.""" + value: String @deprecated(reason: "Use replacement") + ): String @deprecated(reason: "Use current") + url: Url + state: State + } + + # Legacy query description. + type Query { + record(filter: Filter): Result @documented + } + ''' + } + + private static String extensionsSdl() { + return ''' + directive @mark(value: String = "marked") repeatable on + SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | INTERFACE | + UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + + schema { + query: Query + } + + extend schema @mark(value: "schema extension") { + mutation: Mutation + } + + scalar Token + extend scalar Token @mark + + interface Node { + id: ID! + } + + extend interface Node @mark { + label: String + } + + type Item implements Node { + id: ID! + label: String + } + + type Other implements Node { + id: ID! + label: String + } + + union Result = Item + extend union Result @mark = Other + + enum Status { + FIRST + } + + extend enum Status @mark { + SECOND @mark + } + + input Filter { + first: String + } + + extend input Filter @mark { + second: Status @mark + } + + type Query { + base: Item + } + + extend type Query @mark { + added(filter: Filter): Result @mark + } + + type Mutation { + update(token: Token): Item + } + ''' + } + + private static String defaultRootsAndUnusedTypesSdl() { + return ''' + scalar UnusedScalar + + type UnusedObject { + value: UnusedScalar + } + + type Query { + query: String + } + + type Mutation { + mutate: Boolean + } + + type Subscription { + changed: Int + } + ''' + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUTest.groovy b/src/test/groovy/graphql/schema/universe/SUTest.groovy new file mode 100644 index 0000000000..99253e1d57 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUTest.groovy @@ -0,0 +1,887 @@ +package graphql.schema.universe + +import graphql.AssertException +import graphql.Scalars +import graphql.TestUtil +import graphql.language.StringValue +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLEnumType +import graphql.schema.GraphQLEnumValueDefinition +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLInputObjectField +import graphql.schema.GraphQLInputObjectType +import graphql.schema.GraphQLList +import graphql.schema.GraphQLNonNull +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import graphql.schema.GraphqlTypeComparatorRegistry +import graphql.schema.InputValueWithState +import graphql.schema.idl.SchemaPrinter +import spock.lang.Specification + +import java.util.EnumSet + +import static graphql.introspection.Introspection.DirectiveLocation.FIELD_DEFINITION +import static graphql.introspection.Introspection.DirectiveLocation.OBJECT + +class SUTest extends Specification { + + def "three derived schemas reuse vertices and unchanged adjacency"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def string = universe.newScalarType("String") + def foo = universe.newField("foo") + def foo2 = universe.newField("foo2") + + def schema1 = universe.newSchema("S1") + .queryType(query) + .addField(query, foo) + .setFieldType(foo, string) + .build() + + when: + def schema2 = schema1.transform("S2", builder -> builder + .removeField(query, foo) + .addField(query, foo2) + .setFieldType(foo2, string)) + def schema3 = schema1.transform("S3", builder -> builder + .addField(query, foo2) + .setFieldType(foo2, string)) + + then: + schema1.queryType.is(query) + schema2.queryType.is(query) + schema3.queryType.is(query) + + schema1.getFields(query)*.name == ["foo"] + schema2.getFields(query)*.name == ["foo2"] + schema3.getFields(query)*.name == ["foo", "foo2"] + + schema1.getField(query, "foo").is(foo) + schema1.getField(query, "foo2") == null + schema2.getField(query, "foo") == null + schema2.getField(query, "foo2").is(foo2) + + schema1.getType(foo).is(string) + schema2.getType(foo2).is(string) + schema3.getType(foo).is(string) + schema3.getType(foo2).is(string) + + !schema1.root.is(schema2.root) + !schema1.root.is(schema3.root) + schema1.sharesOutgoingEdgesWith(schema2, foo) + schema1.sharesOutgoingEdgesWith(schema3, foo) + !schema1.sharesOutgoingEdgesWith(schema2, query) + + universe.vertexCount == 7 + } + + def "transforming one branch does not change its parent or sibling"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def string = universe.newScalarType("String") + def foo = universe.newField("foo") + def bar = universe.newField("bar") + def base = universe.newSchema("base") + .queryType(query) + .addField(query, foo) + .setFieldType(foo, string) + .build() + + when: + def withoutFoo = base.transform("withoutFoo", builder -> + builder.removeField(query, foo)) + def withBar = base.transform("withBar", builder -> builder + .addField(query, bar) + .setFieldType(bar, string)) + + then: + base.getFields(query)*.name == ["foo"] + withoutFoo.getFields(query).isEmpty() + withBar.getFields(query)*.name == ["foo", "bar"] + withoutFoo.getField(query, "bar") == null + } + + def "universe registers schemas by name in creation order"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def first = universe.newSchema("first") + .queryType(query) + .build() + def second = first.transform("second", builder -> {}) + + expect: + universe.getSchema("first").is(first) + universe.getSchema("second").is(second) + universe.getSchema("missing") == null + universe.schemas == [first, second] + universe.schemasByName.keySet().toList() == ["first", "second"] + + when: + universe.newSchema("first") + .queryType(query) + .build() + + then: + thrown(AssertException) + universe.schemas == [first, second] + } + + def "exact duplicate edges are folded but duplicate child names are rejected"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def firstFoo = universe.newField("foo") + def secondFoo = universe.newField("foo") + + when: + def schema = universe.newSchema("duplicates") + .queryType(query) + .addField(query, firstFoo) + .addField(query, firstFoo) + .build() + + then: + schema.getFields(query) == [firstFoo] + + when: + universe.newSchema("invalid") + .queryType(query) + .addField(query, firstFoo) + .addField(query, secondFoo) + .build() + + then: + thrown(AssertException) + } + + def "single valued edges can be replaced explicitly"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def foo = universe.newField("foo") + def string = universe.newScalarType("String") + def integer = universe.newScalarType("Int") + def schema = universe.newSchema("base") + .queryType(query) + .addField(query, foo) + .setFieldType(foo, string) + .build() + + when: + def changed = schema.transform("changed", builder -> + builder.setFieldType(foo, integer)) + + then: + schema.getType(foo).is(string) + !schema.getType(foo).is(integer) + !changed.getType(foo).is(string) + changed.getType(foo).is(integer) + } + + def "vertices from another universe are rejected"() { + given: + def first = new SchemaUniverse() + def second = new SchemaUniverse() + def query = first.newObjectType("Query") + def foreignField = second.newField("foo") + + when: + first.newSchema("invalid") + .queryType(query) + .addField(query, foreignField) + + then: + thrown(AssertException) + } + + def "typed builder helpers cover every schema relationship"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def mutation = universe.newObjectType("Mutation") + def subscription = universe.newObjectType("Subscription") + def user = universe.newObjectType("User") + def node = universe.newInterfaceType("Node") + def resource = universe.newInterfaceType("Resource") + def searchResult = universe.newUnionType("SearchResult") + def status = universe.newEnumType("Status") + def active = universe.newEnumValue("ACTIVE") + def inactive = universe.newEnumValue("INACTIVE") + def filter = universe.newInputObjectType("Filter") + def term = universe.newInputField("term") + def string = universe.newScalarType("String") + def integer = universe.newScalarType("Int") + def search = universe.newField("search") + def id = universe.newField("id") + def limit = universe.newArgument("limit") + def tag = universe.newDirective("tag") + def label = universe.newArgument("label") + def list = universe.newListType() + def nonNull = universe.newNonNullType() + def firstTag = universe.newAppliedDirective("tag") + def secondTag = universe.newAppliedDirective("tag") + def schemaTag = universe.newAppliedDirective("schemaTag") + + def schema = universe.newSchema("complete") + .setQueryType(query) + .setMutationType(mutation) + .setSubscriptionType(subscription) + .addAdditionalType(user) + .addAdditionalType(node) + .addAdditionalType(resource) + .addAdditionalType(searchResult) + .addAdditionalType(status) + .addAdditionalType(filter) + .addAdditionalType(string) + .addAdditionalType(integer) + .addDirectiveDefinition(tag) + .addField(query, search) + .addField(node, id) + .setWrappedType(list, string) + .setWrappedType(nonNull, list) + .setFieldType(search, nonNull) + .setFieldType(id, string) + .addArgument(search, limit) + .setArgumentType(limit, integer) + .addArgument(tag, label) + .setArgumentType(label, string) + .addInterface(user, node) + .addInterface(node, resource) + .addUnionMember(searchResult, user) + .addEnumValue(status, active) + .addEnumValue(status, inactive) + .addInputField(filter, term) + .setInputFieldType(term, string) + .addAppliedDirective(user, firstTag) + .addAppliedDirective(user, secondTag) + .addSchemaAppliedDirective(schemaTag) + .build() + + expect: + schema.queryType.is(query) + schema.mutationType.is(mutation) + schema.subscriptionType.is(subscription) + schema.getField(query, "search").is(search) + schema.getField(node, "id").is(id) + schema.getType(search).is(nonNull) + schema.getWrappedType(nonNull).is(list) + schema.getWrappedType(list).is(string) + schema.getArgument(search, "limit").is(limit) + schema.getArgument(tag, "label").is(label) + schema.getInterfaces(user) == [node] + schema.getInterfaces(node) == [resource] + schema.getUnionMembers(searchResult) == [user] + schema.getEnumValues(status) == [active, inactive] + schema.getInputFields(filter) == [term] + schema.getAppliedDirectives(user) == [firstTag, secondTag] + schema.schemaAppliedDirectives == [schemaTag] + + when: + def removed = schema.transform("removed", builder -> builder + .removeField(query, search) + .removeField(node, "id") + .removeArgument(search, limit) + .removeArgument(tag, "label") + .removeInterface(user, node) + .removeInterface(node, "Resource") + .removeUnionMember(searchResult, user) + .removeEnumValue(status, "ACTIVE") + .removeInputField(filter, term) + .removeAppliedDirective(user, firstTag) + .removeSchemaAppliedDirective(schemaTag) + .removeDirectiveDefinition(tag) + .removeAdditionalType(integer)) + + then: + removed.getFields(query).isEmpty() + removed.getFields(node).isEmpty() + removed.getArguments(search).isEmpty() + removed.getArguments(tag).isEmpty() + removed.getInterfaces(user).isEmpty() + removed.getInterfaces(node).isEmpty() + removed.getUnionMembers(searchResult).isEmpty() + removed.getEnumValues(status) == [inactive] + removed.getInputFields(filter).isEmpty() + removed.getAppliedDirectives(user) == [secondTag] + removed.schemaAppliedDirectives.isEmpty() + removed.getDirectiveDefinition("tag") == null + removed.getScalarType("Int") == null + + when: + def withoutTags = schema.transform("withoutTags", builder -> + builder.removeAppliedDirectives(user, "tag")) + def cleared = schema.transform("cleared", builder -> builder + .setMutationType(null) + .setSubscriptionType(null) + .clearFields(query) + .clearFields(node) + .clearArguments(search) + .clearArguments(tag) + .clearInterfaces(user) + .clearInterfaces(node) + .clearUnionMembers(searchResult) + .clearEnumValues(status) + .clearInputFields(filter) + .clearAppliedDirectives(user) + .clearSchemaAppliedDirectives() + .clearDirectiveDefinitions() + .clearAdditionalTypes()) + + then: + withoutTags.getAppliedDirectives(user).isEmpty() + cleared.mutationType == null + cleared.subscriptionType == null + cleared.getFields(query).isEmpty() + cleared.getFields(node).isEmpty() + cleared.getArguments(search).isEmpty() + cleared.getArguments(tag).isEmpty() + cleared.getInterfaces(user).isEmpty() + cleared.getInterfaces(node).isEmpty() + cleared.getUnionMembers(searchResult).isEmpty() + cleared.getEnumValues(status).isEmpty() + cleared.getInputFields(filter).isEmpty() + cleared.getAppliedDirectives(user).isEmpty() + cleared.schemaAppliedDirectives.isEmpty() + cleared.directiveDefinitions.isEmpty() + cleared.additionalTypes.isEmpty() + + and: + schema.getFields(query) == [search] + schema.getAppliedDirectives(user) == [firstTag, secondTag] + } + + def "typed type setters reject invalid input and output types"() { + given: + def universe = new SchemaUniverse() + def field = universe.newField("field") + def argument = universe.newArgument("argument") + def inputField = universe.newInputField("input") + def objectType = universe.newObjectType("Object") + def inputObjectType = universe.newInputObjectType("Input") + def firstNonNull = universe.newNonNullType() + def secondNonNull = universe.newNonNullType() + def builder = universe.newSchema("invalid") + + when: + builder.setFieldType(field, inputObjectType) + + then: + thrown(AssertException) + + when: + builder.setArgumentType(argument, objectType) + + then: + thrown(AssertException) + + when: + builder.setInputFieldType(inputField, objectType) + + then: + thrown(AssertException) + + when: + builder.setWrappedType(firstNonNull, secondNonNull) + + then: + thrown(AssertException) + } + + def "an existing GraphQLSchema can seed a universe"() { + given: + def role = GraphQLEnumType.newEnum() + .name("Role") + .value(GraphQLEnumValueDefinition.newEnumValueDefinition().name("ADMIN").build()) + .build() + def filter = GraphQLInputObjectType.newInputObject() + .name("Filter") + .field(GraphQLInputObjectField.newInputObjectField() + .name("role") + .type(role)) + .build() + def namesType = GraphQLNonNull.nonNull( + GraphQLList.list(GraphQLNonNull.nonNull(Scalars.GraphQLString))) + def query = GraphQLObjectType.newObject() + .name("Query") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("names") + .type(namesType) + .argument(GraphQLArgument.newArgument() + .name("filter") + .type(filter))) + .build() + def graphQLSchema = GraphQLSchema.newSchema() + .query(query) + .additionalType(role) + .additionalType(filter) + .build() + def universe = new SchemaUniverse() + + when: + def schema = universe.importSchema("imported", graphQLSchema) + def universeQuery = schema.queryType + def names = schema.getField(universeQuery, "names") + def filterArgument = schema.getArgument(names, "filter") + def outerNonNull = schema.getType(names) + def list = schema.getWrappedType(outerNonNull) + def innerNonNull = schema.getWrappedType(list) + def string = schema.getWrappedType(innerNonNull) + + then: + universe.getSchema("imported").is(schema) + universeQuery instanceof SUObjectType + names instanceof SUField + filterArgument instanceof SUArgument + schema.getType(filterArgument) instanceof SUInputObjectType + outerNonNull instanceof SUNonNullType + list instanceof SUListType + innerNonNull instanceof SUNonNullType + string instanceof SUScalarType + string.name == "String" + + and: + def universeRole = schema.getEnumType("Role") + universeRole instanceof SUEnumType + schema.getEnumValue(universeRole, "ADMIN") instanceof SUEnumValue + } + + def "input value states are intrinsic schema data"() { + given: + def universe = new SchemaUniverse() + def string = universe.newScalarType("String") + def literal = InputValueWithState.newLiteralValue(StringValue.newStringValue("literal").build()) + def externalNull = InputValueWithState.newExternalValue(null) + def internal = InputValueWithState.newInternalValue([answer: 42]) + + when: + def literalArgument = universe.newArgument("literal", null, literal, null) + def nullInputField = universe.newInputField("nullable", null, externalNull, null) + def internalAppliedArgument = + universe.newAppliedDirectiveArgument("internal", string, internal, null) + def unsetArgument = universe.newArgument("unset") + + then: + literalArgument.argumentDefaultValue.is(literal) + literalArgument.argumentDefaultValue.isLiteral() + nullInputField.inputFieldDefaultValue.is(externalNull) + nullInputField.inputFieldDefaultValue.isExternal() + nullInputField.inputFieldDefaultValue.value == null + internalAppliedArgument.argumentValue.is(internal) + internalAppliedArgument.argumentValue.isInternal() + internalAppliedArgument.argumentValue.value == [answer: 42] + unsetArgument.argumentDefaultValue.isNotSet() + } + + def "changing a default value uses a new vertex and leaves the parent unchanged"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def field = universe.newField("search") + def integer = universe.newScalarType("Int") + def originalArgument = universe.newArgument( + "limit", null, InputValueWithState.newExternalValue(10)) + def changedArgument = universe.newArgument( + "limit", null, InputValueWithState.newExternalValue(20)) + def base = universe.newSchema("base") + .queryType(query) + .addField(query, field) + .addArgument(field, originalArgument) + .setArgumentType(originalArgument, integer) + .build() + + when: + def changed = base.transform("changed", builder -> builder + .removeArgument(field, "limit") + .addArgument(field, changedArgument) + .setArgumentType(changedArgument, integer)) + + then: + base.getArgument(field, "limit").is(originalArgument) + changed.getArgument(field, "limit").is(changedArgument) + originalArgument.argumentDefaultValue.value == 10 + changedArgument.argumentDefaultValue.value == 20 + !base.sharesOutgoingEdgesWith(changed, field) + base.sharesOutgoingEdgesWith(changed, originalArgument) + } + + def "import preserves defaults and complete directive metadata"() { + given: + def graphQLSchema = TestUtil.schema(''' + directive @tag(label: String = "fallback") repeatable on OBJECT | FIELD_DEFINITION + + input Filter { + limit: Int = 10 + } + + type Query @tag(label: "query") { + search(filter: Filter, limit: Int = 5): String @tag(label: null) + } + ''') + def universe = new SchemaUniverse() + + when: + def schema = universe.importSchema("directives", graphQLSchema) + def directive = schema.getDirectiveDefinition("tag") + def directiveArgument = schema.getArgument(directive, "label") + def filter = schema.getInputObjectType("Filter") + def inputField = schema.getInputField(filter, "limit") + def query = schema.queryType + def search = schema.getField(query, "search") + def fieldArgument = schema.getArgument(search, "limit") + def queryTag = schema.getAppliedDirectives(query) + .find { it.name == "tag" } + def queryTagArgument = schema.getArgument(queryTag, "label") + def fieldTag = schema.getAppliedDirectives(search) + .find { it.name == "tag" } + def fieldTagArgument = schema.getArgument(fieldTag, "label") + + then: + directive instanceof SUDirective + directive.repeatable + directive.validLocations() == EnumSet.of(OBJECT, FIELD_DEFINITION) + directive.definition.name == "tag" + directiveArgument.argumentDefaultValue.isLiteral() + directiveArgument.argumentDefaultValue.value.value == "fallback" + directiveArgument.definition.name == "label" + + and: + inputField.inputFieldDefaultValue.isLiteral() + inputField.inputFieldDefaultValue.value.value == BigInteger.TEN + fieldArgument.argumentDefaultValue.isLiteral() + fieldArgument.argumentDefaultValue.value.value == 5 + + and: + queryTag.definition.name == "tag" + queryTagArgument.argumentValue.isLiteral() + queryTagArgument.argumentValue.value.value == "query" + queryTagArgument.definition.name == "label" + fieldTagArgument.argumentValue.isLiteral() + fieldTagArgument.argumentValue.value.class.simpleName == "NullValue" + } + + def "import normalizes intrinsic deprecation and specifiedBy metadata"() { + given: + def date = GraphQLScalarType.newScalar(Scalars.GraphQLString) + .name("Date") + .specifiedByUrl("https://example.com/date") + .build() + def oldField = GraphQLFieldDefinition.newFieldDefinition() + .name("old") + .type(Scalars.GraphQLString) + .deprecate("Use current") + .build() + def query = GraphQLObjectType.newObject() + .name("Query") + .field(oldField) + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("date") + .type(date)) + .build() + def graphQLSchema = GraphQLSchema.newSchema() + .query(query) + .additionalType(date) + .build() + + when: + def schema = new SchemaUniverse().importSchema("normalized", graphQLSchema) + def old = schema.getField(schema.queryType, "old") + def deprecated = schema.getAppliedDirectives(old, "deprecated")[0] + def specifiedBy = + schema.getAppliedDirectives(schema.getScalarType("Date"), "specifiedBy")[0] + + then: + schema.getArgument(deprecated, "reason").argumentValue.isLiteral() + schema.getArgument(deprecated, "reason").argumentValue.value.value == "Use current" + schema.getArgument(specifiedBy, "url").argumentValue.isLiteral() + schema.getArgument(specifiedBy, "url").argumentValue.value.value == + "https://example.com/date" + } + + def "changing an applied argument replaces its immutable directive occurrence"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def integer = universe.newScalarType("Int") + def originalArgument = universe.newAppliedDirectiveArgument( + "version", integer, InputValueWithState.newExternalValue(1)) + def originalDirective = + universe.newAppliedDirective("tag", [originalArgument]) + def base = universe.newSchema("base") + .queryType(query) + .addAdditionalType(integer) + .addAppliedDirective(query, originalDirective) + .build() + + when: + def changedArgument = universe.newAppliedDirectiveArgument( + "version", integer, InputValueWithState.newExternalValue(2)) + def changedDirective = + universe.newAppliedDirective("tag", [changedArgument]) + def changed = base.transform("changed", builder -> builder + .removeAppliedDirective(query, originalDirective) + .addAppliedDirective(query, changedDirective)) + + then: + base.getAppliedDirectives(query) == [originalDirective] + changed.getAppliedDirectives(query) == [changedDirective] + base.getArgument(originalDirective, "version").argumentValue.value == 1 + changed.getArgument(changedDirective, "version").argumentValue.value == 2 + base.getType(originalArgument).is(integer) + changed.getType(changedArgument).is(integer) + + and: + universe.vertexCount == 6 + base.storedEdgeCount == 3 + changed.storedEdgeCount == 3 + } + + def "applied directive payload rejects duplicate names and foreign universes"() { + given: + def first = new SchemaUniverse() + def second = new SchemaUniverse() + def firstString = first.newScalarType("String") + def secondString = second.newScalarType("String") + def firstArgument = first.newAppliedDirectiveArgument("value", firstString) + def duplicateArgument = first.newAppliedDirectiveArgument("value", firstString) + def foreignArgument = second.newAppliedDirectiveArgument("value", secondString) + + when: + first.newAppliedDirective("tag", [firstArgument, duplicateArgument]) + + then: + thrown(AssertException) + + when: + first.newAppliedDirective("foreign", [foreignArgument]) + + then: + thrown(AssertException) + } + + def "an imported schema can be exported with the same represented structure"() { + given: + def graphQLSchema = TestUtil.schema(''' + directive @tag(label: String = "fallback") repeatable on SCHEMA | OBJECT | FIELD_DEFINITION + + scalar Date + + interface Node { + id: ID! + } + + type User implements Node @tag(label: "user") { + id: ID! + name: String @tag + } + + enum Status { + ACTIVE + OLD @deprecated(reason: "legacy") + } + + input Filter { + limit: Int = 10 + status: Status + } + + union Result = User + + schema @tag(label: "schema") { + query: Root + } + + type Root { + search(filter: Filter, at: Date): [Result!]! + } + ''') + def universe = new SchemaUniverse() + def schema = universe.importSchema("source", graphQLSchema) + + when: + def exported = schema.toGraphQLSchema() + + then: + canonicalSchema(exported) == canonicalSchema(graphQLSchema) + ((GraphQLInputObjectType) exported.getType("Filter")) + .getField("limit") + .inputFieldDefaultValue + .isLiteral() + exported.getSchemaAppliedDirectives("tag")[0] + .getArgument("label") + .argumentValue + .isLiteral() + } + + def "typed helpers cover the complete schema topology"() { + given: + def graphQLSchema = TestUtil.schema(''' + directive @tag(label: String) repeatable on SCHEMA | OBJECT | FIELD_DEFINITION + + schema @tag(label: "schema") { + query: Query + mutation: Mutation + subscription: Subscription + } + + interface Node { + id: ID! + } + + interface Resource implements Node { + id: ID! + } + + type User implements Node & Resource @tag(label: "one") @tag(label: "two") { + id: ID! + name: String @tag(label: "field") + } + + union SearchResult = User + + enum Role { + ADMIN + USER + } + + input Filter { + role: Role + } + + type Query { + user(filter: Filter): User + users: [User!]! + search: SearchResult + } + + type Mutation { + noop: Boolean + } + + type Subscription { + changed: Boolean + } + ''') + def universe = new SchemaUniverse() + + when: + def schema = universe.importSchema("helpers", graphQLSchema) + def query = schema.queryType + def user = schema.getObjectType("User") + def node = schema.getInterfaceType("Node") + def resource = schema.getInterfaceType("Resource") + def searchResult = schema.getUnionType("SearchResult") + def role = schema.getEnumType("Role") + def filter = schema.getInputObjectType("Filter") + def tag = schema.getDirectiveDefinition("tag") + + then: + schema.rootTypes*.name == ["Query", "Mutation", "Subscription"] + schema.types.containsAll([query, user, node, resource, searchResult, role, filter]) + schema.getType("Query").is(query) + schema.getType("missing") == null + schema.getObjectType("Role") == null + schema.getScalarType("String").name == "String" + schema.additionalTypes.contains(user) + + and: + schema.getFields(user)*.name == ["id", "name"] + schema.getField(user, "name").name == "name" + schema.getFields(node)*.name == ["id"] + schema.getField(node, "id").name == "id" + + and: + schema.getInterfaces(user)*.name == ["Node", "Resource"] + schema.getInterface(user, "Node").is(node) + schema.getInterfaces(resource)*.name == ["Node"] + schema.getInterface(resource, "Node").is(node) + + and: + schema.getUnionMembers(searchResult) == [user] + schema.getUnionMember(searchResult, "User").is(user) + schema.getEnumValues(role)*.name == ["ADMIN", "USER"] + schema.getEnumValue(role, "ADMIN").name == "ADMIN" + schema.getInputFields(filter)*.name == ["role"] + schema.getInputField(filter, "role").name == "role" + schema.getType(schema.getInputField(filter, "role")).is(role) + + and: + def userField = schema.getField(query, "user") + def filterArgument = schema.getArgument(userField, "filter") + schema.getArguments(userField) == [filterArgument] + schema.getType(userField).is(user) + schema.getType(filterArgument).is(filter) + + and: + def usersField = schema.getField(query, "users") + def outerNonNull = schema.getType(usersField) as SUNonNullType + def list = schema.getWrappedType(outerNonNull) as SUListType + def innerNonNull = schema.getWrappedType(list) as SUNonNullType + schema.getWrappedType(innerNonNull).is(user) + + and: + schema.directiveDefinitions.contains(tag) + schema.getArguments(tag)*.name == ["label"] + schema.getArgument(tag, "label").name == "label" + schema.getType(schema.getArgument(tag, "label")).name == "String" + + and: + schema.schemaAppliedDirectives.size() == 1 + def schemaTag = schema.schemaAppliedDirectives[0] + schema.getSchemaAppliedDirectives("tag") == [schemaTag] + schema.getArgument(schemaTag, "label").argumentValue.value.value == "schema" + schema.getType(schema.getArgument(schemaTag, "label")).name == "String" + + and: + def userTags = schema.getAppliedDirectives(user, "tag") + userTags.size() == 2 + userTags.collect { schema.getArgument(it, "label").argumentValue.value.value } == ["one", "two"] + schema.getArguments(userTags[0])*.name == ["label"] + schema.getAppliedDirectives(schema.getField(user, "name"), "tag").size() == 1 + } + + def "large snapshots share every unchanged source adjacency"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def string = universe.newScalarType("String") + def integer = universe.newScalarType("Int") + def fields = (0..<1024).collect { universe.newField("field${it}") } + def builder = universe.newSchema("large").queryType(query) + fields.each { field -> + builder.addField(query, field) + builder.setFieldType(field, string) + } + def base = builder.build() + + when: + def changed = base.transform("changed", transform -> { + for (int i = 0; i < fields.size(); i += 127) { + transform.setFieldType(fields[i], integer) + } + }) + + then: + base.getFields(query).size() == 1024 + changed.getFields(query).size() == 1024 + base.sharesOutgoingEdgesWith(changed, query) + + and: + fields.indices.every { index -> + def changedSource = index % 127 == 0 + base.sharesOutgoingEdgesWith(changed, fields[index]) != changedSource + } + } + + private static String canonicalSchema(GraphQLSchema schema) { + def options = SchemaPrinter.Options.defaultOptions() + .includeSchemaDefinition(true) + .includeScalarTypes(true) + .setComparators(GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) + return new SchemaPrinter(options).print(schema) + } +} From 5de336e4ced6d0723a2e5dd3ba62b743b8a3c105 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 3 Aug 2026 00:42:58 +0000 Subject: [PATCH 02/32] Allow removing schemas from a schema universe --- .../schema/universe/SchemaUniverse.java | 38 +++++++++- .../graphql/schema/universe/SUTest.groovy | 69 +++++++++++++++++++ 2 files changed, 105 insertions(+), 2 deletions(-) diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java index c945fb34e3..9df787ae4a 100644 --- a/src/main/java/graphql/schema/universe/SchemaUniverse.java +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -30,10 +30,11 @@ import static graphql.util.Interning.intern; /** - * An append-only store of schema vertices shared by many immutable schema snapshots. + * A store of append-only schema vertices shared by many immutable schema snapshots. * *

A universe is safe for concurrent readers. Vertex creation is serialized, while reading a - * published vertex or schema does not require locking.

+ * published vertex or schema does not require locking. Schemas can be removed from the universe's + * registry, but removing a schema does not remove its vertices from the universe.

*/ @ExperimentalApi @NullMarked @@ -75,6 +76,39 @@ public SUSchema parseSchema(String name, String sdl) { return schemasByName.get(assertNotNull(name)); } + /** + * Removes and returns the registered schema with the given name. + * + *

This only removes the schema from this universe's registry. The schema remains usable if + * it is referenced elsewhere, and its vertices remain stored in this universe.

+ * + * @return the removed schema, or {@code null} if no schema has that name + */ + public synchronized @Nullable SUSchema removeSchema(String name) { + SUSchema removed = schemasByName.remove(assertNotNull(name)); + if (removed != null) { + assertTrue(schemas.remove(removed), "Schema registry is inconsistent"); + } + return removed; + } + + /** + * Removes the exact schema instance from this universe's registry. + * + *

If its name has since been reused by another schema, that replacement is not removed.

+ * + * @return {@code true} if the schema was registered and removed + */ + public synchronized boolean removeSchema(SUSchema schema) { + SUSchema ownedSchema = assertNotNull(schema); + assertTrue(ownedSchema.getUniverse() == this, "Schema belongs to another universe"); + boolean removed = schemasByName.remove(ownedSchema.getName(), ownedSchema); + if (removed) { + assertTrue(schemas.remove(ownedSchema), "Schema registry is inconsistent"); + } + return removed; + } + public List getSchemas() { return List.copyOf(schemas); } diff --git a/src/test/groovy/graphql/schema/universe/SUTest.groovy b/src/test/groovy/graphql/schema/universe/SUTest.groovy index 99253e1d57..0ed5dcecdc 100644 --- a/src/test/groovy/graphql/schema/universe/SUTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUTest.groovy @@ -131,6 +131,75 @@ class SUTest extends Specification { universe.schemas == [first, second] } + def "schemas can be removed from the universe registry"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def first = universe.newSchema("first") + .queryType(query) + .build() + def second = first.transform("second", builder -> {}) + + when: + def removed = universe.removeSchema("first") + + then: + removed.is(first) + universe.getSchema("first") == null + universe.schemas == [second] + universe.schemasByName == [second: second] + first.queryType.is(query) + universe.removeSchema("missing") == null + + when: + def replacement = second.transform("first", builder -> {}) + + then: + universe.schemas == [second, replacement] + universe.getSchema("first").is(replacement) + + when: + def removedOldInstance = universe.removeSchema(first) + + then: + !removedOldInstance + universe.getSchema("first").is(replacement) + + when: + def removedReplacement = universe.removeSchema(replacement) + + then: + removedReplacement + universe.schemas == [second] + universe.schemasByName == [second: second] + } + + def "removing a schema does not remove its vertices"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def field = universe.newField("foo") + def string = universe.newScalarType("String") + def schema = universe.newSchema("schema") + .queryType(query) + .addField(query, field) + .setFieldType(field, string) + .build() + def root = schema.root + def vertexCount = universe.vertexCount + + when: + universe.removeSchema(schema) + + then: + universe.schemas.isEmpty() + universe.vertexCount == vertexCount + universe.getVertex(root.id).is(root) + universe.getVertex(query.id).is(query) + universe.getVertex(field.id).is(field) + universe.getVertex(string.id).is(string) + } + def "exact duplicate edges are folded but duplicate child names are rejected"() { given: def universe = new SchemaUniverse() From 3ba00bc439c636d5216d1c0db06653435b4b1d3e Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 3 Aug 2026 00:43:09 +0000 Subject: [PATCH 03/32] Preserve applied directive order in schema universes --- docs/schema-universe.md | 13 +- .../schema/universe/MutablePackedEdgeSet.java | 95 +++++++-- .../schema/universe/PackedEdgeSet.java | 50 +++-- .../graphql/schema/universe/SUEdgeKind.java | 47 +++-- .../schema/universe/SUSchemaBuilder.java | 56 ++++++ .../schema/universe/PackedEdgeSetTest.groovy | 89 +++++++++ .../SUAppliedDirectiveOrderTest.groovy | 180 ++++++++++++++++++ 7 files changed, 485 insertions(+), 45 deletions(-) create mode 100644 src/test/groovy/graphql/schema/universe/PackedEdgeSetTest.groovy create mode 100644 src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 7a096c3213..1464e9664c 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -71,11 +71,14 @@ An edge is packed into one primitive `long`: ``` The source ID is the key of the adjacency map, so it is not repeated per edge. Immutable outgoing -arrays are sorted by kind and target-name ID. This provides: +arrays group edges by kind. Most kind ranges are sorted by target-name ID and target ID. Applied +directive ranges retain attachment order so schema and SDL round trips preserve directive order. +This provides: - 8 bytes per stored edge in an adjacency array; -- binary search by relationship and GraphQL name; -- deterministic iteration; +- binary search by relationship and GraphQL name for unordered edge kinds; +- a linear scan over the normally small applied-directive range; +- deterministic iteration with directive attachment order preserved; - no edge objects or boxed endpoint IDs. The 24-bit name ID limits one universe to 16,777,215 distinct names. Vertex IDs are non-negative @@ -94,8 +97,8 @@ adjacency: | Operation | Time | New retained storage | | --- | --- | --- | | Read outgoing adjacency | `O(log32 V)` | none | -| Child lookup by name | `O(log32 V + log d)` | none | -| Add/remove edge | `O(d)` transient, then sort at build | one changed primitive array | +| Child lookup by name | `O(log32 V + log d)` unordered, `O(log32 V + d)` ordered | none | +| Add/remove edge | `O(d)` transient, then canonicalize at build | one changed primitive array | | Publish `k` changed sources | `O(k log32 V)` | copied trie paths plus `k` arrays | | Derive with no type edits | effectively root replacement | one root array and two copied trie paths | diff --git a/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java b/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java index d5f6d802ab..09fe5574db 100644 --- a/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java +++ b/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java @@ -44,6 +44,25 @@ public void add(long edge) { edges[size++] = edge; } + /** + * Replaces the first exact occurrence of a packed edge without changing its position. + * + * @param currentEdge the edge to replace + * @param replacementEdge the replacement edge + * + * @return {@code true} when the current edge was present + */ + public boolean replace(long currentEdge, long replacementEdge) { + for (int i = 0; i < size; i++) { + if (edges[i] != currentEdge) { + continue; + } + edges[i] = replacementEdge; + return true; + } + return false; + } + /** * Removes the first exact occurrence of a packed edge. * @@ -98,8 +117,8 @@ public void removeKind(SUEdgeKind kind) { /** * Produces a canonical immutable edge set. * - *

The result is sorted, exact duplicates are removed, single-valued edge kinds are checked, - * and kinds requiring unique target names are validated.

+ *

The result is grouped by edge kind. Unordered kinds are sorted, ordered kinds retain first + * encounter order, exact duplicates are removed, and cardinality rules are validated.

* * @return the immutable edge set */ @@ -107,11 +126,13 @@ public PackedEdgeSet freeze() { if (size == 0) { return PackedEdgeSet.empty(); } - long[] sorted = Arrays.copyOf(edges, size); - Arrays.sort(sorted); - int uniqueSize = removeExactDuplicates(sorted); - validateCardinality(sorted, uniqueSize); - return new PackedEdgeSet(Arrays.copyOf(sorted, uniqueSize)); + long[] canonical = Arrays.copyOf(edges, size); + Arrays.sort(canonical); + int uniqueSize = removeExactDuplicates(canonical); + canonical = Arrays.copyOf(canonical, uniqueSize); + restoreOrderedRanges(canonical); + validateCardinality(canonical); + return new PackedEdgeSet(canonical); } private int removeExactDuplicates(long[] sorted) { @@ -125,10 +146,62 @@ private int removeExactDuplicates(long[] sorted) { return writeIndex; } - private void validateCardinality(long[] sorted, int uniqueSize) { - for (int i = 1; i < uniqueSize; i++) { - long previous = sorted[i - 1]; - long current = sorted[i]; + private void restoreOrderedRanges(long[] canonical) { + for (SUEdgeKind kind : SUEdgeKind.values()) { + if (!kind.isOrdered()) { + continue; + } + restoreOrderedRange(canonical, kind); + } + } + + private void restoreOrderedRange(long[] canonical, SUEdgeKind kind) { + int start = firstIndex(canonical, kind); + if (start == canonical.length) { + return; + } + int end = endIndex(canonical, start, kind); + long[] sortedRange = Arrays.copyOfRange(canonical, start, end); + boolean[] emitted = new boolean[sortedRange.length]; + int writeIndex = start; + for (int i = 0; i < size; i++) { + long edge = edges[i]; + if (PackedEdgeSet.edgeKind(edge) != kind) { + continue; + } + int sortedIndex = Arrays.binarySearch(sortedRange, edge); + assertTrue(sortedIndex >= 0, "Ordered edge is missing from its canonical range"); + if (emitted[sortedIndex]) { + continue; + } + canonical[writeIndex++] = edge; + emitted[sortedIndex] = true; + } + assertTrue(writeIndex == end, "Ordered edge range was not completely restored"); + } + + private int firstIndex(long[] canonical, SUEdgeKind kind) { + for (int i = 0; i < canonical.length; i++) { + if (PackedEdgeSet.edgeKind(canonical[i]) == kind) { + return i; + } + } + return canonical.length; + } + + private int endIndex(long[] canonical, int start, SUEdgeKind kind) { + int index = start; + while (index < canonical.length + && PackedEdgeSet.edgeKind(canonical[index]) == kind) { + index++; + } + return index; + } + + private void validateCardinality(long[] canonical) { + for (int i = 1; i < canonical.length; i++) { + long previous = canonical[i - 1]; + long current = canonical[i]; SUEdgeKind kind = PackedEdgeSet.edgeKind(current); if (kind != PackedEdgeSet.edgeKind(previous)) { continue; diff --git a/src/main/java/graphql/schema/universe/PackedEdgeSet.java b/src/main/java/graphql/schema/universe/PackedEdgeSet.java index f7090fc7f6..088cf874f5 100644 --- a/src/main/java/graphql/schema/universe/PackedEdgeSet.java +++ b/src/main/java/graphql/schema/universe/PackedEdgeSet.java @@ -8,7 +8,7 @@ import static graphql.Assert.assertTrue; /** - * The immutable, sorted outgoing adjacency of one source vertex. + * The immutable outgoing adjacency of one source vertex. * *

Each edge occupies one {@code long} with this layout:

* @@ -20,8 +20,9 @@ * +--------------+----------------------+-----------------------+ * * - *

The packed values are sorted by kind, name ID, and target ID. Edges of one kind are therefore - * contiguous, while exact and name-based lookups can use binary search. Instances are values in + *

The packed values are grouped by kind. Unordered kinds are sorted by name ID and target ID, + * while ordered kinds retain attachment order. Exact and name-based lookups use binary search for + * unordered kinds and scan the normally small ordered ranges. Instances are values in * {@link PersistentEdgeMap}; unchanged source vertices share the same instance across schema * snapshots.

*/ @@ -37,12 +38,12 @@ public final class PackedEdgeSet { private final long[] edges; /** - * Creates a set that takes ownership of an already sorted and validated edge array. + * Creates a set that takes ownership of an already grouped and validated edge array. * *

The array is not copied and must not be modified after construction. General callers * should build through {@link MutablePackedEdgeSet#freeze()}.

* - * @param edges sorted, unique packed edges + * @param edges kind-grouped, unique packed edges */ public PackedEdgeSet(long[] edges) { this.edges = edges; @@ -102,14 +103,25 @@ public boolean isEmpty() { * @return {@code true} when the exact edge exists */ public boolean contains(SUEdgeKind kind, int nameId, int targetId) { - return Arrays.binarySearch(edges, pack(kind, nameId, targetId)) >= 0; + long edge = pack(kind, nameId, targetId); + int start = firstIndex(kind); + int end = endIndex(kind); + if (!kind.isOrdered()) { + return Arrays.binarySearch(edges, start, end, edge) >= 0; + } + for (int i = start; i < end; i++) { + if (edges[i] == edge) { + return true; + } + } + return false; } /** * Finds a target by relationship kind and target name. * - *

For edge kinds that permit duplicate names, this returns the first target in target-ID - * order.

+ *

For edge kinds that permit duplicate names, this returns the first target in attachment + * order for ordered kinds and target-ID order for unordered kinds.

* * @param kind the relationship kind * @param nameId the target name ID @@ -117,9 +129,19 @@ public boolean contains(SUEdgeKind kind, int nameId, int targetId) { * @return the target vertex ID, or {@code -1} when absent */ public int targetByName(SUEdgeKind kind, int nameId) { + int start = firstIndex(kind); + int end = endIndex(kind); + if (kind.isOrdered()) { + for (int i = start; i < end; i++) { + if (edgeNameId(edges[i]) == nameId) { + return targetId(edges[i]); + } + } + return -1; + } long prefix = prefix(kind, nameId); - int index = lowerBound(prefix); - if (index == edges.length || edgePrefix(edges[index]) != prefix) { + int index = lowerBound(prefix, start, end); + if (index == end || edgePrefix(edges[index]) != prefix) { return -1; } return targetId(edges[index]); @@ -226,8 +248,12 @@ private static long edgePrefix(long edge) { } private int lowerBound(long value) { - int low = 0; - int high = edges.length; + return lowerBound(value, 0, edges.length); + } + + private int lowerBound(long value, int start, int end) { + int low = start; + int high = end; while (low < high) { int middle = (low + high) >>> 1; if (edges[middle] < value) { diff --git a/src/main/java/graphql/schema/universe/SUEdgeKind.java b/src/main/java/graphql/schema/universe/SUEdgeKind.java index a00163ca32..15e25a2cf9 100644 --- a/src/main/java/graphql/schema/universe/SUEdgeKind.java +++ b/src/main/java/graphql/schema/universe/SUEdgeKind.java @@ -10,26 +10,27 @@ * *

Each kind has a stable compact code stored in the high eight bits of a packed edge. * {@code single} limits the kind to one target per source. {@code uniqueName} allows multiple - * targets but rejects two targets with the same universe-interned name. Applied directives permit - * duplicate names because repeatable directive occurrences must remain distinct.

+ * targets but rejects two targets with the same universe-interned name. {@code ordered} preserves + * attachment order instead of sorting a kind's targets. Applied directives permit duplicate names + * because repeatable directive occurrences must remain distinct.

*/ @Internal @NullMarked public enum SUEdgeKind { - QUERY_TYPE(1, true, true), - MUTATION_TYPE(2, true, true), - SUBSCRIPTION_TYPE(3, true, true), - ADDITIONAL_TYPE(4, false, true), - DIRECTIVE_DEFINITION(5, false, true), - FIELD(6, false, true), - ARGUMENT(7, false, true), - TYPE(8, true, true), - IMPLEMENTS(9, false, true), - UNION_MEMBER(10, false, true), - ENUM_VALUE(11, false, true), - INPUT_FIELD(12, false, true), - APPLIED_DIRECTIVE(13, false, false), - WRAPPED_TYPE(15, true, true); + QUERY_TYPE(1, true, true, false), + MUTATION_TYPE(2, true, true, false), + SUBSCRIPTION_TYPE(3, true, true, false), + ADDITIONAL_TYPE(4, false, true, false), + DIRECTIVE_DEFINITION(5, false, true, false), + FIELD(6, false, true, false), + ARGUMENT(7, false, true, false), + TYPE(8, true, true, false), + IMPLEMENTS(9, false, true, false), + UNION_MEMBER(10, false, true, false), + ENUM_VALUE(11, false, true, false), + INPUT_FIELD(12, false, true, false), + APPLIED_DIRECTIVE(13, false, false, true), + WRAPPED_TYPE(15, true, true, false); private static final SUEdgeKind[] BY_CODE = new SUEdgeKind[16]; @@ -42,11 +43,13 @@ public enum SUEdgeKind { private final int code; private final boolean single; private final boolean uniqueName; + private final boolean ordered; - SUEdgeKind(int code, boolean single, boolean uniqueName) { + SUEdgeKind(int code, boolean single, boolean uniqueName, boolean ordered) { this.code = code; this.single = single; this.uniqueName = uniqueName; + this.ordered = ordered; } /** @@ -79,6 +82,16 @@ public boolean isUniqueName() { return uniqueName; } + /** + * Reports whether targets retain their attachment order. + * + * @return {@code true} when targets are not sorted within their edge-kind range + */ + @Internal + public boolean isOrdered() { + return ordered; + } + /** * Resolves a packed edge code. * diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index d423a26bb5..26072fd9bd 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -6,6 +6,7 @@ import org.jspecify.annotations.Nullable; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import static graphql.Assert.assertNotNull; @@ -119,6 +120,17 @@ public SUSchemaBuilder clearSchemaAppliedDirectives() { return clearAppliedDirectives(root); } + public SUSchemaBuilder replaceSchemaAppliedDirective( + SUAppliedDirective current, + SUAppliedDirective replacement) { + return replaceAppliedDirective(root, current, replacement); + } + + public SUSchemaBuilder replaceSchemaAppliedDirectives( + List directives) { + return replaceAppliedDirectives(root, directives); + } + public SUSchemaBuilder addField(SUObjectType objectType, SUField field) { return addEdge(objectType, SUEdgeKind.FIELD, field); } @@ -312,6 +324,50 @@ public SUSchemaBuilder clearAppliedDirectives(SUAppliedDirectiveContainer contai return removeEdges(containerVertex(container), SUEdgeKind.APPLIED_DIRECTIVE); } + public SUSchemaBuilder replaceAppliedDirective( + SUAppliedDirectiveContainer container, + SUAppliedDirective current, + SUAppliedDirective replacement) { + assertCanChange(); + SUVertex source = containerVertex(container); + assertOwned(current); + assertEdgeShape(source, SUEdgeKind.APPLIED_DIRECTIVE, replacement); + long currentEdge = PackedEdgeSet.pack( + SUEdgeKind.APPLIED_DIRECTIVE, + current.getNameId(), + current.getId()); + long replacementEdge = PackedEdgeSet.pack( + SUEdgeKind.APPLIED_DIRECTIVE, + replacement.getNameId(), + replacement.getId()); + assertTrue( + mutableEdges(source).replace(currentEdge, replacementEdge), + "Applied directive %s is not attached to container %s", + current, + source); + return this; + } + + public SUSchemaBuilder replaceAppliedDirectives( + SUAppliedDirectiveContainer container, + List directives) { + assertCanChange(); + SUVertex source = containerVertex(container); + List replacements = assertNotNull(directives); + for (SUAppliedDirective directive : replacements) { + assertEdgeShape(source, SUEdgeKind.APPLIED_DIRECTIVE, directive); + } + MutablePackedEdgeSet edges = mutableEdges(source); + edges.removeKind(SUEdgeKind.APPLIED_DIRECTIVE); + for (SUAppliedDirective directive : replacements) { + edges.add(PackedEdgeSet.pack( + SUEdgeKind.APPLIED_DIRECTIVE, + directive.getNameId(), + directive.getId())); + } + return this; + } + public SUSchemaBuilder setWrappedType(SUListType listType, SUType wrappedType) { return setEdge(listType, SUEdgeKind.WRAPPED_TYPE, wrappedType); } diff --git a/src/test/groovy/graphql/schema/universe/PackedEdgeSetTest.groovy b/src/test/groovy/graphql/schema/universe/PackedEdgeSetTest.groovy new file mode 100644 index 0000000000..0b4df7e0b7 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/PackedEdgeSetTest.groovy @@ -0,0 +1,89 @@ +package graphql.schema.universe + +import spock.lang.Specification + +class PackedEdgeSetTest extends Specification { + + def "ordered ranges retain encounter order while other ranges remain sorted"() { + given: + def fieldB = PackedEdgeSet.pack(SUEdgeKind.FIELD, 20, 2) + def fieldA = PackedEdgeSet.pack(SUEdgeKind.FIELD, 10, 1) + def firstZ = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 30, 30) + def a = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 10, 10) + def secondZ = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 30, 31) + def m = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 20, 20) + def wrappedType = PackedEdgeSet.pack(SUEdgeKind.WRAPPED_TYPE, 0, 99) + def mutable = new MutablePackedEdgeSet(PackedEdgeSet.empty()) + + mutable.add(wrappedType) + mutable.add(firstZ) + mutable.add(fieldB) + mutable.add(a) + mutable.add(secondZ) + mutable.add(firstZ) + mutable.add(m) + mutable.add(fieldA) + + when: + def frozen = mutable.freeze() + + then: + targets(frozen, SUEdgeKind.FIELD) == [1, 2] + targets(frozen, SUEdgeKind.APPLIED_DIRECTIVE) == [30, 10, 31, 20] + targets(frozen, SUEdgeKind.WRAPPED_TYPE) == [99] + frozen.size() == 7 + + and: + frozen.contains(SUEdgeKind.FIELD, 10, 1) + frozen.contains(SUEdgeKind.APPLIED_DIRECTIVE, 30, 30) + frozen.contains(SUEdgeKind.APPLIED_DIRECTIVE, 30, 31) + frozen.contains(SUEdgeKind.WRAPPED_TYPE, 0, 99) + !frozen.contains(SUEdgeKind.APPLIED_DIRECTIVE, 30, 32) + + and: + frozen.targetByName(SUEdgeKind.FIELD, 20) == 2 + frozen.targetByName(SUEdgeKind.APPLIED_DIRECTIVE, 30) == 30 + frozen.targetByName(SUEdgeKind.WRAPPED_TYPE, 0) == 99 + frozen.targetByName(SUEdgeKind.APPLIED_DIRECTIVE, 40) == -1 + + and: + frozen.endIndex(SUEdgeKind.FIELD) == + frozen.firstIndex(SUEdgeKind.APPLIED_DIRECTIVE) + frozen.endIndex(SUEdgeKind.APPLIED_DIRECTIVE) == + frozen.firstIndex(SUEdgeKind.WRAPPED_TYPE) + } + + def "replacing an ordered edge retains its array position"() { + given: + def first = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 30, 30) + def current = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 10, 10) + def replacement = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 5, 5) + def last = PackedEdgeSet.pack(SUEdgeKind.APPLIED_DIRECTIVE, 20, 20) + def mutable = new MutablePackedEdgeSet(PackedEdgeSet.empty()) + mutable.add(first) + mutable.add(current) + mutable.add(last) + + when: + def replaced = mutable.replace(current, replacement) + def frozen = mutable.freeze() + + then: + replaced + targets(frozen, SUEdgeKind.APPLIED_DIRECTIVE) == [30, 5, 20] + + when: + def missing = mutable.replace(current, first) + + then: + !missing + } + + private static List targets(PackedEdgeSet edges, SUEdgeKind kind) { + def result = [] + for (int i = edges.firstIndex(kind); i < edges.endIndex(kind); i++) { + result.add(edges.targetIdAt(i)) + } + return result + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy new file mode 100644 index 0000000000..0c518c0f59 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy @@ -0,0 +1,180 @@ +package graphql.schema.universe + +import graphql.AssertException +import graphql.TestUtil +import graphql.schema.idl.SchemaParser +import spock.lang.Specification + +class SUAppliedDirectiveOrderTest extends Specification { + + def "native builders retain applied directive order across transformations"() { + given: + def universe = new SchemaUniverse() + def a = universe.newAppliedDirective("a") + def m = universe.newAppliedDirective("m") + def firstZ = universe.newAppliedDirective("z") + def secondZ = universe.newAppliedDirective("z") + def b = universe.newAppliedDirective("b") + def query = universe.newObjectType("Query") + def field = universe.newField("value") + def string = universe.newScalarType("String") + def base = universe.newSchema("base") + .queryType(query) + .addField(query, field) + .setFieldType(field, string) + .addAppliedDirective(query, firstZ) + .addAppliedDirective(query, a) + .addAppliedDirective(query, secondZ) + .addAppliedDirective(query, m) + .addSchemaAppliedDirective(m) + .addSchemaAppliedDirective(a) + .build() + + expect: + base.getAppliedDirectives(query) == [firstZ, a, secondZ, m] + base.getAppliedDirectives(query, "z") == [firstZ, secondZ] + base.getChild(query, SUEdgeKind.APPLIED_DIRECTIVE, "z").is(firstZ) + base.containsEdge(query, SUEdgeKind.APPLIED_DIRECTIVE, secondZ) + base.schemaAppliedDirectives == [m, a] + + when: + def removed = base.transform("removed", builder -> + builder.removeAppliedDirective(query, a)) + def appended = removed.transform("appended", builder -> + builder.addAppliedDirective(query, a) + .addSchemaAppliedDirective(firstZ)) + def replaced = base.transform("replaced", builder -> + builder.replaceAppliedDirective(query, secondZ, b) + .replaceSchemaAppliedDirective(m, firstZ)) + def reordered = base.transform("reordered", builder -> + builder.replaceAppliedDirectives(query, [m, secondZ, a, firstZ]) + .replaceSchemaAppliedDirectives([a, m])) + + then: + removed.getAppliedDirectives(query) == [firstZ, secondZ, m] + appended.getAppliedDirectives(query) == [firstZ, secondZ, m, a] + appended.schemaAppliedDirectives == [m, a, firstZ] + replaced.getAppliedDirectives(query) == [firstZ, a, b, m] + replaced.schemaAppliedDirectives == [firstZ, a] + reordered.getAppliedDirectives(query) == [m, secondZ, a, firstZ] + reordered.schemaAppliedDirectives == [a, m] + + and: + base.getAppliedDirectives(query) == [firstZ, a, secondZ, m] + base.schemaAppliedDirectives == [m, a] + base.sharesOutgoingEdgesWith(removed, field) + + when: + base.transform("invalid", builder -> + builder.replaceAppliedDirective(query, b, a)) + + then: + thrown(AssertException) + } + + def "SDL and GraphQLSchema round trips retain order on every directive container"() { + given: + def registry = new SchemaParser().parse(directivesEverywhereSdl()) + def graphQLSchema = TestUtil.schema(directivesEverywhereSdl()) + + when: + def nativeSchema = importWithKnownNameOrder("native", registry) + def importedSchema = importWithKnownNameOrder("graphql", graphQLSchema) + def exportedSchema = importWithKnownNameOrder( + "exported", + importedSchema.toGraphQLSchema()) + + then: + directiveOrders(nativeSchema).values().every { it == expectedOrder() } + directiveOrders(importedSchema).values().every { it == expectedOrder() } + directiveOrders(exportedSchema).values().every { it == expectedOrder() } + } + + private static SUSchema importWithKnownNameOrder(String name, Object source) { + def universe = new SchemaUniverse() + universe.newAppliedDirective("a") + universe.newAppliedDirective("m") + universe.newAppliedDirective("z") + if (source instanceof graphql.schema.GraphQLSchema) { + return universe.importSchema(name, (graphql.schema.GraphQLSchema) source) + } + return universe.importSchema( + name, + (graphql.schema.idl.TypeDefinitionRegistry) source) + } + + private static Map> directiveOrders(SUSchema schema) { + def query = schema.queryType + def objectField = schema.getField(query, "id") + def objectArgument = schema.getArgument(objectField, "format") + def node = schema.getInterfaceType("Node") + def interfaceField = schema.getField(node, "id") + def interfaceArgument = schema.getArgument(interfaceField, "format") + def mode = schema.getEnumType("Mode") + def filter = schema.getInputObjectType("Filter") + return [ + schema : schema.schemaAppliedDirectives*.name, + scalar : schema.getAppliedDirectives( + schema.getScalarType("Custom"))*.name, + object : schema.getAppliedDirectives(query)*.name, + objectField : schema.getAppliedDirectives(objectField)*.name, + objectArgument : schema.getAppliedDirectives(objectArgument)*.name, + interfaceType : schema.getAppliedDirectives(node)*.name, + interfaceField : schema.getAppliedDirectives(interfaceField)*.name, + interfaceArgument: schema.getAppliedDirectives(interfaceArgument)*.name, + unionType : schema.getAppliedDirectives( + schema.getUnionType("Result"))*.name, + enumType : schema.getAppliedDirectives(mode)*.name, + enumValue : schema.getAppliedDirectives( + schema.getEnumValue(mode, "ONE"))*.name, + inputObject : schema.getAppliedDirectives(filter)*.name, + inputField : schema.getAppliedDirectives( + schema.getInputField(filter, "value"))*.name + ] + } + + private static List expectedOrder() { + return ["z", "a", "z", "m"] + } + + private static String directivesEverywhereSdl() { + return ''' + directive @a on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | + ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | + INPUT_OBJECT | INPUT_FIELD_DEFINITION + directive @m on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | + ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | + INPUT_OBJECT | INPUT_FIELD_DEFINITION + directive @z repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | + ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | + INPUT_OBJECT | INPUT_FIELD_DEFINITION + + schema @z @a @z @m { + query: Query + } + + scalar Custom @z @a @z @m + + interface Node @z @a @z @m { + id(format: String @z @a @z @m): ID @z @a @z @m + } + + type Query implements Node @z @a @z @m { + id(format: String @z @a @z @m): ID @z @a @z @m + result(filter: Filter): Result + custom: Custom + mode: Mode + } + + union Result @z @a @z @m = Query + + enum Mode @z @a @z @m { + ONE @z @a @z @m + } + + input Filter @z @a @z @m { + value: String @z @a @z @m + } + ''' + } +} From d415edfd0644addaad2838f2d83af8c28eaa2538 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Mon, 3 Aug 2026 01:09:19 +0000 Subject: [PATCH 04/32] Restore legacy schema printer coverage --- .../schema/idl/SchemaPrinterLegacyTest.groovy | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 src/test/groovy/graphql/schema/idl/SchemaPrinterLegacyTest.groovy diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterLegacyTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterLegacyTest.groovy new file mode 100644 index 0000000000..ce917f4ac9 --- /dev/null +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterLegacyTest.groovy @@ -0,0 +1,362 @@ +package graphql.schema.idl + +import graphql.Scalars +import graphql.TestUtil +import graphql.introspection.Introspection +import graphql.schema.GraphQLAppliedDirective +import graphql.schema.GraphQLAppliedDirectiveArgument +import graphql.schema.GraphQLCodeRegistry +import graphql.schema.GraphQLDirective +import graphql.schema.GraphQLEnumType +import graphql.schema.GraphQLEnumValueDefinition +import graphql.schema.GraphQLInputObjectType +import graphql.schema.GraphQLInterfaceType +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import graphql.schema.GraphQLSchemaElement +import graphql.schema.GraphQLUnionType +import graphql.schema.TypeResolver +import spock.lang.Specification + +import static graphql.schema.GraphQLArgument.newArgument +import static graphql.schema.GraphQLEnumType.newEnum +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLInputObjectField.newInputObjectField +import static graphql.schema.GraphQLInputObjectType.newInputObject +import static graphql.schema.GraphQLInterfaceType.newInterface +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLScalarType.newScalar +import static graphql.schema.GraphQLUnionType.newUnionType +import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring +import static graphql.schema.idl.SchemaPrinter.Options.defaultOptions + +class SchemaPrinterLegacyTest extends Specification { + + def "AST-aware semantic fallback remains equivalent to the semantic writer"() { + given: + def schema = programmaticSchema() + def optionSets = [ + defaultOptions() + .includeSchemaDefinition(true) + .includeScalarTypes(true) + .includeDirectives(true), + defaultOptions() + .includeSchemaDefinition(true) + .includeScalarTypes(true) + .descriptionsAsHashComments(true), + defaultOptions() + .includeSchemaDefinition(false) + .includeScalarTypes(false) + .includeDirectives(false) + .includeDirectiveDefinitions(false) + ] + + expect: + optionSets.every { options -> + def semantic = new SchemaPrinter( + options.useAstDefinitions(false)).print(schema) + def astAwareFallback = new SchemaPrinter( + options.useAstDefinitions(true)).print(schema) + astAwareFallback == semantic + } + } + + def "AST-aware semantic fallback handles default and omitted schema elements"() { + given: + def schema = minimalSchema() + def optionSets = [ + defaultOptions() + .includeSchemaDefinition(true), + defaultOptions() + .includeSchemaDefinition(false), + defaultOptions() + .includeSchemaDefinition(true) + .includeDirectiveDefinition { false }, + defaultOptions() + .includeSchemaDefinition(true) + .includeDirectiveDefinitions(false), + defaultOptions() + .includeSchemaDefinition(true) + .includeIntrospectionTypes(true) + ] + + expect: + optionSets.every { options -> + def semantic = new SchemaPrinter( + options.useAstDefinitions(false)).print(schema) + def astAwareFallback = new SchemaPrinter( + options.useAstDefinitions(true)).print(schema) + astAwareFallback == semantic + } + } + + def "direct legacy printing retains AST comments for every schema element kind"() { + given: + def registry = new SchemaParser().parse(commentsEverywhereSdl()) + def wiring = newRuntimeWiring() + .scalar(TestUtil.mockScalar(registry.scalars().get("Custom"))) + .type(TestUtil.mockTypeRuntimeWiring("Node", true)) + .type(TestUtil.mockTypeRuntimeWiring("Search", true)) + .build() + def generatorOptions = + SchemaGenerator.Options.defaultOptions().useCommentsAsDescriptions(false) + def schema = + new SchemaGenerator().makeExecutableSchema(generatorOptions, registry, wiring) + List elements = [] + elements.addAll(schema.allTypesAsList.findAll { !it.name.startsWith("__") }) + elements.addAll(schema.directives) + def printerOptions = defaultOptions() + .includeScalarTypes(true) + .includeAstDefinitionComments(true) + .descriptionsAsHashComments(true) + + when: + def result = new SchemaPrinter(printerOptions).print(elements) + + then: + [ + "# scalar comment", + "# interface comment", + "# interface field comment", + "# interface argument comment", + "# object comment", + "# union comment", + "# enum comment", + "# enum value comment", + "# input object comment", + "# input field comment", + "# directive comment", + "# directive argument comment" + ].every { result.contains(it) } + } + + private static GraphQLSchema programmaticSchema() { + def appliedTag = GraphQLAppliedDirective.newDirective() + .name("tag") + .argument(GraphQLAppliedDirectiveArgument.newArgument() + .name("label") + .type(Scalars.GraphQLString) + .valueProgrammatic("schema") + .build()) + .build() + def tagDefinition = GraphQLDirective.newDirective() + .name("tag") + .description("A repeatable tag") + .repeatable(true) + .validLocations( + Introspection.DirectiveLocation.SCHEMA, + Introspection.DirectiveLocation.OBJECT, + Introspection.DirectiveLocation.FIELD_DEFINITION) + .argument(newArgument() + .name("label") + .description("Tag label") + .type(Scalars.GraphQLString) + .build()) + .build() + GraphQLScalarType custom = newScalar() + .name("Custom") + .description("A custom\nscalar") + .specifiedByUrl("https://example.com/custom") + .coercing(TestUtil.mockCoercing()) + .build() + GraphQLInterfaceType node = newInterface() + .name("Node") + .description("A node") + .field(newFieldDefinition() + .name("id") + .type(Scalars.GraphQLID) + .build()) + .build() + GraphQLInterfaceType resource = newInterface() + .name("Resource") + .description("A resource") + .withInterface(node) + .field(newFieldDefinition() + .name("id") + .type(Scalars.GraphQLID) + .build()) + .field(newFieldDefinition() + .name("url") + .type(Scalars.GraphQLString) + .build()) + .build() + GraphQLEnumType status = newEnum() + .name("Status") + .description("Result status") + .value(GraphQLEnumValueDefinition.newEnumValueDefinition() + .name("CURRENT") + .build()) + .value(GraphQLEnumValueDefinition.newEnumValueDefinition() + .name("OLD") + .description("Old status") + .deprecationReason("Use CURRENT") + .build()) + .build() + GraphQLInputObjectType filter = newInputObject() + .name("Filter") + .description("Search filter") + .field(newInputObjectField() + .name("limit") + .description("Result limit") + .type(Scalars.GraphQLInt) + .defaultValueProgrammatic(10) + .build()) + .field(newInputObjectField() + .name("old") + .type(Scalars.GraphQLString) + .deprecate("Use limit") + .build()) + .build() + GraphQLObjectType first = newObject() + .name("FirstResult") + .description("First result") + .withInterfaces(node, resource) + .field(newFieldDefinition() + .name("id") + .type(Scalars.GraphQLID) + .build()) + .field(newFieldDefinition() + .name("url") + .type(Scalars.GraphQLString) + .build()) + .build() + GraphQLObjectType second = newObject() + .name("SecondResult") + .description("Second result") + .withInterfaces(node, resource) + .field(newFieldDefinition() + .name("id") + .type(Scalars.GraphQLID) + .build()) + .field(newFieldDefinition() + .name("url") + .type(Scalars.GraphQLString) + .build()) + .build() + GraphQLUnionType search = newUnionType() + .name("Search") + .description("Search result") + .possibleTypes(first, second) + .build() + GraphQLObjectType query = newObject() + .name("RootQuery") + .description("Query root") + .withAppliedDirective(appliedTag) + .field(newFieldDefinition() + .name("search") + .description("Search field") + .type(search) + .deprecate("Use currentSearch") + .argument(newArgument() + .name("filter") + .description("Filter argument") + .type(filter) + .deprecate("Use options") + .build()) + .build()) + .field(newFieldDefinition() + .name("custom") + .type(custom) + .build()) + .field(newFieldDefinition() + .name("status") + .type(status) + .build()) + .build() + GraphQLObjectType mutation = newObject() + .name("RootMutation") + .field(newFieldDefinition() + .name("change") + .type(first) + .build()) + .build() + GraphQLObjectType subscription = newObject() + .name("RootSubscription") + .field(newFieldDefinition() + .name("changed") + .type(second) + .build()) + .build() + TypeResolver firstResultResolver = { environment -> first } as TypeResolver + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + .typeResolver(node, firstResultResolver) + .typeResolver(resource, firstResultResolver) + .typeResolver(search, firstResultResolver) + .build() + + return GraphQLSchema.newSchema() + .description("Programmatic\nschema") + .query(query) + .mutation(mutation) + .subscription(subscription) + .additionalDirective(tagDefinition) + .withSchemaAppliedDirective(appliedTag) + .codeRegistry(codeRegistry) + .build() + } + + private static GraphQLSchema minimalSchema() { + def query = newObject() + .name("Query") + .field(newFieldDefinition() + .name("value") + .type(Scalars.GraphQLString) + .build()) + .build() + return GraphQLSchema.newSchema() + .query(query) + .build() + } + + private static String commentsEverywhereSdl() { + return ''' + # directive comment + directive @tag( + # directive argument comment + label: String + ) on OBJECT + + # scalar comment + scalar Custom + + # interface comment + interface Node { + # interface field comment + id( + # interface argument comment + format: String + ): ID + } + + # object comment + type First implements Node { + id(format: String): ID + } + + type Second implements Node { + id(format: String): ID + } + + # union comment + union Search = First | Second + + # enum comment + enum Status { + # enum value comment + OLD + } + + # input object comment + input Filter { + # input field comment + value: String + } + + type Query { + search(filter: Filter, status: Status): Search + custom: Custom + } + ''' + } +} From 4c48f6ed2ce47bd8c359fb6b8fc6a93db0deeffa Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 4 Aug 2026 00:25:55 +0000 Subject: [PATCH 05/32] Clarify schema universe lifecycle --- docs/schema-universe.md | 19 ++++++++++++++----- .../graphql/schema/universe/SUSchema.java | 6 +++++- .../schema/universe/SchemaUniverse.java | 17 ++++++++++++----- .../graphql/schema/universe/SUTest.groovy | 9 +++++++-- 4 files changed, 38 insertions(+), 13 deletions(-) diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 1464e9664c..e8a2fa7940 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -32,6 +32,10 @@ Every successfully built snapshot registers itself with its universe under its u `getSchemasByName()` return immutable views in registration order. Imports and transformations use the same registration path. Reusing a schema name in one universe is rejected. +Registry membership defines a snapshot's supported lifetime. Removing a snapshot ends that lifetime +even if application code still holds a reference to the `SUSchema`. Removal does not immediately +reclaim its vertices, but no traversal, export, or transformation is guaranteed after removal. + The snapshot can retain adjacency for a currently unreachable vertex. This dormant adjacency is not part of the effective rooted graph; it allows a later derived schema to reattach an unchanged subgraph at constant edit scope. Consequently, `getStoredEdgeCount()` is a storage count rather than @@ -182,15 +186,20 @@ intrinsic payloads before round-trip conversion can be lossless. ## Lifecycle and compaction -The prototype is append-only and retains history. Production lifecycle support should use one of -these policies: +The prototype currently retains vertex history after schemas are removed. A schema is live exactly +while its exact instance is present in the universe registry; holding an external Java reference +does not extend its lifetime. Production lifecycle support should use one of these policies: 1. Retain all versions for a bounded universe. -2. Reference-count snapshots and periodically mark vertices/adjacency reachable from live roots. +2. Periodically mark vertices and stored adjacency used by registered snapshots, then reclaim the + remainder. 3. Compact live schemas into a new universe, optionally using CSR or a succinct frozen format. -Compaction is preferable to per-edit reference counting unless schema deletion is frequent, because -reference counts add writes and contention to every transformation. +Marking must include dormant adjacency retained by a registered snapshot, not only the effective +graph reachable from its root. Otherwise cleanup would break the supported constant-scope +reattachment of unchanged subgraphs. Compaction is preferable to per-edit reference counting unless +schema deletion is frequent, because reference counts add writes and contention to every +transformation. ## Required measurements diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index cfeb133f70..a55b865397 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -17,7 +17,11 @@ import static graphql.Assert.assertTrue; /** - * An immutable graph view selecting one schema from a {@link SchemaUniverse}. + * An immutable graph view selecting one registered schema from a {@link SchemaUniverse}. + * + *

This schema is supported only while its exact instance remains registered in its universe. + * Removing it ends that supported lifetime even if application code retains a reference. Methods + * are not guaranteed to remain usable after removal, and its vertices may later be reclaimed.

*/ @ExperimentalApi @NullMarked diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java index 9df787ae4a..43142f2330 100644 --- a/src/main/java/graphql/schema/universe/SchemaUniverse.java +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -33,8 +33,9 @@ * A store of append-only schema vertices shared by many immutable schema snapshots. * *

A universe is safe for concurrent readers. Vertex creation is serialized, while reading a - * published vertex or schema does not require locking. Schemas can be removed from the universe's - * registry, but removing a schema does not remove its vertices from the universe.

+ * published vertex or schema does not require locking. A schema is supported only while its exact + * instance is registered in the universe. Removing a schema ends that supported lifetime, although + * its vertices are not reclaimed by the removal operation itself.

*/ @ExperimentalApi @NullMarked @@ -79,8 +80,9 @@ public SUSchema parseSchema(String name, String sdl) { /** * Removes and returns the registered schema with the given name. * - *

This only removes the schema from this universe's registry. The schema remains usable if - * it is referenced elsewhere, and its vertices remain stored in this universe.

+ *

Removal ends the schema's supported lifetime. Holding another reference to the returned + * schema does not keep it usable. Its vertices remain stored until a separate cleanup operation + * reclaims vertices that are unused by every registered schema.

* * @return the removed schema, or {@code null} if no schema has that name */ @@ -95,7 +97,9 @@ public SUSchema parseSchema(String name, String sdl) { /** * Removes the exact schema instance from this universe's registry. * - *

If its name has since been reused by another schema, that replacement is not removed.

+ *

If its name has since been reused by another schema, that replacement is not removed. + * Successful removal ends the removed schema's supported lifetime. Holding another reference + * to it does not keep it usable.

* * @return {@code true} if the schema was registered and removed */ @@ -406,6 +410,9 @@ public boolean owns(SUVertex vertex) { @Internal public SUSchemaBuilder transformBuilder(SUSchema schema, String name) { assertTrue(schema.getUniverse() == this, "Schema belongs to another universe"); + assertTrue( + schemasByName.get(schema.getName()) == schema, + "Schema is not registered in this universe"); return new SUSchemaBuilder( this, newSchemaRoot(name, schema.getRoot().getDescription()), diff --git a/src/test/groovy/graphql/schema/universe/SUTest.groovy b/src/test/groovy/graphql/schema/universe/SUTest.groovy index 0ed5dcecdc..3753450327 100644 --- a/src/test/groovy/graphql/schema/universe/SUTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUTest.groovy @@ -148,9 +148,14 @@ class SUTest extends Specification { universe.getSchema("first") == null universe.schemas == [second] universe.schemasByName == [second: second] - first.queryType.is(query) universe.removeSchema("missing") == null + when: + first.transform("fromRemoved", builder -> {}) + + then: + thrown(AssertException) + when: def replacement = second.transform("first", builder -> {}) @@ -174,7 +179,7 @@ class SUTest extends Specification { universe.schemasByName == [second: second] } - def "removing a schema does not remove its vertices"() { + def "removing a schema leaves its vertices stored until cleanup"() { given: def universe = new SchemaUniverse() def query = universe.newObjectType("Query") From e78d9e8e58732d443252acc7ddbd9abd21abca17 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 4 Aug 2026 00:58:12 +0000 Subject: [PATCH 06/32] Reclaim unused schema universe vertices --- docs/schema-universe.md | 36 ++-- .../schema/universe/EdgeMapBranch.java | 13 ++ .../schema/universe/EdgeMapEntryVisitor.java | 21 ++ .../graphql/schema/universe/EdgeMapLeaf.java | 12 ++ .../graphql/schema/universe/EdgeMapNode.java | 11 ++ .../schema/universe/PersistentEdgeMap.java | 23 +++ .../schema/universe/SchemaUniverse.java | 180 +++++++++++++++--- .../graphql/schema/universe/VertexChunk.java | 109 +++++++++++ .../universe/PersistentEdgeMapTest.groovy | 46 +++++ .../universe/SchemaUniverseCleanupTest.groovy | 177 +++++++++++++++++ 10 files changed, 584 insertions(+), 44 deletions(-) create mode 100644 src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java create mode 100644 src/main/java/graphql/schema/universe/VertexChunk.java create mode 100644 src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy create mode 100644 src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy diff --git a/docs/schema-universe.md b/docs/schema-universe.md index e8a2fa7940..252af00fd7 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -19,9 +19,10 @@ small edits. This is materially different from reconciling unrelated schemas. ## Model -`SchemaUniverse` is an append-only vertex arena. Every vertex receives a dense integer ID. There is -one Java vertex type for each supported GraphQL schema-element kind. A vertex contains intrinsic -scalar properties such as kind, name, and description, but no child references. +`SchemaUniverse` is a chunked vertex arena. Every vertex receives a monotonically increasing integer +ID. Cleanup can reclaim unused vertices and leave holes, but IDs are never reused. There is one Java +vertex type for each supported GraphQL schema-element kind. A vertex contains intrinsic scalar +properties such as kind, name, and description, but no child references. `SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex. The effective schema is the graph reachable from that root. Root edges identify operation roots, @@ -106,8 +107,8 @@ adjacency: | Publish `k` changed sources | `O(k log32 V)` | copied trie paths plus `k` arrays | | Derive with no type edits | effectively root replacement | one root array and two copied trie paths | -The append-only vertex arena uses chunked arrays. Vertex creation is serialized; published vertex -and schema reads are lock-free. +The vertex arena uses chunked arrays. Vertex creation and cleanup are serialized; published vertex +and registered-schema reads are lock-free. ## Why this representation @@ -186,20 +187,25 @@ intrinsic payloads before round-trip conversion can be lossless. ## Lifecycle and compaction -The prototype currently retains vertex history after schemas are removed. A schema is live exactly -while its exact instance is present in the universe registry; holding an external Java reference -does not extend its lifetime. Production lifecycle support should use one of these policies: +Removing a schema does not immediately scan or reclaim vertices. A schema is live exactly while its +exact instance is present in the universe registry; holding an external Java reference does not +extend its lifetime. `cleanupUnusedVertices()` periodically reclaims vertices unused by registered +schemas. -1. Retain all versions for a bounded universe. -2. Periodically mark vertices and stored adjacency used by registered snapshots, then reclaim the - remainder. -3. Compact live schemas into a new universe, optionally using CSR or a succinct frozen format. +Cleanup marks every registered root and every source and target in the registered snapshots' +complete persistent edge maps. It identity-deduplicates structurally shared HAMT nodes, so unchanged +subtrees are scanned once rather than once per schema. Applied-directive argument type IDs are +marked separately because those references are intrinsic values rather than edges. Marking must include dormant adjacency retained by a registered snapshot, not only the effective graph reachable from its root. Otherwise cleanup would break the supported constant-scope -reattachment of unchanged subgraphs. Compaction is preferable to per-edit reference counting unless -schema deletion is frequent, because reference counts add writes and contention to every -transformation. +reattachment of unchanged subgraphs. The sweep clears unmarked arena slots and releases empty +chunks. Reclaimed IDs remain invalid permanently, while `getVertexCount()` reports retained +vertices. Cleanup must not overlap schema construction, import, or transformation; reads through +registered schemas can continue. + +For long-lived universes with enough churn that sparse IDs or the monotonic ID limit become +material, compacting registered schemas into a new universe remains a possible archival operation. ## Required measurements diff --git a/src/main/java/graphql/schema/universe/EdgeMapBranch.java b/src/main/java/graphql/schema/universe/EdgeMapBranch.java index 3d2a5b3710..ff67255130 100644 --- a/src/main/java/graphql/schema/universe/EdgeMapBranch.java +++ b/src/main/java/graphql/schema/universe/EdgeMapBranch.java @@ -5,6 +5,7 @@ import org.jspecify.annotations.Nullable; import java.util.Arrays; +import java.util.Set; /** * An immutable, bitmap-compressed 32-way branch in the persistent edge map. @@ -108,6 +109,18 @@ public EdgeMapNode put(int key, int hash, PackedEdgeSet value, int shift) { return removeChild(bit, index); } + @Override + public void visitEntries( + Set visitedNodes, + EdgeMapEntryVisitor visitor) { + if (!visitedNodes.add(this)) { + return; + } + for (EdgeMapNode child : children) { + child.visitEntries(visitedNodes, visitor); + } + } + private EdgeMapNode insert(int bit, int index, EdgeMapNode child) { EdgeMapNode[] newChildren = new EdgeMapNode[children.length + 1]; System.arraycopy(children, 0, newChildren, 0, index); diff --git a/src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java b/src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java new file mode 100644 index 0000000000..010e6a1471 --- /dev/null +++ b/src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java @@ -0,0 +1,21 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +/** + * Visits one source binding in a {@link PersistentEdgeMap}. + */ +@Internal +@NullMarked +@FunctionalInterface +public interface EdgeMapEntryVisitor { + + /** + * Visits the complete outgoing edge set stored for one source vertex. + * + * @param sourceId the source vertex ID + * @param edges the complete non-empty outgoing edge set + */ + void visit(int sourceId, PackedEdgeSet edges); +} diff --git a/src/main/java/graphql/schema/universe/EdgeMapLeaf.java b/src/main/java/graphql/schema/universe/EdgeMapLeaf.java index 6346269be3..b2001d9b07 100644 --- a/src/main/java/graphql/schema/universe/EdgeMapLeaf.java +++ b/src/main/java/graphql/schema/universe/EdgeMapLeaf.java @@ -4,6 +4,8 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.Set; + /** * An immutable terminal mapping from one source vertex ID to its outgoing edges. * @@ -51,6 +53,16 @@ public EdgeMapNode put(int requestedKey, int requestedHash, PackedEdgeSet reques return key == requestedKey ? null : this; } + @Override + public void visitEntries( + Set visitedNodes, + EdgeMapEntryVisitor visitor) { + if (!visitedNodes.add(this)) { + return; + } + visitor.visit(key, value); + } + /** * Returns the mixed hash used to place this leaf in the trie. * diff --git a/src/main/java/graphql/schema/universe/EdgeMapNode.java b/src/main/java/graphql/schema/universe/EdgeMapNode.java index 30129bdaa5..11bcfad9c2 100644 --- a/src/main/java/graphql/schema/universe/EdgeMapNode.java +++ b/src/main/java/graphql/schema/universe/EdgeMapNode.java @@ -4,6 +4,8 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.Set; + /** * A node in the persistent map from source vertex IDs to their complete outgoing edge sets. * @@ -53,4 +55,13 @@ public interface EdgeMapNode { * @return this node when the key is absent, a replacement node, or {@code null} if empty */ @Nullable EdgeMapNode remove(int key, int hash, int shift); + + /** + * Visits entries below this node unless the node was already visited through another + * structurally shared map. + * + * @param visitedNodes identity-based set of previously visited nodes + * @param visitor the entry visitor + */ + void visitEntries(Set visitedNodes, EdgeMapEntryVisitor visitor); } diff --git a/src/main/java/graphql/schema/universe/PersistentEdgeMap.java b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java index ce3e88dd0b..ff8b2037df 100644 --- a/src/main/java/graphql/schema/universe/PersistentEdgeMap.java +++ b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java @@ -4,6 +4,10 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.Set; + +import static graphql.Assert.assertNotNull; + /** * An immutable map from source vertex IDs to complete outgoing {@link PackedEdgeSet}s. * @@ -94,6 +98,25 @@ public PersistentEdgeMap remove(int key) { return root == newRoot ? this : new PersistentEdgeMap(newRoot); } + /** + * Visits every entry not already covered by a structurally shared node in another map. + * + *

The supplied set must use identity equality. Reusing it across schema snapshots avoids + * revisiting unchanged HAMT subtrees.

+ * + * @param visitedNodes identity-based set of previously visited nodes + * @param visitor the entry visitor + */ + public void visitUniqueEntries( + Set visitedNodes, + EdgeMapEntryVisitor visitor) { + assertNotNull(visitedNodes); + assertNotNull(visitor); + if (root != null) { + root.visitEntries(visitedNodes, visitor); + } + } + private static int mix(int value) { int result = value; result ^= result >>> 16; diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java index 43142f2330..f0242c9c3f 100644 --- a/src/main/java/graphql/schema/universe/SchemaUniverse.java +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -1,5 +1,6 @@ package graphql.schema.universe; +import graphql.AssertException; import graphql.ExperimentalApi; import graphql.Internal; import graphql.introspection.Introspection.DirectiveLocation; @@ -15,8 +16,10 @@ import org.jspecify.annotations.Nullable; import java.util.Arrays; +import java.util.BitSet; import java.util.Collections; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -30,12 +33,13 @@ import static graphql.util.Interning.intern; /** - * A store of append-only schema vertices shared by many immutable schema snapshots. + * A store of schema vertices shared by many immutable schema snapshots. * *

A universe is safe for concurrent readers. Vertex creation is serialized, while reading a * published vertex or schema does not require locking. A schema is supported only while its exact * instance is registered in the universe. Removing a schema ends that supported lifetime, although - * its vertices are not reclaimed by the removal operation itself.

+ * its vertices are not reclaimed by the removal operation itself. Vertex IDs increase + * monotonically and are never reused, including after unused vertices are reclaimed.

*/ @ExperimentalApi @NullMarked @@ -49,8 +53,10 @@ public final class SchemaUniverse { private final Map nameIds = new ConcurrentHashMap<>(); private final Map schemasByName = new ConcurrentHashMap<>(); private final ConcurrentLinkedQueue schemas = new ConcurrentLinkedQueue<>(); - private volatile SUVertex[][] vertexChunks = new SUVertex[1][]; + private volatile VertexChunk[] vertexChunks = + new VertexChunk[]{VertexChunk.empty()}; private volatile int vertexCount; + private volatile int nextVertexId; private int nextNameId = 1; public SUSchemaBuilder newSchema(String name) { @@ -125,13 +131,39 @@ public Map getSchemasByName() { return Collections.unmodifiableMap(result); } + /** + * Reclaims vertices that are unused by every registered schema. + * + *

Liveness includes each registered schema root, every source and target in its complete + * stored adjacency, and input types referenced by applied-directive arguments. Complete stored + * adjacency is considered so dormant subgraphs remain available for later transformations.

+ * + *

Reclaimed vertex IDs are never reused. This operation may therefore leave holes in the + * chunked vertex arena. It must not run concurrently with schema construction, importing, or + * transformation. Registered schemas may be read concurrently.

+ * + * @return the number of vertices reclaimed + */ + public synchronized int cleanupUnusedVertices() { + BitSet liveVertexIds = new BitSet(); + Set visitedNodes = + Collections.newSetFromMap(new IdentityHashMap<>()); + for (SUSchema schema : schemas) { + markVertex(liveVertexIds, schema.getRoot().getId()); + schema.getEdgeMap().visitUniqueEntries( + visitedNodes, + (sourceId, edges) -> markEdgeBinding(liveVertexIds, sourceId, edges)); + } + return sweepUnusedVertices(liveVertexIds); + } + public synchronized SUObjectType newObjectType(String name) { return newObjectType(name, null); } public synchronized SUObjectType newObjectType(String name, @Nullable String description) { String validName = named(name); - SUObjectType vertex = new SUObjectType(vertexCount, nameId(validName), validName, description); + SUObjectType vertex = new SUObjectType(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -141,7 +173,7 @@ public synchronized SUField newField(String name) { public synchronized SUField newField(String name, @Nullable String description) { String validName = named(name); - SUField vertex = new SUField(vertexCount, nameId(validName), validName, description); + SUField vertex = new SUField(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -151,7 +183,7 @@ public synchronized SUInterfaceType newInterfaceType(String name) { public synchronized SUInterfaceType newInterfaceType(String name, @Nullable String description) { String validName = named(name); - SUInterfaceType vertex = new SUInterfaceType(vertexCount, nameId(validName), validName, description); + SUInterfaceType vertex = new SUInterfaceType(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -161,7 +193,7 @@ public synchronized SUUnionType newUnionType(String name) { public synchronized SUUnionType newUnionType(String name, @Nullable String description) { String validName = named(name); - SUUnionType vertex = new SUUnionType(vertexCount, nameId(validName), validName, description); + SUUnionType vertex = new SUUnionType(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -171,7 +203,7 @@ public synchronized SUEnumType newEnumType(String name) { public synchronized SUEnumType newEnumType(String name, @Nullable String description) { String validName = named(name); - SUEnumType vertex = new SUEnumType(vertexCount, nameId(validName), validName, description); + SUEnumType vertex = new SUEnumType(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -181,7 +213,7 @@ public synchronized SUEnumValue newEnumValue(String name) { public synchronized SUEnumValue newEnumValue(String name, @Nullable String description) { String validName = named(name); - SUEnumValue vertex = new SUEnumValue(vertexCount, nameId(validName), validName, description); + SUEnumValue vertex = new SUEnumValue(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -191,7 +223,7 @@ public synchronized SUScalarType newScalarType(String name) { public synchronized SUScalarType newScalarType(String name, @Nullable String description) { String validName = named(name); - SUScalarType vertex = new SUScalarType(vertexCount, nameId(validName), validName, description); + SUScalarType vertex = new SUScalarType(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -201,7 +233,7 @@ public synchronized SUInputObjectType newInputObjectType(String name) { public synchronized SUInputObjectType newInputObjectType(String name, @Nullable String description) { String validName = named(name); - SUInputObjectType vertex = new SUInputObjectType(vertexCount, nameId(validName), validName, description); + SUInputObjectType vertex = new SUInputObjectType(nextVertexId, nameId(validName), validName, description); return append(vertex); } @@ -227,7 +259,7 @@ public synchronized SUInputField newInputField( @Nullable InputValueDefinition definition) { String validName = named(name); SUInputField vertex = new SUInputField( - vertexCount, + nextVertexId, nameId(validName), validName, description, @@ -258,7 +290,7 @@ public synchronized SUArgument newArgument( @Nullable InputValueDefinition definition) { String validName = named(name); SUArgument vertex = new SUArgument( - vertexCount, + nextVertexId, nameId(validName), validName, description, @@ -291,7 +323,7 @@ public synchronized SUDirective newDirective( @Nullable DirectiveDefinition definition) { String validName = named(name); SUDirective vertex = new SUDirective( - vertexCount, + nextVertexId, nameId(validName), validName, description, @@ -336,7 +368,7 @@ public synchronized SUAppliedDirective newAppliedDirective( } SUAppliedDirective vertex = new SUAppliedDirective( - vertexCount, + nextVertexId, nameId(validName), validName, definition, @@ -377,22 +409,41 @@ public synchronized SUAppliedDirectiveArgument newAppliedDirectiveArgument( } public synchronized SUListType newListType() { - return append(new SUListType(vertexCount)); + return append(new SUListType(nextVertexId)); } public synchronized SUNonNullType newNonNullType() { - return append(new SUNonNullType(vertexCount)); + return append(new SUNonNullType(nextVertexId)); } + /** + * Returns the number of vertices currently retained by this universe. + * + *

This is not an ID allocation bound because cleanup can leave holes and IDs are never + * reused.

+ * + * @return the number of retained vertices + */ public int getVertexCount() { return vertexCount; } + /** + * Returns a retained vertex by its stable ID. + * + * @param id the vertex ID + * + * @return the retained vertex + * + * @throws AssertException if the ID is unknown or has been reclaimed + */ public SUVertex getVertex(int id) { - int currentSize = vertexCount; - assertTrue(id >= 0 && id < currentSize, "Unknown schema universe vertex id %s", id); - SUVertex[][] chunks = vertexChunks; - return assertNotNull(chunks[id >>> VERTEX_CHUNK_SHIFT][id & VERTEX_CHUNK_MASK]); + int currentLimit = nextVertexId; + assertTrue(id >= 0 && id < currentLimit, "Unknown schema universe vertex id %s", id); + return assertNotNull( + vertexOrNull(id), + "Schema universe vertex id %s has been reclaimed", + id); } @Internal @@ -404,7 +455,7 @@ public int getNameId(String name) { @Internal public boolean owns(SUVertex vertex) { int id = vertex.getId(); - return id >= 0 && id < vertexCount && getVertex(id) == vertex; + return id >= 0 && id < nextVertexId && vertexOrNull(id) == vertex; } @Internal @@ -431,7 +482,7 @@ public synchronized void registerSchema(SUSchema schema) { private synchronized SUSchemaRoot newSchemaRoot(String name, @Nullable String description) { String validName = named(name); - SUSchemaRoot root = new SUSchemaRoot(vertexCount, nameId(validName), validName, description); + SUSchemaRoot root = new SUSchemaRoot(nextVertexId, nameId(validName), validName, description); return append(root); } @@ -460,21 +511,92 @@ private boolean isInputType(SUVertexKind kind) { private T append(T vertex) { int id = vertex.getId(); + assertTrue(id == nextVertexId, "Unexpected schema universe vertex id %s", id); int chunkIndex = id >>> VERTEX_CHUNK_SHIFT; ensureChunk(chunkIndex); - vertexChunks[chunkIndex][id & VERTEX_CHUNK_MASK] = vertex; - vertexCount = id + 1; + vertexChunks[chunkIndex].set(id & VERTEX_CHUNK_MASK, vertex); + nextVertexId = id + 1; + vertexCount++; return vertex; } + private void markEdgeBinding( + BitSet liveVertexIds, + int sourceId, + PackedEdgeSet edges) { + markVertex(liveVertexIds, sourceId); + for (int i = 0; i < edges.size(); i++) { + markVertex(liveVertexIds, edges.targetIdAt(i)); + } + } + + private void markVertex(BitSet liveVertexIds, int id) { + if (liveVertexIds.get(id)) { + return; + } + SUVertex vertex = getVertex(id); + liveVertexIds.set(id); + if (!(vertex instanceof SUAppliedDirective)) { + return; + } + SUAppliedDirective directive = (SUAppliedDirective) vertex; + for (SUAppliedDirectiveArgument argument : directive.getArguments()) { + markVertex(liveVertexIds, argument.getTypeId()); + } + } + + private int sweepUnusedVertices(BitSet liveVertexIds) { + VertexChunk[] chunks = vertexChunks; + int removed = 0; + int lastOccupiedChunk = -1; + for (int chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { + VertexChunk chunk = chunks[chunkIndex]; + if (!chunk.isAllocated()) { + continue; + } + int firstId = chunkIndex << VERTEX_CHUNK_SHIFT; + int slotCount = Math.min(VERTEX_CHUNK_SIZE, nextVertexId - firstId); + removed += chunk.reclaimUnmarked(liveVertexIds, firstId, slotCount); + if (chunk.hasVertices()) { + lastOccupiedChunk = chunkIndex; + continue; + } + chunks[chunkIndex] = VertexChunk.empty(); + } + int retainedChunkCount = Math.max(1, lastOccupiedChunk + 1); + if (retainedChunkCount < chunks.length) { + chunks = Arrays.copyOf(chunks, retainedChunkCount); + } + vertexCount -= removed; + vertexChunks = chunks; + return removed; + } + + private @Nullable SUVertex vertexOrNull(int id) { + VertexChunk[] chunks = vertexChunks; + int chunkIndex = id >>> VERTEX_CHUNK_SHIFT; + if (chunkIndex >= chunks.length) { + return null; + } + VertexChunk chunk = chunks[chunkIndex]; + if (!chunk.isAllocated()) { + return null; + } + return chunk.get(id & VERTEX_CHUNK_MASK); + } + private void ensureChunk(int chunkIndex) { - SUVertex[][] chunks = vertexChunks; + VertexChunk[] chunks = vertexChunks; if (chunkIndex >= chunks.length) { - chunks = Arrays.copyOf(chunks, Math.max(chunkIndex + 1, chunks.length * 2)); + int oldLength = chunks.length; + chunks = Arrays.copyOf( + chunks, + Math.max(chunkIndex + 1, chunks.length * 2)); + Arrays.fill(chunks, oldLength, chunks.length, VertexChunk.empty()); vertexChunks = chunks; } - if (chunks[chunkIndex] == null) { - chunks[chunkIndex] = new SUVertex[VERTEX_CHUNK_SIZE]; + if (!chunks[chunkIndex].isAllocated()) { + chunks[chunkIndex] = new VertexChunk(VERTEX_CHUNK_SIZE); } } } diff --git a/src/main/java/graphql/schema/universe/VertexChunk.java b/src/main/java/graphql/schema/universe/VertexChunk.java new file mode 100644 index 0000000000..1082853073 --- /dev/null +++ b/src/main/java/graphql/schema/universe/VertexChunk.java @@ -0,0 +1,109 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.BitSet; + +/** + * A fixed-size arena chunk whose vertex slots may be reclaimed. + */ +@Internal +@NullMarked +public final class VertexChunk { + + private static final VertexChunk EMPTY = new VertexChunk(0); + + private final @Nullable SUVertex[] vertices; + private int retainedCount; + + /** + * Creates an allocated chunk with the requested number of nullable slots. + * + * @param size the number of slots + */ + public VertexChunk(int size) { + this.vertices = new @Nullable SUVertex[size]; + } + + /** + * Returns the shared unallocated chunk marker. + * + * @return the empty chunk + */ + public static VertexChunk empty() { + return EMPTY; + } + + /** + * Returns whether this chunk has allocated slots. + * + * @return {@code true} for an allocated chunk + */ + public boolean isAllocated() { + return vertices.length != 0; + } + + /** + * Returns one vertex slot. + * + * @param index the index within the chunk + * + * @return the stored vertex, or {@code null} for a reclaimed slot + */ + public @Nullable SUVertex get(int index) { + return vertices[index]; + } + + /** + * Updates one vertex slot. + * + * @param index the index within the chunk + * @param vertex the vertex, or {@code null} to reclaim the slot + */ + public void set(int index, @Nullable SUVertex vertex) { + SUVertex previous = vertices[index]; + if (previous == null && vertex != null) { + retainedCount++; + } + if (previous != null && vertex == null) { + retainedCount--; + } + vertices[index] = vertex; + } + + /** + * Reclaims every slot not marked in the supplied live-ID set. + * + * @param liveVertexIds IDs retained by registered schemas + * @param firstId the global ID of slot zero + * @param slotCount the allocated slots within the current ID watermark + * + * @return the number of slots reclaimed + */ + public int reclaimUnmarked( + BitSet liveVertexIds, + int firstId, + int slotCount) { + int removed = 0; + for (int offset = 0; offset < slotCount; offset++) { + SUVertex vertex = vertices[offset]; + if (vertex == null || liveVertexIds.get(firstId + offset)) { + continue; + } + set(offset, null); + removed++; + } + return removed; + } + + /** + * Returns whether this chunk contains any retained vertices. + * + * @return {@code true} when at least one slot is occupied + */ + public boolean hasVertices() { + return retainedCount != 0; + } +} diff --git a/src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy b/src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy new file mode 100644 index 0000000000..e821a74ba7 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy @@ -0,0 +1,46 @@ +package graphql.schema.universe + +import spock.lang.Specification + +class PersistentEdgeMapTest extends Specification { + + def "unique entry traversal skips structurally shared nodes"() { + given: + def firstEdges = edgesTo(101) + def secondEdges = edgesTo(102) + def thirdEdges = edgesTo(103) + def base = PersistentEdgeMap.empty() + .put(1, firstEdges) + .put(2, secondEdges) + def changed = base.put(3, thirdEdges) + def visitedNodes = Collections.newSetFromMap(new IdentityHashMap()) + def visitedEntries = [] + + when: + PersistentEdgeMap.empty().visitUniqueEntries( + visitedNodes, + (sourceId, edges) -> visitedEntries.add([sourceId, edges])) + base.visitUniqueEntries( + visitedNodes, + (sourceId, edges) -> visitedEntries.add([sourceId, edges])) + changed.visitUniqueEntries( + visitedNodes, + (sourceId, edges) -> visitedEntries.add([sourceId, edges])) + changed.visitUniqueEntries( + visitedNodes, + (sourceId, edges) -> visitedEntries.add([sourceId, edges])) + + then: + visitedEntries.size() == 3 + visitedEntries.collect { it[0] }.toSet() == [1, 2, 3] as Set + visitedEntries.find { it[0] == 1 }[1].is(firstEdges) + visitedEntries.find { it[0] == 2 }[1].is(secondEdges) + visitedEntries.find { it[0] == 3 }[1].is(thirdEdges) + } + + private static PackedEdgeSet edgesTo(int targetId) { + new PackedEdgeSet([ + PackedEdgeSet.pack(SUEdgeKind.TYPE, 1, targetId) + ] as long[]) + } +} diff --git a/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy new file mode 100644 index 0000000000..1c4566bdda --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy @@ -0,0 +1,177 @@ +package graphql.schema.universe + +import graphql.AssertException +import graphql.schema.InputValueWithState +import spock.lang.Specification + +class SchemaUniverseCleanupTest extends Specification { + + def "cleanup reclaims removed and detached vertices without affecting registered schemas"() { + given: + def universe = new SchemaUniverse() + def sharedString = universe.newScalarType("String") + def firstQuery = universe.newObjectType("FirstQuery") + def firstField = universe.newField("first") + def first = universe.newSchema("first") + .queryType(firstQuery) + .addField(firstQuery, firstField) + .setFieldType(firstField, sharedString) + .build() + def secondQuery = universe.newObjectType("SecondQuery") + def secondField = universe.newField("second") + def second = universe.newSchema("second") + .queryType(secondQuery) + .addField(secondQuery, secondField) + .setFieldType(secondField, sharedString) + .build() + def detached = universe.newEnumType("Detached") + def firstRoot = first.root + + when: + universe.removeSchema(first) + def reclaimed = universe.cleanupUnusedVertices() + + then: + reclaimed == 4 + universe.vertexCount == 4 + universe.cleanupUnusedVertices() == 0 + universe.owns(sharedString) + universe.owns(second.root) + universe.owns(secondQuery) + universe.owns(secondField) + !universe.owns(firstRoot) + !universe.owns(firstQuery) + !universe.owns(firstField) + !universe.owns(detached) + second.queryType.is(secondQuery) + second.getField(secondQuery, "second").is(secondField) + second.getType(secondField).is(sharedString) + + when: + universe.getVertex(firstQuery.id) + + then: + thrown(AssertException) + + when: + def next = universe.newEnumType("Next") + + then: + next.id > detached.id + universe.vertexCount == 5 + universe.getVertex(next.id).is(next) + } + + def "cleanup retains dormant adjacency stored by a registered schema"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def extra = universe.newObjectType("Extra") + def field = universe.newField("value") + def string = universe.newScalarType("String") + def base = universe.newSchema("base") + .queryType(query) + .addAdditionalType(extra) + .addField(extra, field) + .setFieldType(field, string) + .build() + def withoutExtra = base.transform( + "withoutExtra", + builder -> builder.removeAdditionalType(extra)) + def baseRoot = base.root + + expect: + universe.cleanupUnusedVertices() == 0 + + when: + universe.removeSchema(base) + def reclaimed = universe.cleanupUnusedVertices() + + then: + reclaimed == 1 + !universe.owns(baseRoot) + universe.owns(extra) + universe.owns(field) + universe.owns(string) + withoutExtra.getType("Extra") == null + + when: + def reattached = withoutExtra.transform( + "reattached", + builder -> builder.addAdditionalType(extra)) + + then: + reattached.getObjectType("Extra").is(extra) + reattached.getField(extra, "value").is(field) + reattached.getType(field).is(string) + } + + def "cleanup retains types referenced by applied directive arguments"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def string = universe.newScalarType("String") + def orphan = universe.newScalarType("Orphan") + def argument = universe.newAppliedDirectiveArgument( + "value", + string, + InputValueWithState.newExternalValue("tagged")) + def appliedDirective = universe.newAppliedDirective("tag", [argument]) + def schema = universe.newSchema("schema") + .queryType(query) + .addAppliedDirective(query, appliedDirective) + .build() + + when: + def reclaimed = universe.cleanupUnusedVertices() + + then: + reclaimed == 1 + !universe.owns(orphan) + universe.owns(string) + schema.getType(argument).is(string) + schema.getAppliedDirectives(query) == [appliedDirective] + } + + def "cleanup releases empty chunks and allocation continues above reclaimed ids"() { + given: + def universe = new SchemaUniverse() + def vertices = (0..1024).collect { + universe.newField("field${it}") + } + def lastId = vertices.last().id + + when: + def reclaimed = universe.cleanupUnusedVertices() + + then: + reclaimed == 1025 + universe.vertexCount == 0 + !universe.owns(vertices.first()) + !universe.owns(vertices.last()) + + when: + universe.getVertex(lastId) + + then: + thrown(AssertException) + + when: + def next = universe.newObjectType("Next") + + then: + next.id == lastId + 1 + universe.vertexCount == 1 + universe.getVertex(next.id).is(next) + + when: + def schema = universe.newSchema("next") + .queryType(next) + .build() + + then: + universe.cleanupUnusedVertices() == 0 + universe.vertexCount == 2 + schema.queryType.is(next) + } +} From f70bfb4c80ece5dbffa82c9a00268623a8e12e97 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 4 Aug 2026 01:33:51 +0000 Subject: [PATCH 07/32] Add per-schema vertex metadata --- docs/schema-universe.md | 46 +++-- .../schema/universe/EdgeMapBranch.java | 156 --------------- .../schema/universe/EdgeMapEntryVisitor.java | 21 -- .../graphql/schema/universe/EdgeMapLeaf.java | 74 ------- .../graphql/schema/universe/EdgeMapNode.java | 67 ------- .../graphql/schema/universe/IntMapBranch.java | 168 ++++++++++++++++ .../schema/universe/IntMapEntryVisitor.java | 23 +++ .../graphql/schema/universe/IntMapLeaf.java | 77 ++++++++ .../graphql/schema/universe/IntMapNode.java | 65 ++++++ .../schema/universe/PersistentEdgeMap.java | 29 +-- .../schema/universe/PersistentIntMap.java | 126 ++++++++++++ .../graphql/schema/universe/SUSchema.java | 40 +++- .../schema/universe/SUSchemaBuilder.java | 173 +++++++++++++++- .../schema/universe/SchemaUniverse.java | 14 +- .../universe/PersistentEdgeMapTest.groovy | 3 +- .../universe/PersistentIntMapTest.groovy | 87 ++++++++ .../universe/SUSchemaMetadataTest.groovy | 187 ++++++++++++++++++ 17 files changed, 999 insertions(+), 357 deletions(-) delete mode 100644 src/main/java/graphql/schema/universe/EdgeMapBranch.java delete mode 100644 src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java delete mode 100644 src/main/java/graphql/schema/universe/EdgeMapLeaf.java delete mode 100644 src/main/java/graphql/schema/universe/EdgeMapNode.java create mode 100644 src/main/java/graphql/schema/universe/IntMapBranch.java create mode 100644 src/main/java/graphql/schema/universe/IntMapEntryVisitor.java create mode 100644 src/main/java/graphql/schema/universe/IntMapLeaf.java create mode 100644 src/main/java/graphql/schema/universe/IntMapNode.java create mode 100644 src/main/java/graphql/schema/universe/PersistentIntMap.java create mode 100644 src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 252af00fd7..76b0d572f3 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -24,9 +24,9 @@ ID. Cleanup can reclaim unused vertices and leave holes, but IDs are never reuse vertex type for each supported GraphQL schema-element kind. A vertex contains intrinsic scalar properties such as kind, name, and description, but no child references. -`SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex. The -effective schema is the graph reachable from that root. Root edges identify operation roots, -non-root named types, and directive definitions. +`SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex and sparse +user-controlled metadata keyed by vertex ID. The effective schema is the graph reachable from that +root. Root edges identify operation roots, non-root named types, and directive definitions. Every successfully built snapshot registers itself with its universe under its unique name. `SchemaUniverse.getSchema(name)` selects one snapshot directly, while `getSchemas()` and @@ -91,10 +91,10 @@ Java integers. ## Persistent snapshots -Outgoing arrays are stored in a specialized persistent hash-array mapped trie keyed by source -vertex ID. A transform batches changes by source, freezes each changed outgoing array once, and -path-copies only the affected trie branches. All unchanged branches and outgoing arrays are shared -by identity with the parent schema. +Outgoing arrays and metadata associations are stored in persistent hash-array mapped tries keyed by +vertex ID. A transform batches changes, freezes each changed value once, and path-copies only the +affected trie branches. All unchanged branches, outgoing arrays, and metadata maps are shared by +identity with the parent schema. Approximate operation costs, where `d` is source out-degree and `V` is the number of sources with adjacency: @@ -103,9 +103,13 @@ adjacency: | --- | --- | --- | | Read outgoing adjacency | `O(log32 V)` | none | | Child lookup by name | `O(log32 V + log d)` unordered, `O(log32 V + d)` ordered | none | +| Read vertex metadata | `O(log32 M)` plus metadata-map lookup | none | | Add/remove edge | `O(d)` transient, then canonicalize at build | one changed primitive array | +| Edit vertex metadata | `O(log32 M + keys(vertex))` | one trie path and metadata map | | Publish `k` changed sources | `O(k log32 V)` | copied trie paths plus `k` arrays | -| Derive with no type edits | effectively root replacement | one root array and two copied trie paths | +| Derive with no type edits | effectively root replacement | root edge paths, plus metadata paths when the root is annotated | + +Here `M` is the number of vertices with metadata in the snapshot. The vertex arena uses chunked arrays. Vertex creation and cleanup are serialized; published vertex and registered-schema reads are lock-free. @@ -163,6 +167,23 @@ with respect to a mutable value supplied by an application. Directive repeatability and valid locations are also intrinsic. Locations are stored as a compact bit mask. Directive-definition and applied-directive AST nodes are retained when available. +## User metadata + +Each `SUSchema` can associate an immutable `Map` with any vertex in its universe. +The association belongs to the schema snapshot, so schemas sharing the same vertex may expose +different metadata. Transformations structurally share unchanged metadata and automatically move +schema-root metadata to the derived schema's new root. + +The map is copied when supplied and rejects null keys and values. Values are retained by reference, +so immutability is shallow with respect to mutable application objects. Metadata has no GraphQL +semantics, is not included in SDL or `GraphQLSchema` export, and is never interpreted by the +universe. Metadata values containing vertices do not create liveness references. + +A metadata entry itself does keep its keyed vertex live while the schema is registered. Removing +an edge does not remove metadata, allowing an annotation to remain dormant for later reattachment; +applications can explicitly clear metadata when that retention is unwanted. Applied-directive +arguments are embedded values rather than vertices and therefore cannot have independent metadata. + The unresolved policy question is whether a field's `TYPE` edge is part of field identity. The prototype permits a schema snapshot to select a different single `TYPE` edge for the same field vertex. If "specific element" includes its type, this operation should instead be rejected globally @@ -192,10 +213,11 @@ exact instance is present in the universe registry; holding an external Java ref extend its lifetime. `cleanupUnusedVertices()` periodically reclaims vertices unused by registered schemas. -Cleanup marks every registered root and every source and target in the registered snapshots' -complete persistent edge maps. It identity-deduplicates structurally shared HAMT nodes, so unchanged -subtrees are scanned once rather than once per schema. Applied-directive argument type IDs are -marked separately because those references are intrinsic values rather than edges. +Cleanup marks every registered root, every source and target in the registered snapshots' complete +persistent edge maps, and every vertex ID with schema metadata. It identity-deduplicates +structurally shared HAMT nodes, so unchanged subtrees are scanned once rather than once per schema. +Applied-directive argument type IDs are marked separately because those references are intrinsic +values rather than edges. Marking must include dormant adjacency retained by a registered snapshot, not only the effective graph reachable from its root. Otherwise cleanup would break the supported constant-scope diff --git a/src/main/java/graphql/schema/universe/EdgeMapBranch.java b/src/main/java/graphql/schema/universe/EdgeMapBranch.java deleted file mode 100644 index ff67255130..0000000000 --- a/src/main/java/graphql/schema/universe/EdgeMapBranch.java +++ /dev/null @@ -1,156 +0,0 @@ -package graphql.schema.universe; - -import graphql.Internal; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.Arrays; -import java.util.Set; - -/** - * An immutable, bitmap-compressed 32-way branch in the persistent edge map. - * - *

Each bit in {@code bitmap} denotes an occupied logical slot selected by five hash bits. - * {@code children} is a dense array containing only occupied slots, in ascending slot order. The - * array index for a slot is the number of set bits preceding it.

- * - *

Updates copy only this branch's dense child array and replace the affected child. All other - * child nodes remain shared with the previous map version.

- */ -@Internal -@NullMarked -public final class EdgeMapBranch implements EdgeMapNode { - - private final int bitmap; - private final EdgeMapNode[] children; - - /** - * Creates a branch from a bitmap and its dense, slot-ordered child array. - * - * @param bitmap the occupied logical slots - * @param children children for the occupied slots, in ascending slot order - */ - public EdgeMapBranch(int bitmap, EdgeMapNode[] children) { - this.bitmap = bitmap; - this.children = children; - } - - /** - * Builds the minimum branch path needed to distinguish two leaves. - * - *

If both hashes select the same slot at this depth, another single-child branch is created - * and the next five hash bits are examined. {@link PersistentEdgeMap}'s hash mixer is - * one-to-one for integer keys, so distinct source IDs eventually select different slots and no - * separate full-hash collision node is needed.

- * - * @param first the first leaf - * @param second the second leaf - * @param shift the number of hash bits already consumed - * - * @return a branch containing both leaves - */ - public static EdgeMapNode merge(EdgeMapLeaf first, EdgeMapLeaf second, int shift) { - int firstSlot = slot(first.getHash(), shift); - int secondSlot = slot(second.getHash(), shift); - if (firstSlot == secondSlot) { - EdgeMapNode child = merge(first, second, shift + 5); - return new EdgeMapBranch(bit(firstSlot), new EdgeMapNode[]{child}); - } - int combinedBitmap = bit(firstSlot) | bit(secondSlot); - EdgeMapNode[] children = firstSlot < secondSlot - ? new EdgeMapNode[]{first, second} - : new EdgeMapNode[]{second, first}; - return new EdgeMapBranch(combinedBitmap, children); - } - - @Override - public @Nullable PackedEdgeSet get(int key, int hash, int shift) { - int bit = bit(slot(hash, shift)); - if ((bitmap & bit) == 0) { - return null; - } - return children[index(bit)].get(key, hash, shift + 5); - } - - @Override - public EdgeMapNode put(int key, int hash, PackedEdgeSet value, int shift) { - int bit = bit(slot(hash, shift)); - int index = index(bit); - if ((bitmap & bit) == 0) { - return insert(bit, index, new EdgeMapLeaf(key, hash, value)); - } - EdgeMapNode oldChild = children[index]; - EdgeMapNode newChild = oldChild.put(key, hash, value, shift + 5); - if (oldChild == newChild) { - return this; - } - EdgeMapNode[] newChildren = Arrays.copyOf(children, children.length); - newChildren[index] = newChild; - return new EdgeMapBranch(bitmap, newChildren); - } - - @Override - public @Nullable EdgeMapNode remove(int key, int hash, int shift) { - int bit = bit(slot(hash, shift)); - if ((bitmap & bit) == 0) { - return this; - } - int index = index(bit); - EdgeMapNode oldChild = children[index]; - EdgeMapNode newChild = oldChild.remove(key, hash, shift + 5); - if (oldChild == newChild) { - return this; - } - if (newChild != null) { - EdgeMapNode[] newChildren = Arrays.copyOf(children, children.length); - newChildren[index] = newChild; - return new EdgeMapBranch(bitmap, newChildren); - } - return removeChild(bit, index); - } - - @Override - public void visitEntries( - Set visitedNodes, - EdgeMapEntryVisitor visitor) { - if (!visitedNodes.add(this)) { - return; - } - for (EdgeMapNode child : children) { - child.visitEntries(visitedNodes, visitor); - } - } - - private EdgeMapNode insert(int bit, int index, EdgeMapNode child) { - EdgeMapNode[] newChildren = new EdgeMapNode[children.length + 1]; - System.arraycopy(children, 0, newChildren, 0, index); - newChildren[index] = child; - System.arraycopy(children, index, newChildren, index + 1, children.length - index); - return new EdgeMapBranch(bitmap | bit, newChildren); - } - - private @Nullable EdgeMapNode removeChild(int bit, int index) { - if (children.length == 1) { - return null; - } - EdgeMapNode[] newChildren = new EdgeMapNode[children.length - 1]; - System.arraycopy(children, 0, newChildren, 0, index); - System.arraycopy(children, index + 1, newChildren, index, children.length - index - 1); - if (newChildren.length == 1 && newChildren[0] instanceof EdgeMapLeaf) { - return newChildren[0]; - } - return new EdgeMapBranch(bitmap ^ bit, newChildren); - } - - private int index(int bit) { - return Integer.bitCount(bitmap & (bit - 1)); - } - - private static int slot(int hash, int shift) { - return (hash >>> shift) & 31; - } - - private static int bit(int slot) { - return 1 << slot; - } -} diff --git a/src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java b/src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java deleted file mode 100644 index 010e6a1471..0000000000 --- a/src/main/java/graphql/schema/universe/EdgeMapEntryVisitor.java +++ /dev/null @@ -1,21 +0,0 @@ -package graphql.schema.universe; - -import graphql.Internal; -import org.jspecify.annotations.NullMarked; - -/** - * Visits one source binding in a {@link PersistentEdgeMap}. - */ -@Internal -@NullMarked -@FunctionalInterface -public interface EdgeMapEntryVisitor { - - /** - * Visits the complete outgoing edge set stored for one source vertex. - * - * @param sourceId the source vertex ID - * @param edges the complete non-empty outgoing edge set - */ - void visit(int sourceId, PackedEdgeSet edges); -} diff --git a/src/main/java/graphql/schema/universe/EdgeMapLeaf.java b/src/main/java/graphql/schema/universe/EdgeMapLeaf.java deleted file mode 100644 index b2001d9b07..0000000000 --- a/src/main/java/graphql/schema/universe/EdgeMapLeaf.java +++ /dev/null @@ -1,74 +0,0 @@ -package graphql.schema.universe; - -import graphql.Internal; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.Set; - -/** - * An immutable terminal mapping from one source vertex ID to its outgoing edges. - * - *

The mixed hash is retained so that two leaves can be separated into the required branch - * levels without hashing either key again. Leaves compare the original key on lookup, making the - * key authoritative after trie navigation has selected a candidate leaf.

- */ -@Internal -@NullMarked -public final class EdgeMapLeaf implements EdgeMapNode { - - private final int key; - private final int hash; - private final PackedEdgeSet value; - - /** - * Creates a leaf. - * - * @param key the source vertex ID - * @param hash the mixed key hash - * @param value the complete outgoing edge set for the source - */ - public EdgeMapLeaf(int key, int hash, PackedEdgeSet value) { - this.key = key; - this.hash = hash; - this.value = value; - } - - @Override - public @Nullable PackedEdgeSet get(int requestedKey, int requestedHash, int shift) { - return key == requestedKey ? value : null; - } - - @Override - public EdgeMapNode put(int requestedKey, int requestedHash, PackedEdgeSet requestedValue, int shift) { - if (key == requestedKey) { - return value == requestedValue ? this : new EdgeMapLeaf(key, hash, requestedValue); - } - EdgeMapLeaf other = new EdgeMapLeaf(requestedKey, requestedHash, requestedValue); - return EdgeMapBranch.merge(this, other, shift); - } - - @Override - public @Nullable EdgeMapNode remove(int requestedKey, int requestedHash, int shift) { - return key == requestedKey ? null : this; - } - - @Override - public void visitEntries( - Set visitedNodes, - EdgeMapEntryVisitor visitor) { - if (!visitedNodes.add(this)) { - return; - } - visitor.visit(key, value); - } - - /** - * Returns the mixed hash used to place this leaf in the trie. - * - * @return the mixed key hash - */ - public int getHash() { - return hash; - } -} diff --git a/src/main/java/graphql/schema/universe/EdgeMapNode.java b/src/main/java/graphql/schema/universe/EdgeMapNode.java deleted file mode 100644 index 11bcfad9c2..0000000000 --- a/src/main/java/graphql/schema/universe/EdgeMapNode.java +++ /dev/null @@ -1,67 +0,0 @@ -package graphql.schema.universe; - -import graphql.Internal; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.Set; - -/** - * A node in the persistent map from source vertex IDs to their complete outgoing edge sets. - * - *

The map is a bitmap-compressed hash-array mapped trie. Each branch consumes five bits of the - * mixed key hash, so {@code shift} advances by five on every recursive call. Implementations are - * immutable: an update returns either the same node for a no-op or a replacement node that shares - * all unaffected descendants.

- * - *

A nullable node result represents an empty subtree. A missing value is represented by - * {@code null} inside the trie and normalized to {@link PackedEdgeSet#empty()} by - * {@link PersistentEdgeMap}.

- */ -@Internal -@NullMarked -public interface EdgeMapNode { - - /** - * Finds the outgoing edges for {@code key}. - * - * @param key the source vertex ID - * @param hash the mixed key hash - * @param shift the number of hash bits already consumed - * - * @return the stored edge set, or {@code null} when the key is absent - */ - @Nullable PackedEdgeSet get(int key, int hash, int shift); - - /** - * Associates {@code key} with {@code value}. - * - * @param key the source vertex ID - * @param hash the mixed key hash - * @param value the complete non-empty outgoing edge set - * @param shift the number of hash bits already consumed - * - * @return this node for an identity no-op, otherwise a structurally shared replacement - */ - EdgeMapNode put(int key, int hash, PackedEdgeSet value, int shift); - - /** - * Removes {@code key}. - * - * @param key the source vertex ID - * @param hash the mixed key hash - * @param shift the number of hash bits already consumed - * - * @return this node when the key is absent, a replacement node, or {@code null} if empty - */ - @Nullable EdgeMapNode remove(int key, int hash, int shift); - - /** - * Visits entries below this node unless the node was already visited through another - * structurally shared map. - * - * @param visitedNodes identity-based set of previously visited nodes - * @param visitor the entry visitor - */ - void visitEntries(Set visitedNodes, EdgeMapEntryVisitor visitor); -} diff --git a/src/main/java/graphql/schema/universe/IntMapBranch.java b/src/main/java/graphql/schema/universe/IntMapBranch.java new file mode 100644 index 0000000000..5aca952bf3 --- /dev/null +++ b/src/main/java/graphql/schema/universe/IntMapBranch.java @@ -0,0 +1,168 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Arrays; +import java.util.Set; + +/** + * An immutable, bitmap-compressed 32-way branch in a persistent integer map. + * + * @param the stored value type + */ +@Internal +@NullMarked +public final class IntMapBranch implements IntMapNode { + + private final int bitmap; + private final IntMapNode[] children; + + /** + * Creates a branch from a bitmap and its dense, slot-ordered child array. + * + * @param bitmap the occupied logical slots + * @param children children for the occupied slots + */ + public IntMapBranch(int bitmap, IntMapNode[] children) { + this.bitmap = bitmap; + this.children = children; + } + + /** + * Builds the minimum branch path needed to distinguish two leaves. + * + * @param first the first leaf + * @param second the second leaf + * @param shift the number of hash bits already consumed + * @param the stored value type + * + * @return a branch containing both leaves + */ + public static IntMapNode merge( + IntMapLeaf first, + IntMapLeaf second, + int shift) { + int firstSlot = slot(first.getHash(), shift); + int secondSlot = slot(second.getHash(), shift); + if (firstSlot == secondSlot) { + IntMapNode child = merge(first, second, shift + 5); + return new IntMapBranch<>(bit(firstSlot), childArray(child)); + } + int combinedBitmap = bit(firstSlot) | bit(secondSlot); + IntMapNode[] children = firstSlot < secondSlot + ? childArray(first, second) + : childArray(second, first); + return new IntMapBranch<>(combinedBitmap, children); + } + + @Override + public @Nullable V get(int key, int hash, int shift) { + int bit = bit(slot(hash, shift)); + if ((bitmap & bit) == 0) { + return null; + } + return children[index(bit)].get(key, hash, shift + 5); + } + + @Override + public IntMapNode put(int key, int hash, V value, int shift) { + int bit = bit(slot(hash, shift)); + int index = index(bit); + if ((bitmap & bit) == 0) { + return insert(bit, index, new IntMapLeaf<>(key, hash, value)); + } + IntMapNode oldChild = children[index]; + IntMapNode newChild = oldChild.put(key, hash, value, shift + 5); + if (oldChild == newChild) { + return this; + } + IntMapNode[] newChildren = Arrays.copyOf(children, children.length); + newChildren[index] = newChild; + return new IntMapBranch<>(bitmap, newChildren); + } + + @Override + public @Nullable IntMapNode remove(int key, int hash, int shift) { + int bit = bit(slot(hash, shift)); + if ((bitmap & bit) == 0) { + return this; + } + int index = index(bit); + IntMapNode oldChild = children[index]; + IntMapNode newChild = oldChild.remove(key, hash, shift + 5); + if (oldChild == newChild) { + return this; + } + if (newChild != null) { + IntMapNode[] newChildren = Arrays.copyOf(children, children.length); + newChildren[index] = newChild; + return new IntMapBranch<>(bitmap, newChildren); + } + return removeChild(bit, index); + } + + @Override + public void visitEntries( + Set> visitedNodes, + IntMapEntryVisitor visitor) { + if (!visitedNodes.add(this)) { + return; + } + for (IntMapNode child : children) { + child.visitEntries(visitedNodes, visitor); + } + } + + private IntMapNode insert( + int bit, + int index, + IntMapNode child) { + IntMapNode[] newChildren = Arrays.copyOf(children, children.length + 1); + System.arraycopy(children, index, newChildren, index + 1, children.length - index); + newChildren[index] = child; + return new IntMapBranch<>(bitmap | bit, newChildren); + } + + private @Nullable IntMapNode removeChild(int bit, int index) { + if (children.length == 1) { + return null; + } + IntMapNode[] newChildren = Arrays.copyOf(children, children.length - 1); + System.arraycopy(children, index + 1, newChildren, index, children.length - index - 1); + if (newChildren.length == 1 && newChildren[0] instanceof IntMapLeaf) { + return newChildren[0]; + } + return new IntMapBranch<>(bitmap ^ bit, newChildren); + } + + private int index(int bit) { + return Integer.bitCount(bitmap & (bit - 1)); + } + + private static int slot(int hash, int shift) { + return (hash >>> shift) & 31; + } + + private static int bit(int slot) { + return 1 << slot; + } + + @SuppressWarnings("unchecked") + private static IntMapNode[] childArray(IntMapNode child) { + IntMapNode[] children = (IntMapNode[]) new IntMapNode[1]; + children[0] = child; + return children; + } + + @SuppressWarnings("unchecked") + private static IntMapNode[] childArray( + IntMapNode first, + IntMapNode second) { + IntMapNode[] children = (IntMapNode[]) new IntMapNode[2]; + children[0] = first; + children[1] = second; + return children; + } +} diff --git a/src/main/java/graphql/schema/universe/IntMapEntryVisitor.java b/src/main/java/graphql/schema/universe/IntMapEntryVisitor.java new file mode 100644 index 0000000000..6541b4e792 --- /dev/null +++ b/src/main/java/graphql/schema/universe/IntMapEntryVisitor.java @@ -0,0 +1,23 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; + +/** + * Visits one binding in a persistent integer map. + * + * @param the stored value type + */ +@Internal +@NullMarked +@FunctionalInterface +public interface IntMapEntryVisitor { + + /** + * Visits one key and value. + * + * @param key the integer key + * @param value the stored value + */ + void visit(int key, V value); +} diff --git a/src/main/java/graphql/schema/universe/IntMapLeaf.java b/src/main/java/graphql/schema/universe/IntMapLeaf.java new file mode 100644 index 0000000000..ceccdd04d9 --- /dev/null +++ b/src/main/java/graphql/schema/universe/IntMapLeaf.java @@ -0,0 +1,77 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Set; + +/** + * An immutable terminal mapping from one integer key to a value. + * + * @param the stored value type + */ +@Internal +@NullMarked +public final class IntMapLeaf implements IntMapNode { + + private final int key; + private final int hash; + private final V value; + + /** + * Creates a leaf. + * + * @param key the integer key + * @param hash the mixed key hash + * @param value the value + */ + public IntMapLeaf(int key, int hash, V value) { + this.key = key; + this.hash = hash; + this.value = value; + } + + @Override + public @Nullable V get(int requestedKey, int requestedHash, int shift) { + return key == requestedKey ? value : null; + } + + @Override + public IntMapNode put(int requestedKey, int requestedHash, V requestedValue, int shift) { + if (key == requestedKey) { + return value == requestedValue + ? this + : new IntMapLeaf<>(key, hash, requestedValue); + } + IntMapLeaf other = new IntMapLeaf<>( + requestedKey, + requestedHash, + requestedValue); + return IntMapBranch.merge(this, other, shift); + } + + @Override + public @Nullable IntMapNode remove(int requestedKey, int requestedHash, int shift) { + return key == requestedKey ? null : this; + } + + @Override + public void visitEntries( + Set> visitedNodes, + IntMapEntryVisitor visitor) { + if (!visitedNodes.add(this)) { + return; + } + visitor.visit(key, value); + } + + /** + * Returns the mixed hash used to place this leaf in the trie. + * + * @return the mixed key hash + */ + public int getHash() { + return hash; + } +} diff --git a/src/main/java/graphql/schema/universe/IntMapNode.java b/src/main/java/graphql/schema/universe/IntMapNode.java new file mode 100644 index 0000000000..bac7fad63b --- /dev/null +++ b/src/main/java/graphql/schema/universe/IntMapNode.java @@ -0,0 +1,65 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Set; + +/** + * A node in a persistent integer-keyed hash-array mapped trie. + * + *

Each branch consumes five bits of the mixed key hash. Implementations are immutable: an + * update returns either the same node for an identity no-op or a replacement sharing unaffected + * descendants.

+ * + * @param the stored value type + */ +@Internal +@NullMarked +public interface IntMapNode { + + /** + * Finds a value. + * + * @param key the integer key + * @param hash the mixed key hash + * @param shift the number of hash bits already consumed + * + * @return the stored value, or {@code null} when absent + */ + @Nullable V get(int key, int hash, int shift); + + /** + * Associates a key with a non-null value. + * + * @param key the integer key + * @param hash the mixed key hash + * @param value the value + * @param shift the number of hash bits already consumed + * + * @return this node for an identity no-op, otherwise a replacement + */ + IntMapNode put(int key, int hash, V value, int shift); + + /** + * Removes a key. + * + * @param key the integer key + * @param hash the mixed key hash + * @param shift the number of hash bits already consumed + * + * @return this node when absent, a replacement node, or {@code null} if empty + */ + @Nullable IntMapNode remove(int key, int hash, int shift); + + /** + * Visits entries below this node unless it has already been visited through another map. + * + * @param visitedNodes identity-based set of previously visited nodes + * @param visitor the entry visitor + */ + void visitEntries( + Set> visitedNodes, + IntMapEntryVisitor visitor); +} diff --git a/src/main/java/graphql/schema/universe/PersistentEdgeMap.java b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java index ff8b2037df..871260f0bb 100644 --- a/src/main/java/graphql/schema/universe/PersistentEdgeMap.java +++ b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java @@ -26,14 +26,14 @@ public final class PersistentEdgeMap { private static final PersistentEdgeMap EMPTY = new PersistentEdgeMap(null); - private final @Nullable EdgeMapNode root; + private final @Nullable IntMapNode root; /** * Creates a map around an immutable trie root. * * @param root the trie root, or {@code null} for an empty map */ - public PersistentEdgeMap(@Nullable EdgeMapNode root) { + public PersistentEdgeMap(@Nullable IntMapNode root) { this.root = root; } @@ -57,7 +57,7 @@ public PackedEdgeSet get(int key) { if (root == null) { return PackedEdgeSet.empty(); } - PackedEdgeSet result = root.get(key, mix(key), 0); + PackedEdgeSet result = root.get(key, PersistentIntMap.mix(key), 0); return result == null ? PackedEdgeSet.empty() : result; } @@ -76,9 +76,9 @@ public PersistentEdgeMap put(int key, PackedEdgeSet value) { if (value.isEmpty()) { return remove(key); } - int hash = mix(key); - EdgeMapNode newRoot = root == null - ? new EdgeMapLeaf(key, hash, value) + int hash = PersistentIntMap.mix(key); + IntMapNode newRoot = root == null + ? new IntMapLeaf<>(key, hash, value) : root.put(key, hash, value, 0); return root == newRoot ? this : new PersistentEdgeMap(newRoot); } @@ -94,7 +94,8 @@ public PersistentEdgeMap remove(int key) { if (root == null) { return this; } - EdgeMapNode newRoot = root.remove(key, mix(key), 0); + IntMapNode newRoot = + root.remove(key, PersistentIntMap.mix(key), 0); return root == newRoot ? this : new PersistentEdgeMap(newRoot); } @@ -108,22 +109,12 @@ public PersistentEdgeMap remove(int key) { * @param visitor the entry visitor */ public void visitUniqueEntries( - Set visitedNodes, - EdgeMapEntryVisitor visitor) { + Set> visitedNodes, + IntMapEntryVisitor visitor) { assertNotNull(visitedNodes); assertNotNull(visitor); if (root != null) { root.visitEntries(visitedNodes, visitor); } } - - private static int mix(int value) { - int result = value; - result ^= result >>> 16; - result *= 0x7feb352d; - result ^= result >>> 15; - result *= 0x846ca68b; - result ^= result >>> 16; - return result; - } } diff --git a/src/main/java/graphql/schema/universe/PersistentIntMap.java b/src/main/java/graphql/schema/universe/PersistentIntMap.java new file mode 100644 index 0000000000..a2ea6aee92 --- /dev/null +++ b/src/main/java/graphql/schema/universe/PersistentIntMap.java @@ -0,0 +1,126 @@ +package graphql.schema.universe; + +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Set; + +import static graphql.Assert.assertNotNull; + +/** + * An immutable persistent map from integer keys to non-null values. + * + *

The map is a bitmap-compressed hash-array mapped trie with a logical branching factor of 32. + * Updating one key copies only nodes on its trie path.

+ * + * @param the stored value type + */ +@Internal +@NullMarked +public final class PersistentIntMap { + + private static final PersistentIntMap EMPTY = new PersistentIntMap<>(null); + + private final @Nullable IntMapNode root; + + /** + * Creates a map around an immutable trie root. + * + * @param root the trie root, or {@code null} for an empty map + */ + public PersistentIntMap(@Nullable IntMapNode root) { + this.root = root; + } + + /** + * Returns the shared empty map. + * + * @param the stored value type + * + * @return the empty map + */ + @SuppressWarnings("unchecked") + public static PersistentIntMap empty() { + return (PersistentIntMap) EMPTY; + } + + /** + * Returns the value stored for a key. + * + * @param key the integer key + * + * @return the value, or {@code null} when absent + */ + public @Nullable V get(int key) { + if (root == null) { + return null; + } + return root.get(key, mix(key), 0); + } + + /** + * Returns a map associating a key with a value. + * + * @param key the integer key + * @param value the non-null value + * + * @return this map for an identity no-op, otherwise a replacement + */ + public PersistentIntMap put(int key, V value) { + V nonNullValue = assertNotNull(value); + int hash = mix(key); + IntMapNode newRoot = root == null + ? new IntMapLeaf<>(key, hash, nonNullValue) + : root.put(key, hash, nonNullValue, 0); + return root == newRoot ? this : new PersistentIntMap<>(newRoot); + } + + /** + * Returns a map without a key. + * + * @param key the integer key + * + * @return this map when absent, otherwise a replacement + */ + public PersistentIntMap remove(int key) { + if (root == null) { + return this; + } + IntMapNode newRoot = root.remove(key, mix(key), 0); + return root == newRoot ? this : new PersistentIntMap<>(newRoot); + } + + /** + * Visits entries not already covered by a structurally shared node. + * + * @param visitedNodes identity-based set of previously visited nodes + * @param visitor the entry visitor + */ + public void visitUniqueEntries( + Set> visitedNodes, + IntMapEntryVisitor visitor) { + assertNotNull(visitedNodes); + assertNotNull(visitor); + if (root != null) { + root.visitEntries(visitedNodes, visitor); + } + } + + /** + * Mixes an integer key into a trie hash. + * + * @param value the integer key + * + * @return a one-to-one mixed hash + */ + public static int mix(int value) { + int result = value; + result ^= result >>> 16; + result *= 0x7feb352d; + result ^= result >>> 15; + result *= 0x846ca68b; + result ^= result >>> 16; + return result; + } +} diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index a55b865397..d718588fcf 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -30,6 +30,7 @@ public final class SUSchema { private final SchemaUniverse universe; private final SUSchemaRoot root; private final PersistentEdgeMap edgeMap; + private final PersistentIntMap> vertexMetadata; private final int storedEdgeCount; @Internal @@ -37,10 +38,12 @@ public SUSchema( SchemaUniverse universe, SUSchemaRoot root, PersistentEdgeMap edgeMap, + PersistentIntMap> vertexMetadata, int storedEdgeCount) { this.universe = assertNotNull(universe); this.root = assertNotNull(root); this.edgeMap = assertNotNull(edgeMap); + this.vertexMetadata = assertNotNull(vertexMetadata); this.storedEdgeCount = storedEdgeCount; } @@ -52,11 +55,39 @@ public SUSchemaRoot getRoot() { return root; } + /** + * Returns this snapshot's user-controlled metadata for a vertex. + * + *

The returned map is immutable, although values supplied by the application may themselves + * be mutable. Metadata has no GraphQL semantics and is not exported.

+ * + * @param vertex the vertex + * + * @return the metadata, or an empty map when none is stored + */ + public Map getVertexMetadata(SUVertex vertex) { + assertOwned(vertex); + Map metadata = vertexMetadata.get(vertex.getId()); + return metadata == null ? Collections.emptyMap() : metadata; + } + + /** + * Returns one user-controlled metadata value for a vertex. + * + * @param vertex the vertex + * @param key the metadata key + * + * @return the value, or {@code null} when absent + */ + public @Nullable Object getVertexMetadata(SUVertex vertex, String key) { + return getVertexMetadata(vertex).get(assertNotNull(key)); + } + /** * Exports this snapshot as an unexecutable {@link GraphQLSchema}. * - *

The result preserves the type-system topology and metadata represented by this schema. - * Runtime wiring is not retained by a schema universe and therefore cannot be exported.

+ *

The result preserves the GraphQL type-system topology and metadata represented by this + * schema. User-controlled vertex metadata and runtime wiring are not exported.

*/ public GraphQLSchema toGraphQLSchema() { return new SUExporter(this).exportSchema(); @@ -356,6 +387,11 @@ public PersistentEdgeMap getEdgeMap() { return edgeMap; } + @Internal + public PersistentIntMap> getVertexMetadataMap() { + return vertexMetadata; + } + @Internal public PackedEdgeSet getPackedEdges(SUVertex source) { assertOwned(source); diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 26072fd9bd..2cb24bed0f 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -1,5 +1,6 @@ package graphql.schema.universe; +import com.google.common.collect.ImmutableMap; import graphql.ExperimentalApi; import graphql.Internal; import org.jspecify.annotations.NullMarked; @@ -23,6 +24,8 @@ public final class SUSchemaBuilder { private final SUSchemaRoot root; private final @Nullable SUSchema baseSchema; private final Map changedEdges = new LinkedHashMap<>(); + private final Map> changedVertexMetadata = + new LinkedHashMap<>(); private boolean built; @Internal @@ -34,6 +37,7 @@ public SUSchemaBuilder( this.root = assertNotNull(root); this.baseSchema = baseSchema; copyRootEdges(baseSchema); + copyRootMetadata(baseSchema); } public SUSchemaBuilder queryType(SUObjectType queryType) { @@ -48,6 +52,99 @@ public SUSchemaRoot getRoot() { return root; } + /** + * Replaces this snapshot's user-controlled metadata for a vertex. + * + *

The map is copied immediately. An empty map removes the metadata binding.

+ * + * @param vertex the vertex + * @param metadata the metadata + * + * @return this builder + */ + public SUSchemaBuilder vertexMetadata( + SUVertex vertex, + Map metadata) { + assertCanChange(); + assertOwned(vertex); + changedVertexMetadata.put( + vertex.getId(), + copyMetadata(metadata)); + return this; + } + + /** + * Adds or replaces one user-controlled metadata value. + * + * @param vertex the vertex + * @param key the metadata key + * @param value the metadata value + * + * @return this builder + */ + public SUSchemaBuilder vertexMetadata( + SUVertex vertex, + String key, + Object value) { + assertCanChange(); + assertOwned(vertex); + mutableVertexMetadata(vertex).put( + assertNotNull(key), + assertNotNull(value)); + return this; + } + + /** + * Removes one user-controlled metadata value. + * + * @param vertex the vertex + * @param key the metadata key + * + * @return this builder + */ + public SUSchemaBuilder removeVertexMetadata( + SUVertex vertex, + String key) { + assertCanChange(); + assertOwned(vertex); + mutableVertexMetadata(vertex).remove(assertNotNull(key)); + return this; + } + + /** + * Removes all user-controlled metadata for a vertex. + * + * @param vertex the vertex + * + * @return this builder + */ + public SUSchemaBuilder clearVertexMetadata(SUVertex vertex) { + assertCanChange(); + assertOwned(vertex); + changedVertexMetadata.put(vertex.getId(), new LinkedHashMap<>()); + return this; + } + + /** + * Copies user-controlled metadata between vertices in this snapshot. + * + * @param source the source vertex + * @param target the target vertex + * + * @return this builder + */ + public SUSchemaBuilder copyVertexMetadata( + SUVertex source, + SUVertex target) { + assertCanChange(); + assertOwned(source); + assertOwned(target); + changedVertexMetadata.put( + target.getId(), + new LinkedHashMap<>(currentVertexMetadata(source))); + return this; + } + public SUSchemaBuilder mutationType(@Nullable SUObjectType mutationType) { return setMutationType(mutationType); } @@ -436,6 +533,8 @@ public SUSchema build() { assertCanChange(); built = true; PersistentEdgeMap edgeMap = baseEdgeMap(); + PersistentIntMap> metadataMap = + baseVertexMetadataMap(); int edgeCount = baseEdgeCount(); for (Map.Entry entry : changedEdges.entrySet()) { int sourceId = entry.getKey(); @@ -444,9 +543,24 @@ public SUSchema build() { edgeCount += newEdges.size() - oldEdges.size(); edgeMap = edgeMap.put(sourceId, newEdges); } + for (Map.Entry> entry + : changedVertexMetadata.entrySet()) { + int vertexId = entry.getKey(); + Map metadata = ImmutableMap.copyOf(entry.getValue()); + if (metadata.isEmpty()) { + metadataMap = metadataMap.remove(vertexId); + continue; + } + metadataMap = metadataMap.put(vertexId, metadata); + } assertTrue(edgeMap.get(root.getId()).firstTarget(SUEdgeKind.QUERY_TYPE) >= 0, "A schema universe schema requires a query type"); - SUSchema schema = new SUSchema(universe, root, edgeMap, edgeCount); + SUSchema schema = new SUSchema( + universe, + root, + edgeMap, + metadataMap, + edgeCount); universe.registerSchema(schema); return schema; } @@ -468,6 +582,22 @@ private void copyRootEdges(@Nullable SUSchema base) { changedEdges.put(root.getId(), new MutablePackedEdgeSet(base.getPackedEdges(base.getRoot()))); } + private void copyRootMetadata(@Nullable SUSchema base) { + if (base == null) { + return; + } + Map metadata = base.getVertexMetadata(base.getRoot()); + if (metadata.isEmpty()) { + return; + } + changedVertexMetadata.put( + base.getRoot().getId(), + new LinkedHashMap<>()); + changedVertexMetadata.put( + root.getId(), + new LinkedHashMap<>(metadata)); + } + private MutablePackedEdgeSet mutableEdges(SUVertex source) { assertOwned(source); MutablePackedEdgeSet changed = changedEdges.get(source.getId()); @@ -486,10 +616,51 @@ private PersistentEdgeMap baseEdgeMap() { return baseSchema == null ? PersistentEdgeMap.empty() : baseSchema.getEdgeMap(); } + private PersistentIntMap> baseVertexMetadataMap() { + return baseSchema == null + ? PersistentIntMap.empty() + : baseSchema.getVertexMetadataMap(); + } + private int baseEdgeCount() { return baseSchema == null ? 0 : baseSchema.getStoredEdgeCount(); } + private LinkedHashMap mutableVertexMetadata(SUVertex vertex) { + LinkedHashMap changed = + changedVertexMetadata.get(vertex.getId()); + if (changed != null) { + return changed; + } + LinkedHashMap mutable = + new LinkedHashMap<>(currentVertexMetadata(vertex)); + changedVertexMetadata.put(vertex.getId(), mutable); + return mutable; + } + + private Map currentVertexMetadata(SUVertex vertex) { + LinkedHashMap changed = + changedVertexMetadata.get(vertex.getId()); + if (changed != null) { + return changed; + } + return baseSchema == null + ? ImmutableMap.of() + : baseSchema.getVertexMetadata(vertex); + } + + private LinkedHashMap copyMetadata( + Map metadata) { + Map nonNullMetadata = assertNotNull(metadata); + LinkedHashMap copy = new LinkedHashMap<>(); + for (Map.Entry entry : nonNullMetadata.entrySet()) { + copy.put( + assertNotNull(entry.getKey()), + assertNotNull(entry.getValue())); + } + return copy; + } + private void assertCanChange() { assertTrue(!built, "This schema universe builder has already built a schema"); } diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java index f0242c9c3f..b0d3fa06cf 100644 --- a/src/main/java/graphql/schema/universe/SchemaUniverse.java +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -135,8 +135,9 @@ public Map getSchemasByName() { * Reclaims vertices that are unused by every registered schema. * *

Liveness includes each registered schema root, every source and target in its complete - * stored adjacency, and input types referenced by applied-directive arguments. Complete stored - * adjacency is considered so dormant subgraphs remain available for later transformations.

+ * stored adjacency, vertices with user metadata, and input types referenced by + * applied-directive arguments. Complete stored adjacency is considered so dormant subgraphs + * remain available for later transformations.

* *

Reclaimed vertex IDs are never reused. This operation may therefore leave holes in the * chunked vertex arena. It must not run concurrently with schema construction, importing, or @@ -146,13 +147,18 @@ public Map getSchemasByName() { */ public synchronized int cleanupUnusedVertices() { BitSet liveVertexIds = new BitSet(); - Set visitedNodes = + Set> visitedEdgeNodes = + Collections.newSetFromMap(new IdentityHashMap<>()); + Set>> visitedMetadataNodes = Collections.newSetFromMap(new IdentityHashMap<>()); for (SUSchema schema : schemas) { markVertex(liveVertexIds, schema.getRoot().getId()); schema.getEdgeMap().visitUniqueEntries( - visitedNodes, + visitedEdgeNodes, (sourceId, edges) -> markEdgeBinding(liveVertexIds, sourceId, edges)); + schema.getVertexMetadataMap().visitUniqueEntries( + visitedMetadataNodes, + (vertexId, metadata) -> markVertex(liveVertexIds, vertexId)); } return sweepUnusedVertices(liveVertexIds); } diff --git a/src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy b/src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy index e821a74ba7..4c87a3e51f 100644 --- a/src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy +++ b/src/test/groovy/graphql/schema/universe/PersistentEdgeMapTest.groovy @@ -13,7 +13,8 @@ class PersistentEdgeMapTest extends Specification { .put(1, firstEdges) .put(2, secondEdges) def changed = base.put(3, thirdEdges) - def visitedNodes = Collections.newSetFromMap(new IdentityHashMap()) + def visitedNodes = Collections.newSetFromMap( + new IdentityHashMap, Boolean>()) def visitedEntries = [] when: diff --git a/src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy b/src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy new file mode 100644 index 0000000000..6334cd6077 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy @@ -0,0 +1,87 @@ +package graphql.schema.universe + +import spock.lang.Specification + +class PersistentIntMapTest extends Specification { + + def "persistent updates preserve values and identity no-ops"() { + given: + def first = new Object() + def second = new Object() + def replacement = new Object() + def empty = PersistentIntMap.empty() + + when: + def base = empty.put(1, first).put(2, second) + def changed = base.put(1, replacement) + def removed = changed.remove(2) + + then: + empty.get(1) == null + empty.remove(1).is(empty) + base.get(1).is(first) + base.get(2).is(second) + base.put(1, first).is(base) + changed.get(1).is(replacement) + changed.get(2).is(second) + removed.get(1).is(replacement) + removed.get(2) == null + removed.remove(2).is(removed) + base.remove(999).is(base) + } + + def "removal handles keys sharing an initial trie slot"() { + given: + def keys = (0..128) + .groupBy { PersistentIntMap.mix(it) & 31 } + .values() + .find { it.size() > 2 } + .take(3) + def map = PersistentIntMap.empty() + .put(keys[0], "first") + .put(keys[1], "second") + + when: + def unchanged = map.remove(keys[2]) + def one = map.remove(keys[0]) + def empty = one.remove(keys[1]) + + then: + unchanged.is(map) + one.get(keys[0]) == null + one.get(keys[1]) == "second" + empty.get(keys[1]) == null + } + + def "unique traversal skips structurally shared nodes"() { + given: + def base = PersistentIntMap.empty() + .put(1, "first") + .put(2, "second") + def changed = base.put(3, "third") + def visitedNodes = Collections.newSetFromMap( + new IdentityHashMap, Boolean>()) + def entries = [] + + when: + PersistentIntMap.empty().visitUniqueEntries( + visitedNodes, + (key, value) -> entries.add([key, value])) + base.visitUniqueEntries( + visitedNodes, + (key, value) -> entries.add([key, value])) + changed.visitUniqueEntries( + visitedNodes, + (key, value) -> entries.add([key, value])) + changed.visitUniqueEntries( + visitedNodes, + (key, value) -> entries.add([key, value])) + + then: + entries.toSet() == [ + [1, "first"], + [2, "second"], + [3, "third"] + ] as Set + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy new file mode 100644 index 0000000000..af717a6709 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy @@ -0,0 +1,187 @@ +package graphql.schema.universe + +import graphql.AssertException +import graphql.schema.InputValueWithState +import spock.lang.Specification + +class SUSchemaMetadataTest extends Specification { + + def "schema stores an immutable copy of user metadata"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def field = universe.newField("value") + def mutableValue = [] + def supplied = [owner: "platform", payload: mutableValue] + def builder = universe.newSchema("schema") + .queryType(query) + .addField(query, field) + .vertexMetadata(query, supplied) + .vertexMetadata(query, "temporary", true) + .removeVertexMetadata(query, "temporary") + .vertexMetadata(field, "role", "leaf") + + when: + supplied.owner = "changed" + supplied.extra = "late" + def schema = builder.build() + + then: + schema.getVertexMetadata(query) == [ + owner: "platform", + payload: mutableValue + ] + schema.getVertexMetadata(query, "owner") == "platform" + schema.getVertexMetadata(query, "missing") == null + schema.getVertexMetadata(field) == [role: "leaf"] + schema.getVertexMetadata(query).payload.is(mutableValue) + + when: + schema.getVertexMetadata(query).put("forbidden", true) + + then: + thrown(UnsupportedOperationException) + + when: + def replacementValue = [] + def derived = schema.transform( + "derived", + derivedBuilder -> derivedBuilder.vertexMetadata(query, [ + owner: "platform", + payload: replacementValue + ])) + + then: + replacementValue == mutableValue + !replacementValue.is(mutableValue) + derived.getVertexMetadata(query).payload.is(replacementValue) + } + + def "metadata is versioned per schema and root metadata follows transformations"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def field = universe.newField("value") + def copy = universe.newField("copy") + def baseBuilder = universe.newSchema("base") + def baseRoot = baseBuilder.root + def base = baseBuilder + .queryType(query) + .addField(query, field) + .vertexMetadata(baseRoot, [owner: "root"]) + .vertexMetadata(query, [scope: "base", retained: true]) + .vertexMetadata(field, [stable: "value"]) + .build() + + when: + def unchanged = base.transform("unchanged", builder -> { + }) + def derived = base.transform("derived", builder -> builder + .vertexMetadata(query, "scope", "derived") + .removeVertexMetadata(query, "retained") + .clearVertexMetadata(field) + .copyVertexMetadata(query, copy)) + + then: + base.getVertexMetadata(query) == [scope: "base", retained: true] + unchanged.getVertexMetadata(query).is(base.getVertexMetadata(query)) + derived.getVertexMetadata(query) == [scope: "derived"] + derived.getVertexMetadata(copy) == [scope: "derived"] + base.getVertexMetadata(field) == [stable: "value"] + derived.getVertexMetadata(field).isEmpty() + base.getVertexMetadata(baseRoot) == [owner: "root"] + derived.getVertexMetadata(baseRoot).isEmpty() + derived.getVertexMetadata(derived.root) == [owner: "root"] + } + + def "metadata-only vertices and their intrinsic type references survive cleanup"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def tagged = universe.newEnumType("Tagged") + def orphan = universe.newEnumType("Orphan") + def string = universe.newScalarType("String") + def argument = universe.newAppliedDirectiveArgument( + "value", + string, + InputValueWithState.newExternalValue("metadata")) + def directive = universe.newAppliedDirective("tag", [argument]) + def schema = universe.newSchema("schema") + .queryType(query) + .vertexMetadata(tagged, [ + reason: "detached", + opaqueReference: orphan + ]) + .vertexMetadata(directive, [reason: "directive"]) + .build() + + when: + def reclaimed = universe.cleanupUnusedVertices() + + then: + reclaimed == 1 + !universe.owns(orphan) + universe.owns(tagged) + universe.owns(directive) + universe.owns(string) + schema.getVertexMetadata(tagged) == [ + reason: "detached", + opaqueReference: orphan + ] + schema.getVertexMetadata(directive) == [reason: "directive"] + schema.getType(argument).is(string) + + when: + universe.removeSchema(schema) + + then: + universe.cleanupUnusedVertices() == 5 + universe.vertexCount == 0 + } + + def "metadata builder rejects foreign vertices and null map contents"() { + given: + def universe = new SchemaUniverse() + def otherUniverse = new SchemaUniverse() + def query = universe.newObjectType("Query") + def foreign = otherUniverse.newObjectType("Foreign") + def builder = universe.newSchema("schema").queryType(query) + + when: + builder.vertexMetadata(foreign, [key: "value"]) + + then: + thrown(AssertException) + + when: + builder.vertexMetadata(query, (Map) null) + + then: + thrown(AssertException) + + when: + builder.vertexMetadata(query, [(null): "value"]) + + then: + thrown(AssertException) + + when: + builder.vertexMetadata(query, [key: null]) + + then: + thrown(AssertException) + + when: + def schema = builder.vertexMetadata(query, [key: "value"]).build() + schema.getVertexMetadata(foreign) + + then: + thrown(AssertException) + + when: + builder.clearVertexMetadata(query) + + then: + thrown(AssertException) + } +} From 6cae5fa8c142c8770589ebe96c6b379f9de67c92 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 4 Aug 2026 03:13:30 +0000 Subject: [PATCH 08/32] Store named types directly in schema snapshots --- docs/schema-universe.md | 39 +++--- .../benchmark/SchemaUniverseBenchmark.java | 4 +- .../graphql/schema/universe/IntMapBranch.java | 7 + .../graphql/schema/universe/IntMapLeaf.java | 5 + .../graphql/schema/universe/IntMapNode.java | 7 + .../schema/universe/PersistentIntMap.java | 12 ++ .../graphql/schema/universe/SUEdgeKind.java | 1 - .../graphql/schema/universe/SUExporter.java | 14 +- .../graphql/schema/universe/SUImporter.java | 17 +-- .../graphql/schema/universe/SUSchema.java | 43 +++--- .../schema/universe/SUSchemaBuilder.java | 128 ++++++++++++++++-- .../schema/universe/SUSchemaGenerator.java | 6 +- .../schema/universe/SchemaUniverse.java | 13 +- .../universe/PersistentIntMapTest.groovy | 25 ++++ .../universe/SUSchemaGeneratorTest.groovy | 7 +- .../graphql/schema/universe/SUTest.groovy | 95 +++++++++++-- .../universe/SchemaUniverseCleanupTest.groovy | 32 ++++- 17 files changed, 355 insertions(+), 100 deletions(-) diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 76b0d572f3..81076ec0b5 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -24,9 +24,11 @@ ID. Cleanup can reclaim unused vertices and leave holes, but IDs are never reuse vertex type for each supported GraphQL schema-element kind. A vertex contains intrinsic scalar properties such as kind, name, and description, but no child references. -`SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex and sparse -user-controlled metadata keyed by vertex ID. The effective schema is the graph reachable from that -root. Root edges identify operation roots, non-root named types, and directive definitions. +`SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex, a persistent +registry of every named type keyed by universe name ID, and sparse user-controlled metadata keyed +by vertex ID. Root edges identify operation roots and directive definitions. Named-type membership +is independent of reachability, so a schema can retain disconnected types without representing +membership as graph edges. Every successfully built snapshot registers itself with its universe under its unique name. `SchemaUniverse.getSchema(name)` selects one snapshot directly, while `getSchemas()` and @@ -49,7 +51,7 @@ The current API assumes callers traverse from the root or another known-reachabl `SUSchema` is the primary traversal API. Callers normally do not need to inspect edge kinds directly. It provides typed helpers for: -- operation roots, additional types, all named types, and lookup by type name; +- operation roots, all named types, and lookup by type name; - directive definitions and schema-applied directives; - object and interface fields; - field and directive arguments; @@ -91,23 +93,26 @@ Java integers. ## Persistent snapshots -Outgoing arrays and metadata associations are stored in persistent hash-array mapped tries keyed by -vertex ID. A transform batches changes, freezes each changed value once, and path-copies only the -affected trie branches. All unchanged branches, outgoing arrays, and metadata maps are shared by -identity with the parent schema. +Outgoing arrays, named types, and metadata associations are stored in persistent hash-array mapped +tries. Adjacency and metadata use vertex IDs as keys, while the type registry uses universe name +IDs. A transform batches edge changes, freezes each changed value once, and path-copies only the +affected trie branches. All unchanged branches, outgoing arrays, named types, and metadata maps are +shared by identity with the parent schema. -Approximate operation costs, where `d` is source out-degree and `V` is the number of sources with -adjacency: +Approximate operation costs, where `d` is source out-degree, `V` is the number of sources with +adjacency, and `T` is the number of named types: | Operation | Time | New retained storage | | --- | --- | --- | | Read outgoing adjacency | `O(log32 V)` | none | | Child lookup by name | `O(log32 V + log d)` unordered, `O(log32 V + d)` ordered | none | +| Read named type by name | `O(log32 T)` | none | | Read vertex metadata | `O(log32 M)` plus metadata-map lookup | none | | Add/remove edge | `O(d)` transient, then canonicalize at build | one changed primitive array | +| Add/remove named type | `O(log32 T)` | one trie path | | Edit vertex metadata | `O(log32 M + keys(vertex))` | one trie path and metadata map | | Publish `k` changed sources | `O(k log32 V)` | copied trie paths plus `k` arrays | -| Derive with no type edits | effectively root replacement | root edge paths, plus metadata paths when the root is annotated | +| Derive with no type edits | effectively root replacement | root edge paths; type registry is shared | Here `M` is the number of vertices with metadata in the snapshot. @@ -197,7 +202,7 @@ wrapper targets. - object, interface, union, enum, scalar, and input-object types; - fields, arguments, enum values, and input fields; - list and non-null wrappers; -- operation roots, non-root named types, implementations, and union membership; +- operation roots, complete named-type membership, implementations, and union membership; - argument and input-field defaults, preserving all `InputValueWithState` states; - directive repeatability, valid locations, definitions, and modern applied-directive topology; - applied-directive argument values, exact input types, and application AST nodes. @@ -213,11 +218,11 @@ exact instance is present in the universe registry; holding an external Java ref extend its lifetime. `cleanupUnusedVertices()` periodically reclaims vertices unused by registered schemas. -Cleanup marks every registered root, every source and target in the registered snapshots' complete -persistent edge maps, and every vertex ID with schema metadata. It identity-deduplicates -structurally shared HAMT nodes, so unchanged subtrees are scanned once rather than once per schema. -Applied-directive argument type IDs are marked separately because those references are intrinsic -values rather than edges. +Cleanup marks every registered root, every registered named type, every source and target in the +registered snapshots' complete persistent edge maps, and every vertex ID with schema metadata. It +identity-deduplicates structurally shared HAMT nodes, so unchanged subtrees are scanned once rather +than once per schema. Applied-directive argument type IDs are marked separately because those +references are intrinsic values rather than edges. Marking must include dormant adjacency retained by a registered snapshot, not only the effective graph reachable from its root. Otherwise cleanup would break the supported constant-scope diff --git a/src/jmh/java/benchmark/SchemaUniverseBenchmark.java b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java index 0cc029d9c7..4b4f7416fb 100644 --- a/src/jmh/java/benchmark/SchemaUniverseBenchmark.java +++ b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java @@ -533,7 +533,7 @@ private static SUSchema applySUMutation( SUObjectType syntheticType = universe.newObjectType(mutation.syntheticTypeName); SUField valueField = universe.newField("value"); - builder.addAdditionalType(syntheticType) + builder.addType(syntheticType) .addField(syntheticType, valueField) .setFieldType(valueField, stringType); break; @@ -559,7 +559,7 @@ private static SUSchema applySUMutation( .setFieldType(replacementField, stringType); break; case REMOVE_SYNTHETIC_TYPE: - builder.removeAdditionalType(mutation.syntheticTypeName); + builder.removeType(mutation.syntheticTypeName); break; default: throw new IllegalStateException("Unhandled mutation " + mutation.kind); diff --git a/src/main/java/graphql/schema/universe/IntMapBranch.java b/src/main/java/graphql/schema/universe/IntMapBranch.java index 5aca952bf3..5e2caaca4c 100644 --- a/src/main/java/graphql/schema/universe/IntMapBranch.java +++ b/src/main/java/graphql/schema/universe/IntMapBranch.java @@ -115,6 +115,13 @@ public void visitEntries( } } + @Override + public void forEachEntry(IntMapEntryVisitor visitor) { + for (IntMapNode child : children) { + child.forEachEntry(visitor); + } + } + private IntMapNode insert( int bit, int index, diff --git a/src/main/java/graphql/schema/universe/IntMapLeaf.java b/src/main/java/graphql/schema/universe/IntMapLeaf.java index ceccdd04d9..d6ffd54b45 100644 --- a/src/main/java/graphql/schema/universe/IntMapLeaf.java +++ b/src/main/java/graphql/schema/universe/IntMapLeaf.java @@ -66,6 +66,11 @@ public void visitEntries( visitor.visit(key, value); } + @Override + public void forEachEntry(IntMapEntryVisitor visitor) { + visitor.visit(key, value); + } + /** * Returns the mixed hash used to place this leaf in the trie. * diff --git a/src/main/java/graphql/schema/universe/IntMapNode.java b/src/main/java/graphql/schema/universe/IntMapNode.java index bac7fad63b..6f33978ad8 100644 --- a/src/main/java/graphql/schema/universe/IntMapNode.java +++ b/src/main/java/graphql/schema/universe/IntMapNode.java @@ -62,4 +62,11 @@ public interface IntMapNode { void visitEntries( Set> visitedNodes, IntMapEntryVisitor visitor); + + /** + * Visits every entry below this node. + * + * @param visitor the entry visitor + */ + void forEachEntry(IntMapEntryVisitor visitor); } diff --git a/src/main/java/graphql/schema/universe/PersistentIntMap.java b/src/main/java/graphql/schema/universe/PersistentIntMap.java index a2ea6aee92..1bdc187f4d 100644 --- a/src/main/java/graphql/schema/universe/PersistentIntMap.java +++ b/src/main/java/graphql/schema/universe/PersistentIntMap.java @@ -91,6 +91,18 @@ public PersistentIntMap remove(int key) { return root == newRoot ? this : new PersistentIntMap<>(newRoot); } + /** + * Visits every entry. + * + * @param visitor the entry visitor + */ + public void forEachEntry(IntMapEntryVisitor visitor) { + assertNotNull(visitor); + if (root != null) { + root.forEachEntry(visitor); + } + } + /** * Visits entries not already covered by a structurally shared node. * diff --git a/src/main/java/graphql/schema/universe/SUEdgeKind.java b/src/main/java/graphql/schema/universe/SUEdgeKind.java index 15e25a2cf9..2ea2592564 100644 --- a/src/main/java/graphql/schema/universe/SUEdgeKind.java +++ b/src/main/java/graphql/schema/universe/SUEdgeKind.java @@ -20,7 +20,6 @@ public enum SUEdgeKind { QUERY_TYPE(1, true, true, false), MUTATION_TYPE(2, true, true, false), SUBSCRIPTION_TYPE(3, true, true, false), - ADDITIONAL_TYPE(4, false, true, false), DIRECTIVE_DEFINITION(5, false, true, false), FIELD(6, false, true, false), ARGUMENT(7, false, true, false), diff --git a/src/main/java/graphql/schema/universe/SUExporter.java b/src/main/java/graphql/schema/universe/SUExporter.java index 2e273a0b63..cae8d5317d 100644 --- a/src/main/java/graphql/schema/universe/SUExporter.java +++ b/src/main/java/graphql/schema/universe/SUExporter.java @@ -81,10 +81,12 @@ public GraphQLSchema exportSchema() { if (subscriptionType != null) { builder.subscription(objectType(subscriptionType)); } - for (SUVertex additionalType : schema.getAdditionalTypes()) { - if (!Introspection.isIntrospectionTypes(requiredName(additionalType))) { - builder.additionalType(namedType(additionalType)); + for (SUNamedType type : schema.getTypes()) { + if (isOperationType(type) + || Introspection.isIntrospectionTypes(requiredName(type))) { + continue; } + builder.additionalType(namedType(type)); } for (SUDirective directive : schema.getDirectiveDefinitions()) { builder.additionalDirective(exportDirectiveDefinition(directive)); @@ -93,6 +95,12 @@ public GraphQLSchema exportSchema() { return builder.build(); } + private boolean isOperationType(SUNamedType type) { + return type == schema.getQueryType() + || type == schema.getMutationType() + || type == schema.getSubscriptionType(); + } + private GraphQLCodeRegistry exportCodeRegistry() { GraphQLCodeRegistry.Builder builder = GraphQLCodeRegistry.newCodeRegistry(); for (GraphQLNamedType type : namedTypes.values()) { diff --git a/src/main/java/graphql/schema/universe/SUImporter.java b/src/main/java/graphql/schema/universe/SUImporter.java index 8e3071636e..5a323b98b8 100644 --- a/src/main/java/graphql/schema/universe/SUImporter.java +++ b/src/main/java/graphql/schema/universe/SUImporter.java @@ -80,6 +80,7 @@ private void createNamedTypeVertices(List types) { SUNamedType vertex = createNamedTypeVertex(type); namedTypes.put(type.getName(), vertex); elementVertices.put(type, vertex); + builder().addType(vertex); } } @@ -122,18 +123,6 @@ private void addRootTypes(GraphQLSchema schema) { if (subscriptionType != null) { builder().subscriptionType((SUObjectType) namedType(subscriptionType)); } - addNonRootTypes(schema, query); - } - - private void addNonRootTypes(GraphQLSchema schema, SUObjectType query) { - SUNamedType mutation = nullableNamedType(schema.getMutationType()); - SUNamedType subscription = nullableNamedType(schema.getSubscriptionType()); - for (SUNamedType type : namedTypes.values()) { - if (type == query || type == mutation || type == subscription) { - continue; - } - builder().addAdditionalType(type); - } } private void addDirectiveDefinitions(List directives) { @@ -489,10 +478,6 @@ private SUNamedType namedType(GraphQLNamedType type) { return assertNotNull(namedTypes.get(type.getName()), "Type '%s' is not part of the imported schema", type.getName()); } - private @Nullable SUNamedType nullableNamedType(@Nullable GraphQLNamedType type) { - return type == null ? null : namedType(type); - } - private SUSchemaRoot schemaBuilderRoot() { return builder().getRoot(); } diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index d718588fcf..4b561cd50a 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -8,7 +8,7 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.LinkedHashMap; +import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.function.Consumer; @@ -29,21 +29,27 @@ public final class SUSchema { private final SchemaUniverse universe; private final SUSchemaRoot root; + private final PersistentIntMap namedTypesByNameId; private final PersistentEdgeMap edgeMap; private final PersistentIntMap> vertexMetadata; + private final int namedTypeCount; private final int storedEdgeCount; @Internal public SUSchema( SchemaUniverse universe, SUSchemaRoot root, + PersistentIntMap namedTypesByNameId, PersistentEdgeMap edgeMap, PersistentIntMap> vertexMetadata, + int namedTypeCount, int storedEdgeCount) { this.universe = assertNotNull(universe); this.root = assertNotNull(root); + this.namedTypesByNameId = assertNotNull(namedTypesByNameId); this.edgeMap = assertNotNull(edgeMap); this.vertexMetadata = assertNotNull(vertexMetadata); + this.namedTypeCount = namedTypeCount; this.storedEdgeCount = storedEdgeCount; } @@ -119,29 +125,16 @@ public List getRootTypes() { return Collections.unmodifiableList(result); } - public List getAdditionalTypes() { - return getTypedChildren(root, SUEdgeKind.ADDITIONAL_TYPE, SUNamedType.class); - } - public List getTypes() { - Map result = new LinkedHashMap<>(); - for (SUObjectType rootType : getRootTypes()) { - result.put(rootType.getId(), rootType); - } - for (SUNamedType additionalType : getAdditionalTypes()) { - result.put(additionalType.getId(), additionalType); - } - return Collections.unmodifiableList(new ArrayList<>(result.values())); + List result = new ArrayList<>(namedTypeCount); + namedTypesByNameId.forEachEntry((nameId, type) -> result.add(type)); + result.sort(Comparator.comparing(type -> assertNotNull(type.getName()))); + return Collections.unmodifiableList(result); } public @Nullable SUNamedType getType(String name) { - assertNotNull(name); - for (SUObjectType rootType : getRootTypes()) { - if (name.equals(rootType.getName())) { - return rootType; - } - } - return (SUNamedType) getChild(root, SUEdgeKind.ADDITIONAL_TYPE, name); + int nameId = universe.getNameId(assertNotNull(name)); + return nameId < 0 ? null : namedTypesByNameId.get(nameId); } public @Nullable SUObjectType getObjectType(String name) { @@ -387,6 +380,16 @@ public PersistentEdgeMap getEdgeMap() { return edgeMap; } + @Internal + public PersistentIntMap getNamedTypesByNameId() { + return namedTypesByNameId; + } + + @Internal + public int getNamedTypeCount() { + return namedTypeCount; + } + @Internal public PersistentIntMap> getVertexMetadataMap() { return vertexMetadata; diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 2cb24bed0f..3bc3c45218 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -26,6 +26,8 @@ public final class SUSchemaBuilder { private final Map changedEdges = new LinkedHashMap<>(); private final Map> changedVertexMetadata = new LinkedHashMap<>(); + private PersistentIntMap namedTypesByNameId; + private int namedTypeCount; private boolean built; @Internal @@ -36,6 +38,10 @@ public SUSchemaBuilder( this.universe = assertNotNull(universe); this.root = assertNotNull(root); this.baseSchema = baseSchema; + this.namedTypesByNameId = baseSchema == null + ? PersistentIntMap.empty() + : baseSchema.getNamedTypesByNameId(); + this.namedTypeCount = baseSchema == null ? 0 : baseSchema.getNamedTypeCount(); copyRootEdges(baseSchema); copyRootMetadata(baseSchema); } @@ -161,24 +167,82 @@ public SUSchemaBuilder setSubscriptionType(@Nullable SUObjectType subscriptionTy return setOptionalEdge(SUEdgeKind.SUBSCRIPTION_TYPE, subscriptionType); } - public SUSchemaBuilder additionalType(SUNamedType type) { - return addAdditionalType(type); + public SUSchemaBuilder type(SUNamedType type) { + return addType(type); } - public SUSchemaBuilder addAdditionalType(SUNamedType type) { - return addEdge(root, SUEdgeKind.ADDITIONAL_TYPE, type); + /** + * Adds a named type to this schema. + * + * @param type the named type + * + * @return this builder + */ + public SUSchemaBuilder addType(SUNamedType type) { + assertCanChange(); + assertOwned(type); + int nameId = type.getNameId(); + SUNamedType existing = namedTypesByNameId.get(nameId); + assertTrue( + existing == null || existing == type, + "Schema already contains a different type named '%s'", + assertNotNull(type.getName())); + if (existing == null) { + namedTypesByNameId = namedTypesByNameId.put(nameId, type); + namedTypeCount++; + } + return this; } - public SUSchemaBuilder removeAdditionalType(SUNamedType type) { - return removeEdge(root, SUEdgeKind.ADDITIONAL_TYPE, type); + /** + * Removes a named type from this schema. + * + * @param type the named type + * + * @return this builder + */ + public SUSchemaBuilder removeType(SUNamedType type) { + assertCanChange(); + assertOwned(type); + int nameId = type.getNameId(); + if (namedTypesByNameId.get(nameId) == type) { + namedTypesByNameId = namedTypesByNameId.remove(nameId); + namedTypeCount--; + } + return this; } - public SUSchemaBuilder removeAdditionalType(String name) { - return removeEdge(root, SUEdgeKind.ADDITIONAL_TYPE, name); + /** + * Removes the named type with the given name from this schema. + * + * @param name the type name + * + * @return this builder + */ + public SUSchemaBuilder removeType(String name) { + assertCanChange(); + int nameId = universe.getNameId(assertNotNull(name)); + if (nameId >= 0 && namedTypesByNameId.get(nameId) != null) { + namedTypesByNameId = namedTypesByNameId.remove(nameId); + namedTypeCount--; + } + return this; } - public SUSchemaBuilder clearAdditionalTypes() { - return removeEdges(root, SUEdgeKind.ADDITIONAL_TYPE); + /** + * Removes every named type except the current operation types. + * + * @return this builder + */ + public SUSchemaBuilder clearTypes() { + assertCanChange(); + PackedEdgeSet rootEdges = mutableEdges(root).freeze(); + namedTypesByNameId = PersistentIntMap.empty(); + namedTypeCount = 0; + retainOperationType(rootEdges, SUEdgeKind.QUERY_TYPE); + retainOperationType(rootEdges, SUEdgeKind.MUTATION_TYPE); + retainOperationType(rootEdges, SUEdgeKind.SUBSCRIPTION_TYPE); + return this; } public SUSchemaBuilder directiveDefinition(SUDirective directive) { @@ -479,6 +543,8 @@ private SUSchemaBuilder addEdge( SUVertex target) { assertCanChange(); assertEdgeShape(source, kind, target); + addNamedEndpoint(source); + addNamedEndpoint(target); mutableEdges(source).add(PackedEdgeSet.pack(kind, target.getNameId(), target.getId())); return this; } @@ -490,12 +556,20 @@ private SUSchemaBuilder setEdge( assertCanChange(); assertTrue(kind.isSingle(), "Edge kind %s is not single-valued", kind); assertEdgeShape(source, kind, target); + addNamedEndpoint(source); + addNamedEndpoint(target); MutablePackedEdgeSet edges = mutableEdges(source); edges.removeKind(kind); edges.add(PackedEdgeSet.pack(kind, target.getNameId(), target.getId())); return this; } + private void addNamedEndpoint(SUVertex vertex) { + if (vertex instanceof SUNamedType) { + addType((SUNamedType) vertex); + } + } + private SUSchemaBuilder removeEdge( SUVertex source, SUEdgeKind kind, @@ -555,11 +629,14 @@ public SUSchema build() { } assertTrue(edgeMap.get(root.getId()).firstTarget(SUEdgeKind.QUERY_TYPE) >= 0, "A schema universe schema requires a query type"); + assertOperationTypesRegistered(edgeMap); SUSchema schema = new SUSchema( universe, root, + namedTypesByNameId, edgeMap, metadataMap, + namedTypeCount, edgeCount); universe.registerSchema(schema); return schema; @@ -574,6 +651,35 @@ private SUSchemaBuilder setOptionalEdge( return setEdge(root, kind, target); } + private void assertOperationTypesRegistered(PersistentEdgeMap edgeMap) { + assertOperationTypeRegistered(edgeMap, SUEdgeKind.QUERY_TYPE); + assertOperationTypeRegistered(edgeMap, SUEdgeKind.MUTATION_TYPE); + assertOperationTypeRegistered(edgeMap, SUEdgeKind.SUBSCRIPTION_TYPE); + } + + private void retainOperationType( + PackedEdgeSet rootEdges, + SUEdgeKind kind) { + int targetId = rootEdges.firstTarget(kind); + if (targetId >= 0) { + addType((SUNamedType) universe.getVertex(targetId)); + } + } + + private void assertOperationTypeRegistered( + PersistentEdgeMap edgeMap, + SUEdgeKind kind) { + int targetId = edgeMap.get(root.getId()).firstTarget(kind); + if (targetId < 0) { + return; + } + SUNamedType type = (SUNamedType) universe.getVertex(targetId); + assertTrue( + namedTypesByNameId.get(type.getNameId()) == type, + "Operation type '%s' is not part of the schema", + assertNotNull(type.getName())); + } + private void copyRootEdges(@Nullable SUSchema base) { if (base == null) { return; @@ -720,8 +826,6 @@ private boolean isValidEdge( case SUBSCRIPTION_TYPE: return sourceKind == SUVertexKind.SCHEMA && targetKind == SUVertexKind.OBJECT; - case ADDITIONAL_TYPE: - return sourceKind == SUVertexKind.SCHEMA && isNamedType(targetKind); case DIRECTIVE_DEFINITION: return sourceKind == SUVertexKind.SCHEMA && targetKind == SUVertexKind.DIRECTIVE; diff --git a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java index ee34b80758..045d23e177 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java +++ b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java @@ -102,7 +102,7 @@ public SUSchema generate(String name, TypeDefinitionRegistry registry) { expandNamedTypes(definitions); addOperationRoots(definitions); addSchemaDirectives(definitions); - addAdditionalTypes(); + addTypes(); return builder().build(); } @@ -483,7 +483,7 @@ private void addSchemaDirectives(ImmutableTypeDefinitionRegistry registry) { } } - private void addAdditionalTypes() { + private void addTypes() { for (SUNamedType type : namedTypes.values()) { if (type == queryType || type == mutationType || type == subscriptionType) { continue; @@ -493,7 +493,7 @@ private void addAdditionalTypes() { && !referencedTypeNames.contains(type.getName())) { continue; } - builder().addAdditionalType(type); + builder().addType(type); } } diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java index b0d3fa06cf..571c79acf2 100644 --- a/src/main/java/graphql/schema/universe/SchemaUniverse.java +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -134,10 +134,10 @@ public Map getSchemasByName() { /** * Reclaims vertices that are unused by every registered schema. * - *

Liveness includes each registered schema root, every source and target in its complete - * stored adjacency, vertices with user metadata, and input types referenced by - * applied-directive arguments. Complete stored adjacency is considered so dormant subgraphs - * remain available for later transformations.

+ *

Liveness includes each registered schema root, every named type, every source and target + * in its complete stored adjacency, vertices with user metadata, and input types referenced + * by applied-directive arguments. Complete stored adjacency is considered so dormant + * subgraphs remain available for later transformations.

* *

Reclaimed vertex IDs are never reused. This operation may therefore leave holes in the * chunked vertex arena. It must not run concurrently with schema construction, importing, or @@ -149,10 +149,15 @@ public synchronized int cleanupUnusedVertices() { BitSet liveVertexIds = new BitSet(); Set> visitedEdgeNodes = Collections.newSetFromMap(new IdentityHashMap<>()); + Set> visitedNamedTypeNodes = + Collections.newSetFromMap(new IdentityHashMap<>()); Set>> visitedMetadataNodes = Collections.newSetFromMap(new IdentityHashMap<>()); for (SUSchema schema : schemas) { markVertex(liveVertexIds, schema.getRoot().getId()); + schema.getNamedTypesByNameId().visitUniqueEntries( + visitedNamedTypeNodes, + (nameId, type) -> markVertex(liveVertexIds, type.getId())); schema.getEdgeMap().visitUniqueEntries( visitedEdgeNodes, (sourceId, edges) -> markEdgeBinding(liveVertexIds, sourceId, edges)); diff --git a/src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy b/src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy index 6334cd6077..30759f262c 100644 --- a/src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy +++ b/src/test/groovy/graphql/schema/universe/PersistentIntMapTest.groovy @@ -84,4 +84,29 @@ class PersistentIntMapTest extends Specification { [3, "third"] ] as Set } + + def "entry traversal visits every mapping without changing the map"() { + given: + def map = PersistentIntMap.empty() + .put(37, "thirty-seven") + .put(-1, "negative") + .put(0, "zero") + def entries = [:] + + when: + PersistentIntMap.empty().forEachEntry( + (key, value) -> entries[key] = value) + map.forEachEntry( + (key, value) -> entries[key] = value) + + then: + entries == [ + (37): "thirty-seven", + (-1): "negative", + (0): "zero" + ] + map.get(37) == "thirty-seven" + map.get(-1) == "negative" + map.get(0) == "zero" + } } diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy index 199ea75556..8d1f4df8b2 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy @@ -60,7 +60,8 @@ class SUSchemaGeneratorTest extends Specification { schema.queryType.name == "Root" schema.mutationType.name == "Changes" schema.subscriptionType == null - schema.additionalTypes*.name.containsAll(["Node", "Filter", "Choice", "String", "ID", "Boolean"]) + schema.types*.name.containsAll( + ["Root", "Changes", "Node", "Filter", "Choice", "String", "ID", "Boolean"]) and: def root = schema.getObjectType("Root") @@ -111,9 +112,7 @@ class SUSchemaGeneratorTest extends Specification { schema.queryType.name == "Query" schema.mutationType.name == "Mutation" schema.subscriptionType.name == "Subscription" - !schema.additionalTypes*.name.contains("Query") - !schema.additionalTypes*.name.contains("Mutation") - !schema.additionalTypes*.name.contains("Subscription") + schema.types*.name.containsAll(["Query", "Mutation", "Subscription"]) } def "preserves input defaults and all applied directive argument states"() { diff --git a/src/test/groovy/graphql/schema/universe/SUTest.groovy b/src/test/groovy/graphql/schema/universe/SUTest.groovy index 3753450327..17f718c3cb 100644 --- a/src/test/groovy/graphql/schema/universe/SUTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUTest.groovy @@ -273,6 +273,71 @@ class SUTest extends Specification { thrown(AssertException) } + def "edge endpoints register every named type directly on the schema"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def user = universe.newObjectType("User") + def field = universe.newField("user") + def name = universe.newField("name") + def string = universe.newScalarType("String") + + when: + def schema = universe.newSchema("schema") + .queryType(query) + .addField(query, field) + .setFieldType(field, user) + .addField(user, name) + .setFieldType(name, string) + .build() + + then: + schema.types == [query, string, user] + schema.getType("Query").is(query) + schema.getObjectType("User").is(user) + schema.getScalarType("String").is(string) + } + + def "named type registry is structurally shared across schema transforms"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def string = universe.newScalarType("String") + def integer = universe.newScalarType("Int") + def base = universe.newSchema("base") + .queryType(query) + .addType(string) + .build() + + when: + def unchanged = base.transform("unchanged", builder -> builder.addType(string)) + def changed = base.transform("changed", builder -> builder.addType(integer)) + + then: + unchanged.namedTypesByNameId.is(base.namedTypesByNameId) + !changed.namedTypesByNameId.is(base.namedTypesByNameId) + changed.getType("Query").is(query) + changed.getType("String").is(string) + changed.getType("Int").is(integer) + base.getType("Int") == null + } + + def "a schema rejects different named type vertices with the same name"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def duplicate = universe.newScalarType("Query") + + when: + universe.newSchema("invalid") + .queryType(query) + .addType(duplicate) + + then: + def exception = thrown(AssertException) + exception.message.contains("different type named 'Query'") + } + def "typed builder helpers cover every schema relationship"() { given: def universe = new SchemaUniverse() @@ -305,14 +370,14 @@ class SUTest extends Specification { .setQueryType(query) .setMutationType(mutation) .setSubscriptionType(subscription) - .addAdditionalType(user) - .addAdditionalType(node) - .addAdditionalType(resource) - .addAdditionalType(searchResult) - .addAdditionalType(status) - .addAdditionalType(filter) - .addAdditionalType(string) - .addAdditionalType(integer) + .addType(user) + .addType(node) + .addType(resource) + .addType(searchResult) + .addType(status) + .addType(filter) + .addType(string) + .addType(integer) .addDirectiveDefinition(tag) .addField(query, search) .addField(node, id) @@ -369,7 +434,7 @@ class SUTest extends Specification { .removeAppliedDirective(user, firstTag) .removeSchemaAppliedDirective(schemaTag) .removeDirectiveDefinition(tag) - .removeAdditionalType(integer)) + .removeType(integer)) then: removed.getFields(query).isEmpty() @@ -404,7 +469,7 @@ class SUTest extends Specification { .clearAppliedDirectives(user) .clearSchemaAppliedDirectives() .clearDirectiveDefinitions() - .clearAdditionalTypes()) + .clearTypes()) then: withoutTags.getAppliedDirectives(user).isEmpty() @@ -422,7 +487,7 @@ class SUTest extends Specification { cleared.getAppliedDirectives(user).isEmpty() cleared.schemaAppliedDirectives.isEmpty() cleared.directiveDefinitions.isEmpty() - cleared.additionalTypes.isEmpty() + cleared.types == [query] and: schema.getFields(query) == [search] @@ -687,7 +752,7 @@ class SUTest extends Specification { universe.newAppliedDirective("tag", [originalArgument]) def base = universe.newSchema("base") .queryType(query) - .addAdditionalType(integer) + .addType(integer) .addAppliedDirective(query, originalDirective) .build() @@ -710,8 +775,8 @@ class SUTest extends Specification { and: universe.vertexCount == 6 - base.storedEdgeCount == 3 - changed.storedEdgeCount == 3 + base.storedEdgeCount == 2 + changed.storedEdgeCount == 2 } def "applied directive payload rejects duplicate names and foreign universes"() { @@ -860,7 +925,7 @@ class SUTest extends Specification { schema.getType("missing") == null schema.getObjectType("Role") == null schema.getScalarType("String").name == "String" - schema.additionalTypes.contains(user) + schema.types.contains(user) and: schema.getFields(user)*.name == ["id", "name"] diff --git a/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy index 1c4566bdda..411483ce55 100644 --- a/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy @@ -71,13 +71,13 @@ class SchemaUniverseCleanupTest extends Specification { def string = universe.newScalarType("String") def base = universe.newSchema("base") .queryType(query) - .addAdditionalType(extra) + .addType(extra) .addField(extra, field) .setFieldType(field, string) .build() def withoutExtra = base.transform( "withoutExtra", - builder -> builder.removeAdditionalType(extra)) + builder -> builder.removeType(extra)) def baseRoot = base.root expect: @@ -98,7 +98,7 @@ class SchemaUniverseCleanupTest extends Specification { when: def reattached = withoutExtra.transform( "reattached", - builder -> builder.addAdditionalType(extra)) + builder -> builder.addType(extra)) then: reattached.getObjectType("Extra").is(extra) @@ -133,6 +133,32 @@ class SchemaUniverseCleanupTest extends Specification { schema.getAppliedDirectives(query) == [appliedDirective] } + def "cleanup retains isolated registered types until their schema is removed"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def isolated = universe.newEnumType("Isolated") + def schema = universe.newSchema("schema") + .queryType(query) + .addType(isolated) + .build() + def root = schema.root + + expect: + universe.cleanupUnusedVertices() == 0 + universe.owns(isolated) + schema.getEnumType("Isolated").is(isolated) + + when: + universe.removeSchema(schema) + + then: + universe.cleanupUnusedVertices() == 3 + !universe.owns(root) + !universe.owns(query) + !universe.owns(isolated) + } + def "cleanup releases empty chunks and allocation continues above reclaimed ids"() { given: def universe = new SchemaUniverse() From 199215037effe50b63ac162f6b1383ed137938b9 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 4 Aug 2026 07:26:37 +0000 Subject: [PATCH 09/32] Support applied directives on universe directive definitions --- docs/schema-universe.md | 2 +- .../schema/idl/SchemaPrinterWriter.java | 8 +++- .../graphql/schema/universe/SUDirective.java | 2 +- .../graphql/schema/universe/SUExporter.java | 7 +++- .../graphql/schema/universe/SUImporter.java | 16 +++++++ .../schema/universe/SUSchemaBuilder.java | 1 + .../schema/universe/SUSchemaGenerator.java | 16 +++++++ .../SUAppliedDirectiveOrderTest.groovy | 20 +++++++-- .../universe/SUSchemaGeneratorTest.groovy | 42 +++++++++++++++++++ .../universe/SUSchemaRoundTripTest.groovy | 7 +++- 10 files changed, 111 insertions(+), 10 deletions(-) diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 81076ec0b5..662404d1a8 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -52,7 +52,7 @@ The current API assumes callers traverse from the root or another known-reachabl directly. It provides typed helpers for: - operation roots, all named types, and lookup by type name; -- directive definitions and schema-applied directives; +- directive definitions, including their applied directives, and schema-applied directives; - object and interface fields; - field and directive arguments; - declared types and list/non-null wrapped types; diff --git a/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java b/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java index e24dd34ca2..19cd2c3295 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java @@ -311,9 +311,13 @@ private void printDirectiveDefinition(PrintWriter out, Object directive) { } printComments(out, directive, ""); out.format( - "directive @%s%s", + "directive @%s%s%s", access.getName(directive), - argsString(directive)); + argsString(directive), + directivesString( + directive, + access.getAppliedDirectives(directive), + false)); if (access.isRepeatable(directive)) { out.print(" repeatable"); } diff --git a/src/main/java/graphql/schema/universe/SUDirective.java b/src/main/java/graphql/schema/universe/SUDirective.java index badda6312e..8312fac291 100644 --- a/src/main/java/graphql/schema/universe/SUDirective.java +++ b/src/main/java/graphql/schema/universe/SUDirective.java @@ -15,7 +15,7 @@ @ExperimentalApi @NullMarked -public final class SUDirective extends SUVertex { +public final class SUDirective extends SUVertex implements SUAppliedDirectiveContainer { private final boolean repeatable; private final long locationMask; diff --git a/src/main/java/graphql/schema/universe/SUExporter.java b/src/main/java/graphql/schema/universe/SUExporter.java index cae8d5317d..92e21153ee 100644 --- a/src/main/java/graphql/schema/universe/SUExporter.java +++ b/src/main/java/graphql/schema/universe/SUExporter.java @@ -266,7 +266,8 @@ private GraphQLDirective exportDirectiveDefinition(SUDirective directive) { GraphQLDirective.Builder builder = GraphQLDirective.newDirective() .name(requiredName(directive)) .description(directive.getDescription()) - .repeatable(directive.isRepeatable()); + .repeatable(directive.isRepeatable()) + .replaceAppliedDirectives(exportAppliedDirectives(directive)); directive.validLocations().forEach(builder::validLocation); for (SUArgument argument : schema.getArguments(directive)) { builder.argument(exportArgument(argument)); @@ -274,6 +275,10 @@ private GraphQLDirective exportDirectiveDefinition(SUDirective directive) { if (directive.getDefinition() != null) { builder.definition(directive.getDefinition()); } + String deprecationReason = deprecationReason(directive); + if (deprecationReason != null) { + builder.deprecate(deprecationReason); + } return builder.build(); } diff --git a/src/main/java/graphql/schema/universe/SUImporter.java b/src/main/java/graphql/schema/universe/SUImporter.java index 5a323b98b8..57657bea0c 100644 --- a/src/main/java/graphql/schema/universe/SUImporter.java +++ b/src/main/java/graphql/schema/universe/SUImporter.java @@ -136,10 +136,16 @@ private void addDirectiveDefinitions(List directives) { directive.getDefinition()); elementVertices.put(directive, vertex); builder().addDirectiveDefinition(vertex); + } + for (GraphQLDirective directive : directives) { + SUDirective vertex = directiveVertex(directive); for (GraphQLArgument argument : directive.getArguments()) { addArgument(vertex, argument); } } + for (GraphQLDirective directive : directives) { + addAppliedDirectives(directive, directiveVertex(directive)); + } } private void expandNamedTypes(List types) { @@ -356,6 +362,9 @@ private boolean hasDirective( if (container instanceof GraphQLArgument) { return ((GraphQLArgument) container).getDeprecationReason(); } + if (container instanceof GraphQLDirective) { + return ((GraphQLDirective) container).getDeprecationReason(); + } return null; } @@ -474,6 +483,13 @@ private SUAppliedDirectiveArgument appliedArgument(GraphQLAppliedDirectiveArgume return value; } + private SUDirective directiveVertex(GraphQLDirective directive) { + return (SUDirective) assertNotNull( + elementVertices.get(directive), + "Directive '%s' is not part of the imported schema", + directive.getName()); + } + private SUNamedType namedType(GraphQLNamedType type) { return assertNotNull(namedTypes.get(type.getName()), "Type '%s' is not part of the imported schema", type.getName()); } diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 3bc3c45218..5b9d14c999 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -878,6 +878,7 @@ private boolean isImplementingType(SUVertexKind kind) { private boolean isDirectiveContainer(SUVertexKind kind) { return kind == SUVertexKind.SCHEMA + || kind == SUVertexKind.DIRECTIVE || kind == SUVertexKind.OBJECT || kind == SUVertexKind.FIELD || kind == SUVertexKind.INTERFACE diff --git a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java index 045d23e177..c3d793a818 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java +++ b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java @@ -9,6 +9,7 @@ import graphql.language.Description; import graphql.language.Directive; import graphql.language.DirectiveDefinition; +import graphql.language.DirectiveExtensionDefinition; import graphql.language.EnumTypeDefinition; import graphql.language.EnumTypeExtensionDefinition; import graphql.language.EnumValueDefinition; @@ -99,6 +100,7 @@ public SUSchema generate(String name, TypeDefinitionRegistry registry) { createNamedTypes(definitions); createDirectiveDefinitions(definitions); expandDirectiveDefinitions(definitions); + addDirectiveDefinitionDirectives(definitions); expandNamedTypes(definitions); addOperationRoots(definitions); addSchemaDirectives(definitions); @@ -221,6 +223,20 @@ private void expandDirectiveDefinitions(ImmutableTypeDefinitionRegistry registry } } + private void addDirectiveDefinitionDirectives( + ImmutableTypeDefinitionRegistry registry) { + for (DirectiveDefinition definition : registry.getDirectiveDefinitions().values()) { + SUDirective directive = assertNotNull( + directiveDefinitions.get(definition.getName())); + addAppliedDirectives(definition.getDirectives(), directive); + for (DirectiveExtensionDefinition extension : + registry.directiveExtensions() + .getOrDefault(definition.getName(), List.of())) { + addAppliedDirectives(extension.getDirectives(), directive); + } + } + } + private void expandNamedTypes(ImmutableTypeDefinitionRegistry registry) { for (TypeDefinition definition : registry.types().values()) { SUNamedType type = namedType(definition.getName()); diff --git a/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy index 0c518c0f59..02ef372242 100644 --- a/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy @@ -15,6 +15,7 @@ class SUAppliedDirectiveOrderTest extends Specification { def firstZ = universe.newAppliedDirective("z") def secondZ = universe.newAppliedDirective("z") def b = universe.newAppliedDirective("b") + def definition = universe.newDirective("definition") def query = universe.newObjectType("Query") def field = universe.newField("value") def string = universe.newScalarType("String") @@ -22,10 +23,15 @@ class SUAppliedDirectiveOrderTest extends Specification { .queryType(query) .addField(query, field) .setFieldType(field, string) + .addDirectiveDefinition(definition) .addAppliedDirective(query, firstZ) .addAppliedDirective(query, a) .addAppliedDirective(query, secondZ) .addAppliedDirective(query, m) + .addAppliedDirective(definition, firstZ) + .addAppliedDirective(definition, a) + .addAppliedDirective(definition, secondZ) + .addAppliedDirective(definition, m) .addSchemaAppliedDirective(m) .addSchemaAppliedDirective(a) .build() @@ -35,6 +41,7 @@ class SUAppliedDirectiveOrderTest extends Specification { base.getAppliedDirectives(query, "z") == [firstZ, secondZ] base.getChild(query, SUEdgeKind.APPLIED_DIRECTIVE, "z").is(firstZ) base.containsEdge(query, SUEdgeKind.APPLIED_DIRECTIVE, secondZ) + base.getAppliedDirectives(definition) == [firstZ, a, secondZ, m] base.schemaAppliedDirectives == [m, a] when: @@ -48,6 +55,9 @@ class SUAppliedDirectiveOrderTest extends Specification { .replaceSchemaAppliedDirective(m, firstZ)) def reordered = base.transform("reordered", builder -> builder.replaceAppliedDirectives(query, [m, secondZ, a, firstZ]) + .replaceAppliedDirectives( + definition, + [m, secondZ, a, firstZ]) .replaceSchemaAppliedDirectives([a, m])) then: @@ -57,6 +67,7 @@ class SUAppliedDirectiveOrderTest extends Specification { replaced.getAppliedDirectives(query) == [firstZ, a, b, m] replaced.schemaAppliedDirectives == [firstZ, a] reordered.getAppliedDirectives(query) == [m, secondZ, a, firstZ] + reordered.getAppliedDirectives(definition) == [m, secondZ, a, firstZ] reordered.schemaAppliedDirectives == [a, m] and: @@ -114,6 +125,8 @@ class SUAppliedDirectiveOrderTest extends Specification { def filter = schema.getInputObjectType("Filter") return [ schema : schema.schemaAppliedDirectives*.name, + directive : schema.getAppliedDirectives( + schema.getDirectiveDefinition("target"))*.name, scalar : schema.getAppliedDirectives( schema.getScalarType("Custom"))*.name, object : schema.getAppliedDirectives(query)*.name, @@ -141,13 +154,14 @@ class SUAppliedDirectiveOrderTest extends Specification { return ''' directive @a on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | - INPUT_OBJECT | INPUT_FIELD_DEFINITION + INPUT_OBJECT | INPUT_FIELD_DEFINITION | DIRECTIVE_DEFINITION directive @m on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | - INPUT_OBJECT | INPUT_FIELD_DEFINITION + INPUT_OBJECT | INPUT_FIELD_DEFINITION | DIRECTIVE_DEFINITION directive @z repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | - INPUT_OBJECT | INPUT_FIELD_DEFINITION + INPUT_OBJECT | INPUT_FIELD_DEFINITION | DIRECTIVE_DEFINITION + directive @target @z @a @z @m on FIELD_DEFINITION schema @z @a @z @m { query: Query diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy index 8d1f4df8b2..04d66da114 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy @@ -115,6 +115,48 @@ class SUSchemaGeneratorTest extends Specification { schema.types*.name.containsAll(["Query", "Mutation", "Subscription"]) } + def "preserves applied directives on directive definitions and extensions"() { + given: + def sdl = ''' + directive @meta(value: String!) repeatable on DIRECTIVE_DEFINITION + directive @target @meta(value: "definition") + @deprecated(reason: "Use @replacement") on FIELD_DEFINITION + extend directive @target @meta(value: "extension") + + type Query { + value: String + } + ''' + + when: + def generated = new SchemaUniverse().parseSchema("generated", sdl) + def target = generated.getDirectiveDefinition("target") + def exported = generated.toGraphQLSchema() + def imported = new SchemaUniverse().importSchema("imported", exported) + def importedTarget = imported.getDirectiveDefinition("target") + + then: + generated.getAppliedDirectives(target)*.name == + ["meta", "deprecated", "meta"] + generated.getAppliedDirectives(target, "meta") + *.getArgument("value")*.argumentValue*.value*.value == + ["definition", "extension"] + + and: + exported.getDirective("target").deprecated + exported.getDirective("target").deprecationReason == "Use @replacement" + exported.getDirective("target").getAppliedDirectives("meta") + *.getArgument("value")*.argumentValue*.value*.value == + ["definition", "extension"] + + and: + imported.getAppliedDirectives(importedTarget)*.name == + ["meta", "deprecated", "meta"] + imported.getAppliedDirectives(importedTarget, "meta") + *.getArgument("value")*.argumentValue*.value*.value == + ["definition", "extension"] + } + def "preserves input defaults and all applied directive argument states"() { given: def sdl = ''' diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy index 708626364f..ac3e991932 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy @@ -140,11 +140,14 @@ class SUSchemaRoundTripTest extends Specification { text: String = "fallback" count: Int flags: [Boolean!] = [true, false] - config: MetaInput = {enabled: true, mode: FIRST} unset: String ) repeatable on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | - INPUT_OBJECT | INPUT_FIELD_DEFINITION + INPUT_OBJECT | INPUT_FIELD_DEFINITION | DIRECTIVE_DEFINITION + + directive @target @meta(text: "definition") + @deprecated(reason: "Use @replacement") on FIELD_DEFINITION + extend directive @target @meta(text: "extension") schema @meta(text: "schema") @meta(count: 2) { query: Query From 2284c7fdc148014e273929586fb70c46ba867709 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Tue, 4 Aug 2026 21:56:37 +0000 Subject: [PATCH 10/32] Fix named type registration for directive replacement --- .../schema/universe/SUSchemaBuilder.java | 7 +++- .../SUAppliedDirectiveOrderTest.groovy | 32 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 5b9d14c999..931db8dd8b 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -501,11 +501,13 @@ public SUSchemaBuilder replaceAppliedDirective( SUEdgeKind.APPLIED_DIRECTIVE, replacement.getNameId(), replacement.getId()); + boolean replaced = mutableEdges(source).replace(currentEdge, replacementEdge); assertTrue( - mutableEdges(source).replace(currentEdge, replacementEdge), + replaced, "Applied directive %s is not attached to container %s", current, source); + addNamedEndpoint(source); return this; } @@ -518,6 +520,9 @@ public SUSchemaBuilder replaceAppliedDirectives( for (SUAppliedDirective directive : replacements) { assertEdgeShape(source, SUEdgeKind.APPLIED_DIRECTIVE, directive); } + if (!replacements.isEmpty()) { + addNamedEndpoint(source); + } MutablePackedEdgeSet edges = mutableEdges(source); edges.removeKind(SUEdgeKind.APPLIED_DIRECTIVE); for (SUAppliedDirective directive : replacements) { diff --git a/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy index 02ef372242..4d31a73f2d 100644 --- a/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy @@ -83,6 +83,38 @@ class SUAppliedDirectiveOrderTest extends Specification { thrown(AssertException) } + def "replacing applied directives registers named containers"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def custom = universe.newScalarType("Custom") + def unused = universe.newScalarType("Unused") + def first = universe.newAppliedDirective("first") + def second = universe.newAppliedDirective("second") + def replacement = universe.newAppliedDirective("replacement") + + when: + def base = universe.newSchema("base") + .queryType(query) + .replaceAppliedDirectives(custom, [first, second]) + .replaceAppliedDirectives(unused, []) + .build() + + then: + base.getScalarType("Custom").is(custom) + base.getType("Unused") == null + base.getAppliedDirectives(custom) == [first, second] + + when: + def changed = base.transform("changed", builder -> builder + .removeType(custom) + .replaceAppliedDirective(custom, first, replacement)) + + then: + changed.getScalarType("Custom").is(custom) + changed.getAppliedDirectives(custom) == [replacement, second] + } + def "SDL and GraphQLSchema round trips retain order on every directive container"() { given: def registry = new SchemaParser().parse(directivesEverywhereSdl()) From 9525c3da5a75253cbe62308226fdf2caa5278f90 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 5 Aug 2026 04:17:17 +0000 Subject: [PATCH 11/32] Remove incomplete schema universe type removal APIs --- docs/schema-universe.md | 4 +- .../benchmark/SchemaUniverseBenchmark.java | 30 ++++++---- .../schema/universe/SUSchemaBuilder.java | 60 ------------------- .../SUAppliedDirectiveOrderTest.groovy | 7 +-- .../graphql/schema/universe/SUTest.groovy | 10 ++-- .../universe/SchemaUniverseCleanupTest.groovy | 12 ++-- 6 files changed, 32 insertions(+), 91 deletions(-) diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 662404d1a8..8098d6d6cc 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -109,10 +109,10 @@ adjacency, and `T` is the number of named types: | Read named type by name | `O(log32 T)` | none | | Read vertex metadata | `O(log32 M)` plus metadata-map lookup | none | | Add/remove edge | `O(d)` transient, then canonicalize at build | one changed primitive array | -| Add/remove named type | `O(log32 T)` | one trie path | +| Add named type | `O(log32 T)` | one trie path | | Edit vertex metadata | `O(log32 M + keys(vertex))` | one trie path and metadata map | | Publish `k` changed sources | `O(k log32 V)` | copied trie paths plus `k` arrays | -| Derive with no type edits | effectively root replacement | root edge paths; type registry is shared | +| Derive with no named-type additions | effectively root replacement | root edge paths; type registry is shared | Here `M` is the number of vertices with metadata in the snapshot. diff --git a/src/jmh/java/benchmark/SchemaUniverseBenchmark.java b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java index 4b4f7416fb..ba121f36f4 100644 --- a/src/jmh/java/benchmark/SchemaUniverseBenchmark.java +++ b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java @@ -56,8 +56,8 @@ * *

Every version replaces a versioned applied directive on 1,000 object types distributed across * the source schema. Each five-step cycle additionally adds a detached object type, adds and removes - * a field, adds a replacement field, and removes the type. The {@code retain100...} methods print - * exact JOL object-graph footprints. Run the {@code profileRetained100...} methods separately with + * a field, then adds and removes a replacement field. The {@code retain100...} methods print exact + * JOL object-graph footprints. Run the {@code profileRetained100...} methods separately with * async-profiler's sampled live-allocation mode to attribute surviving allocations; use the JOL * values, not the sampled profile totals, for exact retained byte counts:

* @@ -419,8 +419,12 @@ public TraversalControl visitGraphQLObjectType( newStringField(current, mutation.replacementFieldName)); changed = true; break; - case REMOVE_SYNTHETIC_TYPE: - return deleteNode(context); + case REMOVE_REPLACEMENT_FIELD: + changedObject = withRemovedField( + changedObject, + mutation.replacementFieldName); + changed = true; + break; default: break; } @@ -443,8 +447,6 @@ public TraversalControl visitGraphQLObjectType( current, visitor, builder -> builder.additionalType(syntheticType)); - } else if (mutation.kind == EditKind.REMOVE_SYNTHETIC_TYPE) { - transformed = SchemaTransformer.transformSchemaWithDeletes(current, visitor); } else { transformed = SchemaTransformer.transformSchema(current, visitor); } @@ -558,8 +560,12 @@ private static SUSchema applySUMutation( builder.addField(typeForReplacement, replacementField) .setFieldType(replacementField, stringType); break; - case REMOVE_SYNTHETIC_TYPE: - builder.removeType(mutation.syntheticTypeName); + case REMOVE_REPLACEMENT_FIELD: + SUObjectType typeForReplacementRemoval = + requireNonNull(current.getObjectType(mutation.syntheticTypeName)); + builder.removeField( + typeForReplacementRemoval, + mutation.replacementFieldName); break; default: throw new IllegalStateException("Unhandled mutation " + mutation.kind); @@ -643,9 +649,8 @@ private static void verifyFinalSyntheticType( MutationPlan plan) { int mutationCount = plan.mutations.size(); int finalCycle = (mutationCount - 1) / EditKind.values().length; - boolean shouldBePresent = mutationCount % EditKind.values().length != 0; boolean isPresent = schema.getType(SYNTHETIC_PREFIX + "Type" + finalCycle) != null; - if (isPresent != shouldBePresent) { + if (!isPresent) { throw new IllegalStateException("Final synthetic GraphQLSchema type state differs"); } } @@ -655,9 +660,8 @@ private static void verifyFinalSyntheticType( MutationPlan plan) { int mutationCount = plan.mutations.size(); int finalCycle = (mutationCount - 1) / EditKind.values().length; - boolean shouldBePresent = mutationCount % EditKind.values().length != 0; boolean isPresent = schema.getType(SYNTHETIC_PREFIX + "Type" + finalCycle) != null; - if (isPresent != shouldBePresent) { + if (!isPresent) { throw new IllegalStateException("Final synthetic SUSchema type state differs"); } } @@ -857,7 +861,7 @@ private enum EditKind { ADD_SYNTHETIC_FIELD, REMOVE_SYNTHETIC_FIELD, ADD_REPLACEMENT_FIELD, - REMOVE_SYNTHETIC_TYPE + REMOVE_REPLACEMENT_FIELD } private static final class Mutation { diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 931db8dd8b..2a05a1c77f 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -194,57 +194,6 @@ public SUSchemaBuilder addType(SUNamedType type) { return this; } - /** - * Removes a named type from this schema. - * - * @param type the named type - * - * @return this builder - */ - public SUSchemaBuilder removeType(SUNamedType type) { - assertCanChange(); - assertOwned(type); - int nameId = type.getNameId(); - if (namedTypesByNameId.get(nameId) == type) { - namedTypesByNameId = namedTypesByNameId.remove(nameId); - namedTypeCount--; - } - return this; - } - - /** - * Removes the named type with the given name from this schema. - * - * @param name the type name - * - * @return this builder - */ - public SUSchemaBuilder removeType(String name) { - assertCanChange(); - int nameId = universe.getNameId(assertNotNull(name)); - if (nameId >= 0 && namedTypesByNameId.get(nameId) != null) { - namedTypesByNameId = namedTypesByNameId.remove(nameId); - namedTypeCount--; - } - return this; - } - - /** - * Removes every named type except the current operation types. - * - * @return this builder - */ - public SUSchemaBuilder clearTypes() { - assertCanChange(); - PackedEdgeSet rootEdges = mutableEdges(root).freeze(); - namedTypesByNameId = PersistentIntMap.empty(); - namedTypeCount = 0; - retainOperationType(rootEdges, SUEdgeKind.QUERY_TYPE); - retainOperationType(rootEdges, SUEdgeKind.MUTATION_TYPE); - retainOperationType(rootEdges, SUEdgeKind.SUBSCRIPTION_TYPE); - return this; - } - public SUSchemaBuilder directiveDefinition(SUDirective directive) { return addDirectiveDefinition(directive); } @@ -662,15 +611,6 @@ private void assertOperationTypesRegistered(PersistentEdgeMap edgeMap) { assertOperationTypeRegistered(edgeMap, SUEdgeKind.SUBSCRIPTION_TYPE); } - private void retainOperationType( - PackedEdgeSet rootEdges, - SUEdgeKind kind) { - int targetId = rootEdges.firstTarget(kind); - if (targetId >= 0) { - addType((SUNamedType) universe.getVertex(targetId)); - } - } - private void assertOperationTypeRegistered( PersistentEdgeMap edgeMap, SUEdgeKind kind) { diff --git a/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy index 4d31a73f2d..a2904f7458 100644 --- a/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUAppliedDirectiveOrderTest.groovy @@ -83,7 +83,7 @@ class SUAppliedDirectiveOrderTest extends Specification { thrown(AssertException) } - def "replacing applied directives registers named containers"() { + def "replacing applied directives preserves named container registration"() { given: def universe = new SchemaUniverse() def query = universe.newObjectType("Query") @@ -106,9 +106,8 @@ class SUAppliedDirectiveOrderTest extends Specification { base.getAppliedDirectives(custom) == [first, second] when: - def changed = base.transform("changed", builder -> builder - .removeType(custom) - .replaceAppliedDirective(custom, first, replacement)) + def changed = base.transform("changed", builder -> + builder.replaceAppliedDirective(custom, first, replacement)) then: changed.getScalarType("Custom").is(custom) diff --git a/src/test/groovy/graphql/schema/universe/SUTest.groovy b/src/test/groovy/graphql/schema/universe/SUTest.groovy index 17f718c3cb..7475964ca1 100644 --- a/src/test/groovy/graphql/schema/universe/SUTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUTest.groovy @@ -433,8 +433,7 @@ class SUTest extends Specification { .removeInputField(filter, term) .removeAppliedDirective(user, firstTag) .removeSchemaAppliedDirective(schemaTag) - .removeDirectiveDefinition(tag) - .removeType(integer)) + .removeDirectiveDefinition(tag)) then: removed.getFields(query).isEmpty() @@ -449,7 +448,7 @@ class SUTest extends Specification { removed.getAppliedDirectives(user) == [secondTag] removed.schemaAppliedDirectives.isEmpty() removed.getDirectiveDefinition("tag") == null - removed.getScalarType("Int") == null + removed.getScalarType("Int").is(integer) when: def withoutTags = schema.transform("withoutTags", builder -> @@ -468,8 +467,7 @@ class SUTest extends Specification { .clearInputFields(filter) .clearAppliedDirectives(user) .clearSchemaAppliedDirectives() - .clearDirectiveDefinitions() - .clearTypes()) + .clearDirectiveDefinitions()) then: withoutTags.getAppliedDirectives(user).isEmpty() @@ -487,7 +485,7 @@ class SUTest extends Specification { cleared.getAppliedDirectives(user).isEmpty() cleared.schemaAppliedDirectives.isEmpty() cleared.directiveDefinitions.isEmpty() - cleared.types == [query] + cleared.types == schema.types and: schema.getFields(query) == [search] diff --git a/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy index 411483ce55..66014b8650 100644 --- a/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy @@ -75,9 +75,9 @@ class SchemaUniverseCleanupTest extends Specification { .addField(extra, field) .setFieldType(field, string) .build() - def withoutExtra = base.transform( - "withoutExtra", - builder -> builder.removeType(extra)) + def withoutField = base.transform( + "withoutField", + builder -> builder.removeField(extra, field)) def baseRoot = base.root expect: @@ -93,12 +93,12 @@ class SchemaUniverseCleanupTest extends Specification { universe.owns(extra) universe.owns(field) universe.owns(string) - withoutExtra.getType("Extra") == null + withoutField.getField(extra, "value") == null when: - def reattached = withoutExtra.transform( + def reattached = withoutField.transform( "reattached", - builder -> builder.addType(extra)) + builder -> builder.addField(extra, field)) then: reattached.getObjectType("Extra").is(extra) From 975e5b1a8b0455e393042f8139a5c8611b1f41bf Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 5 Aug 2026 04:34:23 +0000 Subject: [PATCH 12/32] Index schema universe interface implementations --- docs/schema-universe.md | 23 ++- .../schema/universe/PackedEdgeSet.java | 12 ++ .../graphql/schema/universe/SUSchema.java | 22 +++ .../schema/universe/SUSchemaBuilder.java | 105 +++++++++++ .../SUSchemaImplementationsTest.groovy | 172 ++++++++++++++++++ 5 files changed, 324 insertions(+), 10 deletions(-) create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 8098d6d6cc..7bf1e029e7 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -25,10 +25,11 @@ vertex type for each supported GraphQL schema-element kind. A vertex contains in properties such as kind, name, and description, but no child references. `SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex, a persistent -registry of every named type keyed by universe name ID, and sparse user-controlled metadata keyed -by vertex ID. Root edges identify operation roots and directive definitions. Named-type membership -is independent of reachability, so a schema can retain disconnected types without representing -membership as graph edges. +registry of every named type keyed by universe name ID, a persistent reverse index from interface +vertex ID to implementing object types, and sparse user-controlled metadata keyed by vertex ID. +Root edges identify operation roots and directive definitions. Named-type membership is independent +of reachability, so a schema can retain disconnected types without representing membership as graph +edges. Every successfully built snapshot registers itself with its universe under its unique name. `SchemaUniverse.getSchema(name)` selects one snapshot directly, while `getSchemas()` and @@ -93,20 +94,22 @@ Java integers. ## Persistent snapshots -Outgoing arrays, named types, and metadata associations are stored in persistent hash-array mapped -tries. Adjacency and metadata use vertex IDs as keys, while the type registry uses universe name -IDs. A transform batches edge changes, freezes each changed value once, and path-copies only the -affected trie branches. All unchanged branches, outgoing arrays, named types, and metadata maps are -shared by identity with the parent schema. +Outgoing arrays, named types, interface implementations, and metadata associations are stored in +persistent hash-array mapped tries. Adjacency, implementations, and metadata use vertex IDs as keys, +while the type registry uses universe name IDs. A transform batches edge changes, freezes each +changed value once, and path-copies only the affected trie branches. All unchanged branches, +outgoing arrays, implementation lists, named types, and metadata maps are shared by identity with +the parent schema. Approximate operation costs, where `d` is source out-degree, `V` is the number of sources with -adjacency, and `T` is the number of named types: +adjacency, `T` is the number of named types, and `I` is the number of indexed interfaces: | Operation | Time | New retained storage | | --- | --- | --- | | Read outgoing adjacency | `O(log32 V)` | none | | Child lookup by name | `O(log32 V + log d)` unordered, `O(log32 V + d)` ordered | none | | Read named type by name | `O(log32 T)` | none | +| Read interface implementations | `O(log32 I)` | none | | Read vertex metadata | `O(log32 M)` plus metadata-map lookup | none | | Add/remove edge | `O(d)` transient, then canonicalize at build | one changed primitive array | | Add named type | `O(log32 T)` | one trie path | diff --git a/src/main/java/graphql/schema/universe/PackedEdgeSet.java b/src/main/java/graphql/schema/universe/PackedEdgeSet.java index 088cf874f5..784c4dcaac 100644 --- a/src/main/java/graphql/schema/universe/PackedEdgeSet.java +++ b/src/main/java/graphql/schema/universe/PackedEdgeSet.java @@ -197,6 +197,18 @@ public int targetIdAt(int index) { return targetId(edges[index]); } + /** + * Extracts the target name ID at an array index. + * + * @param index an index between {@link #firstIndex(SUEdgeKind)} and + * {@link #endIndex(SUEdgeKind)} + * + * @return the target name ID + */ + public int targetNameIdAt(int index) { + return edgeNameId(edges[index]); + } + /** * Copies the packed edge array for mutable editing. * diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index 4b561cd50a..7d3bed5a09 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -30,6 +30,7 @@ public final class SUSchema { private final SchemaUniverse universe; private final SUSchemaRoot root; private final PersistentIntMap namedTypesByNameId; + private final PersistentIntMap> implementationsByInterfaceId; private final PersistentEdgeMap edgeMap; private final PersistentIntMap> vertexMetadata; private final int namedTypeCount; @@ -40,6 +41,7 @@ public SUSchema( SchemaUniverse universe, SUSchemaRoot root, PersistentIntMap namedTypesByNameId, + PersistentIntMap> implementationsByInterfaceId, PersistentEdgeMap edgeMap, PersistentIntMap> vertexMetadata, int namedTypeCount, @@ -47,6 +49,7 @@ public SUSchema( this.universe = assertNotNull(universe); this.root = assertNotNull(root); this.namedTypesByNameId = assertNotNull(namedTypesByNameId); + this.implementationsByInterfaceId = assertNotNull(implementationsByInterfaceId); this.edgeMap = assertNotNull(edgeMap); this.vertexMetadata = assertNotNull(vertexMetadata); this.namedTypeCount = namedTypeCount; @@ -238,6 +241,20 @@ public List getInterfaces(SUInterfaceType interfaceType) { return getTypedChildren(interfaceType, SUEdgeKind.IMPLEMENTS, SUInterfaceType.class); } + /** + * Returns the object types that directly implement an interface, sorted by name. + * + * @param interfaceType the interface + * + * @return the immutable implementation list + */ + public List getImplementations(SUInterfaceType interfaceType) { + assertOwned(interfaceType); + List implementations = + implementationsByInterfaceId.get(interfaceType.getId()); + return implementations == null ? Collections.emptyList() : implementations; + } + public @Nullable SUInterfaceType getInterface(SUObjectType objectType, String name) { return getTypedChild(objectType, SUEdgeKind.IMPLEMENTS, name, SUInterfaceType.class); } @@ -385,6 +402,11 @@ public PersistentIntMap getNamedTypesByNameId() { return namedTypesByNameId; } + @Internal + public PersistentIntMap> getImplementationsByInterfaceId() { + return implementationsByInterfaceId; + } + @Internal public int getNamedTypeCount() { return namedTypeCount; diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 2a05a1c77f..38ab3a260c 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -6,6 +6,8 @@ import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; +import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -26,6 +28,8 @@ public final class SUSchemaBuilder { private final Map changedEdges = new LinkedHashMap<>(); private final Map> changedVertexMetadata = new LinkedHashMap<>(); + private final Map> changedImplementations = + new LinkedHashMap<>(); private PersistentIntMap namedTypesByNameId; private int namedTypeCount; private boolean built; @@ -561,6 +565,8 @@ public SUSchema build() { assertCanChange(); built = true; PersistentEdgeMap edgeMap = baseEdgeMap(); + PersistentIntMap> implementationsByInterfaceId = + baseImplementationsByInterfaceId(); PersistentIntMap> metadataMap = baseVertexMetadataMap(); int edgeCount = baseEdgeCount(); @@ -568,9 +574,12 @@ public SUSchema build() { int sourceId = entry.getKey(); PackedEdgeSet oldEdges = edgeMap.get(sourceId); PackedEdgeSet newEdges = entry.getValue().freeze(); + updateImplementations(sourceId, oldEdges, newEdges); edgeCount += newEdges.size() - oldEdges.size(); edgeMap = edgeMap.put(sourceId, newEdges); } + implementationsByInterfaceId = + freezeImplementations(implementationsByInterfaceId); for (Map.Entry> entry : changedVertexMetadata.entrySet()) { int vertexId = entry.getKey(); @@ -588,6 +597,7 @@ public SUSchema build() { universe, root, namedTypesByNameId, + implementationsByInterfaceId, edgeMap, metadataMap, namedTypeCount, @@ -667,6 +677,12 @@ private PersistentEdgeMap baseEdgeMap() { return baseSchema == null ? PersistentEdgeMap.empty() : baseSchema.getEdgeMap(); } + private PersistentIntMap> baseImplementationsByInterfaceId() { + return baseSchema == null + ? PersistentIntMap.empty() + : baseSchema.getImplementationsByInterfaceId(); + } + private PersistentIntMap> baseVertexMetadataMap() { return baseSchema == null ? PersistentIntMap.empty() @@ -677,6 +693,95 @@ private int baseEdgeCount() { return baseSchema == null ? 0 : baseSchema.getStoredEdgeCount(); } + private void updateImplementations( + int sourceId, + PackedEdgeSet oldEdges, + PackedEdgeSet newEdges) { + SUVertex source = universe.getVertex(sourceId); + if (!(source instanceof SUObjectType)) { + return; + } + SUObjectType objectType = (SUObjectType) source; + removeMissingImplementationTargets(objectType, oldEdges, newEdges); + addMissingImplementationTargets(objectType, oldEdges, newEdges); + } + + private void removeMissingImplementationTargets( + SUObjectType objectType, + PackedEdgeSet oldEdges, + PackedEdgeSet newEdges) { + int start = oldEdges.firstIndex(SUEdgeKind.IMPLEMENTS); + int end = oldEdges.endIndex(SUEdgeKind.IMPLEMENTS); + for (int i = start; i < end; i++) { + int interfaceId = oldEdges.targetIdAt(i); + int interfaceNameId = oldEdges.targetNameIdAt(i); + if (newEdges.contains( + SUEdgeKind.IMPLEMENTS, + interfaceNameId, + interfaceId)) { + continue; + } + mutableImplementations(interfaceId).remove(objectType.getId()); + } + } + + private void addMissingImplementationTargets( + SUObjectType objectType, + PackedEdgeSet oldEdges, + PackedEdgeSet newEdges) { + int start = newEdges.firstIndex(SUEdgeKind.IMPLEMENTS); + int end = newEdges.endIndex(SUEdgeKind.IMPLEMENTS); + for (int i = start; i < end; i++) { + int interfaceId = newEdges.targetIdAt(i); + int interfaceNameId = newEdges.targetNameIdAt(i); + if (oldEdges.contains( + SUEdgeKind.IMPLEMENTS, + interfaceNameId, + interfaceId)) { + continue; + } + mutableImplementations(interfaceId) + .put(objectType.getId(), objectType); + } + } + + private LinkedHashMap mutableImplementations( + int interfaceId) { + LinkedHashMap changed = + changedImplementations.get(interfaceId); + if (changed != null) { + return changed; + } + LinkedHashMap mutable = new LinkedHashMap<>(); + List current = baseImplementationsByInterfaceId().get(interfaceId); + if (current != null) { + for (SUObjectType objectType : current) { + mutable.put(objectType.getId(), objectType); + } + } + changedImplementations.put(interfaceId, mutable); + return mutable; + } + + private PersistentIntMap> freezeImplementations( + PersistentIntMap> implementationsByInterfaceId) { + PersistentIntMap> result = implementationsByInterfaceId; + for (Map.Entry> entry + : changedImplementations.entrySet()) { + int interfaceId = entry.getKey(); + List implementations = + new ArrayList<>(entry.getValue().values()); + if (implementations.isEmpty()) { + result = result.remove(interfaceId); + continue; + } + implementations.sort(Comparator.comparing( + objectType -> assertNotNull(objectType.getName()))); + result = result.put(interfaceId, List.copyOf(implementations)); + } + return result; + } + private LinkedHashMap mutableVertexMetadata(SUVertex vertex) { LinkedHashMap changed = changedVertexMetadata.get(vertex.getId()); diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy new file mode 100644 index 0000000000..3ef1a2934c --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy @@ -0,0 +1,172 @@ +package graphql.schema.universe + +import graphql.AssertException +import graphql.TestUtil +import spock.lang.Specification + +class SUSchemaImplementationsTest extends Specification { + + def "implementations are indexed eagerly in name order"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def resource = universe.newInterfaceType("Resource") + def empty = universe.newInterfaceType("Empty") + def zebra = universe.newObjectType("Zebra") + def alpha = universe.newObjectType("Alpha") + def middle = universe.newObjectType("Middle") + + when: + def schema = universe.newSchema("schema") + .queryType(query) + .addType(empty) + .addInterface(resource, node) + .addInterface(zebra, node) + .addInterface(alpha, resource) + .addInterface(alpha, node) + .addInterface(middle, resource) + .build() + + then: + schema.getImplementations(node) == [alpha, zebra] + schema.getImplementations(resource) == [alpha, middle] + schema.getImplementations(empty).isEmpty() + !schema.getImplementations(node).contains(resource) + + when: + schema.getImplementations(node).add(query) + + then: + thrown(UnsupportedOperationException) + } + + def "interface edits update only affected implementation buckets"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def resource = universe.newInterfaceType("Resource") + def first = universe.newObjectType("First") + def second = universe.newObjectType("Second") + def third = universe.newObjectType("Third") + def base = universe.newSchema("base") + .queryType(query) + .addInterface(first, node) + .addInterface(second, node) + .addInterface(second, resource) + .addInterface(third, resource) + .build() + + when: + def nodeChanged = base.transform("nodeChanged", builder -> builder + .removeInterface(first, node) + .addInterface(third, node)) + def resourceChanged = base.transform("resourceChanged", builder -> + builder.removeInterface(second, "Resource")) + def cleared = base.transform("cleared", builder -> + builder.clearInterfaces(second)) + + then: + base.getImplementations(node) == [first, second] + base.getImplementations(resource) == [second, third] + + nodeChanged.getImplementations(node) == [second, third] + nodeChanged.getImplementations(resource).is(base.getImplementations(resource)) + + resourceChanged.getImplementations(node).is(base.getImplementations(node)) + resourceChanged.getImplementations(resource) == [third] + + cleared.getImplementations(node) == [first] + cleared.getImplementations(resource) == [third] + } + + def "unrelated and canonical no-op transforms share the complete index"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def object = universe.newObjectType("Object") + def field = universe.newField("field") + def string = universe.newScalarType("String") + def base = universe.newSchema("base") + .queryType(query) + .addInterface(object, node) + .build() + + when: + def fieldChanged = base.transform("fieldChanged", builder -> builder + .addField(object, field) + .setFieldType(field, string)) + def duplicateRoundTrip = base.transform("duplicateRoundTrip", builder -> builder + .addInterface(object, node) + .removeInterface(object, node)) + def removeAndRestore = base.transform("removeAndRestore", builder -> builder + .removeInterface(object, node) + .addInterface(object, node)) + def removed = base.transform("removed", builder -> + builder.removeInterface(object, node)) + + then: + fieldChanged.implementationsByInterfaceId.is(base.implementationsByInterfaceId) + duplicateRoundTrip.implementationsByInterfaceId.is(base.implementationsByInterfaceId) + removeAndRestore.implementationsByInterfaceId.is(base.implementationsByInterfaceId) + fieldChanged.getImplementations(node).is(base.getImplementations(node)) + removed.getImplementations(node).isEmpty() + removed.implementationsByInterfaceId.get(node.id) == null + } + + def "foreign interface vertices are rejected"() { + given: + def first = new SchemaUniverse() + def second = new SchemaUniverse() + def query = first.newObjectType("Query") + def foreign = second.newInterfaceType("Foreign") + def schema = first.newSchema("schema") + .queryType(query) + .build() + + when: + schema.getImplementations(foreign) + + then: + thrown(AssertException) + } + + def "SDL generation and GraphQLSchema import build the same implementation index"() { + given: + def sdl = ''' + interface Node { + id: ID! + } + + interface Resource implements Node { + id: ID! + } + + type Zebra implements Node { + id: ID! + } + + type Alpha implements Node & Resource { + id: ID! + } + + type Query { + node: Node + } + ''' + def generated = new SchemaUniverse().parseSchema("generated", sdl) + def imported = new SchemaUniverse().importSchema("imported", TestUtil.schema(sdl)) + + expect: + generated.getImplementations(generated.getInterfaceType("Node"))*.name == + ["Alpha", "Zebra"] + generated.getImplementations(generated.getInterfaceType("Resource"))*.name == + ["Alpha"] + imported.getImplementations(imported.getInterfaceType("Node"))*.name == + ["Alpha", "Zebra"] + imported.getImplementations(imported.getInterfaceType("Resource"))*.name == + ["Alpha"] + } +} From b1e585e23fdba75080b200b297e76a6b26146be5 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 5 Aug 2026 20:22:09 +0000 Subject: [PATCH 13/32] Add schema universe possible type lookups --- .../schema/universe/SUCompositeType.java | 24 +++ .../schema/universe/SUInterfaceType.java | 2 +- .../graphql/schema/universe/SUObjectType.java | 2 +- .../graphql/schema/universe/SUSchema.java | 43 +++++ .../graphql/schema/universe/SUUnionType.java | 2 +- .../universe/SUSchemaPossibleTypesTest.groovy | 181 ++++++++++++++++++ 6 files changed, 251 insertions(+), 3 deletions(-) create mode 100644 src/main/java/graphql/schema/universe/SUCompositeType.java create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaPossibleTypesTest.groovy diff --git a/src/main/java/graphql/schema/universe/SUCompositeType.java b/src/main/java/graphql/schema/universe/SUCompositeType.java new file mode 100644 index 0000000000..a51df4168c --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUCompositeType.java @@ -0,0 +1,24 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; +import graphql.Internal; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * A named GraphQL type that can be the type condition of a selection set. + */ +@ExperimentalApi +@NullMarked +public abstract class SUCompositeType extends SUNamedType { + + @Internal + public SUCompositeType( + int id, + int nameId, + SUVertexKind kind, + String name, + @Nullable String description) { + super(id, nameId, kind, name, description); + } +} diff --git a/src/main/java/graphql/schema/universe/SUInterfaceType.java b/src/main/java/graphql/schema/universe/SUInterfaceType.java index 9e6befaeb0..7ac8cf3f5f 100644 --- a/src/main/java/graphql/schema/universe/SUInterfaceType.java +++ b/src/main/java/graphql/schema/universe/SUInterfaceType.java @@ -7,7 +7,7 @@ @ExperimentalApi @NullMarked -public final class SUInterfaceType extends SUNamedType { +public final class SUInterfaceType extends SUCompositeType { @Internal public SUInterfaceType(int id, int nameId, String name, @Nullable String description) { diff --git a/src/main/java/graphql/schema/universe/SUObjectType.java b/src/main/java/graphql/schema/universe/SUObjectType.java index 5b0b91caf5..c14d055654 100644 --- a/src/main/java/graphql/schema/universe/SUObjectType.java +++ b/src/main/java/graphql/schema/universe/SUObjectType.java @@ -7,7 +7,7 @@ @ExperimentalApi @NullMarked -public final class SUObjectType extends SUNamedType { +public final class SUObjectType extends SUCompositeType { @Internal public SUObjectType(int id, int nameId, String name, @Nullable String description) { diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index 7d3bed5a09..91847b6b2f 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -255,6 +255,49 @@ public List getImplementations(SUInterfaceType interfaceType) { return implementations == null ? Collections.emptyList() : implementations; } + /** + * Returns the concrete object types that a composite type can represent at runtime. + * + *

An object type represents itself, an interface represents its implementations, and a + * union represents its members.

+ * + * @param compositeType the object, interface, or union type + * + * @return the immutable list of possible object types + */ + public List getPossibleTypes(SUCompositeType compositeType) { + assertOwned(compositeType); + if (compositeType instanceof SUObjectType) { + return Collections.singletonList((SUObjectType) compositeType); + } + if (compositeType instanceof SUInterfaceType) { + return getImplementations((SUInterfaceType) compositeType); + } + return getUnionMembers((SUUnionType) compositeType); + } + + /** + * Reports whether an object type is a possible runtime type of a composite type. + * + * @param compositeType the object, interface, or union type + * @param objectType the possible concrete object type + * + * @return {@code true} when the object is a possible runtime type + */ + public boolean isPossibleType( + SUCompositeType compositeType, + SUObjectType objectType) { + assertOwned(compositeType); + assertOwned(objectType); + if (compositeType instanceof SUObjectType) { + return compositeType == objectType; + } + if (compositeType instanceof SUInterfaceType) { + return containsEdge(objectType, SUEdgeKind.IMPLEMENTS, compositeType); + } + return containsEdge(compositeType, SUEdgeKind.UNION_MEMBER, objectType); + } + public @Nullable SUInterfaceType getInterface(SUObjectType objectType, String name) { return getTypedChild(objectType, SUEdgeKind.IMPLEMENTS, name, SUInterfaceType.class); } diff --git a/src/main/java/graphql/schema/universe/SUUnionType.java b/src/main/java/graphql/schema/universe/SUUnionType.java index bb1bfdada8..408a282064 100644 --- a/src/main/java/graphql/schema/universe/SUUnionType.java +++ b/src/main/java/graphql/schema/universe/SUUnionType.java @@ -7,7 +7,7 @@ @ExperimentalApi @NullMarked -public final class SUUnionType extends SUNamedType { +public final class SUUnionType extends SUCompositeType { @Internal public SUUnionType(int id, int nameId, String name, @Nullable String description) { diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaPossibleTypesTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaPossibleTypesTest.groovy new file mode 100644 index 0000000000..0f59674722 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaPossibleTypesTest.groovy @@ -0,0 +1,181 @@ +package graphql.schema.universe + +import graphql.AssertException +import spock.lang.Specification + +class SUSchemaPossibleTypesTest extends Specification { + + def "getPossibleTypes supports every composite type"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def emptyInterface = universe.newInterfaceType("EmptyInterface") + def search = universe.newUnionType("Search") + def emptyUnion = universe.newUnionType("EmptyUnion") + def alpha = universe.newObjectType("Alpha") + def beta = universe.newObjectType("Beta") + def other = universe.newObjectType("Other") + def schema = universe.newSchema("schema") + .queryType(query) + .addType(emptyInterface) + .addType(emptyUnion) + .addType(other) + .addInterface(beta, node) + .addInterface(alpha, node) + .addUnionMember(search, alpha) + .addUnionMember(search, beta) + .build() + + expect: + schema.getPossibleTypes(query) == [query] + schema.getPossibleTypes(alpha) == [alpha] + schema.getPossibleTypes(node) == [alpha, beta] + schema.getPossibleTypes(emptyInterface).isEmpty() + schema.getPossibleTypes(search) == [alpha, beta] + schema.getPossibleTypes(emptyUnion).isEmpty() + } + + def "isPossibleType supports every composite and object combination"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def emptyInterface = universe.newInterfaceType("EmptyInterface") + def search = universe.newUnionType("Search") + def emptyUnion = universe.newUnionType("EmptyUnion") + def alpha = universe.newObjectType("Alpha") + def beta = universe.newObjectType("Beta") + def other = universe.newObjectType("Other") + def schema = universe.newSchema("schema") + .queryType(query) + .addType(emptyInterface) + .addType(emptyUnion) + .addType(other) + .addInterface(alpha, node) + .addInterface(beta, node) + .addUnionMember(search, alpha) + .addUnionMember(search, beta) + .build() + + expect: + schema.isPossibleType(query, query) + !schema.isPossibleType(query, alpha) + schema.isPossibleType(alpha, alpha) + !schema.isPossibleType(alpha, query) + + schema.isPossibleType(node, alpha) + schema.isPossibleType(node, beta) + !schema.isPossibleType(node, query) + !schema.isPossibleType(node, other) + !schema.isPossibleType(emptyInterface, alpha) + + schema.isPossibleType(search, alpha) + schema.isPossibleType(search, beta) + !schema.isPossibleType(search, query) + !schema.isPossibleType(search, other) + !schema.isPossibleType(emptyUnion, alpha) + } + + def "possible type lookups follow their schema snapshot"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def search = universe.newUnionType("Search") + def alpha = universe.newObjectType("Alpha") + def beta = universe.newObjectType("Beta") + def base = universe.newSchema("base") + .queryType(query) + .addInterface(alpha, node) + .addUnionMember(search, alpha) + .addType(beta) + .build() + + when: + def changed = base.transform("changed", builder -> builder + .removeInterface(alpha, node) + .addInterface(beta, node) + .removeUnionMember(search, alpha) + .addUnionMember(search, beta)) + + then: + base.getPossibleTypes(node) == [alpha] + base.getPossibleTypes(search) == [alpha] + base.isPossibleType(node, alpha) + base.isPossibleType(search, alpha) + !base.isPossibleType(node, beta) + !base.isPossibleType(search, beta) + + changed.getPossibleTypes(node) == [beta] + changed.getPossibleTypes(search) == [beta] + !changed.isPossibleType(node, alpha) + !changed.isPossibleType(search, alpha) + changed.isPossibleType(node, beta) + changed.isPossibleType(search, beta) + } + + def "possible type lists are immutable for every composite type"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def search = universe.newUnionType("Search") + def object = universe.newObjectType("Object") + def schema = universe.newSchema("schema") + .queryType(query) + .addInterface(object, node) + .addUnionMember(search, object) + .build() + + when: + schema.getPossibleTypes(query).add(object) + + then: + thrown(UnsupportedOperationException) + + when: + schema.getPossibleTypes(node).add(query) + + then: + thrown(UnsupportedOperationException) + + when: + schema.getPossibleTypes(search).add(query) + + then: + thrown(UnsupportedOperationException) + } + + def "possible type lookups reject vertices from another universe"() { + given: + def first = new SchemaUniverse() + def second = new SchemaUniverse() + def query = first.newObjectType("Query") + def node = first.newInterfaceType("Node") + def foreignInterface = second.newInterfaceType("Foreign") + def foreignObject = second.newObjectType("ForeignObject") + def schema = first.newSchema("schema") + .queryType(query) + .addType(node) + .build() + + when: + schema.getPossibleTypes(foreignInterface) + + then: + thrown(AssertException) + + when: + schema.isPossibleType(foreignInterface, query) + + then: + thrown(AssertException) + + when: + schema.isPossibleType(node, foreignObject) + + then: + thrown(AssertException) + } +} From 8795eac0bf92ac033b35ae78e9de007eb632a439 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 5 Aug 2026 21:17:06 +0000 Subject: [PATCH 14/32] Add schema universe introspection types --- .../graphql/schema/universe/SUEdgeKind.java | 1 + .../graphql/schema/universe/SUExporter.java | 21 +- .../graphql/schema/universe/SUImporter.java | 67 ++++++- .../universe/SUIntrospectionSchemaType.java | 36 ++++ .../graphql/schema/universe/SUSchema.java | 10 + .../schema/universe/SUSchemaBuilder.java | 59 +++++- .../SUSchemaImplementationsTest.groovy | 1 + .../universe/SUSchemaIntrospectionTest.groovy | 181 ++++++++++++++++++ .../universe/SUSchemaMetadataTest.groovy | 3 +- .../universe/SUSchemaPrinterTest.groovy | 3 +- .../universe/SUSchemaRoundTripTest.groovy | 4 +- .../graphql/schema/universe/SUTest.groovy | 12 +- .../universe/SchemaUniverseCleanupTest.groovy | 13 +- 13 files changed, 383 insertions(+), 28 deletions(-) create mode 100644 src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaIntrospectionTest.groovy diff --git a/src/main/java/graphql/schema/universe/SUEdgeKind.java b/src/main/java/graphql/schema/universe/SUEdgeKind.java index 2ea2592564..93b8fe1baf 100644 --- a/src/main/java/graphql/schema/universe/SUEdgeKind.java +++ b/src/main/java/graphql/schema/universe/SUEdgeKind.java @@ -20,6 +20,7 @@ public enum SUEdgeKind { QUERY_TYPE(1, true, true, false), MUTATION_TYPE(2, true, true, false), SUBSCRIPTION_TYPE(3, true, true, false), + INTROSPECTION_SCHEMA_TYPE(4, true, true, false), DIRECTIVE_DEFINITION(5, false, true, false), FIELD(6, false, true, false), ARGUMENT(7, false, true, false), diff --git a/src/main/java/graphql/schema/universe/SUExporter.java b/src/main/java/graphql/schema/universe/SUExporter.java index 92e21153ee..65231aad90 100644 --- a/src/main/java/graphql/schema/universe/SUExporter.java +++ b/src/main/java/graphql/schema/universe/SUExporter.java @@ -2,7 +2,6 @@ import graphql.Directives; import graphql.ExperimentalApi; -import graphql.introspection.Introspection; import graphql.language.StringValue; import graphql.language.Value; import graphql.schema.GraphQLAppliedDirective; @@ -56,20 +55,30 @@ public final class SUExporter { private final SUSchema schema; private final Map namedTypes = new LinkedHashMap<>(); + private boolean resolveKnownNamedTypes; public SUExporter(SUSchema schema) { this.schema = assertNotNull(schema); } public GraphQLSchema exportSchema() { - for (SUVertex type : schema.getTypes()) { - if (!Introspection.isIntrospectionTypes(requiredName(type))) { + SUObjectType introspectionSchemaType = + schema.getIntrospectionSchemaType(); + for (SUNamedType type : schema.getTypes()) { + if (type != introspectionSchemaType) { namedTypes.put(type.getId(), exportNamedType(type)); } } + resolveKnownNamedTypes = true; + namedTypes.put( + introspectionSchemaType.getId(), + exportNamedType(introspectionSchemaType)); + resolveKnownNamedTypes = false; GraphQLSchema.Builder builder = GraphQLSchema.newSchema() .query(objectType(schema.getQueryType())) + .introspectionSchemaType( + objectType(introspectionSchemaType)) .description(schema.getRoot().getDescription()) .codeRegistry(exportCodeRegistry()); @@ -83,7 +92,7 @@ public GraphQLSchema exportSchema() { } for (SUNamedType type : schema.getTypes()) { if (isOperationType(type) - || Introspection.isIntrospectionTypes(requiredName(type))) { + || type == introspectionSchemaType) { continue; } builder.additionalType(namedType(type)); @@ -315,6 +324,10 @@ private GraphQLType exportType(SUVertex type) { if (type instanceof SUNonNullType) { return GraphQLNonNull.nonNull(exportType(requiredWrappedType((SUNonNullType) type))); } + GraphQLNamedType knownType = namedTypes.get(type.getId()); + if (resolveKnownNamedTypes && knownType != null) { + return knownType; + } return typeReference(type); } diff --git a/src/main/java/graphql/schema/universe/SUImporter.java b/src/main/java/graphql/schema/universe/SUImporter.java index 57657bea0c..5bceb2fb19 100644 --- a/src/main/java/graphql/schema/universe/SUImporter.java +++ b/src/main/java/graphql/schema/universe/SUImporter.java @@ -3,6 +3,7 @@ import graphql.Directives; import graphql.DirectivesUtil; import graphql.ExperimentalApi; +import graphql.Internal; import graphql.language.StringValue; import graphql.schema.GraphQLAppliedDirective; import graphql.schema.GraphQLAppliedDirectiveArgument; @@ -26,6 +27,8 @@ import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeReference; import graphql.schema.GraphQLUnionType; +import graphql.schema.SchemaTraverser; +import graphql.schema.impl.GraphQLTypeCollectingVisitor; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -37,6 +40,7 @@ import static graphql.Assert.assertNotNull; import static graphql.Assert.assertShouldNeverHappen; +import static graphql.Assert.assertTrue; /** * Imports the type-system topology of an existing {@link GraphQLSchema} into a universe. @@ -65,6 +69,8 @@ public SUSchema importSchema(String name, GraphQLSchema schema) { schemaBuilder = universe.newSchema(name, schema.getDescription()); createNamedTypeVertices(schema.getAllTypesAsList()); addRootTypes(schema); + builder().introspectionSchemaType( + (SUObjectType) namedType(schema.getIntrospectionSchemaType())); addDirectiveDefinitions(schema.getDirectives()); expandNamedTypes(schema.getAllTypesAsList()); addAppliedDirectives( @@ -75,15 +81,74 @@ public SUSchema importSchema(String name, GraphQLSchema schema) { return builder().build(); } + @Internal + public SUObjectType importIntrospectionSchemaType( + SUSchemaBuilder targetBuilder, + GraphQLObjectType introspectionSchemaType) { + assertTrue(schemaBuilder == null, "This schema universe importer has already been used"); + schemaBuilder = assertNotNull(targetBuilder); + for (GraphQLDirective directive : Directives.BUILT_IN_DIRECTIVES) { + directiveDefinitions.put(directive.getName(), directive); + } + List types = + collectNamedTypes(assertNotNull(introspectionSchemaType)); + createNamedTypeVertices(types); + expandNamedTypes(types); + SUObjectType imported = + (SUObjectType) namedType(introspectionSchemaType); + builder().introspectionSchemaType(imported); + return imported; + } + + private List collectNamedTypes( + GraphQLObjectType introspectionSchemaType) { + GraphQLTypeCollectingVisitor collectingVisitor = + new GraphQLTypeCollectingVisitor(); + SchemaTraverser traverser = + new SchemaTraverser(GraphQLSchemaElement::getChildren); + traverser.depthFirst( + collectingVisitor, + List.of(introspectionSchemaType)); + return new ArrayList<>( + collectingVisitor.getResult().values()); + } + private void createNamedTypeVertices(List types) { for (GraphQLNamedType type : types) { - SUNamedType vertex = createNamedTypeVertex(type); + SUNamedType vertex = builder().getNamedType(type.getName()); + if (vertex == null) { + vertex = createNamedTypeVertex(type); + } else { + assertCompatibleNamedType(type, vertex); + } namedTypes.put(type.getName(), vertex); elementVertices.put(type, vertex); builder().addType(vertex); } } + private void assertCompatibleNamedType( + GraphQLNamedType graphQLType, + SUNamedType universeType) { + boolean compatible = + graphQLType instanceof GraphQLObjectType + && universeType instanceof SUObjectType + || graphQLType instanceof GraphQLInterfaceType + && universeType instanceof SUInterfaceType + || graphQLType instanceof GraphQLUnionType + && universeType instanceof SUUnionType + || graphQLType instanceof GraphQLEnumType + && universeType instanceof SUEnumType + || graphQLType instanceof GraphQLScalarType + && universeType instanceof SUScalarType + || graphQLType instanceof GraphQLInputObjectType + && universeType instanceof SUInputObjectType; + assertTrue( + compatible, + "Type '%s' has an incompatible schema universe kind", + graphQLType.getName()); + } + private SUNamedType createNamedTypeVertex(GraphQLNamedType type) { if (type instanceof GraphQLObjectType) { GraphQLObjectType objectType = (GraphQLObjectType) type; diff --git a/src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java b/src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java new file mode 100644 index 0000000000..49c10d213f --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java @@ -0,0 +1,36 @@ +package graphql.schema.universe; + +import graphql.Internal; +import graphql.introspection.Introspection; +import org.jspecify.annotations.NullMarked; + +import static graphql.Assert.assertNotNull; + +/** + * Imports the canonical GraphQL introspection type graph into a schema universe schema. + */ +@Internal +@NullMarked +public final class SUIntrospectionSchemaType { + + private final SchemaUniverse universe; + + @Internal + public SUIntrospectionSchemaType(SchemaUniverse universe) { + this.universe = assertNotNull(universe); + } + + /** + * Imports and attaches the graph rooted at {@link Introspection#__Schema}. + * + * @param builder the target schema builder + * + * @return the imported introspection graph root + */ + @Internal + public SUObjectType addTo(SUSchemaBuilder builder) { + return new SUImporter(universe).importIntrospectionSchemaType( + assertNotNull(builder), + Introspection.__Schema); + } +} diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index 91847b6b2f..2cc5cee277 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -114,6 +114,16 @@ public SUObjectType getQueryType() { return (SUObjectType) getSingleTarget(root, SUEdgeKind.SUBSCRIPTION_TYPE); } + /** + * Returns the root of this schema's introspection type graph. + * + * @return the introspection schema object type + */ + public SUObjectType getIntrospectionSchemaType() { + return (SUObjectType) assertNotNull( + getSingleTarget(root, SUEdgeKind.INTROSPECTION_SCHEMA_TYPE)); + } + public List getRootTypes() { List result = new ArrayList<>(3); result.add(getQueryType()); diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 38ab3a260c..d4264da803 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -171,6 +171,26 @@ public SUSchemaBuilder setSubscriptionType(@Nullable SUObjectType subscriptionTy return setOptionalEdge(SUEdgeKind.SUBSCRIPTION_TYPE, subscriptionType); } + /** + * Replaces the default introspection schema type for this snapshot. + * + * @param introspectionSchemaType the introspection graph root + * + * @return this builder + */ + public SUSchemaBuilder introspectionSchemaType( + SUObjectType introspectionSchemaType) { + return setIntrospectionSchemaType(introspectionSchemaType); + } + + public SUSchemaBuilder setIntrospectionSchemaType( + SUObjectType introspectionSchemaType) { + return setEdge( + root, + SUEdgeKind.INTROSPECTION_SCHEMA_TYPE, + introspectionSchemaType); + } + public SUSchemaBuilder type(SUNamedType type) { return addType(type); } @@ -563,6 +583,7 @@ private SUSchemaBuilder removeEdges( public SUSchema build() { assertCanChange(); + ensureIntrospectionSchemaType(); built = true; PersistentEdgeMap edgeMap = baseEdgeMap(); PersistentIntMap> implementationsByInterfaceId = @@ -592,7 +613,11 @@ public SUSchema build() { } assertTrue(edgeMap.get(root.getId()).firstTarget(SUEdgeKind.QUERY_TYPE) >= 0, "A schema universe schema requires a query type"); - assertOperationTypesRegistered(edgeMap); + assertTrue( + edgeMap.get(root.getId()) + .firstTarget(SUEdgeKind.INTROSPECTION_SCHEMA_TYPE) >= 0, + "A schema universe schema requires an introspection schema type"); + assertRootTypesRegistered(edgeMap); SUSchema schema = new SUSchema( universe, root, @@ -606,6 +631,16 @@ public SUSchema build() { return schema; } + private void ensureIntrospectionSchemaType() { + int targetId = mutableEdges(root) + .freeze() + .firstTarget(SUEdgeKind.INTROSPECTION_SCHEMA_TYPE); + if (targetId >= 0) { + return; + } + new SUIntrospectionSchemaType(universe).addTo(this); + } + private SUSchemaBuilder setOptionalEdge( SUEdgeKind kind, @Nullable SUObjectType target) { @@ -615,13 +650,16 @@ private SUSchemaBuilder setOptionalEdge( return setEdge(root, kind, target); } - private void assertOperationTypesRegistered(PersistentEdgeMap edgeMap) { - assertOperationTypeRegistered(edgeMap, SUEdgeKind.QUERY_TYPE); - assertOperationTypeRegistered(edgeMap, SUEdgeKind.MUTATION_TYPE); - assertOperationTypeRegistered(edgeMap, SUEdgeKind.SUBSCRIPTION_TYPE); + private void assertRootTypesRegistered(PersistentEdgeMap edgeMap) { + assertRootTypeRegistered(edgeMap, SUEdgeKind.QUERY_TYPE); + assertRootTypeRegistered(edgeMap, SUEdgeKind.MUTATION_TYPE); + assertRootTypeRegistered(edgeMap, SUEdgeKind.SUBSCRIPTION_TYPE); + assertRootTypeRegistered( + edgeMap, + SUEdgeKind.INTROSPECTION_SCHEMA_TYPE); } - private void assertOperationTypeRegistered( + private void assertRootTypeRegistered( PersistentEdgeMap edgeMap, SUEdgeKind kind) { int targetId = edgeMap.get(root.getId()).firstTarget(kind); @@ -631,7 +669,7 @@ private void assertOperationTypeRegistered( SUNamedType type = (SUNamedType) universe.getVertex(targetId); assertTrue( namedTypesByNameId.get(type.getNameId()) == type, - "Operation type '%s' is not part of the schema", + "Root type '%s' is not part of the schema", assertNotNull(type.getName())); } @@ -821,6 +859,12 @@ private void assertCanChange() { assertTrue(!built, "This schema universe builder has already built a schema"); } + @Internal + public @Nullable SUNamedType getNamedType(String name) { + int nameId = universe.getNameId(assertNotNull(name)); + return nameId < 0 ? null : namedTypesByNameId.get(nameId); + } + private void assertOwned(SUVertex vertex) { assertTrue(universe.owns(vertex), "Vertex %s belongs to another schema universe", vertex); } @@ -874,6 +918,7 @@ private boolean isValidEdge( case QUERY_TYPE: case MUTATION_TYPE: case SUBSCRIPTION_TYPE: + case INTROSPECTION_SCHEMA_TYPE: return sourceKind == SUVertexKind.SCHEMA && targetKind == SUVertexKind.OBJECT; case DIRECTIVE_DEFINITION: diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy index 3ef1a2934c..ce719bbb90 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaImplementationsTest.groovy @@ -91,6 +91,7 @@ class SUSchemaImplementationsTest extends Specification { def string = universe.newScalarType("String") def base = universe.newSchema("base") .queryType(query) + .addType(string) .addInterface(object, node) .build() diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaIntrospectionTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaIntrospectionTest.groovy new file mode 100644 index 0000000000..5c2fc75efb --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaIntrospectionTest.groovy @@ -0,0 +1,181 @@ +package graphql.schema.universe + +import graphql.TestUtil +import graphql.introspection.Introspection +import graphql.introspection.IntrospectionWithDirectivesSupport +import graphql.schema.GraphQLSchema +import graphql.schema.GraphQLTypeUtil +import graphql.schema.GraphqlTypeComparatorRegistry +import graphql.schema.idl.SchemaPrinter +import spock.lang.Specification + +class SUSchemaIntrospectionTest extends Specification { + + def "every schema gets the canonical introspection graph by default"() { + given: + def universe = new SchemaUniverse() + + when: + def schema = universe.parseSchema("schema", "type Query { value: String }") + + then: + schema.introspectionSchemaType.is(schema.getObjectType("__Schema")) + schema.introspectionSchemaType.name == Introspection.__Schema.name + schema.getField(schema.queryType, "__schema") == null + schema.getField(schema.queryType, "__type") == null + + and: + standardIntrospectionNames().every { schema.getType(it) != null } + schema.getFields(schema.introspectionSchemaType)*.name == + Introspection.__Schema.fieldDefinitions*.name + + and: + def typesField = schema.getField(schema.introspectionSchemaType, "types") + unwrap(schema, schema.getType(typesField)).is(schema.getObjectType("__Type")) + } + + def "an explicit introspection root suppresses the canonical default"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def custom = universe.newObjectType("CustomIntrospection") + + when: + def schema = universe.newSchema("schema") + .queryType(query) + .introspectionSchemaType(custom) + .build() + + then: + schema.introspectionSchemaType.is(custom) + schema.getType("__Schema") == null + schema.types == [custom, query] + } + + def "default introspection reuses scalar types registered before build"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def string = universe.newScalarType("String") + def booleanType = universe.newScalarType("Boolean") + + when: + def schema = universe.newSchema("schema") + .queryType(query) + .addType(string) + .addType(booleanType) + .build() + + then: + schema.getScalarType("String").is(string) + schema.getScalarType("Boolean").is(booleanType) + schema.introspectionSchemaType.name == "__Schema" + } + + def "schema transforms inherit and share their introspection topology"() { + given: + def universe = new SchemaUniverse() + def base = universe.parseSchema("base", "type Query { value: String }") + + when: + def transformed = base.transform("transformed", builder -> { + }) + + then: + transformed.introspectionSchemaType.is(base.introspectionSchemaType) + transformed.sharesOutgoingEdgesWith( + base, + base.introspectionSchemaType) + transformed.getObjectType("__Type").is(base.getObjectType("__Type")) + } + + def "custom GraphQLSchema introspection topology remains schema specific"() { + given: + GraphQLSchema graphQLSchema = + TestUtil.schema("type Query { value: String }") + GraphQLSchema enhancedGraphQLSchema = + new IntrospectionWithDirectivesSupport( + environment -> true, + "_custom_") + .apply(graphQLSchema) + def universe = new SchemaUniverse() + + when: + def standard = universe.importSchema("standard", graphQLSchema) + def enhanced = + universe.importSchema("enhanced", enhancedGraphQLSchema) + + then: + !standard.introspectionSchemaType.is(enhanced.introspectionSchemaType) + standard.getObjectType("_custom_AppliedDirective") == null + enhanced.getObjectType("_custom_AppliedDirective") != null + enhanced.getObjectType("_custom_DirectiveArgument") != null + + and: + ["__Schema", "__Type", "__Field", "__EnumValue", "__InputValue", "__Directive"] + .every { + standard.getField( + standard.getObjectType(it), + "appliedDirectives") == null && + enhanced.getField( + enhanced.getObjectType(it), + "appliedDirectives") != null + } + enhanced.getEnumType("__TypeKind") != null + enhanced.getEnumType("__DirectiveLocation") != null + + when: + def exported = enhanced.toGraphQLSchema() + + then: + exported.introspectionSchemaType.getFieldDefinition("appliedDirectives") != null + exported.getType("_custom_AppliedDirective") != null + GraphQLTypeUtil.unwrapAll( + exported.introspectionSchemaFieldDefinition.type) + .is(exported.introspectionSchemaType) + GraphQLTypeUtil.unwrapAll( + exported.introspectionTypeFieldDefinition.type) + .is(exported.getType("__Type")) + exported.queryType.getFieldDefinition("__schema") == null + exported.queryType.getFieldDefinition("__type") == null + } + + def "default SU introspection topology prints identically to GraphQLSchema"() { + given: + def source = TestUtil.schema("type Query { value: String }") + def exported = new SchemaUniverse() + .parseSchema("schema", "type Query { value: String }") + .toGraphQLSchema() + def options = SchemaPrinter.Options.defaultOptions() + .includeIntrospectionTypes(true) + .includeSchemaDefinition(true) + .setComparators(GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) + def printer = new SchemaPrinter(options) + + expect: + printer.print(exported) == printer.print(source) + } + + private static Set standardIntrospectionNames() { + return [ + Introspection.__Schema, + Introspection.__Type, + Introspection.__Field, + Introspection.__InputValue, + Introspection.__EnumValue, + Introspection.__Directive, + Introspection.__TypeKind, + Introspection.__DirectiveLocation + ]*.name as Set + } + + private static SUType unwrap(SUSchema schema, SUType type) { + SUType current = type + while (current instanceof SUListType || current instanceof SUNonNullType) { + current = current instanceof SUListType + ? schema.getWrappedType((SUListType) current) + : schema.getWrappedType((SUNonNullType) current) + } + return current + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy index af717a6709..00e172ccdd 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaMetadataTest.groovy @@ -132,10 +132,11 @@ class SUSchemaMetadataTest extends Specification { schema.getType(argument).is(string) when: + def retainedVertexCount = universe.vertexCount universe.removeSchema(schema) then: - universe.cleanupUnusedVertices() == 5 + universe.cleanupUnusedVertices() == retainedVertexCount universe.vertexCount == 0 } diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy index 546b2b1a85..76f5087839 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy @@ -31,8 +31,7 @@ class SUSchemaPrinterTest extends Specification { directSdl == graphQLSdl and: - def expectedTypeNames = graphQLSchema.allTypesAsList*.name - .findAll { !it.startsWith("__") } as Set + def expectedTypeNames = graphQLSchema.allTypesAsList*.name as Set (direct.types*.name as Set) == expectedTypeNames } diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy index ac3e991932..bac0c9b5cc 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy @@ -65,9 +65,7 @@ class SUSchemaRoundTripTest extends Specification { query : schema.queryType.name, mutation : schema.mutationType?.name, subscription: schema.subscriptionType?.name, - types : schema.allTypesAsList*.name - .findAll { !it.startsWith("__") } - .toSorted(), + types : schema.allTypesAsList*.name.toSorted(), directives: schema.directives*.name.toSorted() ] } diff --git a/src/test/groovy/graphql/schema/universe/SUTest.groovy b/src/test/groovy/graphql/schema/universe/SUTest.groovy index 7475964ca1..cc1ae63dac 100644 --- a/src/test/groovy/graphql/schema/universe/SUTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUTest.groovy @@ -40,6 +40,7 @@ class SUTest extends Specification { .addField(query, foo) .setFieldType(foo, string) .build() + def baseVertexCount = universe.vertexCount when: def schema2 = schema1.transform("S2", builder -> builder @@ -75,7 +76,7 @@ class SUTest extends Specification { schema1.sharesOutgoingEdgesWith(schema3, foo) !schema1.sharesOutgoingEdgesWith(schema2, query) - universe.vertexCount == 7 + universe.vertexCount == baseVertexCount + 2 } def "transforming one branch does not change its parent or sibling"() { @@ -292,7 +293,8 @@ class SUTest extends Specification { .build() then: - schema.types == [query, string, user] + schema.types.containsAll([query, string, user]) + schema.getObjectType("__Schema").is(schema.introspectionSchemaType) schema.getType("Query").is(query) schema.getObjectType("User").is(user) schema.getScalarType("String").is(string) @@ -753,6 +755,7 @@ class SUTest extends Specification { .addType(integer) .addAppliedDirective(query, originalDirective) .build() + def baseVertexCount = universe.vertexCount when: def changedArgument = universe.newAppliedDirectiveArgument( @@ -772,9 +775,8 @@ class SUTest extends Specification { changed.getType(changedArgument).is(integer) and: - universe.vertexCount == 6 - base.storedEdgeCount == 2 - changed.storedEdgeCount == 2 + universe.vertexCount == baseVertexCount + 2 + changed.storedEdgeCount == base.storedEdgeCount } def "applied directive payload rejects duplicate names and foreign universes"() { diff --git a/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy index 66014b8650..eb0d860a65 100644 --- a/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SchemaUniverseCleanupTest.groovy @@ -32,8 +32,7 @@ class SchemaUniverseCleanupTest extends Specification { def reclaimed = universe.cleanupUnusedVertices() then: - reclaimed == 4 - universe.vertexCount == 4 + reclaimed > 4 universe.cleanupUnusedVertices() == 0 universe.owns(sharedString) universe.owns(second.root) @@ -54,11 +53,12 @@ class SchemaUniverseCleanupTest extends Specification { thrown(AssertException) when: + def retainedVertexCount = universe.vertexCount def next = universe.newEnumType("Next") then: next.id > detached.id - universe.vertexCount == 5 + universe.vertexCount == retainedVertexCount + 1 universe.getVertex(next.id).is(next) } @@ -150,10 +150,12 @@ class SchemaUniverseCleanupTest extends Specification { schema.getEnumType("Isolated").is(isolated) when: + def registeredVertexCount = universe.vertexCount universe.removeSchema(schema) then: - universe.cleanupUnusedVertices() == 3 + universe.cleanupUnusedVertices() == registeredVertexCount + universe.vertexCount == 0 !universe.owns(root) !universe.owns(query) !universe.owns(isolated) @@ -197,7 +199,8 @@ class SchemaUniverseCleanupTest extends Specification { then: universe.cleanupUnusedVertices() == 0 - universe.vertexCount == 2 + universe.vertexCount > 2 schema.queryType.is(next) + schema.introspectionSchemaType.name == "__Schema" } } From 3b1a06832a8e9f68eb979f15c5c8c51f963085e3 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Wed, 5 Aug 2026 23:00:35 +0000 Subject: [PATCH 15/32] Add executable schema views --- .../java/graphql/schema/ExecutableSchema.java | 110 +++++ .../java/graphql/schema/GraphQLArgument.java | 2 +- .../graphql/schema/GraphQLCompositeType.java | 3 +- .../java/graphql/schema/GraphQLDirective.java | 2 +- .../java/graphql/schema/GraphQLEnumType.java | 2 +- .../schema/GraphQLEnumValueDefinition.java | 2 +- .../schema/GraphQLFieldDefinition.java | 2 +- .../schema/GraphQLFieldsContainer.java | 3 +- .../schema/GraphQLInputFieldsContainer.java | 5 +- .../schema/GraphQLInputObjectField.java | 2 +- .../schema/GraphQLInputObjectType.java | 2 +- .../java/graphql/schema/GraphQLInputType.java | 3 +- .../graphql/schema/GraphQLInterfaceType.java | 2 +- src/main/java/graphql/schema/GraphQLList.java | 2 +- .../graphql/schema/GraphQLModifiedType.java | 2 +- .../java/graphql/schema/GraphQLNamedType.java | 3 +- .../java/graphql/schema/GraphQLNonNull.java | 2 +- .../graphql/schema/GraphQLObjectType.java | 2 +- .../graphql/schema/GraphQLOutputType.java | 2 +- .../graphql/schema/GraphQLScalarType.java | 2 +- .../java/graphql/schema/GraphQLSchema.java | 145 +++++- src/main/java/graphql/schema/GraphQLType.java | 2 +- .../java/graphql/schema/GraphQLUnionType.java | 2 +- .../java/graphql/schema/SchemaArgument.java | 18 + .../java/graphql/schema/SchemaComposite.java | 12 + .../java/graphql/schema/SchemaDirective.java | 27 ++ src/main/java/graphql/schema/SchemaEnum.java | 20 + .../java/graphql/schema/SchemaEnumValue.java | 14 + src/main/java/graphql/schema/SchemaField.java | 23 + .../graphql/schema/SchemaFieldsContainer.java | 12 + .../java/graphql/schema/SchemaInputField.java | 18 + .../schema/SchemaInputFieldsContainer.java | 12 + .../graphql/schema/SchemaInputObject.java | 15 + .../java/graphql/schema/SchemaInputType.java | 12 + .../java/graphql/schema/SchemaInterface.java | 12 + src/main/java/graphql/schema/SchemaList.java | 13 + .../graphql/schema/SchemaModifiedType.java | 14 + .../java/graphql/schema/SchemaNamedType.java | 14 + .../java/graphql/schema/SchemaNonNull.java | 13 + .../java/graphql/schema/SchemaObject.java | 12 + .../java/graphql/schema/SchemaOutputType.java | 12 + .../java/graphql/schema/SchemaScalar.java | 13 + src/main/java/graphql/schema/SchemaType.java | 12 + src/main/java/graphql/schema/SchemaUnion.java | 12 + .../graphql/schema/universe/IntMapBranch.java | 2 - .../schema/universe/IntMapEntryVisitor.java | 2 - .../graphql/schema/universe/IntMapLeaf.java | 2 - .../graphql/schema/universe/IntMapNode.java | 2 - .../schema/universe/MutablePackedEdgeSet.java | 2 - .../schema/universe/PackedEdgeSet.java | 2 - .../schema/universe/PersistentEdgeMap.java | 2 - .../schema/universe/PersistentIntMap.java | 2 - .../schema/universe/SUAppliedDirective.java | 2 - .../universe/SUAppliedDirectiveArgument.java | 2 - .../universe/SUAppliedDirectiveContainer.java | 2 - .../graphql/schema/universe/SUArgument.java | 2 - .../schema/universe/SUCompositeType.java | 2 - .../graphql/schema/universe/SUDirective.java | 2 - .../graphql/schema/universe/SUEdgeKind.java | 2 - .../graphql/schema/universe/SUEnumType.java | 2 - .../graphql/schema/universe/SUEnumValue.java | 2 - .../graphql/schema/universe/SUExporter.java | 2 - .../java/graphql/schema/universe/SUField.java | 2 - .../graphql/schema/universe/SUImporter.java | 2 - .../graphql/schema/universe/SUInputField.java | 2 - .../schema/universe/SUInputObjectType.java | 2 - .../schema/universe/SUInterfaceType.java | 2 - .../universe/SUIntrospectionSchemaType.java | 2 - .../graphql/schema/universe/SUListType.java | 2 - .../graphql/schema/universe/SUNamedType.java | 2 - .../schema/universe/SUNonNullType.java | 2 - .../graphql/schema/universe/SUObjectType.java | 2 - .../graphql/schema/universe/SUScalarType.java | 2 - .../graphql/schema/universe/SUSchema.java | 2 - .../schema/universe/SUSchemaBuilder.java | 2 - .../schema/universe/SUSchemaGenerator.java | 2 - .../schema/universe/SUSchemaPrinter.java | 2 - .../graphql/schema/universe/SUSchemaRoot.java | 2 - .../java/graphql/schema/universe/SUType.java | 2 - .../graphql/schema/universe/SUUnionType.java | 2 - .../graphql/schema/universe/SUVertex.java | 2 - .../graphql/schema/universe/SUVertexKind.java | 2 - .../schema/universe/SchemaUniverse.java | 2 - .../graphql/schema/universe/VertexChunk.java | 2 - .../graphql/schema/universe/package-info.java | 4 + .../view/AbstractSUSchemaElement.java | 58 +++ .../view/AbstractSUSchemaNamedType.java | 27 ++ .../universe/view/AbstractSUSchemaType.java | 25 + .../universe/view/SUSchemaArgument.java | 46 ++ .../universe/view/SUSchemaDirective.java | 72 +++ .../schema/universe/view/SUSchemaEnum.java | 51 ++ .../universe/view/SUSchemaEnumValue.java | 24 + .../view/SUSchemaExecutableSchema.java | 456 ++++++++++++++++++ .../view/SUSchemaExecutableSchemaBuilder.java | 104 ++++ .../schema/universe/view/SUSchemaField.java | 71 +++ .../universe/view/SUSchemaInputField.java | 46 ++ .../universe/view/SUSchemaInputObject.java | 33 ++ .../universe/view/SUSchemaInterface.java | 17 + .../view/SUSchemaIntrospectionArgument.java | 39 ++ .../view/SUSchemaIntrospectionField.java | 76 +++ .../schema/universe/view/SUSchemaList.java | 35 ++ .../schema/universe/view/SUSchemaNonNull.java | 35 ++ .../schema/universe/view/SUSchemaObject.java | 17 + .../schema/universe/view/SUSchemaScalar.java | 17 + .../view/SUSchemaSyntheticNonNull.java | 24 + .../schema/universe/view/SUSchemaUnion.java | 17 + .../schema/universe/view/package-info.java | 4 + .../schema/ExecutableSchemaTest.groovy | 337 +++++++++++++ .../graphql/schema/GraphQLSchemaTest.groovy | 7 +- 109 files changed, 2224 insertions(+), 112 deletions(-) create mode 100644 src/main/java/graphql/schema/ExecutableSchema.java create mode 100644 src/main/java/graphql/schema/SchemaArgument.java create mode 100644 src/main/java/graphql/schema/SchemaComposite.java create mode 100644 src/main/java/graphql/schema/SchemaDirective.java create mode 100644 src/main/java/graphql/schema/SchemaEnum.java create mode 100644 src/main/java/graphql/schema/SchemaEnumValue.java create mode 100644 src/main/java/graphql/schema/SchemaField.java create mode 100644 src/main/java/graphql/schema/SchemaFieldsContainer.java create mode 100644 src/main/java/graphql/schema/SchemaInputField.java create mode 100644 src/main/java/graphql/schema/SchemaInputFieldsContainer.java create mode 100644 src/main/java/graphql/schema/SchemaInputObject.java create mode 100644 src/main/java/graphql/schema/SchemaInputType.java create mode 100644 src/main/java/graphql/schema/SchemaInterface.java create mode 100644 src/main/java/graphql/schema/SchemaList.java create mode 100644 src/main/java/graphql/schema/SchemaModifiedType.java create mode 100644 src/main/java/graphql/schema/SchemaNamedType.java create mode 100644 src/main/java/graphql/schema/SchemaNonNull.java create mode 100644 src/main/java/graphql/schema/SchemaObject.java create mode 100644 src/main/java/graphql/schema/SchemaOutputType.java create mode 100644 src/main/java/graphql/schema/SchemaScalar.java create mode 100644 src/main/java/graphql/schema/SchemaType.java create mode 100644 src/main/java/graphql/schema/SchemaUnion.java create mode 100644 src/main/java/graphql/schema/universe/package-info.java create mode 100644 src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java create mode 100644 src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java create mode 100644 src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaArgument.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaDirective.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaEnum.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchema.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchemaBuilder.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaField.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaInputField.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaInterface.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaList.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaObject.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaScalar.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaUnion.java create mode 100644 src/main/java/graphql/schema/universe/view/package-info.java create mode 100644 src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy diff --git a/src/main/java/graphql/schema/ExecutableSchema.java b/src/main/java/graphql/schema/ExecutableSchema.java new file mode 100644 index 0000000000..b06e90537d --- /dev/null +++ b/src/main/java/graphql/schema/ExecutableSchema.java @@ -0,0 +1,110 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * An executable view of a GraphQL schema. + * + *

The view owns schema-dependent behavior such as field visibility and scalar coercion, while + * the {@code Schema*} interfaces expose the type-system shape used by operation processing.

+ */ +@ExperimentalApi +@NullMarked +public interface ExecutableSchema { + + /** + * @return the query root + */ + SchemaObject getQueryType(); + + /** + * @return the mutation root, or {@code null} when absent + */ + @Nullable SchemaObject getMutationType(); + + /** + * @return the subscription root, or {@code null} when absent + */ + @Nullable SchemaObject getSubscriptionType(); + + /** + * @param name the type name + * + * @return the named type, or {@code null} when absent + */ + @Nullable SchemaType getType(String name); + + /** + * @param name the directive name + * + * @return the directive definition, or {@code null} when absent + */ + @Nullable SchemaDirective getDirective(String name); + + /** + * @return this schema's introspection schema type + */ + SchemaObject getIntrospectionSchemaType(); + + /** + * Returns a visible field, including GraphQL introspection meta-fields. + * + * @param parentType the composite parent + * @param fieldName the field name + * + * @return the field, or {@code null} when absent or invisible + */ + @Nullable SchemaField getField(SchemaComposite parentType, String fieldName); + + /** + * @param parentType the field container + * + * @return the visible fields declared by the container + */ + List getFields(SchemaFieldsContainer parentType); + + /** + * @param parentType the input object + * @param fieldName the input field name + * + * @return the visible input field, or {@code null} when absent or invisible + */ + @Nullable SchemaInputField getInputField( + SchemaInputObject parentType, + String fieldName); + + /** + * @param parentType the input object + * + * @return the visible fields declared by the input object + */ + List getInputFields(SchemaInputObject parentType); + + /** + * @param compositeType an object, interface, or union + * + * @return the concrete object types represented by the composite type + */ + List getPossibleTypes(SchemaComposite compositeType); + + /** + * @param compositeType an object, interface, or union + * @param objectType the concrete object type + * + * @return whether the object is represented by the composite type + */ + boolean isPossibleType( + SchemaComposite compositeType, + SchemaObject objectType); + + /** + * @param scalarType the scalar + * + * @return the schema-specific scalar coercer + */ + Coercing getScalarCoercing(SchemaScalar scalarType); +} diff --git a/src/main/java/graphql/schema/GraphQLArgument.java b/src/main/java/graphql/schema/GraphQLArgument.java index 924cdf1198..6ddf94471f 100644 --- a/src/main/java/graphql/schema/GraphQLArgument.java +++ b/src/main/java/graphql/schema/GraphQLArgument.java @@ -46,7 +46,7 @@ * as opposed to its schema definition itself. */ @PublicApi -public class GraphQLArgument implements GraphQLNamedSchemaElement, GraphQLInputValueDefinition { +public class GraphQLArgument implements GraphQLNamedSchemaElement, GraphQLInputValueDefinition, SchemaArgument { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/GraphQLCompositeType.java b/src/main/java/graphql/schema/GraphQLCompositeType.java index a8cf3c4ed6..51af932f15 100644 --- a/src/main/java/graphql/schema/GraphQLCompositeType.java +++ b/src/main/java/graphql/schema/GraphQLCompositeType.java @@ -4,5 +4,6 @@ import graphql.PublicApi; @PublicApi -public interface GraphQLCompositeType extends GraphQLNamedOutputType { +public interface GraphQLCompositeType + extends GraphQLNamedOutputType, SchemaComposite { } diff --git a/src/main/java/graphql/schema/GraphQLDirective.java b/src/main/java/graphql/schema/GraphQLDirective.java index 108fcf51b5..2f3ff95aca 100644 --- a/src/main/java/graphql/schema/GraphQLDirective.java +++ b/src/main/java/graphql/schema/GraphQLDirective.java @@ -42,7 +42,7 @@ */ @PublicApi @NullMarked -public class GraphQLDirective implements GraphQLNamedSchemaElement, GraphQLDirectiveContainer { +public class GraphQLDirective implements GraphQLNamedSchemaElement, GraphQLDirectiveContainer, SchemaDirective { private final String name; private final boolean repeatable; diff --git a/src/main/java/graphql/schema/GraphQLEnumType.java b/src/main/java/graphql/schema/GraphQLEnumType.java index d65022acc3..692d1ab49d 100644 --- a/src/main/java/graphql/schema/GraphQLEnumType.java +++ b/src/main/java/graphql/schema/GraphQLEnumType.java @@ -43,7 +43,7 @@ */ @PublicApi @NullMarked -public class GraphQLEnumType implements GraphQLNamedInputType, GraphQLNamedOutputType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer { +public class GraphQLEnumType implements GraphQLNamedInputType, GraphQLNamedOutputType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, SchemaEnum { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java b/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java index b06aed763c..39e4707514 100644 --- a/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java +++ b/src/main/java/graphql/schema/GraphQLEnumValueDefinition.java @@ -25,7 +25,7 @@ * @see graphql.schema.GraphQLEnumType */ @PublicApi -public class GraphQLEnumValueDefinition implements GraphQLNamedSchemaElement, GraphQLDirectiveContainer { +public class GraphQLEnumValueDefinition implements GraphQLNamedSchemaElement, GraphQLDirectiveContainer, SchemaEnumValue { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/GraphQLFieldDefinition.java b/src/main/java/graphql/schema/GraphQLFieldDefinition.java index 779421c2c1..1f8aa0db6f 100644 --- a/src/main/java/graphql/schema/GraphQLFieldDefinition.java +++ b/src/main/java/graphql/schema/GraphQLFieldDefinition.java @@ -34,7 +34,7 @@ * See https://graphql.org/learn/queries/#fields for more details on the concept. */ @PublicApi -public class GraphQLFieldDefinition implements GraphQLNamedSchemaElement, GraphQLDirectiveContainer { +public class GraphQLFieldDefinition implements GraphQLNamedSchemaElement, GraphQLDirectiveContainer, SchemaField { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/GraphQLFieldsContainer.java b/src/main/java/graphql/schema/GraphQLFieldsContainer.java index 449fb78f22..c1ae8564a3 100644 --- a/src/main/java/graphql/schema/GraphQLFieldsContainer.java +++ b/src/main/java/graphql/schema/GraphQLFieldsContainer.java @@ -12,7 +12,8 @@ * @see graphql.schema.GraphQLInterfaceType */ @PublicApi -public interface GraphQLFieldsContainer extends GraphQLCompositeType { +public interface GraphQLFieldsContainer + extends GraphQLCompositeType, SchemaFieldsContainer { GraphQLFieldDefinition getFieldDefinition(String name); diff --git a/src/main/java/graphql/schema/GraphQLInputFieldsContainer.java b/src/main/java/graphql/schema/GraphQLInputFieldsContainer.java index bac018bdf9..5417048c55 100644 --- a/src/main/java/graphql/schema/GraphQLInputFieldsContainer.java +++ b/src/main/java/graphql/schema/GraphQLInputFieldsContainer.java @@ -10,9 +10,10 @@ * @see graphql.schema.GraphQLInputType */ @PublicApi -public interface GraphQLInputFieldsContainer extends GraphQLNamedType { +public interface GraphQLInputFieldsContainer + extends GraphQLNamedType, SchemaInputFieldsContainer { GraphQLInputObjectField getFieldDefinition(String name); List getFieldDefinitions(); -} \ No newline at end of file +} diff --git a/src/main/java/graphql/schema/GraphQLInputObjectField.java b/src/main/java/graphql/schema/GraphQLInputObjectField.java index 23323393b8..6886addd55 100644 --- a/src/main/java/graphql/schema/GraphQLInputObjectField.java +++ b/src/main/java/graphql/schema/GraphQLInputObjectField.java @@ -30,7 +30,7 @@ * See https://graphql.org/learn/schema/#input-types for more details on the concept. */ @PublicApi -public class GraphQLInputObjectField implements GraphQLNamedSchemaElement, GraphQLInputValueDefinition { +public class GraphQLInputObjectField implements GraphQLNamedSchemaElement, GraphQLInputValueDefinition, SchemaInputField { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/GraphQLInputObjectType.java b/src/main/java/graphql/schema/GraphQLInputObjectType.java index 95ac38adef..0561d05f98 100644 --- a/src/main/java/graphql/schema/GraphQLInputObjectType.java +++ b/src/main/java/graphql/schema/GraphQLInputObjectType.java @@ -35,7 +35,7 @@ * See https://graphql.org/learn/schema/#input-types for more details on the concept */ @PublicApi -public class GraphQLInputObjectType implements GraphQLNamedInputType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLInputFieldsContainer, GraphQLDirectiveContainer { +public class GraphQLInputObjectType implements GraphQLNamedInputType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLInputFieldsContainer, GraphQLDirectiveContainer, SchemaInputObject { private final String name; private final boolean isOneOf; diff --git a/src/main/java/graphql/schema/GraphQLInputType.java b/src/main/java/graphql/schema/GraphQLInputType.java index 46fcf91301..fe2c400d60 100644 --- a/src/main/java/graphql/schema/GraphQLInputType.java +++ b/src/main/java/graphql/schema/GraphQLInputType.java @@ -8,5 +8,6 @@ * to {@link graphql.schema.GraphQLOutputType}s which can only be used as graphql response output. */ @PublicApi -public interface GraphQLInputType extends GraphQLType, GraphQLInputSchemaElement { +public interface GraphQLInputType + extends GraphQLType, GraphQLInputSchemaElement, SchemaInputType { } diff --git a/src/main/java/graphql/schema/GraphQLInterfaceType.java b/src/main/java/graphql/schema/GraphQLInterfaceType.java index eba21cc9aa..8e60a0dec1 100644 --- a/src/main/java/graphql/schema/GraphQLInterfaceType.java +++ b/src/main/java/graphql/schema/GraphQLInterfaceType.java @@ -38,7 +38,7 @@ * See https://graphql.org/learn/schema/#interfaces for more details on the concept. */ @PublicApi -public class GraphQLInterfaceType implements GraphQLNamedType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, GraphQLImplementingType { +public class GraphQLInterfaceType implements GraphQLNamedType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, GraphQLImplementingType, SchemaInterface { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/GraphQLList.java b/src/main/java/graphql/schema/GraphQLList.java index 2e66b66430..be8fabd3d8 100644 --- a/src/main/java/graphql/schema/GraphQLList.java +++ b/src/main/java/graphql/schema/GraphQLList.java @@ -20,7 +20,7 @@ */ @PublicApi @NullMarked -public class GraphQLList implements GraphQLType, GraphQLInputType, GraphQLOutputType, GraphQLModifiedType, GraphQLNullableType { +public class GraphQLList implements GraphQLType, GraphQLInputType, GraphQLOutputType, GraphQLModifiedType, GraphQLNullableType, SchemaList { private final GraphQLType originalWrappedType; private @Nullable GraphQLType replacedWrappedType; diff --git a/src/main/java/graphql/schema/GraphQLModifiedType.java b/src/main/java/graphql/schema/GraphQLModifiedType.java index 27e2ecc047..43449c9b50 100644 --- a/src/main/java/graphql/schema/GraphQLModifiedType.java +++ b/src/main/java/graphql/schema/GraphQLModifiedType.java @@ -10,7 +10,7 @@ * @see graphql.schema.GraphQLList */ @PublicApi -public interface GraphQLModifiedType extends GraphQLType { +public interface GraphQLModifiedType extends GraphQLType, SchemaModifiedType { GraphQLType getWrappedType(); } diff --git a/src/main/java/graphql/schema/GraphQLNamedType.java b/src/main/java/graphql/schema/GraphQLNamedType.java index b58448d89d..45d7cee6c8 100644 --- a/src/main/java/graphql/schema/GraphQLNamedType.java +++ b/src/main/java/graphql/schema/GraphQLNamedType.java @@ -6,7 +6,8 @@ * A GraphQLType which is also a named element, which means it has a getName() method. */ @PublicApi -public interface GraphQLNamedType extends GraphQLType, GraphQLNamedSchemaElement { +public interface GraphQLNamedType + extends GraphQLType, GraphQLNamedSchemaElement, SchemaNamedType { } diff --git a/src/main/java/graphql/schema/GraphQLNonNull.java b/src/main/java/graphql/schema/GraphQLNonNull.java index 914b38429f..7d35947474 100644 --- a/src/main/java/graphql/schema/GraphQLNonNull.java +++ b/src/main/java/graphql/schema/GraphQLNonNull.java @@ -18,7 +18,7 @@ * See https://graphql.org/learn/schema/#lists-and-non-null for more details on the concept */ @PublicApi -public class GraphQLNonNull implements GraphQLType, GraphQLInputType, GraphQLOutputType, GraphQLModifiedType { +public class GraphQLNonNull implements GraphQLType, GraphQLInputType, GraphQLOutputType, GraphQLModifiedType, SchemaNonNull { /** * A factory method for creating non null types so that when used with static imports allows diff --git a/src/main/java/graphql/schema/GraphQLObjectType.java b/src/main/java/graphql/schema/GraphQLObjectType.java index 199c9cc009..45455feb0c 100644 --- a/src/main/java/graphql/schema/GraphQLObjectType.java +++ b/src/main/java/graphql/schema/GraphQLObjectType.java @@ -38,7 +38,7 @@ * See https://graphql.org/learn/schema/#object-types-and-fields for more details on the concept. */ @PublicApi -public class GraphQLObjectType implements GraphQLNamedOutputType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, GraphQLImplementingType { +public class GraphQLObjectType implements GraphQLNamedOutputType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, GraphQLImplementingType, SchemaObject { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/GraphQLOutputType.java b/src/main/java/graphql/schema/GraphQLOutputType.java index 22e5c99392..a7b5899ea6 100644 --- a/src/main/java/graphql/schema/GraphQLOutputType.java +++ b/src/main/java/graphql/schema/GraphQLOutputType.java @@ -8,5 +8,5 @@ * to {@link graphql.schema.GraphQLInputType}s which can only be used as graphql mutation input. */ @PublicApi -public interface GraphQLOutputType extends GraphQLType { +public interface GraphQLOutputType extends GraphQLType, SchemaOutputType { } diff --git a/src/main/java/graphql/schema/GraphQLScalarType.java b/src/main/java/graphql/schema/GraphQLScalarType.java index 94614bbcad..c865195148 100644 --- a/src/main/java/graphql/schema/GraphQLScalarType.java +++ b/src/main/java/graphql/schema/GraphQLScalarType.java @@ -41,7 +41,7 @@ */ @PublicApi @NullMarked -public class GraphQLScalarType implements GraphQLNamedInputType, GraphQLNamedOutputType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer { +public class GraphQLScalarType implements GraphQLNamedInputType, GraphQLNamedOutputType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, SchemaScalar { private final String name; private final @Nullable String description; diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 1ba9ed962c..0528e11959 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -52,7 +52,7 @@ */ @PublicApi @NullMarked -public class GraphQLSchema { +public class GraphQLSchema implements ExecutableSchema { private final GraphQLObjectType queryType; private final @Nullable GraphQLObjectType mutationType; @@ -458,6 +458,69 @@ public boolean containsType(String typeName) { return null; } + @Override + public @Nullable GraphQLFieldDefinition getField( + SchemaComposite parentType, + String fieldName) { + assertNotNull(parentType); + assertNotNull(fieldName); + if (parentType == queryType + && fieldName.equals(introspectionSchemaField.getName())) { + return introspectionSchemaField; + } + if (parentType == queryType + && fieldName.equals(introspectionTypeField.getName())) { + return introspectionTypeField; + } + if (fieldName.equals(__typename.getName())) { + return __typename; + } + if (!(parentType instanceof GraphQLFieldsContainer)) { + return null; + } + return codeRegistry + .getFieldVisibility() + .getFieldDefinition( + (GraphQLFieldsContainer) parentType, + fieldName); + } + + @Override + public List getFields( + SchemaFieldsContainer parentType) { + assertTrue( + parentType instanceof GraphQLFieldsContainer, + "The field container must belong to this GraphQLSchema"); + return codeRegistry + .getFieldVisibility() + .getFieldDefinitions((GraphQLFieldsContainer) parentType); + } + + @Override + public @Nullable GraphQLInputObjectField getInputField( + SchemaInputObject parentType, + String fieldName) { + assertTrue( + parentType instanceof GraphQLInputObjectType, + "The input object must belong to this GraphQLSchema"); + return codeRegistry + .getFieldVisibility() + .getFieldDefinition( + (GraphQLInputObjectType) parentType, + assertNotNull(fieldName)); + } + + @Override + public List getInputFields( + SchemaInputObject parentType) { + assertTrue( + parentType instanceof GraphQLInputObjectType, + "The input object must belong to this GraphQLSchema"); + return codeRegistry + .getFieldVisibility() + .getFieldDefinitions((GraphQLInputObjectType) parentType); + } + /** * @return all the named types in the scheme as a map from name to named type */ @@ -501,11 +564,9 @@ public List getAllElementsAsList() { /** * Returns true if a specified concrete type is a possible type of a provided abstract type. - * If the provided abstract type is: - * - an interface, it checks whether the concrete type is one of its implementations. - * - a union, it checks whether the concrete type is one of its possible types. + * An interface represents its implementations and a union represents its members. * - * @param abstractType abstract type either interface or union + * @param abstractType interface or union type * @param concreteType concrete type * * @return true if possible type, false otherwise. @@ -517,7 +578,79 @@ public boolean isPossibleType(GraphQLNamedType abstractType, GraphQLObjectType c } else if (abstractType instanceof GraphQLUnionType) { return ((GraphQLUnionType) abstractType).isPossibleType(concreteType); } - return assertShouldNeverHappen("Unsupported abstract type %s. Abstract types supported are Union and Interface.", abstractType.getName()); + return assertShouldNeverHappen( + "Unsupported abstract type %s. Abstract types supported are Union and Interface.", + abstractType.getName()); + } + + /** + * Returns whether an object type is represented by a GraphQL composite type. + * + *

This overload preserves unambiguous source calls alongside the existing + * {@link #isPossibleType(GraphQLNamedType, GraphQLObjectType)} method and the generic + * {@link ExecutableSchema} contract.

+ * + * @param compositeType object, interface, or union type + * @param objectType concrete object type + * + * @return true if the object is a possible type + */ + public boolean isPossibleType( + GraphQLCompositeType compositeType, + GraphQLObjectType objectType) { + if (compositeType instanceof GraphQLObjectType) { + return compositeType == objectType; + } + return isPossibleType( + (GraphQLNamedType) compositeType, + objectType); + } + + @Override + public List getPossibleTypes( + SchemaComposite compositeType) { + if (compositeType instanceof GraphQLObjectType) { + return singletonList((GraphQLObjectType) compositeType); + } + if (compositeType instanceof GraphQLInterfaceType) { + return assertNotNull( + getImplementations((GraphQLInterfaceType) compositeType)); + } + assertTrue( + compositeType instanceof GraphQLUnionType, + "The composite type must belong to this GraphQLSchema"); + List result = new ArrayList<>(); + for (GraphQLNamedOutputType type : + ((GraphQLUnionType) compositeType).getTypes()) { + assertTrue( + type instanceof GraphQLObjectType, + "Union type references must be resolved"); + result.add((GraphQLObjectType) type); + } + return ImmutableList.copyOf(result); + } + + @Override + public boolean isPossibleType( + SchemaComposite compositeType, + SchemaObject objectType) { + assertTrue( + objectType instanceof GraphQLObjectType, + "The object type must belong to this GraphQLSchema"); + assertTrue( + compositeType instanceof GraphQLCompositeType, + "The composite type must belong to this GraphQLSchema"); + return isPossibleType( + (GraphQLCompositeType) compositeType, + (GraphQLObjectType) objectType); + } + + @Override + public Coercing getScalarCoercing(SchemaScalar scalarType) { + assertTrue( + scalarType instanceof GraphQLScalarType, + "The scalar type must belong to this GraphQLSchema"); + return ((GraphQLScalarType) scalarType).getCoercing(); } /** diff --git a/src/main/java/graphql/schema/GraphQLType.java b/src/main/java/graphql/schema/GraphQLType.java index cb332f2f0c..b907316f72 100644 --- a/src/main/java/graphql/schema/GraphQLType.java +++ b/src/main/java/graphql/schema/GraphQLType.java @@ -11,5 +11,5 @@ */ @PublicApi @NullMarked -public interface GraphQLType extends GraphQLSchemaElement { +public interface GraphQLType extends GraphQLSchemaElement, SchemaType { } diff --git a/src/main/java/graphql/schema/GraphQLUnionType.java b/src/main/java/graphql/schema/GraphQLUnionType.java index 68e0bd5718..9577dc5329 100644 --- a/src/main/java/graphql/schema/GraphQLUnionType.java +++ b/src/main/java/graphql/schema/GraphQLUnionType.java @@ -40,7 +40,7 @@ */ @PublicApi @NullMarked -public class GraphQLUnionType implements GraphQLNamedOutputType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer { +public class GraphQLUnionType implements GraphQLNamedOutputType, GraphQLCompositeType, GraphQLUnmodifiedType, GraphQLNullableType, GraphQLDirectiveContainer, SchemaUnion { private final String name; private final String description; diff --git a/src/main/java/graphql/schema/SchemaArgument.java b/src/main/java/graphql/schema/SchemaArgument.java new file mode 100644 index 0000000000..0f8983f718 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaArgument.java @@ -0,0 +1,18 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * An argument declared by a field or directive. + */ +@ExperimentalApi +@NullMarked +public interface SchemaArgument { + + String getName(); + + SchemaInputType getType(); + + InputValueWithState getArgumentDefaultValue(); +} diff --git a/src/main/java/graphql/schema/SchemaComposite.java b/src/main/java/graphql/schema/SchemaComposite.java new file mode 100644 index 0000000000..3ff3041aec --- /dev/null +++ b/src/main/java/graphql/schema/SchemaComposite.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A named output type that can own a selection set. + */ +@ExperimentalApi +@NullMarked +public interface SchemaComposite extends SchemaNamedType, SchemaOutputType { +} diff --git a/src/main/java/graphql/schema/SchemaDirective.java b/src/main/java/graphql/schema/SchemaDirective.java new file mode 100644 index 0000000000..17b437fbcb --- /dev/null +++ b/src/main/java/graphql/schema/SchemaDirective.java @@ -0,0 +1,27 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import graphql.introspection.Introspection.DirectiveLocation; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; +import java.util.Set; + +/** + * A GraphQL directive definition. + */ +@ExperimentalApi +@NullMarked +public interface SchemaDirective { + + String getName(); + + boolean isRepeatable(); + + Set validLocations(); + + List getArguments(); + + @Nullable SchemaArgument getArgument(String name); +} diff --git a/src/main/java/graphql/schema/SchemaEnum.java b/src/main/java/graphql/schema/SchemaEnum.java new file mode 100644 index 0000000000..743b2c640f --- /dev/null +++ b/src/main/java/graphql/schema/SchemaEnum.java @@ -0,0 +1,20 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * A GraphQL enum type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaEnum + extends SchemaNamedType, SchemaInputType, SchemaOutputType { + + List getValues(); + + @Nullable SchemaEnumValue getValue(String name); +} diff --git a/src/main/java/graphql/schema/SchemaEnumValue.java b/src/main/java/graphql/schema/SchemaEnumValue.java new file mode 100644 index 0000000000..2c11623ba8 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaEnumValue.java @@ -0,0 +1,14 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A value declared by a GraphQL enum type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaEnumValue { + + String getName(); +} diff --git a/src/main/java/graphql/schema/SchemaField.java b/src/main/java/graphql/schema/SchemaField.java new file mode 100644 index 0000000000..78ca7fcd80 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaField.java @@ -0,0 +1,23 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * A field declared by an object or interface type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaField { + + String getName(); + + SchemaOutputType getType(); + + List getArguments(); + + @Nullable SchemaArgument getArgument(String name); +} diff --git a/src/main/java/graphql/schema/SchemaFieldsContainer.java b/src/main/java/graphql/schema/SchemaFieldsContainer.java new file mode 100644 index 0000000000..df28697754 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaFieldsContainer.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A composite type that declares output fields. + */ +@ExperimentalApi +@NullMarked +public interface SchemaFieldsContainer extends SchemaComposite { +} diff --git a/src/main/java/graphql/schema/SchemaInputField.java b/src/main/java/graphql/schema/SchemaInputField.java new file mode 100644 index 0000000000..a0c6b376f0 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaInputField.java @@ -0,0 +1,18 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A field declared by an input object type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaInputField { + + String getName(); + + SchemaInputType getType(); + + InputValueWithState getInputFieldDefaultValue(); +} diff --git a/src/main/java/graphql/schema/SchemaInputFieldsContainer.java b/src/main/java/graphql/schema/SchemaInputFieldsContainer.java new file mode 100644 index 0000000000..b161db92d1 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaInputFieldsContainer.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A named type that declares input fields. + */ +@ExperimentalApi +@NullMarked +public interface SchemaInputFieldsContainer extends SchemaNamedType { +} diff --git a/src/main/java/graphql/schema/SchemaInputObject.java b/src/main/java/graphql/schema/SchemaInputObject.java new file mode 100644 index 0000000000..0e5db08300 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaInputObject.java @@ -0,0 +1,15 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A GraphQL input object type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaInputObject + extends SchemaInputFieldsContainer, SchemaInputType { + + boolean isOneOf(); +} diff --git a/src/main/java/graphql/schema/SchemaInputType.java b/src/main/java/graphql/schema/SchemaInputType.java new file mode 100644 index 0000000000..03e0f0ea04 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaInputType.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A type that can be used as GraphQL input. + */ +@ExperimentalApi +@NullMarked +public interface SchemaInputType extends SchemaType { +} diff --git a/src/main/java/graphql/schema/SchemaInterface.java b/src/main/java/graphql/schema/SchemaInterface.java new file mode 100644 index 0000000000..a1f2429d7b --- /dev/null +++ b/src/main/java/graphql/schema/SchemaInterface.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A GraphQL interface type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaInterface extends SchemaFieldsContainer { +} diff --git a/src/main/java/graphql/schema/SchemaList.java b/src/main/java/graphql/schema/SchemaList.java new file mode 100644 index 0000000000..465892b4dd --- /dev/null +++ b/src/main/java/graphql/schema/SchemaList.java @@ -0,0 +1,13 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A GraphQL list type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaList + extends SchemaModifiedType, SchemaInputType, SchemaOutputType { +} diff --git a/src/main/java/graphql/schema/SchemaModifiedType.java b/src/main/java/graphql/schema/SchemaModifiedType.java new file mode 100644 index 0000000000..c1ebc40076 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaModifiedType.java @@ -0,0 +1,14 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A type that wraps and modifies another type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaModifiedType extends SchemaType { + + SchemaType getWrappedType(); +} diff --git a/src/main/java/graphql/schema/SchemaNamedType.java b/src/main/java/graphql/schema/SchemaNamedType.java new file mode 100644 index 0000000000..4f287587d1 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaNamedType.java @@ -0,0 +1,14 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A named type exposed by an {@link ExecutableSchema}. + */ +@ExperimentalApi +@NullMarked +public interface SchemaNamedType extends SchemaType { + + String getName(); +} diff --git a/src/main/java/graphql/schema/SchemaNonNull.java b/src/main/java/graphql/schema/SchemaNonNull.java new file mode 100644 index 0000000000..8df3c8d6d9 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaNonNull.java @@ -0,0 +1,13 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A GraphQL non-null type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaNonNull + extends SchemaModifiedType, SchemaInputType, SchemaOutputType { +} diff --git a/src/main/java/graphql/schema/SchemaObject.java b/src/main/java/graphql/schema/SchemaObject.java new file mode 100644 index 0000000000..361cc1ab74 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaObject.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A GraphQL object type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaObject extends SchemaFieldsContainer { +} diff --git a/src/main/java/graphql/schema/SchemaOutputType.java b/src/main/java/graphql/schema/SchemaOutputType.java new file mode 100644 index 0000000000..59f8151060 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaOutputType.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A type that can be used as GraphQL output. + */ +@ExperimentalApi +@NullMarked +public interface SchemaOutputType extends SchemaType { +} diff --git a/src/main/java/graphql/schema/SchemaScalar.java b/src/main/java/graphql/schema/SchemaScalar.java new file mode 100644 index 0000000000..28dbd96169 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaScalar.java @@ -0,0 +1,13 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A GraphQL scalar type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaScalar + extends SchemaNamedType, SchemaInputType, SchemaOutputType { +} diff --git a/src/main/java/graphql/schema/SchemaType.java b/src/main/java/graphql/schema/SchemaType.java new file mode 100644 index 0000000000..e4da1d716b --- /dev/null +++ b/src/main/java/graphql/schema/SchemaType.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A type exposed by an {@link ExecutableSchema}. + */ +@ExperimentalApi +@NullMarked +public interface SchemaType { +} diff --git a/src/main/java/graphql/schema/SchemaUnion.java b/src/main/java/graphql/schema/SchemaUnion.java new file mode 100644 index 0000000000..4140e67f3e --- /dev/null +++ b/src/main/java/graphql/schema/SchemaUnion.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A GraphQL union type. + */ +@ExperimentalApi +@NullMarked +public interface SchemaUnion extends SchemaComposite { +} diff --git a/src/main/java/graphql/schema/universe/IntMapBranch.java b/src/main/java/graphql/schema/universe/IntMapBranch.java index 5e2caaca4c..3ededb0034 100644 --- a/src/main/java/graphql/schema/universe/IntMapBranch.java +++ b/src/main/java/graphql/schema/universe/IntMapBranch.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.Arrays; @@ -13,7 +12,6 @@ * @param the stored value type */ @Internal -@NullMarked public final class IntMapBranch implements IntMapNode { private final int bitmap; diff --git a/src/main/java/graphql/schema/universe/IntMapEntryVisitor.java b/src/main/java/graphql/schema/universe/IntMapEntryVisitor.java index 6541b4e792..450412d7dd 100644 --- a/src/main/java/graphql/schema/universe/IntMapEntryVisitor.java +++ b/src/main/java/graphql/schema/universe/IntMapEntryVisitor.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; /** * Visits one binding in a persistent integer map. @@ -9,7 +8,6 @@ * @param the stored value type */ @Internal -@NullMarked @FunctionalInterface public interface IntMapEntryVisitor { diff --git a/src/main/java/graphql/schema/universe/IntMapLeaf.java b/src/main/java/graphql/schema/universe/IntMapLeaf.java index d6ffd54b45..a89fd647d9 100644 --- a/src/main/java/graphql/schema/universe/IntMapLeaf.java +++ b/src/main/java/graphql/schema/universe/IntMapLeaf.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.Set; @@ -12,7 +11,6 @@ * @param the stored value type */ @Internal -@NullMarked public final class IntMapLeaf implements IntMapNode { private final int key; diff --git a/src/main/java/graphql/schema/universe/IntMapNode.java b/src/main/java/graphql/schema/universe/IntMapNode.java index 6f33978ad8..946bce06c8 100644 --- a/src/main/java/graphql/schema/universe/IntMapNode.java +++ b/src/main/java/graphql/schema/universe/IntMapNode.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.Set; @@ -16,7 +15,6 @@ * @param the stored value type */ @Internal -@NullMarked public interface IntMapNode { /** diff --git a/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java b/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java index 09fe5574db..d8f67fdfdf 100644 --- a/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java +++ b/src/main/java/graphql/schema/universe/MutablePackedEdgeSet.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import java.util.Arrays; @@ -16,7 +15,6 @@ * validates the cardinality rules declared by {@link SUEdgeKind}.

*/ @Internal -@NullMarked public final class MutablePackedEdgeSet { private long[] edges; diff --git a/src/main/java/graphql/schema/universe/PackedEdgeSet.java b/src/main/java/graphql/schema/universe/PackedEdgeSet.java index 784c4dcaac..e22e6b7273 100644 --- a/src/main/java/graphql/schema/universe/PackedEdgeSet.java +++ b/src/main/java/graphql/schema/universe/PackedEdgeSet.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import java.util.Arrays; @@ -27,7 +26,6 @@ * snapshots.

*/ @Internal -@NullMarked public final class PackedEdgeSet { private static final int MAX_NAME_ID = 0x00ff_ffff; diff --git a/src/main/java/graphql/schema/universe/PersistentEdgeMap.java b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java index 871260f0bb..7ea59c14b5 100644 --- a/src/main/java/graphql/schema/universe/PersistentEdgeMap.java +++ b/src/main/java/graphql/schema/universe/PersistentEdgeMap.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.Set; @@ -21,7 +20,6 @@ * {@code 2^32}. Distinct source IDs therefore cannot produce a full-hash collision.

*/ @Internal -@NullMarked public final class PersistentEdgeMap { private static final PersistentEdgeMap EMPTY = new PersistentEdgeMap(null); diff --git a/src/main/java/graphql/schema/universe/PersistentIntMap.java b/src/main/java/graphql/schema/universe/PersistentIntMap.java index 1bdc187f4d..3a1d3b5459 100644 --- a/src/main/java/graphql/schema/universe/PersistentIntMap.java +++ b/src/main/java/graphql/schema/universe/PersistentIntMap.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.Set; @@ -17,7 +16,6 @@ * @param the stored value type */ @Internal -@NullMarked public final class PersistentIntMap { private static final PersistentIntMap EMPTY = new PersistentIntMap<>(null); diff --git a/src/main/java/graphql/schema/universe/SUAppliedDirective.java b/src/main/java/graphql/schema/universe/SUAppliedDirective.java index 9bf94407f7..4f30108b2e 100644 --- a/src/main/java/graphql/schema/universe/SUAppliedDirective.java +++ b/src/main/java/graphql/schema/universe/SUAppliedDirective.java @@ -3,7 +3,6 @@ import graphql.ExperimentalApi; import graphql.Internal; import graphql.language.Directive; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.Arrays; @@ -13,7 +12,6 @@ import static graphql.Assert.assertNotNull; @ExperimentalApi -@NullMarked public final class SUAppliedDirective extends SUVertex { private final @Nullable Directive definition; diff --git a/src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java b/src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java index 6f7a84d971..1710decf43 100644 --- a/src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java +++ b/src/main/java/graphql/schema/universe/SUAppliedDirectiveArgument.java @@ -4,13 +4,11 @@ import graphql.Internal; import graphql.language.Argument; import graphql.schema.InputValueWithState; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import static graphql.Assert.assertNotNull; @ExperimentalApi -@NullMarked public final class SUAppliedDirectiveArgument { private final SchemaUniverse universe; diff --git a/src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java b/src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java index 46e1685fd4..d54448d5c8 100644 --- a/src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java +++ b/src/main/java/graphql/schema/universe/SUAppliedDirectiveContainer.java @@ -1,12 +1,10 @@ package graphql.schema.universe; import graphql.ExperimentalApi; -import org.jspecify.annotations.NullMarked; /** * Marks a schema-universe vertex that can carry applied directives. */ @ExperimentalApi -@NullMarked public interface SUAppliedDirectiveContainer { } diff --git a/src/main/java/graphql/schema/universe/SUArgument.java b/src/main/java/graphql/schema/universe/SUArgument.java index 554ef046e1..f63272f9a1 100644 --- a/src/main/java/graphql/schema/universe/SUArgument.java +++ b/src/main/java/graphql/schema/universe/SUArgument.java @@ -4,13 +4,11 @@ import graphql.Internal; import graphql.language.InputValueDefinition; import graphql.schema.InputValueWithState; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import static graphql.Assert.assertNotNull; @ExperimentalApi -@NullMarked public final class SUArgument extends SUVertex implements SUAppliedDirectiveContainer { private final InputValueWithState defaultValue; diff --git a/src/main/java/graphql/schema/universe/SUCompositeType.java b/src/main/java/graphql/schema/universe/SUCompositeType.java index a51df4168c..82c1ec1269 100644 --- a/src/main/java/graphql/schema/universe/SUCompositeType.java +++ b/src/main/java/graphql/schema/universe/SUCompositeType.java @@ -2,14 +2,12 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** * A named GraphQL type that can be the type condition of a selection set. */ @ExperimentalApi -@NullMarked public abstract class SUCompositeType extends SUNamedType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUDirective.java b/src/main/java/graphql/schema/universe/SUDirective.java index 8312fac291..7e95d7d1c1 100644 --- a/src/main/java/graphql/schema/universe/SUDirective.java +++ b/src/main/java/graphql/schema/universe/SUDirective.java @@ -4,7 +4,6 @@ import graphql.Internal; import graphql.introspection.Introspection.DirectiveLocation; import graphql.language.DirectiveDefinition; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.EnumSet; @@ -14,7 +13,6 @@ import static graphql.Assert.assertTrue; @ExperimentalApi -@NullMarked public final class SUDirective extends SUVertex implements SUAppliedDirectiveContainer { private final boolean repeatable; diff --git a/src/main/java/graphql/schema/universe/SUEdgeKind.java b/src/main/java/graphql/schema/universe/SUEdgeKind.java index 93b8fe1baf..661140d30f 100644 --- a/src/main/java/graphql/schema/universe/SUEdgeKind.java +++ b/src/main/java/graphql/schema/universe/SUEdgeKind.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import static graphql.Assert.assertTrue; @@ -15,7 +14,6 @@ * because repeatable directive occurrences must remain distinct.

*/ @Internal -@NullMarked public enum SUEdgeKind { QUERY_TYPE(1, true, true, false), MUTATION_TYPE(2, true, true, false), diff --git a/src/main/java/graphql/schema/universe/SUEnumType.java b/src/main/java/graphql/schema/universe/SUEnumType.java index dd1639dc8b..7852538059 100644 --- a/src/main/java/graphql/schema/universe/SUEnumType.java +++ b/src/main/java/graphql/schema/universe/SUEnumType.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUEnumType extends SUNamedType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUEnumValue.java b/src/main/java/graphql/schema/universe/SUEnumValue.java index 8bf4eb20b6..06ed386292 100644 --- a/src/main/java/graphql/schema/universe/SUEnumValue.java +++ b/src/main/java/graphql/schema/universe/SUEnumValue.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUEnumValue extends SUVertex implements SUAppliedDirectiveContainer { @Internal diff --git a/src/main/java/graphql/schema/universe/SUExporter.java b/src/main/java/graphql/schema/universe/SUExporter.java index 65231aad90..aa8309109e 100644 --- a/src/main/java/graphql/schema/universe/SUExporter.java +++ b/src/main/java/graphql/schema/universe/SUExporter.java @@ -30,7 +30,6 @@ import graphql.schema.TypeResolver; import graphql.schema.idl.EchoingWiringFactory; import graphql.schema.idl.ScalarInfo; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.LinkedHashMap; @@ -48,7 +47,6 @@ * scalar coercing implementations, or other runtime wiring.

*/ @ExperimentalApi -@NullMarked public final class SUExporter { private static final TypeResolver UNEXECUTABLE_TYPE_RESOLVER = environment -> null; diff --git a/src/main/java/graphql/schema/universe/SUField.java b/src/main/java/graphql/schema/universe/SUField.java index 184cfdc44f..a08d7fa31b 100644 --- a/src/main/java/graphql/schema/universe/SUField.java +++ b/src/main/java/graphql/schema/universe/SUField.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUField extends SUVertex implements SUAppliedDirectiveContainer { @Internal diff --git a/src/main/java/graphql/schema/universe/SUImporter.java b/src/main/java/graphql/schema/universe/SUImporter.java index 5bceb2fb19..8eb13522c6 100644 --- a/src/main/java/graphql/schema/universe/SUImporter.java +++ b/src/main/java/graphql/schema/universe/SUImporter.java @@ -29,7 +29,6 @@ import graphql.schema.GraphQLUnionType; import graphql.schema.SchemaTraverser; import graphql.schema.impl.GraphQLTypeCollectingVisitor; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -50,7 +49,6 @@ * part of this topology import.

*/ @ExperimentalApi -@NullMarked public final class SUImporter { private final SchemaUniverse universe; diff --git a/src/main/java/graphql/schema/universe/SUInputField.java b/src/main/java/graphql/schema/universe/SUInputField.java index 5afe287572..3afea8512a 100644 --- a/src/main/java/graphql/schema/universe/SUInputField.java +++ b/src/main/java/graphql/schema/universe/SUInputField.java @@ -4,13 +4,11 @@ import graphql.Internal; import graphql.language.InputValueDefinition; import graphql.schema.InputValueWithState; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import static graphql.Assert.assertNotNull; @ExperimentalApi -@NullMarked public final class SUInputField extends SUVertex implements SUAppliedDirectiveContainer { private final InputValueWithState defaultValue; diff --git a/src/main/java/graphql/schema/universe/SUInputObjectType.java b/src/main/java/graphql/schema/universe/SUInputObjectType.java index 23e75d2d6f..2e28d668fe 100644 --- a/src/main/java/graphql/schema/universe/SUInputObjectType.java +++ b/src/main/java/graphql/schema/universe/SUInputObjectType.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUInputObjectType extends SUNamedType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUInterfaceType.java b/src/main/java/graphql/schema/universe/SUInterfaceType.java index 7ac8cf3f5f..22bc63bc80 100644 --- a/src/main/java/graphql/schema/universe/SUInterfaceType.java +++ b/src/main/java/graphql/schema/universe/SUInterfaceType.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUInterfaceType extends SUCompositeType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java b/src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java index 49c10d213f..d28f5b14b5 100644 --- a/src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java +++ b/src/main/java/graphql/schema/universe/SUIntrospectionSchemaType.java @@ -2,7 +2,6 @@ import graphql.Internal; import graphql.introspection.Introspection; -import org.jspecify.annotations.NullMarked; import static graphql.Assert.assertNotNull; @@ -10,7 +9,6 @@ * Imports the canonical GraphQL introspection type graph into a schema universe schema. */ @Internal -@NullMarked public final class SUIntrospectionSchemaType { private final SchemaUniverse universe; diff --git a/src/main/java/graphql/schema/universe/SUListType.java b/src/main/java/graphql/schema/universe/SUListType.java index eba8a342a6..688d9d824a 100644 --- a/src/main/java/graphql/schema/universe/SUListType.java +++ b/src/main/java/graphql/schema/universe/SUListType.java @@ -2,10 +2,8 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; @ExperimentalApi -@NullMarked public final class SUListType extends SUType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUNamedType.java b/src/main/java/graphql/schema/universe/SUNamedType.java index 396300f552..549a3fe90a 100644 --- a/src/main/java/graphql/schema/universe/SUNamedType.java +++ b/src/main/java/graphql/schema/universe/SUNamedType.java @@ -2,14 +2,12 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** * A named GraphQL type vertex. */ @ExperimentalApi -@NullMarked public abstract class SUNamedType extends SUType implements SUAppliedDirectiveContainer { @Internal diff --git a/src/main/java/graphql/schema/universe/SUNonNullType.java b/src/main/java/graphql/schema/universe/SUNonNullType.java index 5f5efed62c..532b2fde4a 100644 --- a/src/main/java/graphql/schema/universe/SUNonNullType.java +++ b/src/main/java/graphql/schema/universe/SUNonNullType.java @@ -2,10 +2,8 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; @ExperimentalApi -@NullMarked public final class SUNonNullType extends SUType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUObjectType.java b/src/main/java/graphql/schema/universe/SUObjectType.java index c14d055654..bc504a1192 100644 --- a/src/main/java/graphql/schema/universe/SUObjectType.java +++ b/src/main/java/graphql/schema/universe/SUObjectType.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUObjectType extends SUCompositeType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUScalarType.java b/src/main/java/graphql/schema/universe/SUScalarType.java index 99edbfc348..caa681df87 100644 --- a/src/main/java/graphql/schema/universe/SUScalarType.java +++ b/src/main/java/graphql/schema/universe/SUScalarType.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUScalarType extends SUNamedType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index 2cc5cee277..29cd69c59e 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -3,7 +3,6 @@ import graphql.ExperimentalApi; import graphql.schema.GraphQLSchema; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -24,7 +23,6 @@ * are not guaranteed to remain usable after removal, and its vertices may later be reclaimed.

*/ @ExperimentalApi -@NullMarked public final class SUSchema { private final SchemaUniverse universe; diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index d4264da803..3204caf914 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -3,7 +3,6 @@ import com.google.common.collect.ImmutableMap; import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -19,7 +18,6 @@ * Builds an immutable schema snapshot, optionally by changing an existing snapshot. */ @ExperimentalApi -@NullMarked public final class SUSchemaBuilder { private final SchemaUniverse universe; diff --git a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java index c3d793a818..9061b086ce 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java +++ b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java @@ -42,7 +42,6 @@ import graphql.schema.idl.SchemaTypeChecker; import graphql.schema.idl.TypeDefinitionRegistry; import graphql.schema.idl.errors.SchemaProblem; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -64,7 +63,6 @@ * Compiles parsed type-system definitions directly into a {@link SUSchema}. */ @ExperimentalApi -@NullMarked public final class SUSchemaGenerator { private static final List BUILT_IN_DIRECTIVE_DEFINITIONS = diff --git a/src/main/java/graphql/schema/universe/SUSchemaPrinter.java b/src/main/java/graphql/schema/universe/SUSchemaPrinter.java index befe272886..2b0abab422 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaPrinter.java +++ b/src/main/java/graphql/schema/universe/SUSchemaPrinter.java @@ -2,7 +2,6 @@ import graphql.ExperimentalApi; import graphql.schema.idl.SchemaPrinter; -import org.jspecify.annotations.NullMarked; import static graphql.Assert.assertNotNull; @@ -14,7 +13,6 @@ * a schema universe does not retain scalar coercing implementations.

*/ @ExperimentalApi -@NullMarked public final class SUSchemaPrinter { private final SchemaPrinter schemaPrinter; diff --git a/src/main/java/graphql/schema/universe/SUSchemaRoot.java b/src/main/java/graphql/schema/universe/SUSchemaRoot.java index 744aeb83e9..9c1ae7fa0f 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaRoot.java +++ b/src/main/java/graphql/schema/universe/SUSchemaRoot.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUSchemaRoot extends SUVertex implements SUAppliedDirectiveContainer { @Internal diff --git a/src/main/java/graphql/schema/universe/SUType.java b/src/main/java/graphql/schema/universe/SUType.java index 11def16290..12eaffc4a6 100644 --- a/src/main/java/graphql/schema/universe/SUType.java +++ b/src/main/java/graphql/schema/universe/SUType.java @@ -2,14 +2,12 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; /** * A GraphQL type vertex, including named types and list/non-null wrappers. */ @ExperimentalApi -@NullMarked public abstract class SUType extends SUVertex { @Internal diff --git a/src/main/java/graphql/schema/universe/SUUnionType.java b/src/main/java/graphql/schema/universe/SUUnionType.java index 408a282064..2136f4b16d 100644 --- a/src/main/java/graphql/schema/universe/SUUnionType.java +++ b/src/main/java/graphql/schema/universe/SUUnionType.java @@ -2,11 +2,9 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @ExperimentalApi -@NullMarked public final class SUUnionType extends SUCompositeType { @Internal diff --git a/src/main/java/graphql/schema/universe/SUVertex.java b/src/main/java/graphql/schema/universe/SUVertex.java index 66258158a7..a18a7b4c28 100644 --- a/src/main/java/graphql/schema/universe/SUVertex.java +++ b/src/main/java/graphql/schema/universe/SUVertex.java @@ -2,7 +2,6 @@ import graphql.ExperimentalApi; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import static graphql.Assert.assertNotNull; @@ -14,7 +13,6 @@ * elements are stored by each {@link SUSchema}.

*/ @ExperimentalApi -@NullMarked public abstract class SUVertex { private final int id; diff --git a/src/main/java/graphql/schema/universe/SUVertexKind.java b/src/main/java/graphql/schema/universe/SUVertexKind.java index ce566e1b6c..0a45049da8 100644 --- a/src/main/java/graphql/schema/universe/SUVertexKind.java +++ b/src/main/java/graphql/schema/universe/SUVertexKind.java @@ -1,13 +1,11 @@ package graphql.schema.universe; import graphql.ExperimentalApi; -import org.jspecify.annotations.NullMarked; /** * The kinds of vertices stored in a {@link SchemaUniverse}. */ @ExperimentalApi -@NullMarked public enum SUVertexKind { SCHEMA, OBJECT, diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java index 571c79acf2..71a671ea3d 100644 --- a/src/main/java/graphql/schema/universe/SchemaUniverse.java +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -12,7 +12,6 @@ import graphql.schema.InputValueWithState; import graphql.schema.idl.SchemaParser; import graphql.schema.idl.TypeDefinitionRegistry; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.Arrays; @@ -42,7 +41,6 @@ * monotonically and are never reused, including after unused vertices are reclaimed.

*/ @ExperimentalApi -@NullMarked public final class SchemaUniverse { private static final int VERTEX_CHUNK_SHIFT = 10; diff --git a/src/main/java/graphql/schema/universe/VertexChunk.java b/src/main/java/graphql/schema/universe/VertexChunk.java index 1082853073..e49f0c44ab 100644 --- a/src/main/java/graphql/schema/universe/VertexChunk.java +++ b/src/main/java/graphql/schema/universe/VertexChunk.java @@ -1,7 +1,6 @@ package graphql.schema.universe; import graphql.Internal; -import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.BitSet; @@ -10,7 +9,6 @@ * A fixed-size arena chunk whose vertex slots may be reclaimed. */ @Internal -@NullMarked public final class VertexChunk { private static final VertexChunk EMPTY = new VertexChunk(0); diff --git a/src/main/java/graphql/schema/universe/package-info.java b/src/main/java/graphql/schema/universe/package-info.java new file mode 100644 index 0000000000..8c6270bb5a --- /dev/null +++ b/src/main/java/graphql/schema/universe/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package graphql.schema.universe; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java new file mode 100644 index 0000000000..27a5504f3b --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java @@ -0,0 +1,58 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.universe.SUVertex; + +import static graphql.Assert.assertNotNull; + +/** + * Shared identity for schema-bound universe adapters. + */ +@Internal +public abstract class AbstractSUSchemaElement { + + private final SUSchemaExecutableSchema executableSchema; + private final SUVertex vertex; + + @Internal + public AbstractSUSchemaElement( + SUSchemaExecutableSchema executableSchema, + SUVertex vertex) { + this.executableSchema = assertNotNull(executableSchema); + this.vertex = assertNotNull(vertex); + } + + @Internal + public final SUSchemaExecutableSchema getExecutableSchema() { + return executableSchema; + } + + @Internal + public final SUVertex getVertex() { + return vertex; + } + + @Override + public final boolean equals(Object other) { + if (this == other) { + return true; + } + if (other == null || getClass() != other.getClass()) { + return false; + } + AbstractSUSchemaElement that = (AbstractSUSchemaElement) other; + return executableSchema.getSchema() == that.executableSchema.getSchema() + && vertex == that.vertex; + } + + @Override + public final int hashCode() { + return 31 * System.identityHashCode(executableSchema.getSchema()) + + vertex.getId(); + } + + @Override + public String toString() { + return vertex.toString(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java new file mode 100644 index 0000000000..f3109c33e9 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java @@ -0,0 +1,27 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaNamedType; +import graphql.schema.universe.SUNamedType; + +import static graphql.Assert.assertNotNull; + +/** + * Shared base for schema-bound named type adapters. + */ +@Internal +public abstract class AbstractSUSchemaNamedType + extends AbstractSUSchemaType implements SchemaNamedType { + + @Internal + public AbstractSUSchemaNamedType( + SUSchemaExecutableSchema executableSchema, + SUNamedType type) { + super(executableSchema, type); + } + + @Override + public final String getName() { + return assertNotNull(getTypeVertex().getName()); + } +} diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java new file mode 100644 index 0000000000..386224959b --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java @@ -0,0 +1,25 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaType; +import graphql.schema.universe.SUType; + +/** + * Shared base for schema-bound universe type adapters. + */ +@Internal +public abstract class AbstractSUSchemaType + extends AbstractSUSchemaElement implements SchemaType { + + @Internal + public AbstractSUSchemaType( + SUSchemaExecutableSchema executableSchema, + SUType type) { + super(executableSchema, type); + } + + @Internal + public final SUType getTypeVertex() { + return (SUType) getVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaArgument.java b/src/main/java/graphql/schema/universe/view/SUSchemaArgument.java new file mode 100644 index 0000000000..65b56f16d4 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaArgument.java @@ -0,0 +1,46 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.InputValueWithState; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaInputType; +import graphql.schema.universe.SUArgument; +import graphql.schema.universe.SUType; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaArgument + extends AbstractSUSchemaElement implements SchemaArgument { + + @Internal + public SUSchemaArgument( + SUSchemaExecutableSchema executableSchema, + SUArgument argument) { + super(executableSchema, argument); + } + + @Override + public String getName() { + return assertNotNull(getVertex().getName()); + } + + @Override + public SchemaInputType getType() { + SUType type = assertNotNull( + getExecutableSchema() + .getSchema() + .getType(getArgumentVertex())); + return getExecutableSchema().adaptInputType(type); + } + + @Override + public InputValueWithState getArgumentDefaultValue() { + return getArgumentVertex().getArgumentDefaultValue(); + } + + @Internal + public SUArgument getArgumentVertex() { + return (SUArgument) getVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java b/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java new file mode 100644 index 0000000000..e0ac80ade4 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java @@ -0,0 +1,72 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.introspection.Introspection.DirectiveLocation; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaDirective; +import graphql.schema.universe.SUArgument; +import graphql.schema.universe.SUDirective; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaDirective + extends AbstractSUSchemaElement implements SchemaDirective { + + @Internal + public SUSchemaDirective( + SUSchemaExecutableSchema executableSchema, + SUDirective directive) { + super(executableSchema, directive); + } + + @Override + public String getName() { + return assertNotNull(getVertex().getName()); + } + + @Override + public boolean isRepeatable() { + return getDirectiveVertex().isRepeatable(); + } + + @Override + public Set validLocations() { + return getDirectiveVertex().validLocations(); + } + + @Override + public List getArguments() { + List arguments = getExecutableSchema() + .getSchema() + .getArguments(getDirectiveVertex()); + List result = + new ArrayList<>(arguments.size()); + for (SUArgument argument : arguments) { + result.add(new SUSchemaArgument(getExecutableSchema(), argument)); + } + return Collections.unmodifiableList(result); + } + + @Override + public @Nullable SchemaArgument getArgument(String name) { + SUArgument argument = getExecutableSchema() + .getSchema() + .getArgument(getDirectiveVertex(), name); + if (argument == null) { + return null; + } + return new SUSchemaArgument(getExecutableSchema(), argument); + } + + @Internal + public SUDirective getDirectiveVertex() { + return (SUDirective) getVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java b/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java new file mode 100644 index 0000000000..1dba280d17 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java @@ -0,0 +1,51 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaEnum; +import graphql.schema.universe.SUEnumType; +import graphql.schema.universe.SUEnumValue; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +@Internal +public final class SUSchemaEnum + extends AbstractSUSchemaNamedType implements SchemaEnum { + + @Internal + public SUSchemaEnum( + SUSchemaExecutableSchema executableSchema, + SUEnumType type) { + super(executableSchema, type); + } + + @Override + public List getValues() { + List values = getExecutableSchema() + .getSchema() + .getEnumValues(getEnumTypeVertex()); + List result = new ArrayList<>(values.size()); + for (SUEnumValue value : values) { + result.add(new SUSchemaEnumValue(getExecutableSchema(), value)); + } + return Collections.unmodifiableList(result); + } + + @Override + public @Nullable SUSchemaEnumValue getValue(String name) { + SUEnumValue value = getExecutableSchema() + .getSchema() + .getEnumValue(getEnumTypeVertex(), name); + if (value == null) { + return null; + } + return new SUSchemaEnumValue(getExecutableSchema(), value); + } + + @Internal + public SUEnumType getEnumTypeVertex() { + return (SUEnumType) getTypeVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java b/src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java new file mode 100644 index 0000000000..3db388d831 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java @@ -0,0 +1,24 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaEnumValue; +import graphql.schema.universe.SUEnumValue; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaEnumValue + extends AbstractSUSchemaElement implements SchemaEnumValue { + + @Internal + public SUSchemaEnumValue( + SUSchemaExecutableSchema executableSchema, + SUEnumValue value) { + super(executableSchema, value); + } + + @Override + public String getName() { + return assertNotNull(getVertex().getName()); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchema.java b/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchema.java new file mode 100644 index 0000000000..a501d4a645 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchema.java @@ -0,0 +1,456 @@ +package graphql.schema.universe.view; + +import graphql.ExperimentalApi; +import graphql.Internal; +import graphql.introspection.Introspection; +import graphql.schema.Coercing; +import graphql.schema.ExecutableSchema; +import graphql.schema.GraphQLNamedType; +import graphql.schema.GraphQLNonNull; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.SchemaComposite; +import graphql.schema.SchemaDirective; +import graphql.schema.SchemaField; +import graphql.schema.SchemaFieldsContainer; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaInputType; +import graphql.schema.SchemaObject; +import graphql.schema.SchemaOutputType; +import graphql.schema.SchemaScalar; +import graphql.schema.SchemaType; +import graphql.schema.universe.PersistentIntMap; +import graphql.schema.universe.SUCompositeType; +import graphql.schema.universe.SUDirective; +import graphql.schema.universe.SUEnumType; +import graphql.schema.universe.SUField; +import graphql.schema.universe.SUInputField; +import graphql.schema.universe.SUInputObjectType; +import graphql.schema.universe.SUInterfaceType; +import graphql.schema.universe.SUListType; +import graphql.schema.universe.SUNamedType; +import graphql.schema.universe.SUNonNullType; +import graphql.schema.universe.SUObjectType; +import graphql.schema.universe.SUScalarType; +import graphql.schema.universe.SUSchema; +import graphql.schema.universe.SUType; +import graphql.schema.universe.SUUnionType; +import graphql.schema.universe.SUVertex; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.Assert.assertTrue; + +/** + * An executable, schema-bound adapter over an {@link SUSchema}. + * + *

Scalar coercers are held by this view because universe vertices contain only reusable + * type-system topology. The adapter has the same supported lifetime as its underlying schema.

+ */ +@ExperimentalApi +public final class SUSchemaExecutableSchema implements ExecutableSchema { + + private final SUSchema schema; + private final PersistentIntMap> coercingByScalarId; + + @Internal + public SUSchemaExecutableSchema( + SUSchema schema, + PersistentIntMap> coercingByScalarId) { + this.schema = assertNotNull(schema); + this.coercingByScalarId = assertNotNull(coercingByScalarId); + } + + public static SUSchemaExecutableSchemaBuilder newExecutableSchema( + SUSchema schema) { + return new SUSchemaExecutableSchemaBuilder(schema); + } + + public static SUSchemaExecutableSchemaBuilder newExecutableSchema( + SUSchemaExecutableSchema executableSchema) { + return new SUSchemaExecutableSchemaBuilder(executableSchema); + } + + /** + * Creates an adapter and copies scalar coercers from the schema that supplied its topology. + * + * @param schema the imported universe schema + * @param graphQLSchema the matching source schema + * + * @return the executable universe schema + */ + public static SUSchemaExecutableSchema fromGraphQLSchema( + SUSchema schema, + GraphQLSchema graphQLSchema) { + return newExecutableSchema(schema) + .scalarCoercings(graphQLSchema) + .build(); + } + + public SUSchemaExecutableSchema transform( + Consumer builderConsumer) { + SUSchemaExecutableSchemaBuilder builder = + newExecutableSchema(this); + assertNotNull(builderConsumer).accept(builder); + return builder.build(); + } + + public SUSchema getSchema() { + return schema; + } + + @Override + public SchemaObject getQueryType() { + return new SUSchemaObject(this, schema.getQueryType()); + } + + @Override + public @Nullable SchemaObject getMutationType() { + SUObjectType type = schema.getMutationType(); + if (type == null) { + return null; + } + return new SUSchemaObject(this, type); + } + + @Override + public @Nullable SchemaObject getSubscriptionType() { + SUObjectType type = schema.getSubscriptionType(); + if (type == null) { + return null; + } + return new SUSchemaObject(this, type); + } + + @Override + public @Nullable SchemaType getType(String name) { + SUNamedType type = schema.getType(assertNotNull(name)); + if (type == null) { + return null; + } + return adaptType(type); + } + + @Override + public @Nullable SchemaDirective getDirective(String name) { + SUDirective directive = schema.getDirectiveDefinition( + assertNotNull(name)); + if (directive == null) { + return null; + } + return new SUSchemaDirective(this, directive); + } + + @Override + public SchemaObject getIntrospectionSchemaType() { + return new SUSchemaObject( + this, + schema.getIntrospectionSchemaType()); + } + + @Override + public @Nullable SchemaField getField( + SchemaComposite parentType, + String fieldName) { + SUVertex parentVertex = elementVertex(parentType); + String name = assertNotNull(fieldName); + SchemaField introspectionField = parentVertex == schema.getQueryType() + ? getQueryIntrospectionField(name) + : null; + if (introspectionField != null) { + return introspectionField; + } + if (name.equals(Introspection.TypeNameMetaFieldDef.getName())) { + return new SUSchemaIntrospectionField( + this, + Introspection.TypeNameMetaFieldDef); + } + SUField field = getFieldVertex(parentVertex, name); + if (field == null) { + return null; + } + return new SUSchemaField(this, field); + } + + private @Nullable SchemaField getQueryIntrospectionField(String name) { + if (name.equals(Introspection.SchemaMetaFieldDef.getName())) { + return new SUSchemaIntrospectionField( + this, + Introspection.SchemaMetaFieldDef, + new SUSchemaSyntheticNonNull( + getIntrospectionSchemaType())); + } + if (name.equals(Introspection.TypeMetaFieldDef.getName())) { + return new SUSchemaIntrospectionField( + this, + Introspection.TypeMetaFieldDef, + introspectionType()); + } + return null; + } + + private SchemaOutputType introspectionType() { + SUObjectType schemaType = schema.getIntrospectionSchemaType(); + SUField typesField = assertNotNull( + schema.getField(schemaType, "types"), + "The introspection schema type must declare a 'types' field"); + SUType type = assertNotNull(schema.getType(typesField)); + return adaptOutputType(unwrap(type)); + } + + private SUType unwrap(SUType type) { + SUType current = type; + while (current instanceof SUListType + || current instanceof SUNonNullType) { + current = wrappedType(current); + } + return current; + } + + private SUType wrappedType(SUType type) { + if (type instanceof SUListType) { + return assertNotNull( + schema.getWrappedType((SUListType) type)); + } + return assertNotNull( + schema.getWrappedType((SUNonNullType) type)); + } + + private @Nullable SUField getFieldVertex( + SUVertex parentVertex, + String fieldName) { + if (parentVertex instanceof SUObjectType) { + return schema.getField( + (SUObjectType) parentVertex, + fieldName); + } + if (parentVertex instanceof SUInterfaceType) { + return schema.getField( + (SUInterfaceType) parentVertex, + fieldName); + } + return null; + } + + @Override + public List getFields( + SchemaFieldsContainer parentType) { + SUVertex parentVertex = elementVertex(parentType); + List fields = getFieldVertices(parentVertex); + List result = new ArrayList<>(fields.size()); + for (SUField field : fields) { + result.add(new SUSchemaField(this, field)); + } + return Collections.unmodifiableList(result); + } + + private List getFieldVertices(SUVertex parentVertex) { + if (parentVertex instanceof SUObjectType) { + return schema.getFields((SUObjectType) parentVertex); + } + assertTrue( + parentVertex instanceof SUInterfaceType, + "The field container must belong to this SUSchema"); + return schema.getFields((SUInterfaceType) parentVertex); + } + + @Override + public @Nullable SchemaInputField getInputField( + SchemaInputObject parentType, + String fieldName) { + SUInputObjectType type = inputObjectVertex(parentType); + SUInputField field = schema.getInputField( + type, + assertNotNull(fieldName)); + if (field == null) { + return null; + } + return new SUSchemaInputField(this, field); + } + + @Override + public List getInputFields( + SchemaInputObject parentType) { + List fields = schema.getInputFields( + inputObjectVertex(parentType)); + List result = + new ArrayList<>(fields.size()); + for (SUInputField field : fields) { + result.add(new SUSchemaInputField(this, field)); + } + return Collections.unmodifiableList(result); + } + + @Override + public List getPossibleTypes( + SchemaComposite compositeType) { + SUCompositeType type = compositeVertex(compositeType); + List possibleTypes = schema.getPossibleTypes(type); + List result = + new ArrayList<>(possibleTypes.size()); + for (SUObjectType possibleType : possibleTypes) { + result.add(new SUSchemaObject(this, possibleType)); + } + return Collections.unmodifiableList(result); + } + + @Override + public boolean isPossibleType( + SchemaComposite compositeType, + SchemaObject objectType) { + return schema.isPossibleType( + compositeVertex(compositeType), + objectVertex(objectType)); + } + + @Override + public Coercing getScalarCoercing(SchemaScalar scalarType) { + SUVertex vertex = elementVertex(scalarType); + assertTrue( + vertex instanceof SUScalarType, + "The scalar type must belong to this SUSchema"); + return assertNotNull( + coercingByScalarId.get(vertex.getId()), + "No coercing is registered for scalar '%s'", + assertNotNull(vertex.getName())); + } + + @Internal + public SchemaType adaptType(SUType type) { + if (type instanceof SUObjectType) { + return new SUSchemaObject(this, (SUObjectType) type); + } + if (type instanceof SUInterfaceType) { + return new SUSchemaInterface(this, (SUInterfaceType) type); + } + if (type instanceof SUUnionType) { + return new SUSchemaUnion(this, (SUUnionType) type); + } + if (type instanceof SUScalarType) { + return new SUSchemaScalar(this, (SUScalarType) type); + } + if (type instanceof SUEnumType) { + return new SUSchemaEnum(this, (SUEnumType) type); + } + if (type instanceof SUInputObjectType) { + return new SUSchemaInputObject( + this, + (SUInputObjectType) type); + } + if (type instanceof SUListType) { + return new SUSchemaList(this, (SUListType) type); + } + if (type instanceof SUNonNullType) { + return new SUSchemaNonNull(this, (SUNonNullType) type); + } + return assertShouldNeverHappen( + "Unsupported schema universe type %s", + type.getClass().getName()); + } + + @Internal + public SchemaInputType adaptInputType(SUType type) { + SchemaType adapted = adaptType(type); + assertTrue( + adapted instanceof SchemaInputType, + "Expected an input type but found %s", + type); + return (SchemaInputType) adapted; + } + + @Internal + public SchemaOutputType adaptOutputType(SUType type) { + SchemaType adapted = adaptType(type); + assertTrue( + adapted instanceof SchemaOutputType, + "Expected an output type but found %s", + type); + return (SchemaOutputType) adapted; + } + + @Internal + public SchemaInputType adaptGraphQLInputType(GraphQLType type) { + SchemaType adapted = adaptGraphQLType(type); + assertTrue( + adapted instanceof SchemaInputType, + "Expected an input type but found %s", + type); + return (SchemaInputType) adapted; + } + + @Internal + public SchemaOutputType adaptGraphQLOutputType(GraphQLType type) { + SchemaType adapted = adaptGraphQLType(type); + assertTrue( + adapted instanceof SchemaOutputType, + "Expected an output type but found %s", + type); + return (SchemaOutputType) adapted; + } + + private SchemaType adaptGraphQLType(GraphQLType type) { + if (type instanceof GraphQLNonNull) { + return new SUSchemaSyntheticNonNull( + adaptGraphQLType( + ((GraphQLNonNull) type).getWrappedType())); + } + assertTrue( + type instanceof GraphQLNamedType, + "Expected a named GraphQL type"); + String name = ((GraphQLNamedType) type).getName(); + return assertNotNull( + getType(name), + "No type named '%s' exists in this SUSchema", + name); + } + + private SUVertex elementVertex(Object element) { + assertTrue( + element instanceof AbstractSUSchemaElement, + "The schema element must belong to a SUSchema"); + AbstractSUSchemaElement adapter = + (AbstractSUSchemaElement) element; + assertTrue( + adapter.getExecutableSchema().getSchema() == schema, + "The schema element must belong to this SUSchema"); + return adapter.getVertex(); + } + + private SUCompositeType compositeVertex( + SchemaComposite compositeType) { + SUVertex vertex = elementVertex(compositeType); + assertTrue( + vertex instanceof SUCompositeType, + "Expected a composite type"); + return (SUCompositeType) vertex; + } + + private SUObjectType objectVertex(SchemaObject objectType) { + SUVertex vertex = elementVertex(objectType); + assertTrue( + vertex instanceof SUObjectType, + "Expected an object type"); + return (SUObjectType) vertex; + } + + private SUInputObjectType inputObjectVertex( + SchemaInputObject inputObjectType) { + SUVertex vertex = elementVertex(inputObjectType); + assertTrue( + vertex instanceof SUInputObjectType, + "Expected an input object type"); + return (SUInputObjectType) vertex; + } + + @Internal + public PersistentIntMap> getCoercingByScalarId() { + return coercingByScalarId; + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchemaBuilder.java b/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchemaBuilder.java new file mode 100644 index 0000000000..827687b200 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchemaBuilder.java @@ -0,0 +1,104 @@ +package graphql.schema.universe.view; + +import graphql.ExperimentalApi; +import graphql.schema.Coercing; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; +import graphql.schema.GraphQLType; +import graphql.schema.universe.PersistentIntMap; +import graphql.schema.universe.SUNamedType; +import graphql.schema.universe.SUScalarType; +import graphql.schema.universe.SUSchema; +import org.jspecify.annotations.NullUnmarked; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertTrue; + +/** + * Builds an executable view over an {@link SUSchema}. + */ +@ExperimentalApi +@NullUnmarked +public final class SUSchemaExecutableSchemaBuilder { + + private final SUSchema schema; + private PersistentIntMap> coercingByScalarId; + + public SUSchemaExecutableSchemaBuilder(SUSchema schema) { + this.schema = assertNotNull(schema); + this.coercingByScalarId = PersistentIntMap.empty(); + } + + public SUSchemaExecutableSchemaBuilder( + SUSchemaExecutableSchema executableSchema) { + this.schema = assertNotNull(executableSchema).getSchema(); + this.coercingByScalarId = executableSchema.getCoercingByScalarId(); + } + + public SUSchemaExecutableSchemaBuilder scalarCoercing( + SUScalarType scalarType, + Coercing coercing) { + SUScalarType scalar = assertNotNull(scalarType); + String scalarName = assertNotNull(scalar.getName()); + assertTrue( + schema.getScalarType(scalarName) == scalar, + "The scalar type must belong to this SUSchema"); + coercingByScalarId = coercingByScalarId.put( + scalar.getId(), + assertNotNull(coercing)); + return this; + } + + public SUSchemaExecutableSchemaBuilder scalarCoercing( + String scalarName, + Coercing coercing) { + SUScalarType scalarType = schema.getScalarType( + assertNotNull(scalarName)); + assertTrue( + scalarType != null, + "No scalar type named '%s' exists in this SUSchema", + scalarName); + return scalarCoercing( + assertNotNull(scalarType), + coercing); + } + + /** + * Copies scalar coercers from a matching {@link GraphQLSchema} by scalar name. + * + * @param graphQLSchema the source executable schema + * + * @return this builder + */ + public SUSchemaExecutableSchemaBuilder scalarCoercings( + GraphQLSchema graphQLSchema) { + GraphQLSchema source = assertNotNull(graphQLSchema); + for (SUNamedType type : schema.getTypes()) { + copyScalarCoercing(source, type); + } + return this; + } + + private void copyScalarCoercing( + GraphQLSchema source, + SUNamedType type) { + if (!(type instanceof SUScalarType)) { + return; + } + GraphQLType sourceType = source.getType( + assertNotNull(type.getName())); + assertTrue( + sourceType instanceof GraphQLScalarType, + "The GraphQLSchema has no matching scalar named '%s'", + type.getName()); + scalarCoercing( + (SUScalarType) type, + ((GraphQLScalarType) sourceType).getCoercing()); + } + + public SUSchemaExecutableSchema build() { + return new SUSchemaExecutableSchema( + schema, + coercingByScalarId); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaField.java b/src/main/java/graphql/schema/universe/view/SUSchemaField.java new file mode 100644 index 0000000000..fdf3061138 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaField.java @@ -0,0 +1,71 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaField; +import graphql.schema.SchemaOutputType; +import graphql.schema.universe.SUArgument; +import graphql.schema.universe.SUField; +import graphql.schema.universe.SUType; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaField + extends AbstractSUSchemaElement implements SchemaField { + + @Internal + public SUSchemaField( + SUSchemaExecutableSchema executableSchema, + SUField field) { + super(executableSchema, field); + } + + @Override + public String getName() { + return assertNotNull(getVertex().getName()); + } + + @Override + public SchemaOutputType getType() { + SUType type = assertNotNull( + getExecutableSchema() + .getSchema() + .getType(getFieldVertex())); + return getExecutableSchema().adaptOutputType(type); + } + + @Override + public List getArguments() { + List arguments = getExecutableSchema() + .getSchema() + .getArguments(getFieldVertex()); + List result = + new ArrayList<>(arguments.size()); + for (SUArgument argument : arguments) { + result.add(new SUSchemaArgument(getExecutableSchema(), argument)); + } + return Collections.unmodifiableList(result); + } + + @Override + public @Nullable SchemaArgument getArgument(String name) { + SUArgument argument = getExecutableSchema() + .getSchema() + .getArgument(getFieldVertex(), name); + if (argument == null) { + return null; + } + return new SUSchemaArgument(getExecutableSchema(), argument); + } + + @Internal + public SUField getFieldVertex() { + return (SUField) getVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInputField.java b/src/main/java/graphql/schema/universe/view/SUSchemaInputField.java new file mode 100644 index 0000000000..1a358c9c27 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInputField.java @@ -0,0 +1,46 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.InputValueWithState; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputType; +import graphql.schema.universe.SUInputField; +import graphql.schema.universe.SUType; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaInputField + extends AbstractSUSchemaElement implements SchemaInputField { + + @Internal + public SUSchemaInputField( + SUSchemaExecutableSchema executableSchema, + SUInputField field) { + super(executableSchema, field); + } + + @Override + public String getName() { + return assertNotNull(getVertex().getName()); + } + + @Override + public SchemaInputType getType() { + SUType type = assertNotNull( + getExecutableSchema() + .getSchema() + .getType(getInputFieldVertex())); + return getExecutableSchema().adaptInputType(type); + } + + @Override + public InputValueWithState getInputFieldDefaultValue() { + return getInputFieldVertex().getInputFieldDefaultValue(); + } + + @Internal + public SUInputField getInputFieldVertex() { + return (SUInputField) getVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java b/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java new file mode 100644 index 0000000000..4d2139b9d9 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java @@ -0,0 +1,33 @@ +package graphql.schema.universe.view; + +import graphql.Directives; +import graphql.Internal; +import graphql.schema.SchemaInputObject; +import graphql.schema.universe.SUInputObjectType; + +@Internal +public final class SUSchemaInputObject + extends AbstractSUSchemaNamedType implements SchemaInputObject { + + @Internal + public SUSchemaInputObject( + SUSchemaExecutableSchema executableSchema, + SUInputObjectType type) { + super(executableSchema, type); + } + + @Override + public boolean isOneOf() { + return !getExecutableSchema() + .getSchema() + .getAppliedDirectives( + getInputObjectTypeVertex(), + Directives.OneOfDirective.getName()) + .isEmpty(); + } + + @Internal + public SUInputObjectType getInputObjectTypeVertex() { + return (SUInputObjectType) getTypeVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java new file mode 100644 index 0000000000..f6f074a9aa --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java @@ -0,0 +1,17 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaInterface; +import graphql.schema.universe.SUInterfaceType; + +@Internal +public final class SUSchemaInterface + extends AbstractSUSchemaNamedType implements SchemaInterface { + + @Internal + public SUSchemaInterface( + SUSchemaExecutableSchema executableSchema, + SUInterfaceType type) { + super(executableSchema, type); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java new file mode 100644 index 0000000000..da84832a5c --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java @@ -0,0 +1,39 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.GraphQLArgument; +import graphql.schema.InputValueWithState; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaInputType; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaIntrospectionArgument implements SchemaArgument { + + private final SUSchemaExecutableSchema executableSchema; + private final GraphQLArgument argument; + + @Internal + public SUSchemaIntrospectionArgument( + SUSchemaExecutableSchema executableSchema, + GraphQLArgument argument) { + this.executableSchema = assertNotNull(executableSchema); + this.argument = assertNotNull(argument); + } + + @Override + public String getName() { + return argument.getName(); + } + + @Override + public SchemaInputType getType() { + return executableSchema.adaptGraphQLInputType(argument.getType()); + } + + @Override + public InputValueWithState getArgumentDefaultValue() { + return argument.getArgumentDefaultValue(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java new file mode 100644 index 0000000000..3d2f68a956 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java @@ -0,0 +1,76 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaField; +import graphql.schema.SchemaOutputType; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaIntrospectionField implements SchemaField { + + private final SUSchemaExecutableSchema executableSchema; + private final GraphQLFieldDefinition field; + private final @Nullable SchemaOutputType type; + + @Internal + public SUSchemaIntrospectionField( + SUSchemaExecutableSchema executableSchema, + GraphQLFieldDefinition field) { + this(executableSchema, field, null); + } + + @Internal + public SUSchemaIntrospectionField( + SUSchemaExecutableSchema executableSchema, + GraphQLFieldDefinition field, + @Nullable SchemaOutputType type) { + this.executableSchema = assertNotNull(executableSchema); + this.field = assertNotNull(field); + this.type = type; + } + + @Override + public String getName() { + return field.getName(); + } + + @Override + public SchemaOutputType getType() { + if (type != null) { + return type; + } + return executableSchema.adaptGraphQLOutputType(field.getType()); + } + + @Override + public List getArguments() { + List result = + new ArrayList<>(field.getArguments().size()); + for (GraphQLArgument argument : field.getArguments()) { + result.add(new SUSchemaIntrospectionArgument( + executableSchema, + argument)); + } + return Collections.unmodifiableList(result); + } + + @Override + public @Nullable SchemaArgument getArgument(String name) { + GraphQLArgument argument = field.getArgument(name); + if (argument == null) { + return null; + } + return new SUSchemaIntrospectionArgument( + executableSchema, + argument); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaList.java b/src/main/java/graphql/schema/universe/view/SUSchemaList.java new file mode 100644 index 0000000000..280baeb38d --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaList.java @@ -0,0 +1,35 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaList; +import graphql.schema.SchemaType; +import graphql.schema.universe.SUListType; +import graphql.schema.universe.SUType; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaList + extends AbstractSUSchemaType implements SchemaList { + + @Internal + public SUSchemaList( + SUSchemaExecutableSchema executableSchema, + SUListType type) { + super(executableSchema, type); + } + + @Override + public SchemaType getWrappedType() { + SUType wrappedType = assertNotNull( + getExecutableSchema() + .getSchema() + .getWrappedType(getListTypeVertex())); + return getExecutableSchema().adaptType(wrappedType); + } + + @Internal + public SUListType getListTypeVertex() { + return (SUListType) getTypeVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java b/src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java new file mode 100644 index 0000000000..37940f8118 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java @@ -0,0 +1,35 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaNonNull; +import graphql.schema.SchemaType; +import graphql.schema.universe.SUNonNullType; +import graphql.schema.universe.SUType; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaNonNull + extends AbstractSUSchemaType implements SchemaNonNull { + + @Internal + public SUSchemaNonNull( + SUSchemaExecutableSchema executableSchema, + SUNonNullType type) { + super(executableSchema, type); + } + + @Override + public SchemaType getWrappedType() { + SUType wrappedType = assertNotNull( + getExecutableSchema() + .getSchema() + .getWrappedType(getNonNullTypeVertex())); + return getExecutableSchema().adaptType(wrappedType); + } + + @Internal + public SUNonNullType getNonNullTypeVertex() { + return (SUNonNullType) getTypeVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaObject.java b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java new file mode 100644 index 0000000000..e59df9d5cd --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java @@ -0,0 +1,17 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaObject; +import graphql.schema.universe.SUObjectType; + +@Internal +public final class SUSchemaObject + extends AbstractSUSchemaNamedType implements SchemaObject { + + @Internal + public SUSchemaObject( + SUSchemaExecutableSchema executableSchema, + SUObjectType type) { + super(executableSchema, type); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java b/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java new file mode 100644 index 0000000000..a0751ee0ef --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java @@ -0,0 +1,17 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaScalar; +import graphql.schema.universe.SUScalarType; + +@Internal +public final class SUSchemaScalar + extends AbstractSUSchemaNamedType implements SchemaScalar { + + @Internal + public SUSchemaScalar( + SUSchemaExecutableSchema executableSchema, + SUScalarType type) { + super(executableSchema, type); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java b/src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java new file mode 100644 index 0000000000..e71eb8fec3 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java @@ -0,0 +1,24 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaNonNull; +import graphql.schema.SchemaType; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaSyntheticNonNull implements SchemaNonNull { + + private final SchemaType wrappedType; + + @Internal + public SUSchemaSyntheticNonNull(SchemaType wrappedType) { + this.wrappedType = assertNotNull(wrappedType); + } + + @Override + public SchemaType getWrappedType() { + return wrappedType; + } + +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java b/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java new file mode 100644 index 0000000000..eb86ced2c3 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java @@ -0,0 +1,17 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaUnion; +import graphql.schema.universe.SUUnionType; + +@Internal +public final class SUSchemaUnion + extends AbstractSUSchemaNamedType implements SchemaUnion { + + @Internal + public SUSchemaUnion( + SUSchemaExecutableSchema executableSchema, + SUUnionType type) { + super(executableSchema, type); + } +} diff --git a/src/main/java/graphql/schema/universe/view/package-info.java b/src/main/java/graphql/schema/universe/view/package-info.java new file mode 100644 index 0000000000..89a55f3b7b --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package graphql.schema.universe.view; + +import org.jspecify.annotations.NullMarked; diff --git a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy new file mode 100644 index 0000000000..5656d68369 --- /dev/null +++ b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy @@ -0,0 +1,337 @@ +package graphql.schema + +import graphql.AssertException +import graphql.TestUtil +import graphql.introspection.Introspection +import graphql.introspection.IntrospectionWithDirectivesSupport +import graphql.schema.universe.SchemaUniverse +import graphql.schema.universe.view.SUSchemaExecutableSchema +import graphql.schema.visibility.BlockedFields +import spock.lang.Specification + +class ExecutableSchemaTest extends Specification { + + def "GraphQLSchema and SUSchema expose the same executable type hierarchy"() { + given: + ExecutableSchema schema = executableSchema + + expect: + schema.queryType instanceof SchemaObject + schema.mutationType.name == "Mutation" + schema.subscriptionType.name == "Subscription" + schema.getType("Node") instanceof SchemaInterface + schema.getType("Search") instanceof SchemaUnion + schema.getType("Date") instanceof SchemaScalar + schema.getType("Color") instanceof SchemaEnum + schema.getType("Choice") instanceof SchemaInputObject + schema.getType("missing") == null + + and: + def node = schema.getType("Node") as SchemaFieldsContainer + schema.getFields(node)*.name == ["id"] + schema.getField(node, "id").type instanceof SchemaNonNull + schema.getField( + schema.getType("Search") as SchemaComposite, + "missing") == null + + and: + def colors = schema.getField(schema.queryType, "colors") + colors.name == "colors" + colors.type instanceof SchemaNonNull + colors.type.wrappedType instanceof SchemaList + colors.type.wrappedType.wrappedType instanceof SchemaNonNull + colors.type.wrappedType.wrappedType.wrappedType.name == "Color" + colors.arguments*.name == ["choice"] + colors.getArgument("choice").type.name == "Choice" + colors.getArgument("missing") == null + + and: + def choice = schema.getType("Choice") as SchemaInputObject + choice.oneOf + schema.getInputFields(choice)*.name == ["color", "date"] + schema.getInputField(choice, "color").type.name == "Color" + schema.getInputField(choice, "color") + .inputFieldDefaultValue.isNotSet() + schema.getInputField(choice, "missing") == null + !(schema.getType("Regular") as SchemaInputObject).oneOf + + and: + def color = schema.getType("Color") as SchemaEnum + color.values*.name == ["RED", "GREEN"] + color.getValue("GREEN").name == "GREEN" + color.getValue("BLUE") == null + + and: + def tag = schema.getDirective("tag") + tag.repeatable + tag.validLocations().contains(Introspection.DirectiveLocation.FIELD) + tag.validLocations().contains(Introspection.DirectiveLocation.QUERY) + tag.arguments*.name == ["value"] + tag.getArgument("value").argumentDefaultValue.isLiteral() + tag.getArgument("missing") == null + schema.getDirective("missing") == null + + and: + schema.getScalarCoercing( + schema.getType("Date") as SchemaScalar) != null + + where: + executableSchema << executableSchemas() + } + + def "GraphQLSchema and SUSchema expose possible types through the shared contract"() { + given: + ExecutableSchema schema = executableSchema + def node = schema.getType("Node") as SchemaComposite + def search = schema.getType("Search") as SchemaComposite + def user = schema.getType("User") as SchemaObject + def query = schema.queryType + + expect: + schema.getPossibleTypes(user) == [user] + schema.getPossibleTypes(node)*.name == ["User"] + schema.getPossibleTypes(search)*.name == ["User"] + schema.isPossibleType(user, user) + schema.isPossibleType(node, user) + schema.isPossibleType(search, user) + !schema.isPossibleType(node, query) + !schema.isPossibleType(search, query) + + where: + executableSchema << executableSchemas() + } + + def "GraphQLSchema executable lookups apply output and input field visibility"() { + given: + def source = TestUtil.schema(''' + input Filter { + visible: String + hidden: String + } + + type Query { + visible(filter: Filter): String + hidden: String + } + ''') + def visibility = BlockedFields.newBlock() + .addPatterns(["Query.hidden", "Filter.hidden"]) + .build() + def codeRegistry = source.codeRegistry.transform { + it.fieldVisibility(visibility) + } + ExecutableSchema schema = source.transform { + it.codeRegistry(codeRegistry) + } + def filter = schema.getType("Filter") as SchemaInputObject + + expect: + schema.getFields(schema.queryType)*.name == ["visible"] + schema.getField(schema.queryType, "visible") != null + schema.getField(schema.queryType, "hidden") == null + schema.getInputFields(filter)*.name == ["visible"] + schema.getInputField(filter, "visible") != null + schema.getInputField(filter, "hidden") == null + + and: + schema.getField(schema.queryType, "__schema") != null + schema.getField(schema.queryType, "__type") != null + schema.getField(schema.queryType, "__typename") != null + schema.mutationType == null + schema.subscriptionType == null + } + + def "introspection meta fields use each executable schema's introspection graph"() { + given: + def source = new IntrospectionWithDirectivesSupport( + environment -> true, + "_custom_") + .apply(TestUtil.schema("type Query { value: String }")) + def universeSchema = new SchemaUniverse() + .importSchema("enhanced", source) + ExecutableSchema schema = SUSchemaExecutableSchema + .fromGraphQLSchema(universeSchema, source) + + expect: + unwrap(schema.getField(schema.queryType, "__schema").type) == + schema.introspectionSchemaType + unwrap(schema.getField(schema.queryType, "__type").type).name == + "__Type" + schema.getField(schema.queryType, "__type") + .getArgument("name").type instanceof SchemaNonNull + unwrap(schema.getField(schema.queryType, "__type") + .getArgument("name").type).name == "String" + + and: + schema.getField( + schema.introspectionSchemaType, + "appliedDirectives") != null + schema.getType("_custom_AppliedDirective") != null + } + + def "typename is available on every composite kind"() { + given: + ExecutableSchema schema = executableSchema + def typeField = schema.getField(schema.queryType, "__type") + def typeNameField = schema.getField( + schema.getType("Search") as SchemaComposite, + "__typename") + + expect: + ["Query", "Node", "Search"].every { + schema.getField( + schema.getType(it) as SchemaComposite, + "__typename")?.name == "__typename" + } + unwrap(schema.getField(schema.queryType, "__schema").type) == + schema.introspectionSchemaType + unwrap(schema.getField(schema.queryType, "__type").type).name == + "__Type" + typeField.arguments*.name == ["name"] + typeField.getArgument("name").name == "name" + typeField.getArgument("name").argumentDefaultValue.isNotSet() + typeField.getArgument("missing") == null + typeNameField.arguments.isEmpty() + typeNameField.type instanceof SchemaNonNull + unwrap(typeNameField.type).name == "String" + + where: + executableSchema << executableSchemas() + } + + def "SUSchema executable views keep scalar coercers outside shared vertices"() { + given: + def source = richSchema() + def universeSchema = new SchemaUniverse() + .importSchema("schema", source) + def original = SUSchemaExecutableSchema + .fromGraphQLSchema(universeSchema, source) + Coercing replacement = Mock() + + when: + def changed = original.transform { + it.scalarCoercing("Date", replacement) + } + + then: + original.getScalarCoercing( + original.getType("Date") as SchemaScalar) + .is(source.getType("Date").coercing) + changed.getScalarCoercing( + changed.getType("Date") as SchemaScalar) + .is(replacement) + + when: + def unbound = SUSchemaExecutableSchema + .newExecutableSchema(universeSchema) + .build() + unbound.getScalarCoercing( + unbound.getType("Date") as SchemaScalar) + + then: + thrown(AssertException) + } + + def "SUSchema executable views reject elements from another schema"() { + given: + def source = richSchema() + def firstUniverseSchema = new SchemaUniverse() + .importSchema("first", source) + def secondUniverseSchema = new SchemaUniverse() + .importSchema("second", source) + def first = SUSchemaExecutableSchema + .fromGraphQLSchema(firstUniverseSchema, source) + def second = SUSchemaExecutableSchema + .fromGraphQLSchema(secondUniverseSchema, source) + + when: + first.getPossibleTypes( + second.getType("Node") as SchemaComposite) + + then: + thrown(AssertException) + + when: + first.isPossibleType( + first.getType("Node") as SchemaComposite, + second.getType("User") as SchemaObject) + + then: + thrown(AssertException) + + when: + SUSchemaExecutableSchema.newExecutableSchema(firstUniverseSchema) + .scalarCoercing( + secondUniverseSchema.getScalarType("Date"), + source.getType("Date").coercing) + + then: + thrown(AssertException) + } + + private static List executableSchemas() { + def graphQLSchema = richSchema() + def universeSchema = new SchemaUniverse() + .importSchema("schema", graphQLSchema) + def suSchema = SUSchemaExecutableSchema + .fromGraphQLSchema(universeSchema, graphQLSchema) + return [graphQLSchema, suSchema] + } + + private static GraphQLSchema richSchema() { + TestUtil.schema(''' + directive @tag(value: String = "fallback") repeatable on FIELD | QUERY + + scalar Date + + enum Color { + RED + GREEN + } + + input Choice @oneOf { + color: Color + date: Date + } + + input Regular { + value: String + } + + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String + } + + union Search = User + + type Query { + node(id: ID!): Node + search: Search + colors(choice: Choice): [Color!]! + regular(input: Regular): String + today: Date + } + + type Mutation { + update: User + } + + type Subscription { + events: User + } + ''') + } + + private static SchemaType unwrap(SchemaType type) { + SchemaType current = type + while (current instanceof SchemaModifiedType) { + current = (current as SchemaModifiedType).wrappedType + } + return current + } +} diff --git a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy index e04e8e2a6f..6151f0c680 100644 --- a/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLSchemaTest.groovy @@ -49,11 +49,10 @@ class GraphQLSchemaTest extends Specification { starWarsSchema.isPossibleType(characterInterface, humanType) } - def "isPossibleType when wrong abstract type is passed expect exception"() { - when: + def "isPossibleType supports object types"() { + expect: starWarsSchema.isPossibleType(humanType, humanType) - then: - thrown(AssertException) + !starWarsSchema.isPossibleType(humanType, droidType) } def "#698 interfaces copied as expected"() { From d0d22b51e1432a75c49ee3095e4173b2c86cff54 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 6 Aug 2026 00:37:51 +0000 Subject: [PATCH 16/32] Use executable schema views for schema printing --- .../java/graphql/schema/ExecutableSchema.java | 53 ++ .../schema/GraphQLAppliedDirective.java | 3 +- .../GraphQLAppliedDirectiveArgument.java | 4 +- .../schema/GraphQLDirectiveContainer.java | 3 +- .../schema/GraphQLNamedSchemaElement.java | 3 +- .../java/graphql/schema/GraphQLSchema.java | 122 +++ .../schema/SchemaAppliedDirective.java | 31 + .../SchemaAppliedDirectiveArgument.java | 26 + .../java/graphql/schema/SchemaArgument.java | 4 +- .../java/graphql/schema/SchemaDirective.java | 4 +- .../schema/SchemaDirectiveContainer.java | 12 + .../java/graphql/schema/SchemaEnumValue.java | 4 +- src/main/java/graphql/schema/SchemaField.java | 4 +- .../java/graphql/schema/SchemaInputField.java | 4 +- .../graphql/schema/SchemaNamedElement.java | 35 + .../java/graphql/schema/SchemaNamedType.java | 5 +- .../java/graphql/schema/SchemaScalar.java | 8 + .../schema/idl/GraphQLSchemaPrintAccess.java | 631 -------------- .../schema/idl/SUSchemaPrintAccess.java | 457 ---------- .../graphql/schema/idl/SchemaPrintAccess.java | 78 -- .../schema/idl/SchemaPrintChildKind.java | 18 - .../schema/idl/SchemaPrintElementKind.java | 25 - .../graphql/schema/idl/SchemaPrinter.java | 22 +- .../schema/idl/SchemaPrinterWriter.java | 789 +++++++++++++----- .../schema/universe/SUSchemaPrinter.java | 31 - .../view/AbstractSUSchemaElement.java | 42 +- .../view/AbstractSUSchemaNamedType.java | 2 +- .../universe/view/AbstractSUSchemaType.java | 2 +- ...bleSchema.java => SUExecutableSchema.java} | 158 +++- ...er.java => SUExecutableSchemaBuilder.java} | 18 +- .../view/SUSchemaAppliedDirective.java | 62 ++ .../SUSchemaAppliedDirectiveArgument.java | 54 ++ .../universe/view/SUSchemaArgument.java | 2 +- .../universe/view/SUSchemaDirective.java | 2 +- .../schema/universe/view/SUSchemaEnum.java | 2 +- .../universe/view/SUSchemaEnumValue.java | 2 +- .../schema/universe/view/SUSchemaField.java | 2 +- .../universe/view/SUSchemaInputField.java | 2 +- .../universe/view/SUSchemaInputObject.java | 2 +- .../universe/view/SUSchemaInterface.java | 2 +- .../view/SUSchemaIntrospectionArgument.java | 21 +- .../view/SUSchemaIntrospectionField.java | 22 +- .../schema/universe/view/SUSchemaList.java | 2 +- .../schema/universe/view/SUSchemaNonNull.java | 2 +- .../schema/universe/view/SUSchemaObject.java | 2 +- .../schema/universe/view/SUSchemaScalar.java | 25 +- .../schema/universe/view/SUSchemaUnion.java | 2 +- .../schema/ExecutableSchemaTest.groovy | 126 ++- ...y => SUExecutableSchemaPrinterTest.groovy} | 70 +- .../universe/SUSchemaRoundTripTest.groovy | 6 +- 50 files changed, 1475 insertions(+), 1533 deletions(-) create mode 100644 src/main/java/graphql/schema/SchemaAppliedDirective.java create mode 100644 src/main/java/graphql/schema/SchemaAppliedDirectiveArgument.java create mode 100644 src/main/java/graphql/schema/SchemaDirectiveContainer.java create mode 100644 src/main/java/graphql/schema/SchemaNamedElement.java delete mode 100644 src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java delete mode 100644 src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java delete mode 100644 src/main/java/graphql/schema/idl/SchemaPrintAccess.java delete mode 100644 src/main/java/graphql/schema/idl/SchemaPrintChildKind.java delete mode 100644 src/main/java/graphql/schema/idl/SchemaPrintElementKind.java delete mode 100644 src/main/java/graphql/schema/universe/SUSchemaPrinter.java rename src/main/java/graphql/schema/universe/view/{SUSchemaExecutableSchema.java => SUExecutableSchema.java} (72%) rename src/main/java/graphql/schema/universe/view/{SUSchemaExecutableSchemaBuilder.java => SUExecutableSchemaBuilder.java} (86%) create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirective.java create mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirectiveArgument.java rename src/test/groovy/graphql/schema/universe/{SUSchemaPrinterTest.groovy => SUExecutableSchemaPrinterTest.groovy} (62%) diff --git a/src/main/java/graphql/schema/ExecutableSchema.java b/src/main/java/graphql/schema/ExecutableSchema.java index b06e90537d..e8dd705a37 100644 --- a/src/main/java/graphql/schema/ExecutableSchema.java +++ b/src/main/java/graphql/schema/ExecutableSchema.java @@ -1,6 +1,7 @@ package graphql.schema; import graphql.ExperimentalApi; +import graphql.language.SchemaDefinition; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -16,6 +17,16 @@ @NullMarked public interface ExecutableSchema { + /** + * @return the schema description, or {@code null} when absent + */ + @Nullable String getDescription(); + + /** + * @return the source schema definition, or {@code null} when unavailable + */ + @Nullable SchemaDefinition getDefinition(); + /** * @return the query root */ @@ -45,6 +56,33 @@ public interface ExecutableSchema { */ @Nullable SchemaDirective getDirective(String name); + /** + * @return all named types in this schema + */ + List getTypes(); + + /** + * @return all directive definitions in this schema + */ + List getDirectives(); + + /** + * Returns the normalized directives applied to the schema itself. + * + * @return applied schema directives + */ + List getAppliedDirectives(); + + /** + * Returns the normalized directives applied to an element. + * + * @param container the directive container + * + * @return applied directives in declaration order + */ + List getAppliedDirectives( + SchemaDirectiveContainer container); + /** * @return this schema's introspection schema type */ @@ -84,6 +122,21 @@ public interface ExecutableSchema { */ List getInputFields(SchemaInputObject parentType); + /** + * @param implementingType an object or interface + * + * @return the interfaces directly implemented by the type + */ + List getInterfaces( + SchemaFieldsContainer implementingType); + + /** + * @param unionType the union + * + * @return the object types declared by the union + */ + List getUnionMembers(SchemaUnion unionType); + /** * @param compositeType an object, interface, or union * diff --git a/src/main/java/graphql/schema/GraphQLAppliedDirective.java b/src/main/java/graphql/schema/GraphQLAppliedDirective.java index 35ac66a77e..2c53ebf98d 100644 --- a/src/main/java/graphql/schema/GraphQLAppliedDirective.java +++ b/src/main/java/graphql/schema/GraphQLAppliedDirective.java @@ -35,7 +35,8 @@ */ @PublicApi @NullMarked -public class GraphQLAppliedDirective implements GraphQLNamedSchemaElement { +public class GraphQLAppliedDirective + implements GraphQLNamedSchemaElement, SchemaAppliedDirective { private final String name; private final ImmutableList arguments; diff --git a/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java b/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java index 4446894ec9..7b6eefd74b 100644 --- a/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java +++ b/src/main/java/graphql/schema/GraphQLAppliedDirectiveArgument.java @@ -26,7 +26,9 @@ * You can think of them as 'instances' of {@link GraphQLArgument}, when applied to a directive on a schema element */ @PublicApi -public class GraphQLAppliedDirectiveArgument implements GraphQLNamedSchemaElement, GraphQLInputSchemaElement { +public class GraphQLAppliedDirectiveArgument + implements GraphQLNamedSchemaElement, GraphQLInputSchemaElement, + SchemaAppliedDirectiveArgument { private final String name; private final InputValueWithState value; diff --git a/src/main/java/graphql/schema/GraphQLDirectiveContainer.java b/src/main/java/graphql/schema/GraphQLDirectiveContainer.java index 4bee55aa36..c42f35bce5 100644 --- a/src/main/java/graphql/schema/GraphQLDirectiveContainer.java +++ b/src/main/java/graphql/schema/GraphQLDirectiveContainer.java @@ -23,7 +23,8 @@ * @see graphql.language.DirectiveDefinition#isRepeatable() */ @PublicApi -public interface GraphQLDirectiveContainer extends GraphQLNamedSchemaElement { +public interface GraphQLDirectiveContainer + extends GraphQLNamedSchemaElement, SchemaDirectiveContainer { String CHILD_DIRECTIVES = "directives"; String CHILD_APPLIED_DIRECTIVES = "appliedDirectives"; diff --git a/src/main/java/graphql/schema/GraphQLNamedSchemaElement.java b/src/main/java/graphql/schema/GraphQLNamedSchemaElement.java index 704bc99623..669de7a675 100644 --- a/src/main/java/graphql/schema/GraphQLNamedSchemaElement.java +++ b/src/main/java/graphql/schema/GraphQLNamedSchemaElement.java @@ -9,7 +9,8 @@ * A Schema element which has a name and also a description and AST Node which it is based on. */ @PublicApi -public interface GraphQLNamedSchemaElement extends GraphQLSchemaElement { +public interface GraphQLNamedSchemaElement + extends GraphQLSchemaElement, SchemaNamedElement { /** * @return the name of this element. This cant be null diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 0528e11959..409bb4b540 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -521,6 +521,39 @@ public List getInputFields( .getFieldDefinitions((GraphQLInputObjectType) parentType); } + @Override + public List getInterfaces( + SchemaFieldsContainer implementingType) { + assertTrue( + implementingType instanceof GraphQLImplementingType, + "The implementing type must belong to this GraphQLSchema"); + List result = new ArrayList<>(); + for (GraphQLNamedOutputType interfaceType : + ((GraphQLImplementingType) implementingType).getInterfaces()) { + assertTrue( + interfaceType instanceof GraphQLInterfaceType, + "Interface type references must be resolved"); + result.add((GraphQLInterfaceType) interfaceType); + } + return ImmutableList.copyOf(result); + } + + @Override + public List getUnionMembers(SchemaUnion unionType) { + assertTrue( + unionType instanceof GraphQLUnionType, + "The union type must belong to this GraphQLSchema"); + List result = new ArrayList<>(); + for (GraphQLNamedOutputType member : + ((GraphQLUnionType) unionType).getTypes()) { + assertTrue( + member instanceof GraphQLObjectType, + "Union type references must be resolved"); + result.add((GraphQLObjectType) member); + } + return ImmutableList.copyOf(result); + } + /** * @return all the named types in the scheme as a map from name to named type */ @@ -537,6 +570,11 @@ public List getAllTypesAsList() { return getAllTypesAsList(typeMap); } + @Override + public List getTypes() { + return getAllTypesAsList(); + } + /** * This returns all the top level {@link GraphQLNamedSchemaElement} named types and directives * in the schema @@ -684,6 +722,90 @@ public List getDirectives() { return directiveDefinitionsHolder.getDirectives(); } + @Override + @SuppressWarnings("deprecation") + public List getAppliedDirectives() { + return DirectivesUtil.toAppliedDirectives( + getSchemaAppliedDirectives(), + getSchemaDirectives()); + } + + @Override + public List getAppliedDirectives( + SchemaDirectiveContainer container) { + assertTrue( + container instanceof GraphQLDirectiveContainer, + "The directive container must belong to this GraphQLSchema"); + GraphQLDirectiveContainer graphQLContainer = + (GraphQLDirectiveContainer) container; + List directives = + DirectivesUtil.toAppliedDirectives(graphQLContainer); + String deprecationReason = getDeprecationReason(graphQLContainer); + if (deprecationReason == null) { + return directives; + } + return withDeprecationReason(directives, deprecationReason); + } + + private @Nullable String getDeprecationReason( + GraphQLDirectiveContainer container) { + if (container instanceof GraphQLFieldDefinition) { + return ((GraphQLFieldDefinition) container).getDeprecationReason(); + } + if (container instanceof GraphQLEnumValueDefinition) { + return ((GraphQLEnumValueDefinition) container).getDeprecationReason(); + } + if (container instanceof GraphQLInputObjectField) { + return ((GraphQLInputObjectField) container).getDeprecationReason(); + } + if (container instanceof GraphQLArgument) { + return ((GraphQLArgument) container).getDeprecationReason(); + } + return null; + } + + private List withDeprecationReason( + List directives, + String reason) { + List result = + new ArrayList<>(directives.size() + 1); + boolean found = false; + for (GraphQLAppliedDirective directive : directives) { + if (!Directives.DeprecatedDirective.getName().equals( + directive.getName())) { + result.add(directive); + continue; + } + found = true; + GraphQLAppliedDirectiveArgument existing = + directive.getArgument("reason"); + if (existing != null + && existing.getArgumentValue() + == InputValueWithState.NOT_SET) { + result.add(directive); + continue; + } + result.add(directive.transform(builder -> + builder.argument(deprecationReasonArgument(reason)))); + } + if (!found) { + result.add(GraphQLAppliedDirective.newDirective() + .name(Directives.DeprecatedDirective.getName()) + .argument(deprecationReasonArgument(reason)) + .build()); + } + return result; + } + + private GraphQLAppliedDirectiveArgument deprecationReasonArgument( + String reason) { + return GraphQLAppliedDirectiveArgument.newArgument() + .name("reason") + .valueProgrammatic(reason) + .type(Scalars.GraphQLString) + .build(); + } + /** * @return a map of non-repeatable directives by directive name */ diff --git a/src/main/java/graphql/schema/SchemaAppliedDirective.java b/src/main/java/graphql/schema/SchemaAppliedDirective.java new file mode 100644 index 0000000000..719b8fa10a --- /dev/null +++ b/src/main/java/graphql/schema/SchemaAppliedDirective.java @@ -0,0 +1,31 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.List; + +/** + * A directive applied to a schema element. + */ +@ExperimentalApi +@NullMarked +public interface SchemaAppliedDirective extends SchemaNamedElement { + + /** + * Returns the arguments supplied to this applied directive. + * + * @return the directive arguments in declaration order + */ + List getArguments(); + + /** + * Returns an argument supplied to this applied directive. + * + * @param name the argument name + * + * @return the named argument, or {@code null} when absent + */ + @Nullable SchemaAppliedDirectiveArgument getArgument(String name); +} diff --git a/src/main/java/graphql/schema/SchemaAppliedDirectiveArgument.java b/src/main/java/graphql/schema/SchemaAppliedDirectiveArgument.java new file mode 100644 index 0000000000..4adb1f0cdc --- /dev/null +++ b/src/main/java/graphql/schema/SchemaAppliedDirectiveArgument.java @@ -0,0 +1,26 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * An argument supplied to an applied directive. + */ +@ExperimentalApi +@NullMarked +public interface SchemaAppliedDirectiveArgument extends SchemaNamedElement { + + /** + * Returns the declared type of this argument. + * + * @return the argument type + */ + SchemaInputType getType(); + + /** + * Returns the value supplied to this argument. + * + * @return the supplied argument value + */ + InputValueWithState getArgumentValue(); +} diff --git a/src/main/java/graphql/schema/SchemaArgument.java b/src/main/java/graphql/schema/SchemaArgument.java index 0f8983f718..f7433fdfe1 100644 --- a/src/main/java/graphql/schema/SchemaArgument.java +++ b/src/main/java/graphql/schema/SchemaArgument.java @@ -8,9 +8,7 @@ */ @ExperimentalApi @NullMarked -public interface SchemaArgument { - - String getName(); +public interface SchemaArgument extends SchemaDirectiveContainer { SchemaInputType getType(); diff --git a/src/main/java/graphql/schema/SchemaDirective.java b/src/main/java/graphql/schema/SchemaDirective.java index 17b437fbcb..ba0520d57d 100644 --- a/src/main/java/graphql/schema/SchemaDirective.java +++ b/src/main/java/graphql/schema/SchemaDirective.java @@ -13,9 +13,7 @@ */ @ExperimentalApi @NullMarked -public interface SchemaDirective { - - String getName(); +public interface SchemaDirective extends SchemaDirectiveContainer { boolean isRepeatable(); diff --git a/src/main/java/graphql/schema/SchemaDirectiveContainer.java b/src/main/java/graphql/schema/SchemaDirectiveContainer.java new file mode 100644 index 0000000000..90f5f2c191 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaDirectiveContainer.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * A schema element on which directives can be applied. + */ +@ExperimentalApi +@NullMarked +public interface SchemaDirectiveContainer extends SchemaNamedElement { +} diff --git a/src/main/java/graphql/schema/SchemaEnumValue.java b/src/main/java/graphql/schema/SchemaEnumValue.java index 2c11623ba8..57a282b744 100644 --- a/src/main/java/graphql/schema/SchemaEnumValue.java +++ b/src/main/java/graphql/schema/SchemaEnumValue.java @@ -8,7 +8,5 @@ */ @ExperimentalApi @NullMarked -public interface SchemaEnumValue { - - String getName(); +public interface SchemaEnumValue extends SchemaDirectiveContainer { } diff --git a/src/main/java/graphql/schema/SchemaField.java b/src/main/java/graphql/schema/SchemaField.java index 78ca7fcd80..4d73b746d5 100644 --- a/src/main/java/graphql/schema/SchemaField.java +++ b/src/main/java/graphql/schema/SchemaField.java @@ -11,9 +11,7 @@ */ @ExperimentalApi @NullMarked -public interface SchemaField { - - String getName(); +public interface SchemaField extends SchemaDirectiveContainer { SchemaOutputType getType(); diff --git a/src/main/java/graphql/schema/SchemaInputField.java b/src/main/java/graphql/schema/SchemaInputField.java index a0c6b376f0..ff6003ebe5 100644 --- a/src/main/java/graphql/schema/SchemaInputField.java +++ b/src/main/java/graphql/schema/SchemaInputField.java @@ -8,9 +8,7 @@ */ @ExperimentalApi @NullMarked -public interface SchemaInputField { - - String getName(); +public interface SchemaInputField extends SchemaDirectiveContainer { SchemaInputType getType(); diff --git a/src/main/java/graphql/schema/SchemaNamedElement.java b/src/main/java/graphql/schema/SchemaNamedElement.java new file mode 100644 index 0000000000..d716c8c6ae --- /dev/null +++ b/src/main/java/graphql/schema/SchemaNamedElement.java @@ -0,0 +1,35 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import graphql.language.Node; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * A named element in a GraphQL schema. + */ +@ExperimentalApi +@NullMarked +public interface SchemaNamedElement { + + /** + * Returns the name of this schema element. + * + * @return the name of this schema element + */ + String getName(); + + /** + * Returns the description of this schema element. + * + * @return the description of this schema element, or {@code null} when absent + */ + @Nullable String getDescription(); + + /** + * Returns the source definition from which this schema element was created. + * + * @return the source definition of this schema element, or {@code null} when unavailable + */ + @Nullable Node getDefinition(); +} diff --git a/src/main/java/graphql/schema/SchemaNamedType.java b/src/main/java/graphql/schema/SchemaNamedType.java index 4f287587d1..b3fcd62cff 100644 --- a/src/main/java/graphql/schema/SchemaNamedType.java +++ b/src/main/java/graphql/schema/SchemaNamedType.java @@ -8,7 +8,6 @@ */ @ExperimentalApi @NullMarked -public interface SchemaNamedType extends SchemaType { - - String getName(); +public interface SchemaNamedType + extends SchemaType, SchemaDirectiveContainer { } diff --git a/src/main/java/graphql/schema/SchemaScalar.java b/src/main/java/graphql/schema/SchemaScalar.java index 28dbd96169..ffd61a6da6 100644 --- a/src/main/java/graphql/schema/SchemaScalar.java +++ b/src/main/java/graphql/schema/SchemaScalar.java @@ -2,6 +2,7 @@ import graphql.ExperimentalApi; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /** * A GraphQL scalar type. @@ -10,4 +11,11 @@ @NullMarked public interface SchemaScalar extends SchemaNamedType, SchemaInputType, SchemaOutputType { + + /** + * Returns the URL identifying the specification implemented by this scalar. + * + * @return the URL identifying the scalar specification, or {@code null} when absent + */ + @Nullable String getSpecifiedByUrl(); } diff --git a/src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java b/src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java deleted file mode 100644 index 7db136c930..0000000000 --- a/src/main/java/graphql/schema/idl/GraphQLSchemaPrintAccess.java +++ /dev/null @@ -1,631 +0,0 @@ -package graphql.schema.idl; - -import graphql.Directives; -import graphql.DirectivesUtil; -import graphql.GraphQLContext; -import graphql.Internal; -import graphql.execution.ValuesResolver; -import graphql.introspection.Introspection.DirectiveLocation; -import graphql.language.Comment; -import graphql.language.Description; -import graphql.language.EnumTypeDefinition; -import graphql.language.EnumValueDefinition; -import graphql.language.FieldDefinition; -import graphql.language.InputObjectTypeDefinition; -import graphql.language.InputValueDefinition; -import graphql.language.InterfaceTypeDefinition; -import graphql.language.Node; -import graphql.language.ObjectTypeDefinition; -import graphql.language.ScalarTypeDefinition; -import graphql.language.SchemaDefinition; -import graphql.language.UnionTypeDefinition; -import graphql.schema.GraphQLAppliedDirective; -import graphql.schema.GraphQLAppliedDirectiveArgument; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLDirective; -import graphql.schema.GraphQLDirectiveContainer; -import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLEnumValueDefinition; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLNamedType; -import graphql.schema.GraphQLNonNull; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLScalarType; -import graphql.schema.GraphQLSchema; -import graphql.schema.GraphQLSchemaElement; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; -import graphql.schema.GraphQLUnionType; -import graphql.schema.GraphqlTypeComparatorEnvironment; -import graphql.schema.InputValueWithState; -import graphql.schema.visibility.GraphqlFieldVisibility; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Locale; -import java.util.Set; -import java.util.stream.Collectors; - -import static graphql.Assert.assertNotNull; -import static graphql.Assert.assertShouldNeverHappen; - -@Internal -@NullMarked -public final class GraphQLSchemaPrintAccess implements SchemaPrintAccess { - - private final GraphQLSchema schema; - private final SchemaPrinter.Options options; - private final GraphqlFieldVisibility visibility; - - public GraphQLSchemaPrintAccess( - GraphQLSchema schema, - SchemaPrinter.Options options) { - this.schema = assertNotNull(schema); - this.options = assertNotNull(options); - this.visibility = schema.getCodeRegistry().getFieldVisibility(); - } - - @Override - public Object getSchema() { - return schema; - } - - @Override - public Object getQueryType() { - return schema.getQueryType(); - } - - @Override - public @Nullable Object getMutationType() { - return schema.getMutationType(); - } - - @Override - public @Nullable Object getSubscriptionType() { - return schema.getSubscriptionType(); - } - - @Override - public List getTypes() { - return new ArrayList<>(schema.getAllTypesAsList()); - } - - @Override - public List getDirectiveDefinitions() { - return new ArrayList<>(schema.getDirectives()); - } - - @Override - public SchemaPrintElementKind getKind(Object element) { - if (element instanceof GraphQLSchema) { - return SchemaPrintElementKind.SCHEMA; - } - if (element instanceof GraphQLObjectType) { - return SchemaPrintElementKind.OBJECT; - } - if (element instanceof GraphQLFieldDefinition) { - return SchemaPrintElementKind.FIELD; - } - if (element instanceof GraphQLInterfaceType) { - return SchemaPrintElementKind.INTERFACE; - } - if (element instanceof GraphQLUnionType) { - return SchemaPrintElementKind.UNION; - } - if (element instanceof GraphQLEnumType) { - return SchemaPrintElementKind.ENUM; - } - if (element instanceof GraphQLEnumValueDefinition) { - return SchemaPrintElementKind.ENUM_VALUE; - } - if (element instanceof GraphQLScalarType) { - return SchemaPrintElementKind.SCALAR; - } - if (element instanceof GraphQLInputObjectType) { - return SchemaPrintElementKind.INPUT_OBJECT; - } - if (element instanceof GraphQLInputObjectField) { - return SchemaPrintElementKind.INPUT_FIELD; - } - if (element instanceof GraphQLArgument) { - return SchemaPrintElementKind.ARGUMENT; - } - if (element instanceof GraphQLDirective) { - return SchemaPrintElementKind.DIRECTIVE; - } - if (element instanceof GraphQLAppliedDirective) { - return SchemaPrintElementKind.APPLIED_DIRECTIVE; - } - if (element instanceof GraphQLAppliedDirectiveArgument) { - return SchemaPrintElementKind.APPLIED_DIRECTIVE_ARGUMENT; - } - if (element instanceof GraphQLList) { - return SchemaPrintElementKind.LIST; - } - if (element instanceof GraphQLNonNull) { - return SchemaPrintElementKind.NON_NULL; - } - return assertShouldNeverHappen( - "Unsupported GraphQL schema print element %s", - element.getClass().getName()); - } - - @Override - public String getName(Object element) { - if (element instanceof GraphQLNamedType) { - return ((GraphQLNamedType) element).getName(); - } - if (element instanceof GraphQLFieldDefinition) { - return ((GraphQLFieldDefinition) element).getName(); - } - if (element instanceof GraphQLInputObjectField) { - return ((GraphQLInputObjectField) element).getName(); - } - if (element instanceof GraphQLEnumValueDefinition) { - return ((GraphQLEnumValueDefinition) element).getName(); - } - if (element instanceof GraphQLArgument) { - return ((GraphQLArgument) element).getName(); - } - if (element instanceof GraphQLDirective) { - return ((GraphQLDirective) element).getName(); - } - if (element instanceof GraphQLAppliedDirective) { - return ((GraphQLAppliedDirective) element).getName(); - } - if (element instanceof GraphQLAppliedDirectiveArgument) { - return ((GraphQLAppliedDirectiveArgument) element).getName(); - } - return assertShouldNeverHappen( - "Schema print element has no name: %s", - element.getClass().getName()); - } - - @Override - public @Nullable String getDescription(Object element) { - if (element instanceof GraphQLSchema) { - GraphQLSchema graphQLSchema = (GraphQLSchema) element; - SchemaDefinition definition = graphQLSchema.getDefinition(); - return description( - graphQLSchema.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLObjectType) { - GraphQLObjectType type = (GraphQLObjectType) element; - ObjectTypeDefinition definition = type.getDefinition(); - return description( - type.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLFieldDefinition) { - GraphQLFieldDefinition field = (GraphQLFieldDefinition) element; - FieldDefinition definition = field.getDefinition(); - return description( - field.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLInterfaceType) { - GraphQLInterfaceType type = (GraphQLInterfaceType) element; - InterfaceTypeDefinition definition = type.getDefinition(); - return description( - type.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLUnionType) { - GraphQLUnionType type = (GraphQLUnionType) element; - UnionTypeDefinition definition = type.getDefinition(); - return description( - type.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLEnumType) { - GraphQLEnumType type = (GraphQLEnumType) element; - EnumTypeDefinition definition = type.getDefinition(); - return description( - type.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLEnumValueDefinition) { - GraphQLEnumValueDefinition value = - (GraphQLEnumValueDefinition) element; - EnumValueDefinition definition = value.getDefinition(); - return description( - value.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLScalarType) { - GraphQLScalarType type = (GraphQLScalarType) element; - ScalarTypeDefinition definition = type.getDefinition(); - return description( - type.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLInputObjectType) { - GraphQLInputObjectType type = (GraphQLInputObjectType) element; - InputObjectTypeDefinition definition = type.getDefinition(); - return description( - type.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLInputObjectField) { - GraphQLInputObjectField field = (GraphQLInputObjectField) element; - InputValueDefinition definition = field.getDefinition(); - return description( - field.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLArgument) { - GraphQLArgument argument = (GraphQLArgument) element; - InputValueDefinition definition = argument.getDefinition(); - return description( - argument.getDescription(), - definition == null ? null : definition.getDescription()); - } - if (element instanceof GraphQLDirective) { - return ((GraphQLDirective) element).getDescription(); - } - return null; - } - - private @Nullable String description( - @Nullable String runtimeDescription, - @Nullable Description astDescription) { - if (runtimeDescription != null && !runtimeDescription.isEmpty()) { - return runtimeDescription; - } - return astDescription == null ? runtimeDescription : astDescription.getContent(); - } - - @Override - public @Nullable String getAstDefinitionComments(Object element) { - Node definition = getDefinition(element); - if (definition == null || definition.getComments().isEmpty()) { - return null; - } - return definition.getComments().stream() - .map(Comment::getContent) - .collect(Collectors.joining("\n", "", "\n")); - } - - @Override - public boolean isIncluded(Object element) { - return options.getIncludeSchemaElement().test((GraphQLSchemaElement) element); - } - - @Override - public boolean isIntrospectionType(Object type) { - return !options.isIncludeIntrospectionTypes() - && getName(type).startsWith("__"); - } - - @Override - public boolean isSpecifiedScalar(Object type) { - return ScalarInfo.isGraphqlSpecifiedScalar((GraphQLScalarType) type); - } - - @Override - public List getFields(Object type) { - if (type instanceof GraphQLObjectType) { - return new ArrayList<>( - visibility.getFieldDefinitions((GraphQLObjectType) type)); - } - return new ArrayList<>( - visibility.getFieldDefinitions((GraphQLInterfaceType) type)); - } - - @Override - public List getArguments(Object fieldOrDirective) { - if (fieldOrDirective instanceof GraphQLFieldDefinition) { - return new ArrayList<>( - ((GraphQLFieldDefinition) fieldOrDirective).getArguments()); - } - return new ArrayList<>(((GraphQLDirective) fieldOrDirective).getArguments()); - } - - @Override - public Object getType(Object typedElement) { - if (typedElement instanceof GraphQLFieldDefinition) { - return ((GraphQLFieldDefinition) typedElement).getType(); - } - if (typedElement instanceof GraphQLArgument) { - return ((GraphQLArgument) typedElement).getType(); - } - if (typedElement instanceof GraphQLInputObjectField) { - return ((GraphQLInputObjectField) typedElement).getType(); - } - return ((GraphQLAppliedDirectiveArgument) typedElement).getType(); - } - - @Override - public List getInterfaces(Object type) { - if (type instanceof GraphQLObjectType) { - return new ArrayList<>(((GraphQLObjectType) type).getInterfaces()); - } - return new ArrayList<>(((GraphQLInterfaceType) type).getInterfaces()); - } - - @Override - public List getUnionMembers(Object unionType) { - return new ArrayList<>(((GraphQLUnionType) unionType).getTypes()); - } - - @Override - public List getEnumValues(Object enumType) { - return new ArrayList<>(((GraphQLEnumType) enumType).getValues()); - } - - @Override - public List getInputFields(Object inputObjectType) { - return new ArrayList<>( - visibility.getFieldDefinitions((GraphQLInputObjectType) inputObjectType)); - } - - @Override - public List getAppliedDirectives(Object container) { - if (container instanceof GraphQLSchema) { - GraphQLSchema graphQLSchema = (GraphQLSchema) container; - return new ArrayList<>(DirectivesUtil.toAppliedDirectives( - graphQLSchema.getSchemaAppliedDirectives(), - graphQLSchema.getSchemaDirectives())); - } - GraphQLDirectiveContainer directiveContainer = - (GraphQLDirectiveContainer) container; - List directives = - DirectivesUtil.toAppliedDirectives(directiveContainer); - if (!isDeprecated(container)) { - return new ArrayList<>(directives); - } - return new ArrayList<>(withDeprecationReason( - directives, - assertNotNull(getDeprecationReason(container)))); - } - - @Override - public List getAppliedDirectiveArguments(Object directive) { - return new ArrayList<>( - ((GraphQLAppliedDirective) directive).getArguments()); - } - - @Override - public InputValueWithState getDefaultValue(Object argumentOrInputField) { - if (argumentOrInputField instanceof GraphQLArgument) { - return ((GraphQLArgument) argumentOrInputField).getArgumentDefaultValue(); - } - return ((GraphQLInputObjectField) argumentOrInputField) - .getInputFieldDefaultValue(); - } - - @Override - public InputValueWithState getAppliedDirectiveArgumentValue(Object argument) { - return ((GraphQLAppliedDirectiveArgument) argument).getArgumentValue(); - } - - @Override - public boolean isRepeatable(Object directive) { - return ((GraphQLDirective) directive).isRepeatable(); - } - - @Override - public Set getDirectiveLocations(Object directive) { - return ((GraphQLDirective) directive).validLocations(); - } - - @Override - public @Nullable String getSpecifiedByUrl(Object scalar) { - return ((GraphQLScalarType) scalar).getSpecifiedByUrl(); - } - - @Override - public String getTypeString(Object type) { - return GraphQLTypeUtil.simplePrint((GraphQLType) type); - } - - @Override - public String printValue(InputValueWithState value, Object type) { - return graphql.language.AstPrinter.printAst(ValuesResolver.valueToLiteral( - value, - (GraphQLInputType) type, - GraphQLContext.getDefault(), - Locale.getDefault())); - } - - @Override - @SuppressWarnings({"rawtypes", "unchecked"}) - public List sort( - @Nullable Object parent, - SchemaPrintChildKind childKind, - List elements) { - Class parentType = - parent == null ? GraphQLSchemaElement.class : elementClass(parent); - Class elementType = childClass(childKind); - GraphqlTypeComparatorEnvironment environment = - GraphqlTypeComparatorEnvironment.newEnvironment() - .parentType(parentType) - .elementType(elementType) - .build(); - Comparator comparator = options.getComparatorRegistry().getComparator(environment); - List result = new ArrayList<>(elements); - result.sort(comparator); - return result; - } - - private @Nullable Node getDefinition(Object element) { - if (element instanceof GraphQLSchema) { - return ((GraphQLSchema) element).getDefinition(); - } - if (element instanceof GraphQLObjectType) { - return ((GraphQLObjectType) element).getDefinition(); - } - if (element instanceof GraphQLFieldDefinition) { - return ((GraphQLFieldDefinition) element).getDefinition(); - } - if (element instanceof GraphQLInterfaceType) { - return ((GraphQLInterfaceType) element).getDefinition(); - } - if (element instanceof GraphQLUnionType) { - return ((GraphQLUnionType) element).getDefinition(); - } - if (element instanceof GraphQLEnumType) { - return ((GraphQLEnumType) element).getDefinition(); - } - if (element instanceof GraphQLEnumValueDefinition) { - return ((GraphQLEnumValueDefinition) element).getDefinition(); - } - if (element instanceof GraphQLScalarType) { - return ((GraphQLScalarType) element).getDefinition(); - } - if (element instanceof GraphQLInputObjectType) { - return ((GraphQLInputObjectType) element).getDefinition(); - } - if (element instanceof GraphQLInputObjectField) { - return ((GraphQLInputObjectField) element).getDefinition(); - } - if (element instanceof GraphQLArgument) { - return ((GraphQLArgument) element).getDefinition(); - } - if (element instanceof GraphQLDirective) { - return ((GraphQLDirective) element).getDefinition(); - } - return null; - } - - private boolean isDeprecated(Object element) { - if (element instanceof GraphQLFieldDefinition) { - return ((GraphQLFieldDefinition) element).isDeprecated(); - } - if (element instanceof GraphQLEnumValueDefinition) { - return ((GraphQLEnumValueDefinition) element).isDeprecated(); - } - if (element instanceof GraphQLInputObjectField) { - return ((GraphQLInputObjectField) element).isDeprecated(); - } - if (element instanceof GraphQLArgument) { - return ((GraphQLArgument) element).isDeprecated(); - } - return false; - } - - private @Nullable String getDeprecationReason(Object element) { - if (element instanceof GraphQLFieldDefinition) { - return ((GraphQLFieldDefinition) element).getDeprecationReason(); - } - if (element instanceof GraphQLEnumValueDefinition) { - return ((GraphQLEnumValueDefinition) element).getDeprecationReason(); - } - if (element instanceof GraphQLInputObjectField) { - return ((GraphQLInputObjectField) element).getDeprecationReason(); - } - if (element instanceof GraphQLArgument) { - return ((GraphQLArgument) element).getDeprecationReason(); - } - return null; - } - - private List withDeprecationReason( - List directives, - String reason) { - List result = new ArrayList<>(directives.size() + 1); - boolean found = false; - for (GraphQLAppliedDirective directive : directives) { - if (!Directives.DeprecatedDirective.getName().equals(directive.getName())) { - result.add(directive); - continue; - } - found = true; - GraphQLAppliedDirectiveArgument existing = directive.getArgument("reason"); - if (existing != null - && existing.getArgumentValue() == InputValueWithState.NOT_SET) { - result.add(directive); - continue; - } - result.add(directive.transform(builder -> - builder.argument(deprecationReasonArgument(reason)))); - } - if (!found) { - result.add(GraphQLAppliedDirective.newDirective() - .name(Directives.DeprecatedDirective.getName()) - .argument(deprecationReasonArgument(reason)) - .build()); - } - return result; - } - - private GraphQLAppliedDirectiveArgument deprecationReasonArgument(String reason) { - return GraphQLAppliedDirectiveArgument.newArgument() - .name("reason") - .valueProgrammatic(reason) - .type(graphql.Scalars.GraphQLString) - .build(); - } - - private Class elementClass(Object element) { - switch (getKind(element)) { - case SCHEMA: - return GraphQLSchemaElement.class; - case OBJECT: - return GraphQLObjectType.class; - case FIELD: - return GraphQLFieldDefinition.class; - case INTERFACE: - return GraphQLInterfaceType.class; - case UNION: - return GraphQLUnionType.class; - case ENUM: - return GraphQLEnumType.class; - case ENUM_VALUE: - return GraphQLEnumValueDefinition.class; - case SCALAR: - return GraphQLScalarType.class; - case INPUT_OBJECT: - return GraphQLInputObjectType.class; - case INPUT_FIELD: - return GraphQLInputObjectField.class; - case ARGUMENT: - return GraphQLArgument.class; - case DIRECTIVE: - return GraphQLDirective.class; - case APPLIED_DIRECTIVE: - return GraphQLAppliedDirective.class; - case APPLIED_DIRECTIVE_ARGUMENT: - return GraphQLAppliedDirectiveArgument.class; - default: - return assertShouldNeverHappen( - "No comparator class for %s", - element.getClass().getName()); - } - } - - private @Nullable Class childClass( - SchemaPrintChildKind childKind) { - switch (childKind) { - case TOP_LEVEL: - return null; - case FIELD: - return GraphQLFieldDefinition.class; - case ARGUMENT: - return GraphQLArgument.class; - case INTERFACE: - case UNION_MEMBER: - return GraphQLOutputType.class; - case ENUM_VALUE: - return GraphQLEnumValueDefinition.class; - case INPUT_FIELD: - return GraphQLInputObjectField.class; - case APPLIED_DIRECTIVE: - return GraphQLAppliedDirective.class; - case APPLIED_DIRECTIVE_ARGUMENT: - return GraphQLAppliedDirectiveArgument.class; - default: - return assertShouldNeverHappen("Unsupported schema print child kind"); - } - } -} diff --git a/src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java b/src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java deleted file mode 100644 index 94d17a022b..0000000000 --- a/src/main/java/graphql/schema/idl/SUSchemaPrintAccess.java +++ /dev/null @@ -1,457 +0,0 @@ -package graphql.schema.idl; - -import graphql.Internal; -import graphql.introspection.Introspection.DirectiveLocation; -import graphql.language.ArrayValue; -import graphql.language.AstPrinter; -import graphql.language.BooleanValue; -import graphql.language.Comment; -import graphql.language.EnumValue; -import graphql.language.FloatValue; -import graphql.language.IntValue; -import graphql.language.Node; -import graphql.language.NullValue; -import graphql.language.ObjectField; -import graphql.language.ObjectValue; -import graphql.language.StringValue; -import graphql.language.Value; -import graphql.schema.GraphqlTypeComparatorRegistry; -import graphql.schema.InputValueWithState; -import graphql.schema.universe.SUAppliedDirective; -import graphql.schema.universe.SUAppliedDirectiveArgument; -import graphql.schema.universe.SUArgument; -import graphql.schema.universe.SUDirective; -import graphql.schema.universe.SUEnumType; -import graphql.schema.universe.SUInputField; -import graphql.schema.universe.SUInputObjectType; -import graphql.schema.universe.SUInterfaceType; -import graphql.schema.universe.SUListType; -import graphql.schema.universe.SUNonNullType; -import graphql.schema.universe.SUObjectType; -import graphql.schema.universe.SUScalarType; -import graphql.schema.universe.SUSchema; -import graphql.schema.universe.SUUnionType; -import graphql.schema.universe.SUVertex; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.lang.reflect.Array; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; - -import static graphql.Assert.assertNotNull; -import static graphql.Assert.assertShouldNeverHappen; - -@Internal -@NullMarked -public final class SUSchemaPrintAccess implements SchemaPrintAccess { - - private final SUSchema schema; - private final SchemaPrinter.Options options; - - public SUSchemaPrintAccess( - SUSchema schema, - SchemaPrinter.Options options) { - this.schema = assertNotNull(schema); - this.options = assertNotNull(options); - } - - @Override - public Object getSchema() { - return schema.getRoot(); - } - - @Override - public Object getQueryType() { - return schema.getQueryType(); - } - - @Override - public @Nullable Object getMutationType() { - return schema.getMutationType(); - } - - @Override - public @Nullable Object getSubscriptionType() { - return schema.getSubscriptionType(); - } - - @Override - public List getTypes() { - return new ArrayList<>(schema.getTypes()); - } - - @Override - public List getDirectiveDefinitions() { - return new ArrayList<>(schema.getDirectiveDefinitions()); - } - - @Override - public SchemaPrintElementKind getKind(Object element) { - if (element instanceof SUAppliedDirectiveArgument) { - return SchemaPrintElementKind.APPLIED_DIRECTIVE_ARGUMENT; - } - SUVertex vertex = (SUVertex) element; - switch (vertex.getKind()) { - case SCHEMA: - return SchemaPrintElementKind.SCHEMA; - case OBJECT: - return SchemaPrintElementKind.OBJECT; - case FIELD: - return SchemaPrintElementKind.FIELD; - case INTERFACE: - return SchemaPrintElementKind.INTERFACE; - case UNION: - return SchemaPrintElementKind.UNION; - case ENUM: - return SchemaPrintElementKind.ENUM; - case ENUM_VALUE: - return SchemaPrintElementKind.ENUM_VALUE; - case SCALAR: - return SchemaPrintElementKind.SCALAR; - case INPUT_OBJECT: - return SchemaPrintElementKind.INPUT_OBJECT; - case INPUT_FIELD: - return SchemaPrintElementKind.INPUT_FIELD; - case ARGUMENT: - return SchemaPrintElementKind.ARGUMENT; - case DIRECTIVE: - return SchemaPrintElementKind.DIRECTIVE; - case APPLIED_DIRECTIVE: - return SchemaPrintElementKind.APPLIED_DIRECTIVE; - case LIST: - return SchemaPrintElementKind.LIST; - case NON_NULL: - return SchemaPrintElementKind.NON_NULL; - default: - return assertShouldNeverHappen( - "Unsupported schema universe print vertex %s", - vertex); - } - } - - @Override - public String getName(Object element) { - if (element instanceof SUAppliedDirectiveArgument) { - return ((SUAppliedDirectiveArgument) element).getName(); - } - return assertNotNull(((SUVertex) element).getName()); - } - - @Override - public @Nullable String getDescription(Object element) { - if (element instanceof SUAppliedDirectiveArgument) { - return null; - } - return ((SUVertex) element).getDescription(); - } - - @Override - public @Nullable String getAstDefinitionComments(Object element) { - Node definition = definition(element); - if (definition == null || definition.getComments().isEmpty()) { - return null; - } - return definition.getComments().stream() - .map(Comment::getContent) - .collect(Collectors.joining("\n", "", "\n")); - } - - @Override - public boolean isIncluded(Object element) { - return true; - } - - @Override - public boolean isIntrospectionType(Object type) { - return !options.isIncludeIntrospectionTypes() - && getName(type).startsWith("__"); - } - - @Override - public boolean isSpecifiedScalar(Object type) { - return ScalarInfo.isGraphqlSpecifiedScalar(getName(type)); - } - - @Override - public List getFields(Object type) { - if (type instanceof SUObjectType) { - return new ArrayList<>(schema.getFields((SUObjectType) type)); - } - return new ArrayList<>(schema.getFields((SUInterfaceType) type)); - } - - @Override - public List getArguments(Object fieldOrDirective) { - if (fieldOrDirective instanceof SUDirective) { - return new ArrayList<>( - schema.getArguments((SUDirective) fieldOrDirective)); - } - return new ArrayList<>( - schema.getArguments((graphql.schema.universe.SUField) fieldOrDirective)); - } - - @Override - public Object getType(Object typedElement) { - if (typedElement instanceof SUAppliedDirectiveArgument) { - return schema.getType((SUAppliedDirectiveArgument) typedElement); - } - if (typedElement instanceof SUArgument) { - return assertNotNull(schema.getType((SUArgument) typedElement)); - } - if (typedElement instanceof SUInputField) { - return assertNotNull(schema.getType((SUInputField) typedElement)); - } - return assertNotNull( - schema.getType((graphql.schema.universe.SUField) typedElement)); - } - - @Override - public List getInterfaces(Object type) { - if (type instanceof SUObjectType) { - return new ArrayList<>(schema.getInterfaces((SUObjectType) type)); - } - return new ArrayList<>(schema.getInterfaces((SUInterfaceType) type)); - } - - @Override - public List getUnionMembers(Object unionType) { - return new ArrayList<>( - schema.getUnionMembers((SUUnionType) unionType)); - } - - @Override - public List getEnumValues(Object enumType) { - return new ArrayList<>( - schema.getEnumValues((SUEnumType) enumType)); - } - - @Override - public List getInputFields(Object inputObjectType) { - return new ArrayList<>( - schema.getInputFields((SUInputObjectType) inputObjectType)); - } - - @Override - public List getAppliedDirectives(Object container) { - return new ArrayList<>( - schema.getAppliedDirectives((SUVertex) container)); - } - - @Override - public List getAppliedDirectiveArguments(Object directive) { - return new ArrayList<>( - schema.getArguments((SUAppliedDirective) directive)); - } - - @Override - public InputValueWithState getDefaultValue(Object argumentOrInputField) { - if (argumentOrInputField instanceof SUArgument) { - return ((SUArgument) argumentOrInputField).getArgumentDefaultValue(); - } - return ((SUInputField) argumentOrInputField).getInputFieldDefaultValue(); - } - - @Override - public InputValueWithState getAppliedDirectiveArgumentValue(Object argument) { - return ((SUAppliedDirectiveArgument) argument).getArgumentValue(); - } - - @Override - public boolean isRepeatable(Object directive) { - return ((SUDirective) directive).isRepeatable(); - } - - @Override - public Set getDirectiveLocations(Object directive) { - return ((SUDirective) directive).validLocations(); - } - - @Override - public @Nullable String getSpecifiedByUrl(Object scalar) { - for (SUAppliedDirective directive : - schema.getAppliedDirectives( - (SUScalarType) scalar, - "specifiedBy")) { - SUAppliedDirectiveArgument url = schema.getArgument(directive, "url"); - if (url == null || url.getArgumentValue().isNotSet()) { - continue; - } - Object value = url.getArgumentValue().getValue(); - if (value instanceof StringValue) { - return ((StringValue) value).getValue(); - } - return value == null ? null : String.valueOf(value); - } - return null; - } - - @Override - public String getTypeString(Object type) { - SUVertex vertex = (SUVertex) type; - if (vertex instanceof SUListType) { - return "[" + getTypeString(assertNotNull( - schema.getWrappedType((SUListType) vertex))) + "]"; - } - if (vertex instanceof SUNonNullType) { - return getTypeString(assertNotNull( - schema.getWrappedType((SUNonNullType) vertex))) + "!"; - } - return assertNotNull(vertex.getName()); - } - - @Override - public String printValue(InputValueWithState value, Object type) { - if (value.isLiteral()) { - return AstPrinter.printAst((Value) assertNotNull(value.getValue())); - } - return AstPrinter.printAst(toLiteral(value.getValue(), (SUVertex) type)); - } - - @Override - public List sort( - @Nullable Object parent, - SchemaPrintChildKind childKind, - List elements) { - List result = new ArrayList<>(elements); - if (options.getComparatorRegistry() == GraphqlTypeComparatorRegistry.AS_IS_REGISTRY) { - return result; - } - Comparator comparator = Comparator.comparing(this::getName); - if (childKind == SchemaPrintChildKind.TOP_LEVEL - && options.getComparatorRegistry() - != GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) { - comparator = Comparator.comparingInt(this::topLevelRank) - .thenComparing(this::getName); - } - result.sort(comparator); - return result; - } - - private @Nullable Node definition(Object element) { - if (element instanceof SUArgument) { - return ((SUArgument) element).getDefinition(); - } - if (element instanceof SUInputField) { - return ((SUInputField) element).getDefinition(); - } - if (element instanceof SUDirective) { - return ((SUDirective) element).getDefinition(); - } - if (element instanceof SUAppliedDirective) { - return ((SUAppliedDirective) element).getDefinition(); - } - if (element instanceof SUAppliedDirectiveArgument) { - return ((SUAppliedDirectiveArgument) element).getDefinition(); - } - return null; - } - - private int topLevelRank(Object element) { - switch (getKind(element)) { - case DIRECTIVE: - return 1; - case INTERFACE: - return 2; - case UNION: - return 3; - case OBJECT: - return 4; - case ENUM: - return 5; - case SCALAR: - return 6; - case INPUT_OBJECT: - return 7; - default: - return 0; - } - } - - private Value toLiteral(@Nullable Object value, SUVertex type) { - if (type instanceof SUNonNullType) { - return toLiteral( - value, - assertNotNull(schema.getWrappedType((SUNonNullType) type))); - } - if (value == null) { - return NullValue.of(); - } - if (type instanceof SUListType) { - return listLiteral(value, (SUListType) type); - } - if (type instanceof SUInputObjectType) { - return objectLiteral(value, (SUInputObjectType) type); - } - if (type instanceof SUEnumType) { - return EnumValue.newEnumValue(String.valueOf(value)).build(); - } - return scalarLiteral(value, assertNotNull(type.getName())); - } - - private Value listLiteral(Object value, SUListType type) { - SUVertex wrappedType = assertNotNull(schema.getWrappedType(type)); - List values = new ArrayList<>(); - if (value instanceof Iterable) { - for (Object item : (Iterable) value) { - values.add(toLiteral(item, wrappedType)); - } - } else if (value.getClass().isArray()) { - for (int i = 0; i < Array.getLength(value); i++) { - values.add(toLiteral(Array.get(value, i), wrappedType)); - } - } else { - values.add(toLiteral(value, wrappedType)); - } - return ArrayValue.newArrayValue().values(values).build(); - } - - private Value objectLiteral(Object value, SUInputObjectType type) { - if (!(value instanceof Map)) { - return assertShouldNeverHappen( - "Cannot print value '%s' for input object '%s' without a map", - value, - getName(type)); - } - Map map = (Map) value; - List fields = new ArrayList<>(); - for (SUInputField field : schema.getInputFields(type)) { - String name = assertNotNull(field.getName()); - if (!map.containsKey(name)) { - continue; - } - fields.add(ObjectField.newObjectField() - .name(name) - .value(toLiteral( - map.get(name), - assertNotNull(schema.getType(field)))) - .build()); - } - return ObjectValue.newObjectValue().objectFields(fields).build(); - } - - private Value scalarLiteral(Object value, String scalarName) { - if ("String".equals(scalarName) || "ID".equals(scalarName)) { - return StringValue.newStringValue(String.valueOf(value)).build(); - } - if ("Boolean".equals(scalarName) && value instanceof Boolean) { - return BooleanValue.newBooleanValue((Boolean) value).build(); - } - if ("Int".equals(scalarName) && value instanceof Number) { - return IntValue.newIntValue(new BigInteger(String.valueOf(value))).build(); - } - if ("Float".equals(scalarName) && value instanceof Number) { - return FloatValue.newFloatValue(new BigDecimal(String.valueOf(value))).build(); - } - return assertShouldNeverHappen( - "Cannot print a non-literal value for custom scalar '%s' " - + "because SUSchema does not retain its coercing", - scalarName); - } -} diff --git a/src/main/java/graphql/schema/idl/SchemaPrintAccess.java b/src/main/java/graphql/schema/idl/SchemaPrintAccess.java deleted file mode 100644 index 681769a36e..0000000000 --- a/src/main/java/graphql/schema/idl/SchemaPrintAccess.java +++ /dev/null @@ -1,78 +0,0 @@ -package graphql.schema.idl; - -import graphql.Internal; -import graphql.introspection.Introspection.DirectiveLocation; -import graphql.schema.InputValueWithState; -import org.jspecify.annotations.NullMarked; -import org.jspecify.annotations.Nullable; - -import java.util.List; -import java.util.Set; - -@Internal -@NullMarked -public interface SchemaPrintAccess { - - Object getSchema(); - - Object getQueryType(); - - @Nullable Object getMutationType(); - - @Nullable Object getSubscriptionType(); - - List getTypes(); - - List getDirectiveDefinitions(); - - SchemaPrintElementKind getKind(Object element); - - String getName(Object element); - - @Nullable String getDescription(Object element); - - @Nullable String getAstDefinitionComments(Object element); - - boolean isIncluded(Object element); - - boolean isIntrospectionType(Object type); - - boolean isSpecifiedScalar(Object type); - - List getFields(Object type); - - List getArguments(Object fieldOrDirective); - - Object getType(Object typedElement); - - List getInterfaces(Object type); - - List getUnionMembers(Object unionType); - - List getEnumValues(Object enumType); - - List getInputFields(Object inputObjectType); - - List getAppliedDirectives(Object container); - - List getAppliedDirectiveArguments(Object directive); - - InputValueWithState getDefaultValue(Object argumentOrInputField); - - InputValueWithState getAppliedDirectiveArgumentValue(Object argument); - - boolean isRepeatable(Object directive); - - Set getDirectiveLocations(Object directive); - - @Nullable String getSpecifiedByUrl(Object scalar); - - String getTypeString(Object type); - - String printValue(InputValueWithState value, Object type); - - List sort( - @Nullable Object parent, - SchemaPrintChildKind childKind, - List elements); -} diff --git a/src/main/java/graphql/schema/idl/SchemaPrintChildKind.java b/src/main/java/graphql/schema/idl/SchemaPrintChildKind.java deleted file mode 100644 index 8e1cc5f3c7..0000000000 --- a/src/main/java/graphql/schema/idl/SchemaPrintChildKind.java +++ /dev/null @@ -1,18 +0,0 @@ -package graphql.schema.idl; - -import graphql.Internal; -import org.jspecify.annotations.NullMarked; - -@Internal -@NullMarked -public enum SchemaPrintChildKind { - TOP_LEVEL, - FIELD, - ARGUMENT, - INTERFACE, - UNION_MEMBER, - ENUM_VALUE, - INPUT_FIELD, - APPLIED_DIRECTIVE, - APPLIED_DIRECTIVE_ARGUMENT -} diff --git a/src/main/java/graphql/schema/idl/SchemaPrintElementKind.java b/src/main/java/graphql/schema/idl/SchemaPrintElementKind.java deleted file mode 100644 index febfe729d3..0000000000 --- a/src/main/java/graphql/schema/idl/SchemaPrintElementKind.java +++ /dev/null @@ -1,25 +0,0 @@ -package graphql.schema.idl; - -import graphql.Internal; -import org.jspecify.annotations.NullMarked; - -@Internal -@NullMarked -public enum SchemaPrintElementKind { - SCHEMA, - OBJECT, - FIELD, - INTERFACE, - UNION, - ENUM, - ENUM_VALUE, - SCALAR, - INPUT_OBJECT, - INPUT_FIELD, - ARGUMENT, - DIRECTIVE, - APPLIED_DIRECTIVE, - APPLIED_DIRECTIVE_ARGUMENT, - LIST, - NON_NULL -} diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index 9e9c7cae85..0e772f75ae 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -25,6 +25,7 @@ import graphql.language.TypeDefinition; import graphql.language.UnionTypeDefinition; import graphql.schema.DefaultGraphqlTypeComparatorRegistry; +import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLAppliedDirective; import graphql.schema.GraphQLAppliedDirectiveArgument; import graphql.schema.GraphQLArgument; @@ -50,7 +51,6 @@ import graphql.schema.GraphqlTypeComparatorEnvironment; import graphql.schema.GraphqlTypeComparatorRegistry; import graphql.schema.InputValueWithState; -import graphql.schema.universe.SUSchema; import graphql.schema.visibility.GraphqlFieldVisibility; import java.io.PrintWriter; @@ -509,10 +509,7 @@ public String print(Document schemaIDL) { */ public String print(GraphQLSchema schema) { if (!options.isUseAstDefinitions()) { - return new SchemaPrinterWriter( - new GraphQLSchemaPrintAccess(schema, options), - options) - .print(); + return print((ExecutableSchema) schema); } StringWriter sw = new StringWriter(); PrintWriter out = new PrintWriter(sw); @@ -540,22 +537,15 @@ public String print(GraphQLSchema schema) { } /** - * Prints a schema-universe snapshot directly without materializing a {@link GraphQLSchema}. + * Prints an executable schema view as canonical semantic SDL. * - *

Schema-universe extensions are flattened into their semantic topology, so this always - * produces canonical semantic SDL even when {@link Options#useAstDefinitions(boolean)} is - * enabled.

- * - * @param schema the schema-universe snapshot + * @param schema the schema view * * @return the logical schema definition */ @ExperimentalApi - public String print(SUSchema schema) { - return new SchemaPrinterWriter( - new SUSchemaPrintAccess(schema, options), - options) - .print(); + public String print(ExecutableSchema schema) { + return new SchemaPrinterWriter(schema, options).print(); } private interface SchemaElementPrinter { diff --git a/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java b/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java index 19cd2c3295..6ca69e23d1 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java @@ -1,8 +1,57 @@ package graphql.schema.idl; +import graphql.GraphQLContext; import graphql.Internal; import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.ArrayValue; +import graphql.language.AstPrinter; +import graphql.language.Comment; +import graphql.language.DescribedNode; +import graphql.language.EnumValue; +import graphql.language.Node; +import graphql.language.NullValue; +import graphql.language.ObjectField; +import graphql.language.ObjectValue; +import graphql.language.Value; +import graphql.schema.ExecutableSchema; +import graphql.schema.GraphQLAppliedDirective; +import graphql.schema.GraphQLAppliedDirectiveArgument; +import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLDirective; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLFieldDefinition; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInterfaceType; +import graphql.schema.GraphQLObjectType; +import graphql.schema.GraphQLOutputType; +import graphql.schema.GraphQLSchemaElement; +import graphql.schema.GraphQLType; +import graphql.schema.GraphQLUnionType; +import graphql.schema.GraphqlTypeComparatorEnvironment; +import graphql.schema.GraphqlTypeComparatorRegistry; import graphql.schema.InputValueWithState; +import graphql.schema.SchemaAppliedDirective; +import graphql.schema.SchemaAppliedDirectiveArgument; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaDirective; +import graphql.schema.SchemaEnum; +import graphql.schema.SchemaEnumValue; +import graphql.schema.SchemaField; +import graphql.schema.SchemaFieldsContainer; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaInputType; +import graphql.schema.SchemaInterface; +import graphql.schema.SchemaList; +import graphql.schema.SchemaModifiedType; +import graphql.schema.SchemaNamedElement; +import graphql.schema.SchemaNamedType; +import graphql.schema.SchemaNonNull; +import graphql.schema.SchemaObject; +import graphql.schema.SchemaScalar; +import graphql.schema.SchemaType; +import graphql.schema.SchemaUnion; +import graphql.util.FpKit; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -10,7 +59,10 @@ import java.io.StringWriter; import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.stream.Collectors; import static graphql.Assert.assertNotNull; @@ -21,13 +73,13 @@ @NullMarked public final class SchemaPrinterWriter { - private final SchemaPrintAccess access; + private final ExecutableSchema schema; private final SchemaPrinter.Options options; public SchemaPrinterWriter( - SchemaPrintAccess access, + ExecutableSchema schema, SchemaPrinter.Options options) { - this.access = assertNotNull(access); + this.schema = assertNotNull(schema); this.options = assertNotNull(options); } @@ -36,197 +88,216 @@ public String print() { PrintWriter out = new PrintWriter(stringWriter); printSchema(out); - List elements = new ArrayList<>(); - elements.addAll(access.getTypes()); - elements.addAll(access.getDirectiveDefinitions()); - elements.removeIf(element -> !access.isIncluded(element)); - elements = access.sort(null, SchemaPrintChildKind.TOP_LEVEL, elements); - for (Object element : elements) { + List elements = new ArrayList<>(); + elements.addAll(schema.getTypes()); + elements.addAll(schema.getDirectives()); + elements.removeIf(element -> !isIncluded(element)); + elements = sort(null, elements, null, true); + for (SchemaNamedElement element : elements) { printElement(out, element); } return trimNewLineChars(stringWriter.toString()); } private void printSchema(PrintWriter out) { - Object queryType = access.getQueryType(); - Object mutationType = access.getMutationType(); - Object subscriptionType = access.getSubscriptionType(); + SchemaObject queryType = schema.getQueryType(); + SchemaObject mutationType = schema.getMutationType(); + SchemaObject subscriptionType = schema.getSubscriptionType(); boolean needsSchemaPrinted = options.isIncludeSchemaDefinition() - || !"Query".equals(access.getName(queryType)); + || !"Query".equals(queryType.getName()); if (mutationType != null - && !"Mutation".equals(access.getName(mutationType))) { + && !"Mutation".equals(mutationType.getName())) { needsSchemaPrinted = true; } if (subscriptionType != null - && !"Subscription".equals(access.getName(subscriptionType))) { + && !"Subscription".equals(subscriptionType.getName())) { needsSchemaPrinted = true; } if (!needsSchemaPrinted) { return; } - Object schema = access.getSchema(); - printComments(out, schema, ""); + printComments( + out, + schema.getDescription(), + schema.getDefinition(), + ""); out.format( "schema %s{\n", - directivesString(schema, access.getAppliedDirectives(schema), true)); - out.format(" query: %s\n", access.getName(queryType)); + directivesString(null, schema.getAppliedDirectives(), true)); + out.format(" query: %s\n", queryType.getName()); if (mutationType != null) { - out.format(" mutation: %s\n", access.getName(mutationType)); + out.format(" mutation: %s\n", mutationType.getName()); } if (subscriptionType != null) { - out.format(" subscription: %s\n", access.getName(subscriptionType)); + out.format(" subscription: %s\n", subscriptionType.getName()); } out.format("}\n\n"); } - private void printElement(PrintWriter out, Object element) { - switch (access.getKind(element)) { - case OBJECT: - printObject(out, element); - return; - case INTERFACE: - printInterface(out, element); - return; - case UNION: - printUnion(out, element); - return; - case ENUM: - printEnum(out, element); - return; - case SCALAR: - printScalar(out, element); - return; - case INPUT_OBJECT: - printInputObject(out, element); - return; - case DIRECTIVE: - printDirectiveDefinition(out, element); - return; - default: - assertShouldNeverHappen( - "Unsupported top-level schema print element %s", - element); - } - } - - private void printObject(PrintWriter out, Object type) { - if (access.isIntrospectionType(type)) { + private void printElement( + PrintWriter out, + SchemaNamedElement element) { + if (element instanceof SchemaObject) { + printObject(out, (SchemaObject) element); + return; + } + if (element instanceof SchemaInterface) { + printInterface(out, (SchemaInterface) element); + return; + } + if (element instanceof SchemaUnion) { + printUnion(out, (SchemaUnion) element); + return; + } + if (element instanceof SchemaEnum) { + printEnum(out, (SchemaEnum) element); + return; + } + if (element instanceof SchemaScalar) { + printScalar(out, (SchemaScalar) element); + return; + } + if (element instanceof SchemaInputObject) { + printInputObject(out, (SchemaInputObject) element); + return; + } + if (element instanceof SchemaDirective) { + printDirectiveDefinition(out, (SchemaDirective) element); + return; + } + assertShouldNeverHappen( + "Unsupported top-level schema print element %s", + element); + } + + private void printObject(PrintWriter out, SchemaObject type) { + if (isIntrospectionType(type)) { return; } printComments(out, type, ""); - out.format("type %s", access.getName(type)); + out.format("type %s", type.getName()); printInterfaces(out, type); out.print(directivesString( type, - access.getAppliedDirectives(type), + schema.getAppliedDirectives(type), false)); printFields(out, type); out.format("\n\n"); } - private void printInterface(PrintWriter out, Object type) { - if (access.isIntrospectionType(type)) { + private void printInterface( + PrintWriter out, + SchemaInterface type) { + if (isIntrospectionType(type)) { return; } printComments(out, type, ""); - out.format("interface %s", access.getName(type)); + out.format("interface %s", type.getName()); printInterfaces(out, type); out.print(directivesString( type, - access.getAppliedDirectives(type), + schema.getAppliedDirectives(type), false)); printFields(out, type); out.format("\n\n"); } - private void printInterfaces(PrintWriter out, Object type) { - List interfaces = access.sort( + private void printInterfaces( + PrintWriter out, + SchemaFieldsContainer type) { + List interfaces = sort( type, - SchemaPrintChildKind.INTERFACE, - access.getInterfaces(type)); + schema.getInterfaces(type), + GraphQLOutputType.class, + false); if (interfaces.isEmpty()) { return; } String names = interfaces.stream() - .map(access::getName) + .map(SchemaInterface::getName) .collect(Collectors.joining(" & ")); out.format(" implements %s", names); } - private void printFields(PrintWriter out, Object type) { - List fields = access.sort( + private void printFields( + PrintWriter out, + SchemaFieldsContainer type) { + List fields = sort( type, - SchemaPrintChildKind.FIELD, - access.getFields(type)); - fields.removeIf(field -> !access.isIncluded(field)); + schema.getFields(type), + GraphQLFieldDefinition.class, + false); + fields.removeIf(field -> !isIncluded(field)); if (fields.isEmpty()) { return; } out.format(" {\n"); - for (Object field : fields) { + for (SchemaField field : fields) { printComments(out, field, " "); out.format( " %s%s: %s%s\n", - access.getName(field), + field.getName(), argsString(field), - access.getTypeString(access.getType(field)), + typeString(field.getType()), directivesString( field, - access.getAppliedDirectives(field), + schema.getAppliedDirectives(field), false)); } out.format("}"); } - private void printUnion(PrintWriter out, Object type) { - if (access.isIntrospectionType(type)) { + private void printUnion(PrintWriter out, SchemaUnion type) { + if (isIntrospectionType(type)) { return; } printComments(out, type, ""); out.format( "union %s%s = ", - access.getName(type), + type.getName(), directivesString( type, - access.getAppliedDirectives(type), + schema.getAppliedDirectives(type), false)); - List members = access.sort( + List members = sort( type, - SchemaPrintChildKind.UNION_MEMBER, - access.getUnionMembers(type)); + schema.getUnionMembers(type), + GraphQLOutputType.class, + false); out.print(members.stream() - .map(access::getName) + .map(SchemaObject::getName) .collect(Collectors.joining(" | "))); out.format("\n\n"); } - private void printEnum(PrintWriter out, Object type) { - if (access.isIntrospectionType(type)) { + private void printEnum(PrintWriter out, SchemaEnum type) { + if (isIntrospectionType(type)) { return; } printComments(out, type, ""); out.format( "enum %s%s", - access.getName(type), + type.getName(), directivesString( type, - access.getAppliedDirectives(type), + schema.getAppliedDirectives(type), false)); - List values = access.sort( + List values = sort( type, - SchemaPrintChildKind.ENUM_VALUE, - access.getEnumValues(type)); + type.getValues(), + GraphQLEnumValueDefinition.class, + false); if (!values.isEmpty()) { out.format(" {\n"); - for (Object value : values) { + for (SchemaEnumValue value : values) { printComments(out, value, " "); out.format( " %s%s\n", - access.getName(value), + value.getName(), directivesString( value, - access.getAppliedDirectives(value), + schema.getAppliedDirectives(value), false)); } out.format("}"); @@ -234,49 +305,55 @@ private void printEnum(PrintWriter out, Object type) { out.format("\n\n"); } - private void printScalar(PrintWriter out, Object type) { - if (!options.isIncludeScalars() || access.isSpecifiedScalar(type)) { + private void printScalar(PrintWriter out, SchemaScalar type) { + if (!options.isIncludeScalars() + || ScalarInfo.isGraphqlSpecifiedScalar(type.getName())) { return; } printComments(out, type, ""); - List directives = access.getAppliedDirectives(type).stream() - .filter(directive -> !"specifiedBy".equals(access.getName(directive))) - .collect(Collectors.toList()); + List directives = + new ArrayList<>(schema.getAppliedDirectives(type)); + directives.removeIf(directive -> + "specifiedBy".equals(directive.getName())); out.format( "scalar %s%s%s\n\n", - access.getName(type), + type.getName(), directivesString(type, directives, false), specifiedByUrlString(type)); } - private String specifiedByUrlString(Object scalar) { - String url = access.getSpecifiedByUrl(scalar); - if (url == null || !options.getIncludeDirective().test("specifiedBy")) { + private String specifiedByUrlString(SchemaScalar scalar) { + String url = scalar.getSpecifiedByUrl(); + if (url == null + || !options.getIncludeDirective().test("specifiedBy")) { return ""; } return " @specifiedBy(url : \"" + escapeJsonString(url) + "\")"; } - private void printInputObject(PrintWriter out, Object type) { - if (access.isIntrospectionType(type)) { + private void printInputObject( + PrintWriter out, + SchemaInputObject type) { + if (isIntrospectionType(type)) { return; } printComments(out, type, ""); out.format( "input %s%s", - access.getName(type), + type.getName(), directivesString( type, - access.getAppliedDirectives(type), + schema.getAppliedDirectives(type), false)); - List fields = access.sort( + List fields = sort( type, - SchemaPrintChildKind.INPUT_FIELD, - access.getInputFields(type)); - fields.removeIf(field -> !access.isIncluded(field)); + schema.getInputFields(type), + GraphQLInputObjectField.class, + false); + fields.removeIf(field -> !isIncluded(field)); if (!fields.isEmpty()) { out.format(" {\n"); - for (Object field : fields) { + for (SchemaInputField field : fields) { printInputField(out, field); } out.format("}"); @@ -284,178 +361,500 @@ private void printInputObject(PrintWriter out, Object type) { out.format("\n\n"); } - private void printInputField(PrintWriter out, Object field) { + private void printInputField( + PrintWriter out, + SchemaInputField field) { printComments(out, field, " "); - Object type = access.getType(field); + SchemaInputType type = field.getType(); out.format( " %s: %s", - access.getName(field), - access.getTypeString(type)); - InputValueWithState defaultValue = access.getDefaultValue(field); + field.getName(), + typeString(type)); + InputValueWithState defaultValue = + field.getInputFieldDefaultValue(); if (defaultValue.isSet()) { - out.format(" = %s", access.printValue(defaultValue, type)); + out.format(" = %s", printValue(defaultValue, type)); } out.print(directivesString( field, - access.getAppliedDirectives(field), + schema.getAppliedDirectives(field), false)); out.format("\n"); } - private void printDirectiveDefinition(PrintWriter out, Object directive) { + private void printDirectiveDefinition( + PrintWriter out, + SchemaDirective directive) { if (!options.isIncludeDirectiveDefinitions() - || !options.getIncludeDirective().test(access.getName(directive)) + || !options.getIncludeDirective().test(directive.getName()) || !options.getIncludeDirectiveDefinition() - .test(access.getName(directive))) { + .test(directive.getName())) { return; } printComments(out, directive, ""); out.format( "directive @%s%s%s", - access.getName(directive), + directive.getName(), argsString(directive), directivesString( directive, - access.getAppliedDirectives(directive), + schema.getAppliedDirectives(directive), false)); - if (access.isRepeatable(directive)) { + if (directive.isRepeatable()) { out.print(" repeatable"); } - String locations = access.getDirectiveLocations(directive).stream() + String locations = directive.validLocations().stream() .map(DirectiveLocation::name) .collect(Collectors.joining(" | ")); out.format(" on %s\n\n", locations); } - private String argsString(Object parent) { - List arguments = access.getArguments(parent); - boolean hasComments = arguments.stream() + private String argsString(SchemaNamedElement parent) { + List parentArguments = + getArguments(parent); + boolean hasComments = parentArguments.stream() .anyMatch(this::hasAstDefinitionComments); - boolean hasDescriptions = arguments.stream() + boolean hasDescriptions = parentArguments.stream() .anyMatch(this::hasDescription); String halfPrefix = hasComments || hasDescriptions ? " " : ""; String prefix = hasComments || hasDescriptions ? " " : ""; - arguments = access.sort( + List arguments = sort( parent, - SchemaPrintChildKind.ARGUMENT, - arguments); - arguments.removeIf(argument -> !access.isIncluded(argument)); + parentArguments, + GraphQLArgument.class, + false); + arguments.removeIf(argument -> !isIncluded(argument)); if (arguments.isEmpty()) { return ""; } + return argumentsString( + arguments, + halfPrefix, + prefix, + hasComments || hasDescriptions); + } + private List getArguments( + SchemaNamedElement parent) { + if (parent instanceof SchemaField) { + return ((SchemaField) parent).getArguments(); + } + return ((SchemaDirective) parent).getArguments(); + } + + private String argumentsString( + List arguments, + String halfPrefix, + String prefix, + boolean multiline) { StringBuilder result = new StringBuilder("("); for (int i = 0; i < arguments.size(); i++) { - Object argument = arguments.get(i); - if (i > 0) { - result.append(","); - if (!hasComments && !hasDescriptions) { - result.append(" "); - } - } - if (hasComments || hasDescriptions) { - result.append("\n"); - } + appendArgumentSeparator(result, i, multiline); + SchemaArgument argument = arguments.get(i); result.append(commentsString(argument, prefix)); - Object type = access.getType(argument); + SchemaInputType type = argument.getType(); result.append(prefix) - .append(access.getName(argument)) + .append(argument.getName()) .append(": ") - .append(access.getTypeString(type)); - InputValueWithState defaultValue = access.getDefaultValue(argument); - if (defaultValue.isSet()) { - result.append(" = ") - .append(access.printValue(defaultValue, type)); - } + .append(typeString(type)); + appendDefaultValue(result, argument, type); result.append(directivesString( argument, - access.getAppliedDirectives(argument), + schema.getAppliedDirectives(argument), false)); } - if (hasComments || hasDescriptions) { + if (multiline) { result.append("\n"); } return result.append(halfPrefix).append(")").toString(); } + private void appendArgumentSeparator( + StringBuilder result, + int index, + boolean multiline) { + if (index > 0) { + result.append(","); + if (!multiline) { + result.append(" "); + } + } + if (multiline) { + result.append("\n"); + } + } + + private void appendDefaultValue( + StringBuilder result, + SchemaArgument argument, + SchemaInputType type) { + InputValueWithState defaultValue = + argument.getArgumentDefaultValue(); + if (!defaultValue.isSet()) { + return; + } + result.append(" = ").append(printValue(defaultValue, type)); + } + private String directivesString( - Object parent, - List directives, + @Nullable SchemaNamedElement parent, + List directives, boolean schemaContainer) { - directives = directives.stream() - .filter(access::isIncluded) + List included = directives.stream() + .filter(this::isIncluded) .filter(directive -> options.getIncludeDirective() - .test(access.getName(directive))) - .collect(Collectors.toList()); - if (directives.isEmpty()) { + .test(directive.getName())) + .collect(Collectors.toCollection(ArrayList::new)); + if (included.isEmpty()) { return ""; } - directives = access.sort( + included = sort( parent, - SchemaPrintChildKind.APPLIED_DIRECTIVE, - directives); - String result = directives.stream() + included, + GraphQLAppliedDirective.class, + false); + String result = included.stream() .map(this::directiveString) .collect(Collectors.joining(" ")); return schemaContainer ? result : " " + result; } - private String directiveString(Object directive) { + private String directiveString(SchemaAppliedDirective directive) { StringBuilder result = new StringBuilder("@") - .append(access.getName(directive)); - List arguments = access.getAppliedDirectiveArguments(directive) - .stream() - .filter(argument -> access.getAppliedDirectiveArgumentValue(argument) - .isSet()) - .collect(Collectors.toList()); - arguments = access.sort( + .append(directive.getName()); + List arguments = + directive.getArguments().stream() + .filter(argument -> argument.getArgumentValue().isSet()) + .collect(Collectors.toCollection(ArrayList::new)); + arguments = sort( directive, - SchemaPrintChildKind.APPLIED_DIRECTIVE_ARGUMENT, - arguments); + arguments, + GraphQLAppliedDirectiveArgument.class, + false); if (arguments.isEmpty()) { return result.toString(); } result.append("("); for (int i = 0; i < arguments.size(); i++) { - Object argument = arguments.get(i); if (i > 0) { result.append(", "); } - result.append(access.getName(argument)) + SchemaAppliedDirectiveArgument argument = arguments.get(i); + result.append(argument.getName()) .append(" : ") - .append(access.printValue( - access.getAppliedDirectiveArgumentValue(argument), - access.getType(argument))); + .append(printValue( + argument.getArgumentValue(), + argument.getType())); } return result.append(")").toString(); } + private String typeString(SchemaType type) { + if (type instanceof SchemaList) { + return "[" + typeString( + ((SchemaList) type).getWrappedType()) + "]"; + } + if (type instanceof SchemaNonNull) { + return typeString( + ((SchemaNonNull) type).getWrappedType()) + "!"; + } + return ((SchemaNamedType) type).getName(); + } + + private String printValue( + InputValueWithState value, + SchemaInputType type) { + if (type instanceof GraphQLType) { + return AstPrinter.printAst( + graphql.execution.ValuesResolver.valueToLiteral( + value, + (GraphQLType) type, + GraphQLContext.getDefault(), + Locale.getDefault())); + } + if (value.isLiteral()) { + return AstPrinter.printAst( + (Value) assertNotNull(value.getValue())); + } + Value literal = valueToLiteral( + value.getValue(), + type, + value.isInternal()); + return AstPrinter.printAst(literal); + } + + private Value valueToLiteral( + @Nullable Object value, + SchemaInputType type, + boolean internal) { + SchemaInputType unwrappedType = unwrapNonNull(type); + if (value == null) { + return NullValue.of(); + } + if (unwrappedType instanceof SchemaList) { + return listLiteral( + value, + (SchemaList) unwrappedType, + internal); + } + if (unwrappedType instanceof SchemaInputObject) { + return objectLiteral( + value, + (SchemaInputObject) unwrappedType, + internal); + } + if (unwrappedType instanceof SchemaEnum) { + return EnumValue.newEnumValue(String.valueOf(value)).build(); + } + return scalarLiteral( + value, + (SchemaScalar) unwrappedType, + internal); + } + + private SchemaInputType unwrapNonNull(SchemaInputType type) { + if (!(type instanceof SchemaNonNull)) { + return type; + } + return (SchemaInputType) + ((SchemaModifiedType) type).getWrappedType(); + } + + private Value listLiteral( + Object value, + SchemaList type, + boolean internal) { + SchemaInputType wrappedType = + (SchemaInputType) type.getWrappedType(); + List values = new ArrayList<>(); + for (Object item : FpKit.toListOrSingletonList(value)) { + values.add(valueToLiteral(item, wrappedType, internal)); + } + return ArrayValue.newArrayValue().values(values).build(); + } + + private Value objectLiteral( + Object value, + SchemaInputObject type, + boolean internal) { + if (!(value instanceof Map)) { + return assertShouldNeverHappen( + "Cannot print value '%s' for input object '%s' " + + "without a map", + value, + type.getName()); + } + Map map = (Map) value; + List fields = new ArrayList<>(); + for (SchemaInputField field : schema.getInputFields(type)) { + if (!map.containsKey(field.getName())) { + continue; + } + fields.add(ObjectField.newObjectField() + .name(field.getName()) + .value(valueToLiteral( + map.get(field.getName()), + field.getType(), + internal)) + .build()); + } + return ObjectValue.newObjectValue().objectFields(fields).build(); + } + + private Value scalarLiteral( + Object value, + SchemaScalar type, + boolean internal) { + Object externalValue = internal + ? schema.getScalarCoercing(type).serialize( + value, + GraphQLContext.getDefault(), + Locale.getDefault()) + : value; + return schema.getScalarCoercing(type).valueToLiteral( + assertNotNull(externalValue), + GraphQLContext.getDefault(), + Locale.getDefault()); + } + + private boolean isIntrospectionType(SchemaNamedType type) { + return !options.isIncludeIntrospectionTypes() + && type.getName().startsWith("__"); + } + + private boolean isIncluded(SchemaNamedElement element) { + if (!(element instanceof GraphQLSchemaElement)) { + return true; + } + return options.getIncludeSchemaElement() + .test((GraphQLSchemaElement) element); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private List sort( + @Nullable SchemaNamedElement parent, + List elements, + @Nullable Class elementType, + boolean topLevel) { + List result = new ArrayList<>(elements); + if (result.size() < 2) { + return result; + } + if (result.get(0) instanceof GraphQLSchemaElement) { + Class parentType = + graphQLParentClass(parent); + GraphqlTypeComparatorEnvironment environment = + GraphqlTypeComparatorEnvironment.newEnvironment() + .parentType(parentType) + .elementType(elementType) + .build(); + Comparator comparator = options.getComparatorRegistry() + .getComparator(environment); + result.sort(comparator); + return result; + } + if (options.getComparatorRegistry() + == GraphqlTypeComparatorRegistry.AS_IS_REGISTRY) { + return result; + } + Comparator comparator = + Comparator.comparing(SchemaNamedElement::getName); + if (topLevel + && options.getComparatorRegistry() + != GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) { + comparator = Comparator + .comparingInt((T element) -> topLevelRank(element)) + .thenComparing(SchemaNamedElement::getName); + } + result.sort(comparator); + return result; + } + + private Class graphQLParentClass( + @Nullable SchemaNamedElement parent) { + if (parent == null) { + return GraphQLSchemaElement.class; + } + if (parent instanceof SchemaObject) { + return GraphQLObjectType.class; + } + if (parent instanceof SchemaInterface) { + return GraphQLInterfaceType.class; + } + if (parent instanceof SchemaUnion) { + return GraphQLUnionType.class; + } + if (parent instanceof SchemaEnum) { + return graphql.schema.GraphQLEnumType.class; + } + if (parent instanceof SchemaInputObject) { + return graphql.schema.GraphQLInputObjectType.class; + } + if (parent instanceof SchemaField) { + return GraphQLFieldDefinition.class; + } + if (parent instanceof SchemaDirective) { + return GraphQLDirective.class; + } + if (parent instanceof SchemaAppliedDirective) { + return GraphQLAppliedDirective.class; + } + return GraphQLSchemaElement.class; + } + + private int topLevelRank(SchemaNamedElement element) { + if (element instanceof SchemaDirective) { + return 1; + } + if (element instanceof SchemaInterface) { + return 2; + } + if (element instanceof SchemaUnion) { + return 3; + } + if (element instanceof SchemaObject) { + return 4; + } + if (element instanceof SchemaEnum) { + return 5; + } + if (element instanceof SchemaScalar) { + return 6; + } + if (element instanceof SchemaInputObject) { + return 7; + } + return 0; + } + private void printComments( PrintWriter out, - Object element, + SchemaNamedElement element, String prefix) { out.print(commentsString(element, prefix)); } - private String commentsString(Object element, String prefix) { + private String commentsString( + SchemaNamedElement element, + String prefix) { + return commentsString( + description(element), + element.getDefinition(), + prefix); + } + + private void printComments( + PrintWriter out, + @Nullable String description, + @Nullable Node definition, + String prefix) { + out.print(commentsString(description, definition, prefix)); + } + + private String commentsString( + @Nullable String description, + @Nullable Node definition, + String prefix) { StringWriter stringWriter = new StringWriter(); PrintWriter out = new PrintWriter(stringWriter); - String description = access.getDescription(element); if (description != null && !description.isEmpty()) { printDescription(out, prefix, description); } if (options.isIncludeAstDefinitionComments()) { - String comments = access.getAstDefinitionComments(element); - if (comments != null && !comments.isEmpty()) { - printHashDescription( - out, - prefix, - Arrays.asList(comments.split("\n"))); - } + printAstDefinitionComments(out, definition, prefix); } return stringWriter.toString(); } + private @Nullable String description(SchemaNamedElement element) { + String description = element.getDescription(); + if (description != null && !description.isEmpty()) { + return description; + } + Node definition = element.getDefinition(); + if (!(definition instanceof DescribedNode)) { + return description; + } + graphql.language.Description astDescription = + ((DescribedNode) definition).getDescription(); + return astDescription == null + ? description + : astDescription.getContent(); + } + + private void printAstDefinitionComments( + PrintWriter out, + @Nullable Node definition, + String prefix) { + if (definition == null || definition.getComments().isEmpty()) { + return; + } + List comments = definition.getComments().stream() + .map(Comment::getContent) + .collect(Collectors.toList()); + printHashDescription(out, prefix, comments); + } + private void printDescription( PrintWriter out, String prefix, @@ -479,7 +878,10 @@ private void printDescription( out.printf("%s\"\"\"\n", prefix); return; } - out.printf("%s\"%s\"\n", prefix, escapeJsonString(lines.get(0))); + out.printf( + "%s\"%s\"\n", + prefix, + escapeJsonString(lines.get(0))); } private void printHashDescription( @@ -491,17 +893,18 @@ private void printHashDescription( } } - private boolean hasDescription(Object element) { - String description = access.getDescription(element); + private boolean hasDescription(SchemaNamedElement element) { + String description = description(element); return description != null && !description.isEmpty(); } - private boolean hasAstDefinitionComments(Object element) { + private boolean hasAstDefinitionComments( + SchemaNamedElement element) { if (!options.isIncludeAstDefinitionComments()) { return false; } - String comments = access.getAstDefinitionComments(element); - return comments != null && !comments.isEmpty(); + Node definition = element.getDefinition(); + return definition != null && !definition.getComments().isEmpty(); } private String trimNewLineChars(String value) { diff --git a/src/main/java/graphql/schema/universe/SUSchemaPrinter.java b/src/main/java/graphql/schema/universe/SUSchemaPrinter.java deleted file mode 100644 index 2b0abab422..0000000000 --- a/src/main/java/graphql/schema/universe/SUSchemaPrinter.java +++ /dev/null @@ -1,31 +0,0 @@ -package graphql.schema.universe; - -import graphql.ExperimentalApi; -import graphql.schema.idl.SchemaPrinter; - -import static graphql.Assert.assertNotNull; - -/** - * Prints a {@link SUSchema} directly as canonical semantic SDL. - * - *

Literal values are preserved directly. Non-literal values for GraphQL specified scalars are - * converted without a runtime schema; non-literal custom scalar values cannot be printed because - * a schema universe does not retain scalar coercing implementations.

- */ -@ExperimentalApi -public final class SUSchemaPrinter { - - private final SchemaPrinter schemaPrinter; - - public SUSchemaPrinter() { - this(SchemaPrinter.Options.defaultOptions()); - } - - public SUSchemaPrinter(SchemaPrinter.Options options) { - schemaPrinter = new SchemaPrinter(assertNotNull(options)); - } - - public String print(SUSchema schema) { - return schemaPrinter.print(assertNotNull(schema)); - } -} diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java index 27a5504f3b..24eb327611 100644 --- a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java @@ -1,7 +1,13 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.language.Node; +import graphql.schema.universe.SUAppliedDirective; +import graphql.schema.universe.SUArgument; +import graphql.schema.universe.SUDirective; +import graphql.schema.universe.SUInputField; import graphql.schema.universe.SUVertex; +import org.jspecify.annotations.Nullable; import static graphql.Assert.assertNotNull; @@ -11,19 +17,19 @@ @Internal public abstract class AbstractSUSchemaElement { - private final SUSchemaExecutableSchema executableSchema; + private final SUExecutableSchema executableSchema; private final SUVertex vertex; @Internal public AbstractSUSchemaElement( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUVertex vertex) { this.executableSchema = assertNotNull(executableSchema); this.vertex = assertNotNull(vertex); } @Internal - public final SUSchemaExecutableSchema getExecutableSchema() { + public final SUExecutableSchema getExecutableSchema() { return executableSchema; } @@ -32,6 +38,36 @@ public final SUVertex getVertex() { return vertex; } + /** + * Returns the description stored on the schema-universe vertex. + * + * @return the description, or {@code null} when absent + */ + public final @Nullable String getDescription() { + return vertex.getDescription(); + } + + /** + * Returns the source definition stored by vertex kinds that retain one. + * + * @return the source definition, or {@code null} when unavailable + */ + public @Nullable Node getDefinition() { + if (vertex instanceof SUArgument) { + return ((SUArgument) vertex).getDefinition(); + } + if (vertex instanceof SUInputField) { + return ((SUInputField) vertex).getDefinition(); + } + if (vertex instanceof SUDirective) { + return ((SUDirective) vertex).getDefinition(); + } + if (vertex instanceof SUAppliedDirective) { + return ((SUAppliedDirective) vertex).getDefinition(); + } + return null; + } + @Override public final boolean equals(Object other) { if (this == other) { diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java index f3109c33e9..f26abaefbc 100644 --- a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java @@ -15,7 +15,7 @@ public abstract class AbstractSUSchemaNamedType @Internal public AbstractSUSchemaNamedType( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUNamedType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java index 386224959b..9b348238ae 100644 --- a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaType.java @@ -13,7 +13,7 @@ public abstract class AbstractSUSchemaType @Internal public AbstractSUSchemaType( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchema.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java similarity index 72% rename from src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchema.java rename to src/main/java/graphql/schema/universe/view/SUExecutableSchema.java index a501d4a645..a334513bc4 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchema.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java @@ -2,25 +2,33 @@ import graphql.ExperimentalApi; import graphql.Internal; +import graphql.Scalars; import graphql.introspection.Introspection; +import graphql.language.SchemaDefinition; import graphql.schema.Coercing; import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLNamedType; import graphql.schema.GraphQLNonNull; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; +import graphql.schema.SchemaAppliedDirective; import graphql.schema.SchemaComposite; import graphql.schema.SchemaDirective; +import graphql.schema.SchemaDirectiveContainer; import graphql.schema.SchemaField; import graphql.schema.SchemaFieldsContainer; import graphql.schema.SchemaInputField; import graphql.schema.SchemaInputObject; import graphql.schema.SchemaInputType; +import graphql.schema.SchemaInterface; +import graphql.schema.SchemaNamedType; import graphql.schema.SchemaObject; import graphql.schema.SchemaOutputType; import graphql.schema.SchemaScalar; import graphql.schema.SchemaType; +import graphql.schema.SchemaUnion; import graphql.schema.universe.PersistentIntMap; +import graphql.schema.universe.SUAppliedDirective; import graphql.schema.universe.SUCompositeType; import graphql.schema.universe.SUDirective; import graphql.schema.universe.SUEnumType; @@ -55,27 +63,27 @@ * type-system topology. The adapter has the same supported lifetime as its underlying schema.

*/ @ExperimentalApi -public final class SUSchemaExecutableSchema implements ExecutableSchema { +public final class SUExecutableSchema implements ExecutableSchema { private final SUSchema schema; private final PersistentIntMap> coercingByScalarId; @Internal - public SUSchemaExecutableSchema( + public SUExecutableSchema( SUSchema schema, PersistentIntMap> coercingByScalarId) { this.schema = assertNotNull(schema); this.coercingByScalarId = assertNotNull(coercingByScalarId); } - public static SUSchemaExecutableSchemaBuilder newExecutableSchema( + public static SUExecutableSchemaBuilder newExecutableSchema( SUSchema schema) { - return new SUSchemaExecutableSchemaBuilder(schema); + return new SUExecutableSchemaBuilder(schema); } - public static SUSchemaExecutableSchemaBuilder newExecutableSchema( - SUSchemaExecutableSchema executableSchema) { - return new SUSchemaExecutableSchemaBuilder(executableSchema); + public static SUExecutableSchemaBuilder newExecutableSchema( + SUExecutableSchema executableSchema) { + return new SUExecutableSchemaBuilder(executableSchema); } /** @@ -86,7 +94,7 @@ public static SUSchemaExecutableSchemaBuilder newExecutableSchema( * * @return the executable universe schema */ - public static SUSchemaExecutableSchema fromGraphQLSchema( + public static SUExecutableSchema fromGraphQLSchema( SUSchema schema, GraphQLSchema graphQLSchema) { return newExecutableSchema(schema) @@ -94,9 +102,9 @@ public static SUSchemaExecutableSchema fromGraphQLSchema( .build(); } - public SUSchemaExecutableSchema transform( - Consumer builderConsumer) { - SUSchemaExecutableSchemaBuilder builder = + public SUExecutableSchema transform( + Consumer builderConsumer) { + SUExecutableSchemaBuilder builder = newExecutableSchema(this); assertNotNull(builderConsumer).accept(builder); return builder.build(); @@ -106,6 +114,16 @@ public SUSchema getSchema() { return schema; } + @Override + public @Nullable String getDescription() { + return schema.getRoot().getDescription(); + } + + @Override + public @Nullable SchemaDefinition getDefinition() { + return null; + } + @Override public SchemaObject getQueryType() { return new SUSchemaObject(this, schema.getQueryType()); @@ -148,6 +166,63 @@ public SchemaObject getQueryType() { return new SUSchemaDirective(this, directive); } + @Override + public List getTypes() { + List types = schema.getTypes(); + List result = new ArrayList<>(types.size()); + for (SUNamedType type : types) { + result.add((SchemaNamedType) adaptType(type)); + } + return Collections.unmodifiableList(result); + } + + @Override + public List getDirectives() { + List directives = schema.getDirectiveDefinitions(); + List result = + new ArrayList<>(directives.size()); + for (SUDirective directive : directives) { + result.add(new SUSchemaDirective(this, directive)); + } + return Collections.unmodifiableList(result); + } + + @Override + public List getAppliedDirectives() { + return adaptAppliedDirectives(schema.getSchemaAppliedDirectives()); + } + + @Override + public List getAppliedDirectives( + SchemaDirectiveContainer container) { + if (container instanceof SUSchemaIntrospectionField) { + assertTrue( + ((SUSchemaIntrospectionField) container) + .getExecutableSchema() == this, + "The directive container must belong to this SUSchema"); + return Collections.emptyList(); + } + if (container instanceof SUSchemaIntrospectionArgument) { + assertTrue( + ((SUSchemaIntrospectionArgument) container) + .getExecutableSchema() == this, + "The directive container must belong to this SUSchema"); + return Collections.emptyList(); + } + return adaptAppliedDirectives( + schema.getAppliedDirectives(elementVertex(container))); + } + + private List adaptAppliedDirectives( + List directives) { + List result = + new ArrayList<>(directives.size()); + for (SUAppliedDirective directive : directives) { + result.add(new SUSchemaAppliedDirective(this, directive)); + } + return Collections.unmodifiableList(result); + } + @Override public SchemaObject getIntrospectionSchemaType() { return new SUSchemaObject( @@ -288,6 +363,40 @@ public List getInputFields( return Collections.unmodifiableList(result); } + @Override + public List getInterfaces( + SchemaFieldsContainer implementingType) { + SUVertex vertex = elementVertex(implementingType); + List interfaces; + if (vertex instanceof SUObjectType) { + interfaces = schema.getInterfaces((SUObjectType) vertex); + } else { + assertTrue( + vertex instanceof SUInterfaceType, + "Expected an object or interface type"); + interfaces = schema.getInterfaces((SUInterfaceType) vertex); + } + List result = + new ArrayList<>(interfaces.size()); + for (SUInterfaceType interfaceType : interfaces) { + result.add(new SUSchemaInterface(this, interfaceType)); + } + return Collections.unmodifiableList(result); + } + + @Override + public List getUnionMembers(SchemaUnion unionType) { + SUVertex vertex = elementVertex(unionType); + assertTrue(vertex instanceof SUUnionType, "Expected a union type"); + List members = + schema.getUnionMembers((SUUnionType) vertex); + List result = new ArrayList<>(members.size()); + for (SUObjectType member : members) { + result.add(new SUSchemaObject(this, member)); + } + return Collections.unmodifiableList(result); + } + @Override public List getPossibleTypes( SchemaComposite compositeType) { @@ -316,12 +425,37 @@ public boolean isPossibleType( assertTrue( vertex instanceof SUScalarType, "The scalar type must belong to this SUSchema"); + Coercing coercing = coercingByScalarId.get(vertex.getId()); + if (coercing != null) { + return coercing; + } + Coercing specifiedCoercing = + specifiedScalarCoercing(assertNotNull(vertex.getName())); return assertNotNull( - coercingByScalarId.get(vertex.getId()), + specifiedCoercing, "No coercing is registered for scalar '%s'", assertNotNull(vertex.getName())); } + private @Nullable Coercing specifiedScalarCoercing(String name) { + if (Scalars.GraphQLString.getName().equals(name)) { + return Scalars.GraphQLString.getCoercing(); + } + if (Scalars.GraphQLBoolean.getName().equals(name)) { + return Scalars.GraphQLBoolean.getCoercing(); + } + if (Scalars.GraphQLInt.getName().equals(name)) { + return Scalars.GraphQLInt.getCoercing(); + } + if (Scalars.GraphQLFloat.getName().equals(name)) { + return Scalars.GraphQLFloat.getCoercing(); + } + if (Scalars.GraphQLID.getName().equals(name)) { + return Scalars.GraphQLID.getCoercing(); + } + return null; + } + @Internal public SchemaType adaptType(SUType type) { if (type instanceof SUObjectType) { diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchemaBuilder.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java similarity index 86% rename from src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchemaBuilder.java rename to src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java index 827687b200..5053a27fd1 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaExecutableSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java @@ -19,23 +19,23 @@ */ @ExperimentalApi @NullUnmarked -public final class SUSchemaExecutableSchemaBuilder { +public final class SUExecutableSchemaBuilder { private final SUSchema schema; private PersistentIntMap> coercingByScalarId; - public SUSchemaExecutableSchemaBuilder(SUSchema schema) { + public SUExecutableSchemaBuilder(SUSchema schema) { this.schema = assertNotNull(schema); this.coercingByScalarId = PersistentIntMap.empty(); } - public SUSchemaExecutableSchemaBuilder( - SUSchemaExecutableSchema executableSchema) { + public SUExecutableSchemaBuilder( + SUExecutableSchema executableSchema) { this.schema = assertNotNull(executableSchema).getSchema(); this.coercingByScalarId = executableSchema.getCoercingByScalarId(); } - public SUSchemaExecutableSchemaBuilder scalarCoercing( + public SUExecutableSchemaBuilder scalarCoercing( SUScalarType scalarType, Coercing coercing) { SUScalarType scalar = assertNotNull(scalarType); @@ -49,7 +49,7 @@ public SUSchemaExecutableSchemaBuilder scalarCoercing( return this; } - public SUSchemaExecutableSchemaBuilder scalarCoercing( + public SUExecutableSchemaBuilder scalarCoercing( String scalarName, Coercing coercing) { SUScalarType scalarType = schema.getScalarType( @@ -70,7 +70,7 @@ public SUSchemaExecutableSchemaBuilder scalarCoercing( * * @return this builder */ - public SUSchemaExecutableSchemaBuilder scalarCoercings( + public SUExecutableSchemaBuilder scalarCoercings( GraphQLSchema graphQLSchema) { GraphQLSchema source = assertNotNull(graphQLSchema); for (SUNamedType type : schema.getTypes()) { @@ -96,8 +96,8 @@ private void copyScalarCoercing( ((GraphQLScalarType) sourceType).getCoercing()); } - public SUSchemaExecutableSchema build() { - return new SUSchemaExecutableSchema( + public SUExecutableSchema build() { + return new SUExecutableSchema( schema, coercingByScalarId); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirective.java b/src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirective.java new file mode 100644 index 0000000000..1b91958ed0 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirective.java @@ -0,0 +1,62 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.schema.SchemaAppliedDirective; +import graphql.schema.SchemaAppliedDirectiveArgument; +import graphql.schema.universe.SUAppliedDirective; +import graphql.schema.universe.SUAppliedDirectiveArgument; +import org.jspecify.annotations.Nullable; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaAppliedDirective + extends AbstractSUSchemaElement implements SchemaAppliedDirective { + + @Internal + public SUSchemaAppliedDirective( + SUExecutableSchema executableSchema, + SUAppliedDirective directive) { + super(executableSchema, directive); + } + + @Override + public String getName() { + return assertNotNull(getVertex().getName()); + } + + @Override + public List getArguments() { + List arguments = + getDirectiveVertex().getArguments(); + List result = + new ArrayList<>(arguments.size()); + for (SUAppliedDirectiveArgument argument : arguments) { + result.add(new SUSchemaAppliedDirectiveArgument( + getExecutableSchema(), + argument)); + } + return Collections.unmodifiableList(result); + } + + @Override + public @Nullable SchemaAppliedDirectiveArgument getArgument(String name) { + SUAppliedDirectiveArgument argument = + getDirectiveVertex().getArgument(assertNotNull(name)); + if (argument == null) { + return null; + } + return new SUSchemaAppliedDirectiveArgument( + getExecutableSchema(), + argument); + } + + @Internal + public SUAppliedDirective getDirectiveVertex() { + return (SUAppliedDirective) getVertex(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirectiveArgument.java b/src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirectiveArgument.java new file mode 100644 index 0000000000..fa1e454e47 --- /dev/null +++ b/src/main/java/graphql/schema/universe/view/SUSchemaAppliedDirectiveArgument.java @@ -0,0 +1,54 @@ +package graphql.schema.universe.view; + +import graphql.Internal; +import graphql.language.Argument; +import graphql.schema.InputValueWithState; +import graphql.schema.SchemaAppliedDirectiveArgument; +import graphql.schema.SchemaInputType; +import graphql.schema.universe.SUAppliedDirectiveArgument; +import graphql.schema.universe.SUType; +import org.jspecify.annotations.Nullable; + +import static graphql.Assert.assertNotNull; + +@Internal +public final class SUSchemaAppliedDirectiveArgument + implements SchemaAppliedDirectiveArgument { + + private final SUExecutableSchema executableSchema; + private final SUAppliedDirectiveArgument argument; + + @Internal + public SUSchemaAppliedDirectiveArgument( + SUExecutableSchema executableSchema, + SUAppliedDirectiveArgument argument) { + this.executableSchema = assertNotNull(executableSchema); + this.argument = assertNotNull(argument); + } + + @Override + public String getName() { + return argument.getName(); + } + + @Override + public @Nullable String getDescription() { + return null; + } + + @Override + public @Nullable Argument getDefinition() { + return argument.getDefinition(); + } + + @Override + public SchemaInputType getType() { + SUType type = executableSchema.getSchema().getType(argument); + return executableSchema.adaptInputType(type); + } + + @Override + public InputValueWithState getArgumentValue() { + return argument.getArgumentValue(); + } +} diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaArgument.java b/src/main/java/graphql/schema/universe/view/SUSchemaArgument.java index 65b56f16d4..a6c8004782 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaArgument.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaArgument.java @@ -15,7 +15,7 @@ public final class SUSchemaArgument @Internal public SUSchemaArgument( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUArgument argument) { super(executableSchema, argument); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java b/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java index e0ac80ade4..1891a56420 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java @@ -21,7 +21,7 @@ public final class SUSchemaDirective @Internal public SUSchemaDirective( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUDirective directive) { super(executableSchema, directive); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java b/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java index 1dba280d17..e30cfe7762 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java @@ -16,7 +16,7 @@ public final class SUSchemaEnum @Internal public SUSchemaEnum( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUEnumType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java b/src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java index 3db388d831..154359c436 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaEnumValue.java @@ -12,7 +12,7 @@ public final class SUSchemaEnumValue @Internal public SUSchemaEnumValue( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUEnumValue value) { super(executableSchema, value); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaField.java b/src/main/java/graphql/schema/universe/view/SUSchemaField.java index fdf3061138..9e2e2a4967 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaField.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaField.java @@ -21,7 +21,7 @@ public final class SUSchemaField @Internal public SUSchemaField( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUField field) { super(executableSchema, field); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInputField.java b/src/main/java/graphql/schema/universe/view/SUSchemaInputField.java index 1a358c9c27..3f8bee9cfb 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaInputField.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInputField.java @@ -15,7 +15,7 @@ public final class SUSchemaInputField @Internal public SUSchemaInputField( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUInputField field) { super(executableSchema, field); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java b/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java index 4d2139b9d9..c2ac15c4b8 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java @@ -11,7 +11,7 @@ public final class SUSchemaInputObject @Internal public SUSchemaInputObject( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUInputObjectType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java index f6f074a9aa..b093870cd6 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java @@ -10,7 +10,7 @@ public final class SUSchemaInterface @Internal public SUSchemaInterface( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUInterfaceType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java index da84832a5c..b49a07ebda 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java @@ -1,22 +1,24 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.language.InputValueDefinition; import graphql.schema.GraphQLArgument; import graphql.schema.InputValueWithState; import graphql.schema.SchemaArgument; import graphql.schema.SchemaInputType; +import org.jspecify.annotations.Nullable; import static graphql.Assert.assertNotNull; @Internal public final class SUSchemaIntrospectionArgument implements SchemaArgument { - private final SUSchemaExecutableSchema executableSchema; + private final SUExecutableSchema executableSchema; private final GraphQLArgument argument; @Internal public SUSchemaIntrospectionArgument( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, GraphQLArgument argument) { this.executableSchema = assertNotNull(executableSchema); this.argument = assertNotNull(argument); @@ -27,6 +29,16 @@ public String getName() { return argument.getName(); } + @Override + public @Nullable String getDescription() { + return argument.getDescription(); + } + + @Override + public @Nullable InputValueDefinition getDefinition() { + return argument.getDefinition(); + } + @Override public SchemaInputType getType() { return executableSchema.adaptGraphQLInputType(argument.getType()); @@ -36,4 +48,9 @@ public SchemaInputType getType() { public InputValueWithState getArgumentDefaultValue() { return argument.getArgumentDefaultValue(); } + + @Internal + public SUExecutableSchema getExecutableSchema() { + return executableSchema; + } } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java index 3d2f68a956..79923aa72c 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java @@ -1,6 +1,7 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.language.FieldDefinition; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.SchemaArgument; @@ -17,20 +18,20 @@ @Internal public final class SUSchemaIntrospectionField implements SchemaField { - private final SUSchemaExecutableSchema executableSchema; + private final SUExecutableSchema executableSchema; private final GraphQLFieldDefinition field; private final @Nullable SchemaOutputType type; @Internal public SUSchemaIntrospectionField( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, GraphQLFieldDefinition field) { this(executableSchema, field, null); } @Internal public SUSchemaIntrospectionField( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, GraphQLFieldDefinition field, @Nullable SchemaOutputType type) { this.executableSchema = assertNotNull(executableSchema); @@ -43,6 +44,16 @@ public String getName() { return field.getName(); } + @Override + public @Nullable String getDescription() { + return field.getDescription(); + } + + @Override + public @Nullable FieldDefinition getDefinition() { + return field.getDefinition(); + } + @Override public SchemaOutputType getType() { if (type != null) { @@ -73,4 +84,9 @@ public List getArguments() { executableSchema, argument); } + + @Internal + public SUExecutableSchema getExecutableSchema() { + return executableSchema; + } } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaList.java b/src/main/java/graphql/schema/universe/view/SUSchemaList.java index 280baeb38d..39dd91bad3 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaList.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaList.java @@ -14,7 +14,7 @@ public final class SUSchemaList @Internal public SUSchemaList( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUListType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java b/src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java index 37940f8118..9ff0003ab3 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaNonNull.java @@ -14,7 +14,7 @@ public final class SUSchemaNonNull @Internal public SUSchemaNonNull( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUNonNullType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaObject.java b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java index e59df9d5cd..73a943880a 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaObject.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java @@ -10,7 +10,7 @@ public final class SUSchemaObject @Internal public SUSchemaObject( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUObjectType type) { super(executableSchema, type); } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java b/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java index a0751ee0ef..b12d58e68a 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java @@ -1,8 +1,11 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.schema.SchemaAppliedDirective; +import graphql.schema.SchemaAppliedDirectiveArgument; import graphql.schema.SchemaScalar; import graphql.schema.universe.SUScalarType; +import org.jspecify.annotations.Nullable; @Internal public final class SUSchemaScalar @@ -10,8 +13,28 @@ public final class SUSchemaScalar @Internal public SUSchemaScalar( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUScalarType type) { super(executableSchema, type); } + + @Override + public @Nullable String getSpecifiedByUrl() { + for (SchemaAppliedDirective directive : + getExecutableSchema().getAppliedDirectives(this)) { + if (!"specifiedBy".equals(directive.getName())) { + continue; + } + SchemaAppliedDirectiveArgument url = directive.getArgument("url"); + if (url == null || url.getArgumentValue().isNotSet()) { + continue; + } + Object value = url.getArgumentValue().getValue(); + if (value instanceof graphql.language.StringValue) { + return ((graphql.language.StringValue) value).getValue(); + } + return value == null ? null : String.valueOf(value); + } + return null; + } } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java b/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java index eb86ced2c3..b69472bba8 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java @@ -10,7 +10,7 @@ public final class SUSchemaUnion @Internal public SUSchemaUnion( - SUSchemaExecutableSchema executableSchema, + SUExecutableSchema executableSchema, SUUnionType type) { super(executableSchema, type); } diff --git a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy index 5656d68369..cfe4628de0 100644 --- a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy @@ -1,11 +1,12 @@ package graphql.schema import graphql.AssertException +import graphql.Scalars import graphql.TestUtil import graphql.introspection.Introspection import graphql.introspection.IntrospectionWithDirectivesSupport import graphql.schema.universe.SchemaUniverse -import graphql.schema.universe.view.SUSchemaExecutableSchema +import graphql.schema.universe.view.SUExecutableSchema import graphql.schema.visibility.BlockedFields import spock.lang.Specification @@ -26,6 +27,21 @@ class ExecutableSchemaTest extends Specification { schema.getType("Choice") instanceof SchemaInputObject schema.getType("missing") == null + and: + schema.types*.name.containsAll([ + "Query", + "Mutation", + "Subscription", + "Node", + "Search", + "User", + "Date", + "Color", + "Choice" + ]) + schema.directives*.name.contains("tag") + schema.appliedDirectives.isEmpty() + and: def node = schema.getType("Node") as SchemaFieldsContainer schema.getFields(node)*.name == ["id"] @@ -61,6 +77,14 @@ class ExecutableSchemaTest extends Specification { color.getValue("GREEN").name == "GREEN" color.getValue("BLUE") == null + and: + def user = schema.getType("User") as SchemaFieldsContainer + schema.getInterfaces(user)*.name == ["Node"] + schema.getInterfaces(node).isEmpty() + schema.getUnionMembers( + schema.getType("Search") as SchemaUnion)*.name == ["User"] + schema.getAppliedDirectives(choice)*.name == ["oneOf"] + and: def tag = schema.getDirective("tag") tag.repeatable @@ -79,6 +103,42 @@ class ExecutableSchemaTest extends Specification { executableSchema << executableSchemas() } + def "GraphQLSchema and SUSchema expose normalized applied directives"() { + given: + ExecutableSchema schema = executableSchema + def query = schema.queryType + def field = schema.getField(query, "value") + def argument = field.getArgument("input") + def directive = schema.getDirective("configured") + + expect: + schema.appliedDirectives*.name == ["meta"] + schema.appliedDirectives[0].getArgument("value") + .argumentValue.value.value == "schema" + schema.getAppliedDirectives(query)*.name == ["meta"] + schema.getAppliedDirectives(field)*.name == ["meta"] + schema.getAppliedDirectives(argument)*.name == + ["meta", "deprecated"] + value(schema.getAppliedDirectives(argument) + .find { it.name == "deprecated" } + .getArgument("reason").argumentValue.value) == "Use other" + schema.getAppliedDirectives(directive)*.name == ["meta"] + + and: + def appliedArgument = schema.getAppliedDirectives(field)[0] + .getArgument("value") + appliedArgument instanceof SchemaAppliedDirectiveArgument + unwrap(appliedArgument.type).name == "String" + appliedArgument.definition != null + + and: + (schema.getType("Date") as SchemaScalar).specifiedByUrl == + "https://example.com/date" + + where: + executableSchema << appliedDirectiveSchemas() + } + def "GraphQLSchema and SUSchema expose possible types through the shared contract"() { given: ExecutableSchema schema = executableSchema @@ -149,7 +209,7 @@ class ExecutableSchemaTest extends Specification { .apply(TestUtil.schema("type Query { value: String }")) def universeSchema = new SchemaUniverse() .importSchema("enhanced", source) - ExecutableSchema schema = SUSchemaExecutableSchema + ExecutableSchema schema = SUExecutableSchema .fromGraphQLSchema(universeSchema, source) expect: @@ -161,6 +221,11 @@ class ExecutableSchemaTest extends Specification { .getArgument("name").type instanceof SchemaNonNull unwrap(schema.getField(schema.queryType, "__type") .getArgument("name").type).name == "String" + schema.getAppliedDirectives( + schema.getField(schema.queryType, "__schema")).isEmpty() + schema.getAppliedDirectives( + schema.getField(schema.queryType, "__type") + .getArgument("name")).isEmpty() and: schema.getField( @@ -204,7 +269,7 @@ class ExecutableSchemaTest extends Specification { def source = richSchema() def universeSchema = new SchemaUniverse() .importSchema("schema", source) - def original = SUSchemaExecutableSchema + def original = SUExecutableSchema .fromGraphQLSchema(universeSchema, source) Coercing replacement = Mock() @@ -222,9 +287,16 @@ class ExecutableSchemaTest extends Specification { .is(replacement) when: - def unbound = SUSchemaExecutableSchema + def unbound = SUExecutableSchema .newExecutableSchema(universeSchema) .build() + + then: + unbound.getScalarCoercing( + unbound.getType("String") as SchemaScalar) + .is(Scalars.GraphQLString.coercing) + + when: unbound.getScalarCoercing( unbound.getType("Date") as SchemaScalar) @@ -239,9 +311,9 @@ class ExecutableSchemaTest extends Specification { .importSchema("first", source) def secondUniverseSchema = new SchemaUniverse() .importSchema("second", source) - def first = SUSchemaExecutableSchema + def first = SUExecutableSchema .fromGraphQLSchema(firstUniverseSchema, source) - def second = SUSchemaExecutableSchema + def second = SUExecutableSchema .fromGraphQLSchema(secondUniverseSchema, source) when: @@ -260,7 +332,7 @@ class ExecutableSchemaTest extends Specification { thrown(AssertException) when: - SUSchemaExecutableSchema.newExecutableSchema(firstUniverseSchema) + SUExecutableSchema.newExecutableSchema(firstUniverseSchema) .scalarCoercing( secondUniverseSchema.getScalarType("Date"), source.getType("Date").coercing) @@ -273,7 +345,38 @@ class ExecutableSchemaTest extends Specification { def graphQLSchema = richSchema() def universeSchema = new SchemaUniverse() .importSchema("schema", graphQLSchema) - def suSchema = SUSchemaExecutableSchema + def suSchema = SUExecutableSchema + .fromGraphQLSchema(universeSchema, graphQLSchema) + return [graphQLSchema, suSchema] + } + + private static List appliedDirectiveSchemas() { + def graphQLSchema = TestUtil.schema(''' + directive @meta(value: String!) repeatable on + SCHEMA | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | + DIRECTIVE_DEFINITION + + directive @configured @meta(value: "directive") + on FIELD_DEFINITION + + schema @meta(value: "schema") { + query: Query + } + + scalar Date + @specifiedBy(url: "https://example.com/date") + + type Query @meta(value: "query") { + value( + input: String + @meta(value: "argument") + @deprecated(reason: "Use other") + ): Date @meta(value: "field") + } + ''') + def universeSchema = new SchemaUniverse() + .importSchema("applied", graphQLSchema) + def suSchema = SUExecutableSchema .fromGraphQLSchema(universeSchema, graphQLSchema) return [graphQLSchema, suSchema] } @@ -334,4 +437,11 @@ class ExecutableSchemaTest extends Specification { } return current } + + private static Object value(Object value) { + if (value instanceof graphql.language.StringValue) { + return value.value + } + return value + } } diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy similarity index 62% rename from src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy rename to src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy index 76f5087839..ec96904e14 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy @@ -1,13 +1,20 @@ package graphql.schema.universe +import graphql.Scalars +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema import graphql.schema.idl.SchemaParser import graphql.schema.idl.SchemaPrinter import graphql.schema.idl.TypeDefinitionRegistry import graphql.schema.idl.UnExecutableSchemaGenerator +import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification import spock.lang.Requires -class SUSchemaPrinterTest extends Specification { +class SUExecutableSchemaPrinterTest extends Specification { def "native SDL import and GraphQLSchema import print the same semantic schema"() { given: @@ -19,11 +26,14 @@ class SUSchemaPrinterTest extends Specification { new SchemaUniverse().importSchema("graphql", graphQLSchema) def options = SchemaPrinter.Options.defaultOptions() .includeSchemaDefinition(true) - def printer = new SUSchemaPrinter(options) + def printer = new SchemaPrinter(options) when: - def directSdl = printer.print(direct) - def viaGraphQLSdl = printer.print(viaGraphQL) + def directSdl = printer.print(executable(direct)) + def viaGraphQLSdl = printer.print( + SUExecutableSchema.fromGraphQLSchema( + viaGraphQL, + graphQLSchema)) def graphQLSdl = new SchemaPrinter(options).print(graphQLSchema) then: @@ -39,13 +49,13 @@ class SUSchemaPrinterTest extends Specification { given: def options = SchemaPrinter.Options.defaultOptions() .includeSchemaDefinition(true) - def printer = new SUSchemaPrinter(options) + def printer = new SchemaPrinter(options) def first = new SchemaUniverse().parseSchema("first", comprehensiveSdl()) when: - def firstSdl = printer.print(first) + def firstSdl = printer.print(executable(first)) def second = new SchemaUniverse().parseSchema("second", firstSdl) - def secondSdl = printer.print(second) + def secondSdl = printer.print(executable(second)) then: firstSdl == secondSdl @@ -55,6 +65,43 @@ class SUSchemaPrinterTest extends Specification { !firstSdl.contains("extend enum") } + def "executable schema printing uses custom scalar coercing for programmatic defaults"() { + given: + def date = GraphQLScalarType.newScalar() + .name("Date") + .coercing(Scalars.GraphQLString.coercing) + .build() + def query = GraphQLObjectType.newObject() + .name("Query") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("value") + .type(Scalars.GraphQLString) + .argument(GraphQLArgument.newArgument() + .name("external") + .type(date) + .defaultValueProgrammatic("2026-08-05")) + .argument(GraphQLArgument.newArgument() + .name("internal") + .type(date) + .defaultValue("2026-08-06"))) + .build() + GraphQLSchema graphQLSchema = GraphQLSchema.newSchema() + .query(query) + .build() + def universeSchema = new SchemaUniverse() + .importSchema("programmatic", graphQLSchema) + def executableSchema = SUExecutableSchema + .fromGraphQLSchema(universeSchema, graphQLSchema) + def printer = new SchemaPrinter() + + expect: + printer.print(executableSchema) == printer.print(graphQLSchema) + printer.print(executableSchema).contains( + 'external: Date = "2026-08-05"') + printer.print(executableSchema).contains( + 'internal: Date = "2026-08-06"') + } + @Requires({ Boolean.getBoolean("graphql.largeSchemaSdlTest") }) def "large schema native import and printing match GraphQLSchema"() { given: @@ -137,7 +184,8 @@ class SUSchemaPrinterTest extends Specification { } private static String resource(String name) { - def resource = SUSchemaPrinterTest.getResource("/" + name) + def resource = + SUExecutableSchemaPrinterTest.getResource("/" + name) assert resource != null return resource.getText("UTF-8") } @@ -147,7 +195,7 @@ class SUSchemaPrinterTest extends Specification { SchemaPrinter.Options options) { TypeDefinitionRegistry registry = new SchemaParser().parse(sdl) def schema = new SchemaUniverse().importSchema("direct_large", registry) - return new SUSchemaPrinter(options).print(schema) + return new SchemaPrinter(options).print(executable(schema)) } private static String printGraphQL( @@ -157,4 +205,8 @@ class SUSchemaPrinterTest extends Specification { def schema = UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) return new SchemaPrinter(options).print(schema) } + + private static SUExecutableSchema executable(SUSchema schema) { + return SUExecutableSchema.newExecutableSchema(schema).build() + } } diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy index bac0c9b5cc..2cc35aacba 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy @@ -6,6 +6,7 @@ import graphql.schema.idl.SchemaParser import graphql.schema.idl.SchemaPrinter import graphql.schema.idl.TypeDefinitionRegistry import graphql.schema.idl.UnExecutableSchemaGenerator +import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification import spock.lang.Unroll @@ -47,7 +48,10 @@ class SUSchemaRoundTripTest extends Specification { SUSchema schema = new SchemaUniverse().importSchema("native_round_trip", registry) return [ - sdl : new SUSchemaPrinter(printerOptions()).print(schema), + sdl : new SchemaPrinter(printerOptions()).print( + SUExecutableSchema + .newExecutableSchema(schema) + .build()), query : schema.queryType.name, mutation : schema.mutationType?.name, subscription: schema.subscriptionType?.name, From 4d0d29bd2425c6f101c248189465b92d5064cfad Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 6 Aug 2026 03:39:35 +0000 Subject: [PATCH 17/32] Support AST-aware schema universe printing --- docs/schema-universe.md | 27 +- .../benchmark/SchemaUniverseBenchmark.java | 89 +- .../java/graphql/schema/ExecutableSchema.java | 6 + .../java/graphql/schema/SchemaDirective.java | 6 + .../java/graphql/schema/SchemaNamedType.java | 10 + .../graphql/schema/idl/SchemaPrinter.java | 1356 ++++++++++++----- .../schema/idl/SchemaPrinterWriter.java | 916 ----------- .../schema/universe/SUAppliedDirective.java | 8 - .../graphql/schema/universe/SUArgument.java | 10 +- .../schema/universe/SUAstDefinitions.java | 36 + .../graphql/schema/universe/SUDirective.java | 10 +- .../graphql/schema/universe/SUExporter.java | 126 +- .../graphql/schema/universe/SUImporter.java | 129 +- .../graphql/schema/universe/SUInputField.java | 10 +- .../graphql/schema/universe/SUSchema.java | 31 +- .../schema/universe/SUSchemaBuilder.java | 7 + .../schema/universe/SUSchemaGenerator.java | 126 +- .../schema/universe/SUSchemaOptions.java | 45 + .../schema/universe/SchemaUniverse.java | 117 +- .../graphql/schema/universe/VertexChunk.java | 64 + .../view/AbstractSUSchemaElement.java | 20 +- .../view/AbstractSUSchemaNamedType.java | 12 + .../universe/view/SUExecutableSchema.java | 13 +- .../universe/view/SUSchemaDirective.java | 10 + .../SchemaPrinterComparatorsTest.groovy | 55 +- .../SUExecutableSchemaPrinterTest.groovy | 34 + .../SUSchemaAstDefinitionsTest.groovy | 405 +++++ .../universe/SUSchemaGeneratorTest.groovy | 2 +- .../graphql/schema/universe/SUTest.groovy | 6 +- .../schema/universe/VertexChunkAstTest.groovy | 79 + 30 files changed, 2313 insertions(+), 1452 deletions(-) delete mode 100644 src/main/java/graphql/schema/idl/SchemaPrinterWriter.java create mode 100644 src/main/java/graphql/schema/universe/SUAstDefinitions.java create mode 100644 src/main/java/graphql/schema/universe/SUSchemaOptions.java create mode 100644 src/test/groovy/graphql/schema/universe/SUSchemaAstDefinitionsTest.groovy create mode 100644 src/test/groovy/graphql/schema/universe/VertexChunkAstTest.groovy diff --git a/docs/schema-universe.md b/docs/schema-universe.md index 7bf1e029e7..c9f35e4eb6 100644 --- a/docs/schema-universe.md +++ b/docs/schema-universe.md @@ -22,7 +22,8 @@ small edits. This is materially different from reconciling unrelated schemas. `SchemaUniverse` is a chunked vertex arena. Every vertex receives a monotonically increasing integer ID. Cleanup can reclaim unused vertices and leave holes, but IDs are never reused. There is one Java vertex type for each supported GraphQL schema-element kind. A vertex contains intrinsic scalar -properties such as kind, name, and description, but no child references. +properties such as kind, name, and description, but no child references. Source AST provenance is +stored in a chunk-aligned sidecar rather than increasing every vertex object. `SUSchema` is an immutable adjacency snapshot plus a schema-specific root vertex, a persistent registry of every named type keyed by universe name ID, a persistent reverse index from interface @@ -173,7 +174,20 @@ payload. External and internal values are retained by reference; snapshot immuta with respect to a mutable value supplied by an application. Directive repeatability and valid locations are also intrinsic. Locations are stored as a compact -bit mask. Directive-definition and applied-directive AST nodes are retained when available. +bit mask. + +Source definitions and extensions are retained by default for the schema root and every +vertex-backed schema element. Each 1,024-slot vertex chunk lazily allocates a matching AST sidecar +array only when at least one slot has source provenance. A slot stores its definition node directly +when it has no extensions; slots with extensions use a small immutable holder for the nullable +definition and extension list. AST nodes are referenced rather than copied and remain shared by +derived schemas that reuse the vertex. + +`SUSchemaOptions.captureAstDefinitions(false)` disables capture for memory-sensitive imports. +Disabled chunks allocate no AST array. `SUAppliedDirectiveArgument` is not a vertex, so its source +argument node remains embedded in that value when capture is enabled. Source AST is provenance: +transforming topology does not rewrite it, and semantic schema printing remains authoritative for +the current topology. ## User metadata @@ -207,12 +221,13 @@ wrapper targets. - list and non-null wrappers; - operation roots, complete named-type membership, implementations, and union membership; - argument and input-field defaults, preserving all `InputValueWithState` states; -- directive repeatability, valid locations, definitions, and modern applied-directive topology; -- applied-directive argument values, exact input types, and application AST nodes. +- directive repeatability, valid locations, and modern applied-directive topology; +- applied-directive argument values and exact input types; +- source AST definitions and extensions for the schema and all represented elements. It does not yet preserve `GraphQLCodeRegistry`, coercing implementations, deprecation metadata, -general non-directive AST definitions/extensions, comparators, or source locations. These need typed -intrinsic payloads before round-trip conversion can be lossless. +or comparators. These need schema-bound behavior or additional intrinsic payloads before round-trip +conversion can be lossless. ## Lifecycle and compaction diff --git a/src/jmh/java/benchmark/SchemaUniverseBenchmark.java b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java index ba121f36f4..f91819f3b8 100644 --- a/src/jmh/java/benchmark/SchemaUniverseBenchmark.java +++ b/src/jmh/java/benchmark/SchemaUniverseBenchmark.java @@ -24,7 +24,9 @@ import graphql.schema.universe.SUObjectType; import graphql.schema.universe.SUScalarType; import graphql.schema.universe.SUSchema; +import graphql.schema.universe.SUSchemaOptions; import graphql.schema.universe.SchemaUniverse; +import graphql.schema.universe.view.SUExecutableSchema; import graphql.util.TraversalControl; import graphql.util.TraverserContext; import org.openjdk.jmh.annotations.Benchmark; @@ -34,6 +36,7 @@ import org.openjdk.jmh.annotations.Measurement; import org.openjdk.jmh.annotations.Mode; import org.openjdk.jmh.annotations.OutputTimeUnit; +import org.openjdk.jmh.annotations.Param; import org.openjdk.jmh.annotations.Scope; import org.openjdk.jmh.annotations.Setup; import org.openjdk.jmh.annotations.State; @@ -59,7 +62,8 @@ * a field, then adds and removes a replacement field. The {@code retain100...} methods print exact * JOL object-graph footprints. Run the {@code profileRetained100...} methods separately with * async-profiler's sampled live-allocation mode to attribute surviving allocations; use the JOL - * values, not the sampled profile totals, for exact retained byte counts:

+ * values, not the sampled profile totals, for exact retained byte counts. Schema-universe + * benchmarks run with AST capture enabled and disabled:

* *
  * ./gradlew jmh \
@@ -131,19 +135,38 @@ public void setup() {
     public static class SUTimingState {
         GraphQLSchema sourceSchema;
         MutationPlan plan;
+        SUSchemaOptions options;
         SchemaUniverse universe;
         SUSchema baseSchema;
+        SUExecutableSchema executableSchema;
+        SchemaPrinter astPrinter;
+
+        @Param({"true", "false"})
+        public boolean captureAstDefinitions;
 
         @Setup(Level.Trial)
         public void setupTrial() {
             sourceSchema = loadBaseSchema();
             plan = MutationPlan.create(sourceSchema);
+            options = SUSchemaOptions.defaultOptions()
+                    .captureAstDefinitions(captureAstDefinitions);
         }
 
         @Setup(Level.Iteration)
         public void setupIteration() {
             universe = new SchemaUniverse();
-            baseSchema = universe.importSchema(BASE_SCHEMA_NAME, sourceSchema);
+            baseSchema = universe.importSchema(
+                    BASE_SCHEMA_NAME,
+                    sourceSchema,
+                    options);
+            executableSchema = SUExecutableSchema.fromGraphQLSchema(
+                    baseSchema,
+                    sourceSchema);
+            SchemaPrinter.Options printerOptions =
+                    SchemaPrinter.Options.defaultOptions()
+                            .includeSchemaDefinition(true)
+                            .useAstDefinitions(true);
+            astPrinter = new SchemaPrinter(printerOptions);
         }
     }
 
@@ -174,17 +197,26 @@ public void reportRetainedMemory() {
     @State(Scope.Benchmark)
     public static class SUMemoryState {
         MutationPlan plan;
+        SUSchemaOptions options;
         SchemaUniverse universe;
         SUSchema baseSchema;
         long baseRetainedBytes;
         SchemaUniverse retainedUniverse;
 
+        @Param({"true", "false"})
+        public boolean captureAstDefinitions;
+
         @Setup(Level.Trial)
         public void setup() {
             GraphQLSchema sourceSchema = loadBaseSchema();
             plan = MutationPlan.create(sourceSchema);
+            options = SUSchemaOptions.defaultOptions()
+                    .captureAstDefinitions(captureAstDefinitions);
             universe = new SchemaUniverse();
-            baseSchema = universe.importSchema(BASE_SCHEMA_NAME, sourceSchema);
+            baseSchema = universe.importSchema(
+                    BASE_SCHEMA_NAME,
+                    sourceSchema,
+                    options);
             baseRetainedBytes = retainedSize(universe);
         }
 
@@ -192,7 +224,8 @@ public void setup() {
         public void reportRetainedMemory() {
             requireNonNull(retainedUniverse, "retained schema universe");
             reportFootprint(
-                    "SUSchema",
+                    "SUSchema[captureAstDefinitions="
+                            + captureAstDefinitions + "]",
                     baseRetainedBytes,
                     retainedSize(retainedUniverse));
         }
@@ -214,19 +247,65 @@ public void setup() {
     @State(Scope.Benchmark)
     public static class SUProfileState {
         MutationPlan plan;
+        SUSchemaOptions options;
         SchemaUniverse universe;
         SUSchema baseSchema;
         SchemaUniverse retainedUniverse;
 
+        @Param({"true", "false"})
+        public boolean captureAstDefinitions;
+
         @Setup(Level.Trial)
         public void setup() {
             GraphQLSchema sourceSchema = loadBaseSchema();
             plan = MutationPlan.create(sourceSchema);
+            options = SUSchemaOptions.defaultOptions()
+                    .captureAstDefinitions(captureAstDefinitions);
             universe = new SchemaUniverse();
-            baseSchema = universe.importSchema(BASE_SCHEMA_NAME, sourceSchema);
+            baseSchema = universe.importSchema(
+                    BASE_SCHEMA_NAME,
+                    sourceSchema,
+                    options);
         }
     }
 
+    @Benchmark
+    @BenchmarkMode(Mode.SingleShotTime)
+    @Warmup(iterations = 1)
+    @Measurement(iterations = 3)
+    @Fork(
+            value = 1,
+            jvmArgsAppend = {
+                    "-Xms8g",
+                    "-Xmx30g",
+                    "-Djdk.attach.allowAttachSelf=true",
+                    "-Djol.magicFieldOffset=true",
+                    "-XX:+EnableDynamicAgentLoading"
+            })
+    public SUSchema importSUSchema(SUTimingState state) {
+        return new SchemaUniverse().importSchema(
+                BASE_SCHEMA_NAME,
+                state.sourceSchema,
+                state.options);
+    }
+
+    @Benchmark
+    @BenchmarkMode(Mode.AverageTime)
+    @Warmup(iterations = 2)
+    @Measurement(iterations = 3)
+    @Fork(
+            value = 1,
+            jvmArgsAppend = {
+                    "-Xms8g",
+                    "-Xmx30g",
+                    "-Djdk.attach.allowAttachSelf=true",
+                    "-Djol.magicFieldOffset=true",
+                    "-XX:+EnableDynamicAgentLoading"
+            })
+    public String printSUSchemaWithAstOption(SUTimingState state) {
+        return state.astPrinter.print(state.executableSchema);
+    }
+
     @Benchmark
     @BenchmarkMode(Mode.SingleShotTime)
     @Warmup(iterations = 0)
diff --git a/src/main/java/graphql/schema/ExecutableSchema.java b/src/main/java/graphql/schema/ExecutableSchema.java
index e8dd705a37..952b7fa293 100644
--- a/src/main/java/graphql/schema/ExecutableSchema.java
+++ b/src/main/java/graphql/schema/ExecutableSchema.java
@@ -2,6 +2,7 @@
 
 import graphql.ExperimentalApi;
 import graphql.language.SchemaDefinition;
+import graphql.language.SchemaExtensionDefinition;
 import org.jspecify.annotations.NullMarked;
 import org.jspecify.annotations.Nullable;
 
@@ -27,6 +28,11 @@ public interface ExecutableSchema {
      */
     @Nullable SchemaDefinition getDefinition();
 
+    /**
+     * @return source schema extension definitions
+     */
+    List getExtensionDefinitions();
+
     /**
      * @return the query root
      */
diff --git a/src/main/java/graphql/schema/SchemaDirective.java b/src/main/java/graphql/schema/SchemaDirective.java
index ba0520d57d..5585e9814c 100644
--- a/src/main/java/graphql/schema/SchemaDirective.java
+++ b/src/main/java/graphql/schema/SchemaDirective.java
@@ -2,6 +2,7 @@
 
 import graphql.ExperimentalApi;
 import graphql.introspection.Introspection.DirectiveLocation;
+import graphql.language.DirectiveExtensionDefinition;
 import org.jspecify.annotations.NullMarked;
 import org.jspecify.annotations.Nullable;
 
@@ -15,6 +16,11 @@
 @NullMarked
 public interface SchemaDirective extends SchemaDirectiveContainer {
 
+    /**
+     * @return source extension definitions for this directive
+     */
+    List getExtensionDefinitions();
+
     boolean isRepeatable();
 
     Set validLocations();
diff --git a/src/main/java/graphql/schema/SchemaNamedType.java b/src/main/java/graphql/schema/SchemaNamedType.java
index b3fcd62cff..9ab0a71e13 100644
--- a/src/main/java/graphql/schema/SchemaNamedType.java
+++ b/src/main/java/graphql/schema/SchemaNamedType.java
@@ -1,8 +1,11 @@
 package graphql.schema;
 
 import graphql.ExperimentalApi;
+import graphql.language.TypeDefinition;
 import org.jspecify.annotations.NullMarked;
 
+import java.util.List;
+
 /**
  * A named type exposed by an {@link ExecutableSchema}.
  */
@@ -10,4 +13,11 @@
 @NullMarked
 public interface SchemaNamedType
         extends SchemaType, SchemaDirectiveContainer {
+
+    /**
+     * @return source extension definitions for this type
+     */
+    default List> getExtensionDefinitions() {
+        return List.of();
+    }
 }
diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java
index 0e772f75ae..7274e36f9b 100644
--- a/src/main/java/graphql/schema/idl/SchemaPrinter.java
+++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java
@@ -7,23 +7,32 @@
 import graphql.GraphQLContext;
 import graphql.PublicApi;
 import graphql.execution.ValuesResolver;
+import graphql.introspection.Introspection.DirectiveLocation;
+import graphql.language.ArrayValue;
 import graphql.language.AstPrinter;
 import graphql.language.Comment;
+import graphql.language.DescribedNode;
 import graphql.language.Description;
 import graphql.language.DirectiveDefinition;
 import graphql.language.Document;
 import graphql.language.EnumTypeDefinition;
+import graphql.language.EnumValue;
 import graphql.language.EnumValueDefinition;
 import graphql.language.FieldDefinition;
 import graphql.language.InputObjectTypeDefinition;
 import graphql.language.InputValueDefinition;
 import graphql.language.InterfaceTypeDefinition;
+import graphql.language.Node;
+import graphql.language.NullValue;
+import graphql.language.ObjectField;
 import graphql.language.ObjectTypeDefinition;
+import graphql.language.ObjectValue;
 import graphql.language.ScalarTypeDefinition;
 import graphql.language.SchemaDefinition;
 import graphql.language.SchemaExtensionDefinition;
 import graphql.language.TypeDefinition;
 import graphql.language.UnionTypeDefinition;
+import graphql.language.Value;
 import graphql.schema.DefaultGraphqlTypeComparatorRegistry;
 import graphql.schema.ExecutableSchema;
 import graphql.schema.GraphQLAppliedDirective;
@@ -51,7 +60,30 @@
 import graphql.schema.GraphqlTypeComparatorEnvironment;
 import graphql.schema.GraphqlTypeComparatorRegistry;
 import graphql.schema.InputValueWithState;
-import graphql.schema.visibility.GraphqlFieldVisibility;
+import graphql.schema.SchemaAppliedDirective;
+import graphql.schema.SchemaAppliedDirectiveArgument;
+import graphql.schema.SchemaArgument;
+import graphql.schema.SchemaDirective;
+import graphql.schema.SchemaDirectiveContainer;
+import graphql.schema.SchemaEnum;
+import graphql.schema.SchemaEnumValue;
+import graphql.schema.SchemaField;
+import graphql.schema.SchemaFieldsContainer;
+import graphql.schema.SchemaInputField;
+import graphql.schema.SchemaInputObject;
+import graphql.schema.SchemaInputType;
+import graphql.schema.SchemaInterface;
+import graphql.schema.SchemaList;
+import graphql.schema.SchemaModifiedType;
+import graphql.schema.SchemaNamedElement;
+import graphql.schema.SchemaNamedType;
+import graphql.schema.SchemaNonNull;
+import graphql.schema.SchemaObject;
+import graphql.schema.SchemaScalar;
+import graphql.schema.SchemaType;
+import graphql.schema.SchemaUnion;
+import graphql.util.FpKit;
+import org.jspecify.annotations.Nullable;
 
 import java.io.PrintWriter;
 import java.io.StringWriter;
@@ -69,7 +101,8 @@
 import static graphql.Directives.DeprecatedDirective;
 import static graphql.Directives.SpecifiedByDirective;
 import static graphql.Scalars.GraphQLString;
-import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY;
+import static graphql.Assert.assertNotNull;
+import static graphql.Assert.assertShouldNeverHappen;
 import static graphql.util.EscapeUtil.escapeJsonString;
 import static java.util.Optional.ofNullable;
 import static java.util.stream.Collectors.joining;
@@ -465,8 +498,6 @@ public Options includeAstDefinitionComments(boolean flag) {
         }
     }
 
-    private final Map, SchemaElementPrinter> printers = new LinkedHashMap<>();
-
     private final Options options;
 
     public SchemaPrinter() {
@@ -475,14 +506,6 @@ public SchemaPrinter() {
 
     public SchemaPrinter(Options options) {
         this.options = options;
-        printers.put(GraphQLSchema.class, schemaPrinter());
-        printers.put(GraphQLDirective.class, directivePrinter());
-        printers.put(GraphQLObjectType.class, objectPrinter());
-        printers.put(GraphQLEnumType.class, enumPrinter());
-        printers.put(GraphQLScalarType.class, scalarPrinter());
-        printers.put(GraphQLInterfaceType.class, interfacePrinter());
-        printers.put(GraphQLUnionType.class, unionPrinter());
-        printers.put(GraphQLInputObjectType.class, inputObjectPrinter());
     }
 
     /**
@@ -508,32 +531,7 @@ public String print(Document schemaIDL) {
      * @return the logical schema definition
      */
     public String print(GraphQLSchema schema) {
-        if (!options.isUseAstDefinitions()) {
-            return print((ExecutableSchema) schema);
-        }
-        StringWriter sw = new StringWriter();
-        PrintWriter out = new PrintWriter(sw);
-
-        GraphqlFieldVisibility visibility = schema.getCodeRegistry().getFieldVisibility();
-
-        printer(schema.getClass()).print(out, schema, visibility);
-        Comparator comparator = getComparator(GraphQLSchemaElement.class, null);
-
-        Stream directivesAndTypes = Stream.concat(
-                schema.getAllTypesAsList().stream(),
-                getSchemaDirectives(schema).stream());
-
-        List elements = directivesAndTypes
-                .map(e -> (GraphQLSchemaElement) e)
-                .filter(options.getIncludeSchemaElement())
-                .sorted(comparator)
-                .collect(toList());
-
-        for (GraphQLSchemaElement element : elements) {
-            printSchemaElement(out, element, visibility);
-        }
-
-        return trimNewLineChars(sw.toString());
+        return print((ExecutableSchema) schema);
     }
 
     /**
@@ -545,44 +543,67 @@ public String print(GraphQLSchema schema) {
      */
     @ExperimentalApi
     public String print(ExecutableSchema schema) {
-        return new SchemaPrinterWriter(schema, options).print();
+        ExecutableSchema executableSchema = assertNotNull(schema);
+        StringWriter sw = new StringWriter();
+        PrintWriter out = new PrintWriter(sw);
+        printSchema(out, executableSchema);
+
+        List elements = new ArrayList<>();
+        elements.addAll(executableSchema.getTypes());
+        elements.addAll(executableSchema.getDirectives());
+        elements.removeIf(element -> !isIncluded(element));
+        elements = sort(null, elements, true);
+        for (SchemaNamedElement element : elements) {
+            printSchemaElement(out, element, executableSchema);
+        }
+        return trimNewLineChars(sw.toString());
     }
 
     private interface SchemaElementPrinter {
 
-        void print(PrintWriter out, T schemaElement, GraphqlFieldVisibility visibility);
+        void print(
+                PrintWriter out,
+                T schemaElement,
+                @Nullable ExecutableSchema schema);
 
     }
 
-    private boolean isIntrospectionType(GraphQLNamedType type) {
+    private boolean isIntrospectionType(SchemaNamedType type) {
         return !options.isIncludeIntrospectionTypes() && type.getName().startsWith("__");
     }
 
-    private SchemaElementPrinter scalarPrinter() {
-        return (out, type, visibility) -> {
+    private SchemaElementPrinter scalarPrinter() {
+        return (out, type, schema) -> {
             if (!options.isIncludeScalars()) {
                 return;
             }
             boolean printScalar;
-            if (ScalarInfo.isGraphqlSpecifiedScalar(type)) {
+            if (ScalarInfo.isGraphqlSpecifiedScalar(type.getName())) {
                 printScalar = false;
                 //noinspection RedundantIfStatement
-                if (!ScalarInfo.isGraphqlSpecifiedScalar(type)) {
+                if (!ScalarInfo.isGraphqlSpecifiedScalar(type.getName())) {
                     printScalar = true;
                 }
             } else {
                 printScalar = true;
             }
             if (printScalar) {
-                if (shouldPrintAsAst(type.getDefinition())) {
+                if (shouldPrintAsAst(
+                        type.getDefinition(),
+                        type.getExtensionDefinitions())) {
                     printAsAst(out, type.getDefinition(), type.getExtensionDefinitions());
                 } else {
                     printComments(out, type, "");
-                    List directives = DirectivesUtil.toAppliedDirectives(type).stream()
+                    List directives =
+                            getAppliedDirectives(schema, type).stream()
                             .filter(d -> !d.getName().equals(SpecifiedByDirective.getName()))
                             .collect(toList());
                     out.format("scalar %s%s%s\n\n", type.getName(),
-                            directivesString(GraphQLScalarType.class, directives),
+                            directivesString(
+                                    schema,
+                                    type,
+                                    directives,
+                                    false),
                             specifiedByUrlString(type));
                 }
             }
@@ -590,28 +611,44 @@ private SchemaElementPrinter scalarPrinter() {
     }
 
 
-    private SchemaElementPrinter enumPrinter() {
-        return (out, type, visibility) -> {
+    private SchemaElementPrinter enumPrinter() {
+        return (out, type, schema) -> {
             if (isIntrospectionType(type)) {
                 return;
             }
 
-            Comparator comparator = getComparator(GraphQLEnumType.class, GraphQLEnumValueDefinition.class);
-
-            if (shouldPrintAsAst(type.getDefinition())) {
+            if (shouldPrintAsAst(
+                    type.getDefinition(),
+                    type.getExtensionDefinitions())) {
                 printAsAst(out, type.getDefinition(), type.getExtensionDefinitions());
             } else {
                 printComments(out, type, "");
-                out.format("enum %s%s", type.getName(), directivesString(GraphQLEnumType.class, type));
-                List values = type.getValues()
-                        .stream()
-                        .sorted(comparator)
-                        .collect(toList());
+                out.format(
+                        "enum %s%s",
+                        type.getName(),
+                        directivesString(
+                                schema,
+                                type,
+                                getAppliedDirectives(schema, type),
+                                false));
+                List values = sort(
+                        type,
+                        type.getValues(),
+                        false);
                 if (values.size() > 0) {
                     out.format(" {\n");
-                    for (GraphQLEnumValueDefinition enumValueDefinition : values) {
+                    for (SchemaEnumValue enumValueDefinition : values) {
                         printComments(out, enumValueDefinition, "  ");
-                        out.format("  %s%s\n", enumValueDefinition.getName(), directivesString(GraphQLEnumValueDefinition.class, enumValueDefinition.isDeprecated(), enumValueDefinition));
+                        out.format(
+                                "  %s%s\n",
+                                enumValueDefinition.getName(),
+                                directivesString(
+                                        schema,
+                                        enumValueDefinition,
+                                        getAppliedDirectives(
+                                                schema,
+                                                enumValueDefinition),
+                                        false));
                     }
                     out.format("}");
                 }
@@ -620,79 +657,113 @@ private SchemaElementPrinter enumPrinter() {
         };
     }
 
-    private void printFieldDefinitions(PrintWriter out, Comparator comparator, List fieldDefinitions) {
+    private void printFieldDefinitions(
+            PrintWriter out,
+            @Nullable ExecutableSchema schema,
+            SchemaFieldsContainer parent,
+            List fieldDefinitions) {
         if (fieldDefinitions.size() == 0) {
             return;
         }
 
         out.format(" {\n");
-        fieldDefinitions
-                .stream()
-                .filter(options.getIncludeSchemaElement())
-                .sorted(comparator)
-                .forEach(fd -> {
-                    printComments(out, fd, "  ");
-
-                    out.format("  %s%s: %s%s\n",
-                            fd.getName(), argsString(GraphQLFieldDefinition.class, fd.getArguments()), typeString(fd.getType()),
-                            directivesString(GraphQLFieldDefinition.class, fd.isDeprecated(), fd));
-                });
+        List fields = sort(
+                parent,
+                fieldDefinitions,
+                false);
+        fields.removeIf(field -> !isIncluded(field));
+        for (SchemaField field : fields) {
+            printComments(out, field, "  ");
+            out.format(
+                    "  %s%s: %s%s\n",
+                    field.getName(),
+                    argsString(schema, field),
+                    typeString(field.getType()),
+                    directivesString(
+                            schema,
+                            field,
+                            getAppliedDirectives(schema, field),
+                            false));
+        }
         out.format("}");
     }
 
-    private SchemaElementPrinter interfacePrinter() {
-        return (out, type, visibility) -> {
+    private SchemaElementPrinter interfacePrinter() {
+        return (out, type, schema) -> {
             if (isIntrospectionType(type)) {
                 return;
             }
 
-            if (shouldPrintAsAst(type.getDefinition())) {
+            if (shouldPrintAsAst(
+                    type.getDefinition(),
+                    type.getExtensionDefinitions())) {
                 printAsAst(out, type.getDefinition(), type.getExtensionDefinitions());
             } else {
                 printComments(out, type, "");
-                if (type.getInterfaces().isEmpty()) {
-                    out.format("interface %s%s", type.getName(), directivesString(GraphQLInterfaceType.class, type));
+                List interfaces =
+                        getInterfaces(schema, type);
+                if (interfaces.isEmpty()) {
+                    out.format(
+                            "interface %s%s",
+                            type.getName(),
+                            directivesString(
+                                    schema,
+                                    type,
+                                    getAppliedDirectives(schema, type),
+                                    false));
                 } else {
-
-                    Comparator implementsComparator = getComparator(GraphQLInterfaceType.class, GraphQLOutputType.class);
-
-                    Stream interfaceNames = type.getInterfaces()
-                            .stream()
-                            .sorted(implementsComparator)
-                            .map(GraphQLNamedType::getName);
+                    List sortedInterfaces = sort(
+                            type,
+                            interfaces,
+                            false);
+                    Stream interfaceNames = sortedInterfaces.stream()
+                            .map(SchemaInterface::getName);
                     out.format("interface %s implements %s%s",
                             type.getName(),
                             interfaceNames.collect(joining(" & ")),
-                            directivesString(GraphQLInterfaceType.class, type));
+                            directivesString(
+                                    schema,
+                                    type,
+                                    getAppliedDirectives(schema, type),
+                                    false));
                 }
 
-                Comparator comparator = getComparator(GraphQLInterfaceType.class, GraphQLFieldDefinition.class);
-
-                printFieldDefinitions(out, comparator, visibility.getFieldDefinitions(type));
+                printFieldDefinitions(
+                        out,
+                        schema,
+                        type,
+                        getFields(schema, type));
                 out.format("\n\n");
             }
         };
     }
 
-    private SchemaElementPrinter unionPrinter() {
-        return (out, type, visibility) -> {
+    private SchemaElementPrinter unionPrinter() {
+        return (out, type, schema) -> {
             if (isIntrospectionType(type)) {
                 return;
             }
 
-            Comparator comparator = getComparator(GraphQLUnionType.class, GraphQLOutputType.class);
-
-            if (shouldPrintAsAst(type.getDefinition())) {
+            if (shouldPrintAsAst(
+                    type.getDefinition(),
+                    type.getExtensionDefinitions())) {
                 printAsAst(out, type.getDefinition(), type.getExtensionDefinitions());
             } else {
                 printComments(out, type, "");
-                out.format("union %s%s = ", type.getName(), directivesString(GraphQLUnionType.class, type));
-                List types = type.getTypes()
-                        .stream()
-                        .sorted(comparator)
-                        .collect(toList());
+                out.format(
+                        "union %s%s = ",
+                        type.getName(),
+                        directivesString(
+                                schema,
+                                type,
+                                getAppliedDirectives(schema, type),
+                                false));
+                List types = sort(
+                        type,
+                        getUnionMembers(schema, type),
+                        false);
                 for (int i = 0; i < types.size(); i++) {
-                    GraphQLNamedOutputType objectType = types.get(i);
+                    SchemaObject objectType = types.get(i);
                     if (i > 0) {
                         out.format(" | ");
                     }
@@ -703,83 +774,119 @@ private SchemaElementPrinter unionPrinter() {
         };
     }
 
-    private SchemaElementPrinter directivePrinter() {
-        return (out, directive, visibility) -> {
+    private SchemaElementPrinter directivePrinter() {
+        return (out, directive, schema) -> {
             boolean isOnEver = options.isIncludeDirectiveDefinitions();
+            boolean isIncluded =
+                    options.getIncludeDirective().test(directive.getName());
             boolean specificTest = options.getIncludeDirectiveDefinition().test(directive.getName());
-            if (isOnEver && specificTest) {
-                String s = directiveDefinition(directive);
+            if (isOnEver && isIncluded && specificTest) {
+                String s = directiveDefinition(schema, directive);
                 out.format("%s", s);
                 out.print("\n\n");
             }
         };
     }
 
-    private SchemaElementPrinter objectPrinter() {
-        return (out, type, visibility) -> {
+    private SchemaElementPrinter objectPrinter() {
+        return (out, type, schema) -> {
             if (isIntrospectionType(type)) {
                 return;
             }
-            if (shouldPrintAsAst(type.getDefinition())) {
+            if (shouldPrintAsAst(
+                    type.getDefinition(),
+                    type.getExtensionDefinitions())) {
                 printAsAst(out, type.getDefinition(), type.getExtensionDefinitions());
             } else {
                 printComments(out, type, "");
-                if (type.getInterfaces().isEmpty()) {
-                    out.format("type %s%s", type.getName(), directivesString(GraphQLObjectType.class, type));
+                List interfaces =
+                        getInterfaces(schema, type);
+                if (interfaces.isEmpty()) {
+                    out.format(
+                            "type %s%s",
+                            type.getName(),
+                            directivesString(
+                                    schema,
+                                    type,
+                                    getAppliedDirectives(schema, type),
+                                    false));
                 } else {
-
-                    Comparator implementsComparator = getComparator(GraphQLObjectType.class, GraphQLOutputType.class);
-
-                    Stream interfaceNames = type.getInterfaces()
-                            .stream()
-                            .sorted(implementsComparator)
-                            .map(GraphQLNamedType::getName);
+                    List sortedInterfaces = sort(
+                            type,
+                            interfaces,
+                            false);
+                    Stream interfaceNames = sortedInterfaces.stream()
+                            .map(SchemaInterface::getName);
                     out.format("type %s implements %s%s",
                             type.getName(),
                             interfaceNames.collect(joining(" & ")),
-                            directivesString(GraphQLObjectType.class, type));
+                            directivesString(
+                                    schema,
+                                    type,
+                                    getAppliedDirectives(schema, type),
+                                    false));
                 }
 
-                Comparator comparator = getComparator(GraphQLObjectType.class, GraphQLFieldDefinition.class);
-
-                printFieldDefinitions(out, comparator, visibility.getFieldDefinitions(type));
+                printFieldDefinitions(
+                        out,
+                        schema,
+                        type,
+                        getFields(schema, type));
                 out.format("\n\n");
             }
         };
     }
 
-    private SchemaElementPrinter inputObjectPrinter() {
-        return (out, type, visibility) -> {
+    private SchemaElementPrinter inputObjectPrinter() {
+        return (out, type, schema) -> {
             if (isIntrospectionType(type)) {
                 return;
             }
-            if (shouldPrintAsAst(type.getDefinition())) {
+            if (shouldPrintAsAst(
+                    type.getDefinition(),
+                    type.getExtensionDefinitions())) {
                 printAsAst(out, type.getDefinition(), type.getExtensionDefinitions());
             } else {
                 printComments(out, type, "");
 
-                Comparator comparator = getComparator(GraphQLInputObjectType.class, GraphQLInputObjectField.class);
-
-                out.format("input %s%s", type.getName(), directivesString(GraphQLInputObjectType.class, type));
-                List inputObjectFields = visibility.getFieldDefinitions(type);
+                out.format(
+                        "input %s%s",
+                        type.getName(),
+                        directivesString(
+                                schema,
+                                type,
+                                getAppliedDirectives(schema, type),
+                                false));
+                List inputObjectFields = sort(
+                        type,
+                        getInputFields(schema, type),
+                        false);
                 if (inputObjectFields.size() > 0) {
                     out.format(" {\n");
-                    inputObjectFields
-                            .stream()
-                            .filter(options.getIncludeSchemaElement())
-                            .sorted(comparator)
-                            .forEach(fd -> {
-                                printComments(out, fd, "  ");
-                                out.format("  %s: %s",
-                                        fd.getName(), typeString(fd.getType()));
-                                if (fd.hasSetDefaultValue()) {
-                                    InputValueWithState defaultValue = fd.getInputFieldDefaultValue();
-                                    String astValue = printAst(defaultValue, fd.getType());
-                                    out.format(" = %s", astValue);
-                                }
-                                out.print(directivesString(GraphQLInputObjectField.class, fd.isDeprecated(), fd));
-                                out.format("\n");
-                            });
+                    inputObjectFields.removeIf(field -> !isIncluded(field));
+                    for (SchemaInputField field : inputObjectFields) {
+                        printComments(out, field, "  ");
+                        out.format(
+                                "  %s: %s",
+                                field.getName(),
+                                typeString(field.getType()));
+                        InputValueWithState defaultValue =
+                                field.getInputFieldDefaultValue();
+                        if (defaultValue.isSet()) {
+                            out.format(
+                                    " = %s",
+                                    printValue(
+                                            schema,
+                                            defaultValue,
+                                            field.getType()));
+                        }
+                        out.print(directivesString(
+                                schema,
+                                field,
+                                getAppliedDirectives(schema, field),
+                                false));
+                        out.format("\n");
+                    }
                     out.format("}");
                 }
                 out.format("\n\n");
@@ -794,19 +901,11 @@ private SchemaElementPrinter inputObjectPrinter() {
      *
      * @return true if we should print using AST nodes
      */
-    private boolean shouldPrintAsAst(TypeDefinition definition) {
-        return options.isUseAstDefinitions() && definition != null;
-    }
-
-    /**
-     * This will return true if the options say to use the AST and we have an AST element
-     *
-     * @param definition the AST schema definition
-     *
-     * @return true if we should print using AST nodes
-     */
-    private boolean shouldPrintAsAst(SchemaDefinition definition) {
-        return options.isUseAstDefinitions() && definition != null;
+    private boolean shouldPrintAsAst(
+            @Nullable Node definition,
+            List> extensionDefinitions) {
+        return options.isUseAstDefinitions()
+                && (definition != null || !extensionDefinitions.isEmpty());
     }
 
     /**
@@ -817,31 +916,21 @@ private boolean shouldPrintAsAst(SchemaDefinition definition) {
      * @param definition the AST type definition
      * @param extensions a list of type definition extensions
      */
-    private void printAsAst(PrintWriter out, TypeDefinition definition, List> extensions) {
-        out.printf("%s\n", AstPrinter.printAst(definition));
-        if (extensions != null) {
-            for (TypeDefinition extension : extensions) {
-                out.printf("\n%s\n", AstPrinter.printAst(extension));
-            }
+    private void printAsAst(
+            PrintWriter out,
+            @Nullable Node definition,
+            List> extensions) {
+        boolean printed = false;
+        if (definition != null) {
+            out.printf("%s\n", AstPrinter.printAst(definition));
+            printed = true;
         }
-        out.print('\n');
-    }
-
-    /**
-     * This will print out a runtime graphql schema block using its AST definition.  This
-     * must be guarded by a called to {@link #shouldPrintAsAst(SchemaDefinition)}
-     *
-     * @param out        the output writer
-     * @param definition the AST schema definition
-     * @param extensions a list of schema definition extensions
-     */
-    private void printAsAst(PrintWriter out, SchemaDefinition definition, List extensions) {
-        out.printf("%s\n", AstPrinter.printAst(definition));
-        if (extensions != null) {
-            for (SchemaExtensionDefinition extension : extensions) {
-                out.printf("\n%s\n", AstPrinter.printAst(extension));
+        for (Node extension : extensions) {
+            if (printed) {
+                out.print('\n');
             }
+            out.printf("%s\n", AstPrinter.printAst(extension));
+            printed = true;
         }
         out.print('\n');
     }
@@ -851,69 +940,107 @@ private static String printAst(InputValueWithState value, GraphQLInputType type)
         return AstPrinter.printAst(ValuesResolver.valueToLiteral(value, type, GraphQLContext.getDefault(), Locale.getDefault()));
     }
 
-    private SchemaElementPrinter schemaPrinter() {
-        return (out, schema, visibility) -> {
-            GraphQLObjectType queryType = schema.getQueryType();
-            GraphQLObjectType mutationType = schema.getMutationType();
-            GraphQLObjectType subscriptionType = schema.getSubscriptionType();
-
-            // when serializing a GraphQL schema using the type system language, a
-            // schema definition should be omitted only if it uses the default root type names.
-            boolean needsSchemaPrinted = options.isIncludeSchemaDefinition();
+    private void printSchema(
+            PrintWriter out,
+            ExecutableSchema schema) {
+        SchemaObject queryType = schema.getQueryType();
+        SchemaObject mutationType = schema.getMutationType();
+        SchemaObject subscriptionType = schema.getSubscriptionType();
+
+        boolean needsSchemaPrinted = options.isIncludeSchemaDefinition()
+                || !"Query".equals(queryType.getName());
+        if (mutationType != null
+                && !"Mutation".equals(mutationType.getName())) {
+            needsSchemaPrinted = true;
+        }
+        if (subscriptionType != null
+                && !"Subscription".equals(subscriptionType.getName())) {
+            needsSchemaPrinted = true;
+        }
+        if (!needsSchemaPrinted) {
+            return;
+        }
+        if (shouldPrintAsAst(
+                schema.getDefinition(),
+                schema.getExtensionDefinitions())) {
+            printAsAst(
+                    out,
+                    schema.getDefinition(),
+                    schema.getExtensionDefinitions());
+            return;
+        }
 
-            if (!needsSchemaPrinted) {
-                if (queryType != null && !queryType.getName().equals("Query")) {
-                    needsSchemaPrinted = true;
-                }
-                if (mutationType != null && !mutationType.getName().equals("Mutation")) {
-                    needsSchemaPrinted = true;
-                }
-                if (subscriptionType != null && !subscriptionType.getName().equals("Subscription")) {
-                    needsSchemaPrinted = true;
-                }
-            }
+        printComments(
+                out,
+                schema.getDescription(),
+                schema.getDefinition(),
+                "");
+        out.format(
+                "schema %s{\n",
+                directivesString(
+                        schema,
+                        null,
+                        schema.getAppliedDirectives(),
+                        true));
+        out.format("  query: %s\n", queryType.getName());
+        if (mutationType != null) {
+            out.format("  mutation: %s\n", mutationType.getName());
+        }
+        if (subscriptionType != null) {
+            out.format("  subscription: %s\n", subscriptionType.getName());
+        }
+        out.format("}\n\n");
+    }
 
-            if (needsSchemaPrinted) {
-                if (shouldPrintAsAst(schema.getDefinition())) {
-                    printAsAst(out, schema.getDefinition(), schema.getExtensionDefinitions());
-                } else {
-                    if (hasAstDefinitionComments(schema) || hasDescription(schema)) {
-                        out.print(printComments(schema, ""));
-                    }
-                    List directives = DirectivesUtil.toAppliedDirectives(schema.getSchemaAppliedDirectives(), schema.getSchemaDirectives());
-                    out.format("schema %s{\n", directivesString(GraphQLSchemaElement.class, directives));
-                    if (queryType != null) {
-                        out.format("  query: %s\n", queryType.getName());
-                    }
-                    if (mutationType != null) {
-                        out.format("  mutation: %s\n", mutationType.getName());
-                    }
-                    if (subscriptionType != null) {
-                        out.format("  subscription: %s\n", subscriptionType.getName());
-                    }
-                    out.format("}\n\n");
-                }
-            }
-        };
+    String typeString(SchemaType type) {
+        if (type instanceof SchemaList) {
+            return "[" + typeString(
+                    ((SchemaList) type).getWrappedType()) + "]";
+        }
+        if (type instanceof SchemaNonNull) {
+            return typeString(
+                    ((SchemaNonNull) type).getWrappedType()) + "!";
+        }
+        return ((SchemaNamedType) type).getName();
     }
 
-    private List getSchemaDirectives(GraphQLSchema schema) {
-        Predicate includePredicate = d -> options.getIncludeDirective().test(d.getName());
-        return schema.getDirectives().stream()
-                .filter(includePredicate)
-                .filter(options.getIncludeSchemaElement())
-                .collect(toList());
+    String argsString(List arguments) {
+        List sortedArguments = sort(
+                null,
+                arguments,
+                false);
+        return formatArgs(null, sortedArguments);
     }
 
-    String typeString(GraphQLType rawType) {
-        return GraphQLTypeUtil.simplePrint(rawType);
+    String argsString(
+            @Nullable Class parentType,
+            List arguments) {
+        List sortedArguments = sortGraphQL(
+                legacyParentType(parentType),
+                GraphQLArgument.class,
+                arguments);
+        return formatArgs(null, sortedArguments);
     }
 
-    String argsString(List arguments) {
-        return argsString(null, arguments);
+    private String argsString(
+            @Nullable ExecutableSchema schema,
+            SchemaNamedElement parent) {
+        List arguments;
+        if (parent instanceof SchemaField) {
+            arguments = ((SchemaField) parent).getArguments();
+        } else {
+            arguments = ((SchemaDirective) parent).getArguments();
+        }
+        List sortedArguments = sort(
+                parent,
+                arguments,
+                false);
+        return formatArgs(schema, sortedArguments);
     }
 
-    String argsString(Class parent, List arguments) {
+    private String formatArgs(
+            @Nullable ExecutableSchema schema,
+            List arguments) {
         boolean hasAstDefinitionComments = arguments.stream().anyMatch(this::hasAstDefinitionComments);
         boolean hasDescriptions = arguments.stream().anyMatch(this::hasDescription);
         String halfPrefix = hasAstDefinitionComments || hasDescriptions ? "  " : "";
@@ -921,14 +1048,10 @@ String argsString(Class parent, List comparator = getComparator(parent, GraphQLArgument.class);
-
-        arguments = arguments
-                .stream()
-                .sorted(comparator)
-                .filter(options.getIncludeSchemaElement())
+        List includedArguments = arguments.stream()
+                .filter(this::isIncluded)
                 .collect(toList());
-        for (GraphQLArgument argument : arguments) {
+        for (SchemaArgument argument : includedArguments) {
             if (count == 0) {
                 sb.append("(");
             } else {
@@ -943,13 +1066,21 @@ String argsString(Class parent, List comparator = getComparator(parentType, GraphQLAppliedDirective.class);
+        included = sort(
+                parent,
+                included,
+                false);
+        return formatDirectives(schema, included, schemaContainer);
+    }
 
-        directives = directives
-                .stream()
-                .sorted(comparator)
-                .collect(toList());
-        for (int i = 0; i < directives.size(); i++) {
-            GraphQLAppliedDirective directive = directives.get(i);
-            sb.append(directiveString(directive));
-            if (i < directives.size() - 1) {
-                sb.append(" ");
-            }
-        }
-        return sb.toString();
+    private List includedDirectives(
+            List directives) {
+        return directives.stream()
+                .filter(this::isIncluded)
+                .filter(directive -> options.getIncludeDirective()
+                        .test(directive.getName()))
+                .collect(Collectors.toCollection(ArrayList::new));
     }
 
-    private String directiveString(GraphQLAppliedDirective directive) {
-        if (!options.getIncludeSchemaElement().test(directive)) {
-            return "";
-        }
-        if (!options.getIncludeDirective().test(directive.getName())) {
+    private String formatDirectives(
+            @Nullable ExecutableSchema schema,
+            List directives,
+            boolean schemaContainer) {
+        if (directives.isEmpty()) {
             return "";
         }
+        String result = directives.stream()
+                .map(directive -> directiveString(schema, directive))
+                .collect(joining(" "));
+        return schemaContainer ? result : " " + result;
+    }
 
+    private String directiveString(
+            @Nullable ExecutableSchema schema,
+            SchemaAppliedDirective directive) {
         StringBuilder sb = new StringBuilder();
         sb.append("@").append(directive.getName());
 
-        Comparator comparator = getComparator(GraphQLAppliedDirective.class, GraphQLAppliedDirectiveArgument.class);
-
-        List args = directive.getArguments();
-        args = args
-                .stream()
+        List args =
+                directive.getArguments().stream()
                 .filter(arg -> arg.getArgumentValue().isSet())
-                .sorted(comparator)
-                .collect(toList());
+                .collect(Collectors.toCollection(ArrayList::new));
+        args = sort(
+                directive,
+                args,
+                false);
         if (!args.isEmpty()) {
             sb.append("(");
             for (int i = 0; i < args.size(); i++) {
-                GraphQLAppliedDirectiveArgument arg = args.get(i);
-                String argValue = null;
-                if (arg.hasSetValue()) {
-                    argValue = printAst(arg.getArgumentValue(), arg.getType());
-                }
+                SchemaAppliedDirectiveArgument arg = args.get(i);
+                String argValue = printValue(
+                        schema,
+                        arg.getArgumentValue(),
+                        arg.getType());
                 if (!isNullOrEmpty(argValue)) {
                     sb.append(arg.getName());
                     sb.append(" : ");
@@ -1130,7 +1277,7 @@ private String getDeprecationReason(GraphQLDirectiveContainer directiveContainer
         }
     }
 
-    private String specifiedByUrlString(GraphQLScalarType scalarType) {
+    private String specifiedByUrlString(SchemaScalar scalarType) {
         String url = scalarType.getSpecifiedByUrl();
         if (url == null || !options.getIncludeDirective().test(SpecifiedByDirective.getName())) {
             return "";
@@ -1138,7 +1285,9 @@ private String specifiedByUrlString(GraphQLScalarType scalarType) {
         return " @specifiedBy(url : \"" + escapeJsonString(url) + "\")";
     }
 
-    private String directiveDefinition(GraphQLDirective directive) {
+    private String directiveDefinition(
+            @Nullable ExecutableSchema schema,
+            SchemaDirective directive) {
         StringBuilder sb = new StringBuilder();
 
         StringWriter sw = new StringWriter();
@@ -1147,18 +1296,12 @@ private String directiveDefinition(GraphQLDirective directive) {
         sb.append(sw);
 
         sb.append("directive @").append(directive.getName());
-
-        Comparator comparator = getComparator(GraphQLDirective.class, GraphQLArgument.class);
-
-        List args = directive.getArguments();
-        args = args
-                .stream()
-                .filter(options.getIncludeSchemaElement())
-                .sorted(comparator)
-                .collect(toList());
-
-        sb.append(argsString(GraphQLDirective.class, args));
-        sb.append(directivesString(GraphQLDirective.class, directive.isDeprecated(), directive));
+        sb.append(argsString(schema, directive));
+        sb.append(directivesString(
+                schema,
+                directive,
+                getAppliedDirectives(schema, directive),
+                false));
 
         if (directive.isRepeatable()) {
             sb.append(" repeatable");
@@ -1172,28 +1315,18 @@ private String directiveDefinition(GraphQLDirective directive) {
         return sb.toString();
     }
 
-
-    @SuppressWarnings("unchecked")
-    private  SchemaElementPrinter printer(Class clazz) {
-        SchemaElementPrinter schemaElementPrinter = printers.get(clazz);
-        if (schemaElementPrinter == null) {
-            Class superClazz = clazz.getSuperclass();
-            if (superClazz != Object.class) {
-                schemaElementPrinter = printer(superClazz);
-            } else {
-                schemaElementPrinter = (out, type, visibility) -> out.print("Type not implemented : " + type + "\n");
-            }
-            printers.put(clazz, schemaElementPrinter);
-        }
-        return (SchemaElementPrinter) schemaElementPrinter;
-    }
-
-
     public String print(GraphQLType type) {
         StringWriter sw = new StringWriter();
         PrintWriter out = new PrintWriter(sw);
 
-        printSchemaElement(out, type, DEFAULT_FIELD_VISIBILITY);
+        if (type instanceof SchemaNamedElement) {
+            printSchemaElement(
+                    out,
+                    (SchemaNamedElement) type,
+                    null);
+        } else {
+            out.print("Type not implemented : " + type + "\n");
+        }
 
         return trimNewLineChars(sw.toString());
     }
@@ -1206,7 +1339,10 @@ public String print(List elements) {
             if (element instanceof GraphQLDirective) {
                 out.print(print(((GraphQLDirective) element)));
             } else if (element instanceof GraphQLType) {
-                printSchemaElement(out, element, DEFAULT_FIELD_VISIBILITY);
+                printSchemaElement(
+                        out,
+                        (SchemaNamedElement) element,
+                        null);
             } else {
                 Assert.assertShouldNeverHappen("How did we miss a %s", element.getClass());
             }
@@ -1215,23 +1351,99 @@ public String print(List elements) {
     }
 
     public String print(GraphQLDirective graphQLDirective) {
-        return directiveDefinition(graphQLDirective);
+        return directiveDefinition(null, graphQLDirective);
     }
 
-    private void printSchemaElement(PrintWriter out, GraphQLSchemaElement schemaElement, GraphqlFieldVisibility visibility) {
-        SchemaElementPrinter printer = printer(schemaElement.getClass());
-        printer.print(out, schemaElement, visibility);
+    private void printSchemaElement(
+            PrintWriter out,
+            SchemaNamedElement schemaElement,
+            @Nullable ExecutableSchema schema) {
+        if (schemaElement instanceof SchemaObject) {
+            objectPrinter().print(
+                    out,
+                    (SchemaObject) schemaElement,
+                    schema);
+            return;
+        }
+        if (schemaElement instanceof SchemaInterface) {
+            interfacePrinter().print(
+                    out,
+                    (SchemaInterface) schemaElement,
+                    schema);
+            return;
+        }
+        if (schemaElement instanceof SchemaUnion) {
+            unionPrinter().print(
+                    out,
+                    (SchemaUnion) schemaElement,
+                    schema);
+            return;
+        }
+        if (schemaElement instanceof SchemaEnum) {
+            enumPrinter().print(
+                    out,
+                    (SchemaEnum) schemaElement,
+                    schema);
+            return;
+        }
+        if (schemaElement instanceof SchemaScalar) {
+            scalarPrinter().print(
+                    out,
+                    (SchemaScalar) schemaElement,
+                    schema);
+            return;
+        }
+        if (schemaElement instanceof SchemaInputObject) {
+            inputObjectPrinter().print(
+                    out,
+                    (SchemaInputObject) schemaElement,
+                    schema);
+            return;
+        }
+        if (schemaElement instanceof SchemaDirective) {
+            directivePrinter().print(
+                    out,
+                    (SchemaDirective) schemaElement,
+                    schema);
+            return;
+        }
+        out.print("Type not implemented : " + schemaElement + "\n");
     }
 
-    private String printComments(Object graphQLType, String prefix) {
+    private String printComments(
+            SchemaNamedElement element,
+            String prefix) {
         StringWriter sw = new StringWriter();
         PrintWriter pw = new PrintWriter(sw);
-        printComments(pw, graphQLType, prefix);
+        printComments(pw, element, prefix);
         return sw.toString();
     }
 
-    private void printComments(PrintWriter out, Object graphQLType, String prefix) {
-        String descriptionText = getDescription(graphQLType);
+    private void printComments(
+            PrintWriter out,
+            SchemaNamedElement element,
+            String prefix) {
+        printComments(
+                out,
+                element.getDescription(),
+                element.getDefinition(),
+                prefix);
+    }
+
+    private void printComments(
+            PrintWriter out,
+            @Nullable String description,
+            @Nullable Node definition,
+            String prefix) {
+        String descriptionText = description;
+        if (isNullOrEmpty(descriptionText)
+                && definition instanceof DescribedNode) {
+            Description astDescription =
+                    ((DescribedNode) definition).getDescription();
+            if (astDescription != null) {
+                descriptionText = astDescription.getContent();
+            }
+        }
         if (!isNullOrEmpty(descriptionText)) {
             List lines = Arrays.asList(descriptionText.split("\n"));
             if (options.isDescriptionsAsHashComments()) {
@@ -1246,7 +1458,9 @@ private void printComments(PrintWriter out, Object graphQLType, String prefix) {
         }
 
         if (options.isIncludeAstDefinitionComments()) {
-            String commentsText = getAstDefinitionComments(graphQLType);
+            String commentsText = definition == null
+                    ? null
+                    : comments(definition.getComments());
             if (!isNullOrEmpty(commentsText)) {
                 List lines = Arrays.asList(commentsText.split("\n"));
                 if (!lines.isEmpty()) {
@@ -1275,51 +1489,10 @@ private void printSingleLineDescription(PrintWriter out, String prefix, String s
         out.printf("%s\"%s\"\n", prefix, desc);
     }
 
-    private boolean hasAstDefinitionComments(Object commentHolder) {
-        String comments = getAstDefinitionComments(commentHolder);
-        return !isNullOrEmpty(comments);
-    }
-
-    private String getAstDefinitionComments(Object commentHolder) {
-        if (commentHolder instanceof GraphQLObjectType) {
-            GraphQLObjectType type = (GraphQLObjectType) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(ObjectTypeDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLEnumType) {
-            GraphQLEnumType type = (GraphQLEnumType) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(EnumTypeDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLFieldDefinition) {
-            GraphQLFieldDefinition type = (GraphQLFieldDefinition) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(FieldDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLEnumValueDefinition) {
-            GraphQLEnumValueDefinition type = (GraphQLEnumValueDefinition) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(EnumValueDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLUnionType) {
-            GraphQLUnionType type = (GraphQLUnionType) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(UnionTypeDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLInputObjectType) {
-            GraphQLInputObjectType type = (GraphQLInputObjectType) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(InputObjectTypeDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLInputObjectField) {
-            GraphQLInputObjectField type = (GraphQLInputObjectField) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(InputValueDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLInterfaceType) {
-            GraphQLInterfaceType type = (GraphQLInterfaceType) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(InterfaceTypeDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLScalarType) {
-            GraphQLScalarType type = (GraphQLScalarType) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(ScalarTypeDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLArgument) {
-            GraphQLArgument type = (GraphQLArgument) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(InputValueDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLDirective) {
-            GraphQLDirective type = (GraphQLDirective) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(DirectiveDefinition::getComments).orElse(null));
-        } else if (commentHolder instanceof GraphQLSchema) {
-            GraphQLSchema type = (GraphQLSchema) commentHolder;
-            return comments(ofNullable(type.getDefinition()).map(SchemaDefinition::getComments).orElse(null));
-        } else {
-            return Assert.assertShouldNeverHappen();
-        }
+    private boolean hasAstDefinitionComments(
+            SchemaNamedElement element) {
+        Node definition = element.getDefinition();
+        return definition != null && !definition.getComments().isEmpty();
     }
 
     private String comments(List comments) {
@@ -1330,51 +1503,21 @@ private String comments(List comments) {
         return s;
     }
 
-    private boolean hasDescription(Object descriptionHolder) {
-        String description = getDescription(descriptionHolder);
+    private boolean hasDescription(SchemaNamedElement element) {
+        String description = getDescription(element);
         return !isNullOrEmpty(description);
     }
 
-    private String getDescription(Object descriptionHolder) {
-        if (descriptionHolder instanceof GraphQLObjectType) {
-            GraphQLObjectType type = (GraphQLObjectType) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(ObjectTypeDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLEnumType) {
-            GraphQLEnumType type = (GraphQLEnumType) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(EnumTypeDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLFieldDefinition) {
-            GraphQLFieldDefinition type = (GraphQLFieldDefinition) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(FieldDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLEnumValueDefinition) {
-            GraphQLEnumValueDefinition type = (GraphQLEnumValueDefinition) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(EnumValueDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLUnionType) {
-            GraphQLUnionType type = (GraphQLUnionType) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(UnionTypeDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLInputObjectType) {
-            GraphQLInputObjectType type = (GraphQLInputObjectType) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(InputObjectTypeDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLInputObjectField) {
-            GraphQLInputObjectField type = (GraphQLInputObjectField) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(InputValueDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLInterfaceType) {
-            GraphQLInterfaceType type = (GraphQLInterfaceType) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(InterfaceTypeDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLScalarType) {
-            GraphQLScalarType type = (GraphQLScalarType) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(ScalarTypeDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLArgument) {
-            GraphQLArgument type = (GraphQLArgument) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(InputValueDefinition::getDescription).orElse(null));
-        } else if (descriptionHolder instanceof GraphQLDirective) {
-            GraphQLDirective type = (GraphQLDirective) descriptionHolder;
-            return description(type.getDescription(), null);
-        } else if (descriptionHolder instanceof GraphQLSchema) {
-            GraphQLSchema type = (GraphQLSchema) descriptionHolder;
-            return description(type.getDescription(), ofNullable(type.getDefinition()).map(SchemaDefinition::getDescription).orElse(null));
-        } else {
-            return Assert.assertShouldNeverHappen();
+    private @Nullable String getDescription(
+            SchemaNamedElement element) {
+        String runtimeDescription = element.getDescription();
+        Node definition = element.getDefinition();
+        if (!(definition instanceof DescribedNode)) {
+            return runtimeDescription;
         }
+        return description(
+                runtimeDescription,
+                ((DescribedNode) definition).getDescription());
     }
 
     String description(String runtimeDescription, Description descriptionAst) {
@@ -1391,7 +1534,396 @@ String description(String runtimeDescription, Description descriptionAst) {
         return descriptionText;
     }
 
-    private Comparator getComparator(Class parentType, Class elementType) {
+    private List getAppliedDirectives(
+            @Nullable ExecutableSchema schema,
+            SchemaDirectiveContainer container) {
+        if (schema != null) {
+            return schema.getAppliedDirectives(container);
+        }
+        GraphQLDirectiveContainer graphQLContainer =
+                (GraphQLDirectiveContainer) container;
+        if (isDeprecated(graphQLContainer)) {
+            return addOrUpdateDeprecatedDirectiveIfNeeded(graphQLContainer);
+        }
+        return DirectivesUtil.toAppliedDirectives(graphQLContainer);
+    }
+
+    private boolean isDeprecated(
+            GraphQLDirectiveContainer container) {
+        if (container instanceof GraphQLFieldDefinition) {
+            return ((GraphQLFieldDefinition) container).isDeprecated();
+        }
+        if (container instanceof GraphQLEnumValueDefinition) {
+            return ((GraphQLEnumValueDefinition) container).isDeprecated();
+        }
+        if (container instanceof GraphQLInputObjectField) {
+            return ((GraphQLInputObjectField) container).isDeprecated();
+        }
+        if (container instanceof GraphQLArgument) {
+            return ((GraphQLArgument) container).isDeprecated();
+        }
+        if (container instanceof GraphQLDirective) {
+            return ((GraphQLDirective) container).isDeprecated();
+        }
+        return false;
+    }
+
+    private List getFields(
+            @Nullable ExecutableSchema schema,
+            SchemaFieldsContainer type) {
+        if (schema != null) {
+            return schema.getFields(type);
+        }
+        if (type instanceof GraphQLObjectType) {
+            return ((GraphQLObjectType) type).getFieldDefinitions();
+        }
+        return ((GraphQLInterfaceType) type).getFieldDefinitions();
+    }
+
+    private List getInputFields(
+            @Nullable ExecutableSchema schema,
+            SchemaInputObject type) {
+        if (schema != null) {
+            return schema.getInputFields(type);
+        }
+        return ((GraphQLInputObjectType) type).getFieldDefinitions();
+    }
+
+    private List getInterfaces(
+            @Nullable ExecutableSchema schema,
+            SchemaFieldsContainer type) {
+        if (schema != null) {
+            return schema.getInterfaces(type);
+        }
+        if (type instanceof GraphQLObjectType) {
+            return ((GraphQLObjectType) type).getInterfaces().stream()
+                    .map(interfaceType -> (SchemaInterface) interfaceType)
+                    .collect(toList());
+        }
+        return ((GraphQLInterfaceType) type).getInterfaces().stream()
+                .map(interfaceType -> (SchemaInterface) interfaceType)
+                .collect(toList());
+    }
+
+    private List getUnionMembers(
+            @Nullable ExecutableSchema schema,
+            SchemaUnion type) {
+        if (schema != null) {
+            return schema.getUnionMembers(type);
+        }
+        return ((GraphQLUnionType) type).getTypes().stream()
+                .map(member -> (SchemaObject) member)
+                .collect(toList());
+    }
+
+    private String printValue(
+            @Nullable ExecutableSchema schema,
+            InputValueWithState value,
+            SchemaInputType type) {
+        if (type instanceof GraphQLType) {
+            return AstPrinter.printAst(
+                    ValuesResolver.valueToLiteral(
+                            value,
+                            (GraphQLType) type,
+                            GraphQLContext.getDefault(),
+                            Locale.getDefault()));
+        }
+        if (value.isLiteral()) {
+            return AstPrinter.printAst(
+                    (Value) assertNotNull(value.getValue()));
+        }
+        return AstPrinter.printAst(valueToLiteral(
+                assertNotNull(schema),
+                value.getValue(),
+                type,
+                value.isInternal()));
+    }
+
+    private Value valueToLiteral(
+            ExecutableSchema schema,
+            @Nullable Object value,
+            SchemaInputType type,
+            boolean internal) {
+        SchemaInputType unwrappedType = unwrapNonNull(type);
+        if (value == null) {
+            return NullValue.of();
+        }
+        if (unwrappedType instanceof SchemaList) {
+            return listLiteral(
+                    schema,
+                    value,
+                    (SchemaList) unwrappedType,
+                    internal);
+        }
+        if (unwrappedType instanceof SchemaInputObject) {
+            return objectLiteral(
+                    schema,
+                    value,
+                    (SchemaInputObject) unwrappedType,
+                    internal);
+        }
+        if (unwrappedType instanceof SchemaEnum) {
+            return EnumValue.newEnumValue(String.valueOf(value)).build();
+        }
+        return scalarLiteral(
+                schema,
+                value,
+                (SchemaScalar) unwrappedType,
+                internal);
+    }
+
+    private SchemaInputType unwrapNonNull(SchemaInputType type) {
+        if (!(type instanceof SchemaNonNull)) {
+            return type;
+        }
+        return (SchemaInputType)
+                ((SchemaModifiedType) type).getWrappedType();
+    }
+
+    private Value listLiteral(
+            ExecutableSchema schema,
+            Object value,
+            SchemaList type,
+            boolean internal) {
+        SchemaInputType wrappedType =
+                (SchemaInputType) type.getWrappedType();
+        List values = new ArrayList<>();
+        for (Object item : FpKit.toListOrSingletonList(value)) {
+            values.add(valueToLiteral(
+                    schema,
+                    item,
+                    wrappedType,
+                    internal));
+        }
+        return ArrayValue.newArrayValue().values(values).build();
+    }
+
+    private Value objectLiteral(
+            ExecutableSchema schema,
+            Object value,
+            SchemaInputObject type,
+            boolean internal) {
+        if (!(value instanceof Map)) {
+            return assertShouldNeverHappen(
+                    "Cannot print value '%s' for input object '%s' "
+                            + "without a map",
+                    value,
+                    type.getName());
+        }
+        Map map = (Map) value;
+        List fields = new ArrayList<>();
+        for (SchemaInputField field : schema.getInputFields(type)) {
+            if (!map.containsKey(field.getName())) {
+                continue;
+            }
+            fields.add(ObjectField.newObjectField()
+                    .name(field.getName())
+                    .value(valueToLiteral(
+                            schema,
+                            map.get(field.getName()),
+                            field.getType(),
+                            internal))
+                    .build());
+        }
+        return ObjectValue.newObjectValue().objectFields(fields).build();
+    }
+
+    private Value scalarLiteral(
+            ExecutableSchema schema,
+            Object value,
+            SchemaScalar type,
+            boolean internal) {
+        Object externalValue = internal
+                ? schema.getScalarCoercing(type).serialize(
+                        value,
+                        GraphQLContext.getDefault(),
+                        Locale.getDefault())
+                : value;
+        return schema.getScalarCoercing(type).valueToLiteral(
+                assertNotNull(externalValue),
+                GraphQLContext.getDefault(),
+                Locale.getDefault());
+    }
+
+    private boolean isIncluded(SchemaNamedElement element) {
+        if (!(element instanceof GraphQLSchemaElement)) {
+            return true;
+        }
+        return options.getIncludeSchemaElement()
+                .test((GraphQLSchemaElement) element);
+    }
+
+    private  List sort(
+            @Nullable SchemaNamedElement parent,
+            List elements,
+            boolean topLevel) {
+        if (elements.size() < 2) {
+            return new ArrayList<>(elements);
+        }
+        if (elements.get(0) instanceof GraphQLSchemaElement) {
+            return sortGraphQL(
+                    graphQLParentClass(parent),
+                    graphQLElementClass(elements.get(0), topLevel),
+                    elements);
+        }
+        List result = new ArrayList<>(elements);
+        if (options.getComparatorRegistry()
+                == GraphqlTypeComparatorRegistry.AS_IS_REGISTRY) {
+            return result;
+        }
+        Comparator comparator =
+                Comparator.comparing(SchemaNamedElement::getName);
+        if (topLevel
+                && options.getComparatorRegistry()
+                != GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) {
+            comparator = Comparator
+                    .comparingInt((T element) -> topLevelRank(element))
+                    .thenComparing(SchemaNamedElement::getName);
+        }
+        result.sort(comparator);
+        return result;
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    private  List sortGraphQL(
+            Class parentType,
+            @Nullable Class elementType,
+            List elements) {
+        List result = new ArrayList<>(elements);
+        if (result.size() < 2) {
+            return result;
+        }
+        Comparator comparator = getComparator(parentType, elementType);
+        result.sort(comparator);
+        return result;
+    }
+
+    private Class legacyParentType(
+            @Nullable Class parentType) {
+        return parentType == null
+                ? GraphQLSchemaElement.class
+                : parentType;
+    }
+
+    private Class graphQLParentClass(
+            @Nullable SchemaNamedElement parent) {
+        if (parent == null) {
+            return GraphQLSchemaElement.class;
+        }
+        if (parent instanceof SchemaObject) {
+            return GraphQLObjectType.class;
+        }
+        if (parent instanceof SchemaInterface) {
+            return GraphQLInterfaceType.class;
+        }
+        if (parent instanceof SchemaUnion) {
+            return GraphQLUnionType.class;
+        }
+        if (parent instanceof SchemaEnum) {
+            return GraphQLEnumType.class;
+        }
+        if (parent instanceof SchemaEnumValue) {
+            return GraphQLEnumValueDefinition.class;
+        }
+        if (parent instanceof SchemaScalar) {
+            return GraphQLScalarType.class;
+        }
+        if (parent instanceof SchemaInputObject) {
+            return GraphQLInputObjectType.class;
+        }
+        if (parent instanceof SchemaInputField) {
+            return GraphQLInputObjectField.class;
+        }
+        if (parent instanceof SchemaField) {
+            return GraphQLFieldDefinition.class;
+        }
+        if (parent instanceof SchemaArgument) {
+            return GraphQLArgument.class;
+        }
+        if (parent instanceof SchemaDirective) {
+            return GraphQLDirective.class;
+        }
+        if (parent instanceof SchemaAppliedDirective) {
+            return GraphQLAppliedDirective.class;
+        }
+        return GraphQLSchemaElement.class;
+    }
+
+    private @Nullable Class
+            graphQLElementClass(
+                    SchemaNamedElement element,
+                    boolean topLevel) {
+        if (topLevel) {
+            return null;
+        }
+        if (element instanceof SchemaObject
+                || element instanceof SchemaInterface) {
+            return GraphQLOutputType.class;
+        }
+        if (element instanceof SchemaEnumValue) {
+            return GraphQLEnumValueDefinition.class;
+        }
+        if (element instanceof SchemaInputField) {
+            return GraphQLInputObjectField.class;
+        }
+        if (element instanceof SchemaField) {
+            return GraphQLFieldDefinition.class;
+        }
+        if (element instanceof SchemaArgument) {
+            return GraphQLArgument.class;
+        }
+        if (element instanceof SchemaAppliedDirectiveArgument) {
+            return GraphQLAppliedDirectiveArgument.class;
+        }
+        if (element instanceof SchemaAppliedDirective) {
+            return GraphQLAppliedDirective.class;
+        }
+        if (element instanceof SchemaDirective) {
+            return GraphQLDirective.class;
+        }
+        if (element instanceof SchemaUnion) {
+            return GraphQLUnionType.class;
+        }
+        if (element instanceof SchemaEnum) {
+            return GraphQLEnumType.class;
+        }
+        if (element instanceof SchemaScalar) {
+            return GraphQLScalarType.class;
+        }
+        if (element instanceof SchemaInputObject) {
+            return GraphQLInputObjectType.class;
+        }
+        return GraphQLSchemaElement.class;
+    }
+
+    private int topLevelRank(SchemaNamedElement element) {
+        if (element instanceof SchemaDirective) {
+            return 1;
+        }
+        if (element instanceof SchemaInterface) {
+            return 2;
+        }
+        if (element instanceof SchemaUnion) {
+            return 3;
+        }
+        if (element instanceof SchemaObject) {
+            return 4;
+        }
+        if (element instanceof SchemaEnum) {
+            return 5;
+        }
+        if (element instanceof SchemaScalar) {
+            return 6;
+        }
+        if (element instanceof SchemaInputObject) {
+            return 7;
+        }
+        return 0;
+    }
+
+    private Comparator getComparator(
+            Class parentType,
+            @Nullable Class elementType) {
         GraphqlTypeComparatorEnvironment environment = GraphqlTypeComparatorEnvironment.newEnvironment()
                 .parentType(parentType)
                 .elementType(elementType)
diff --git a/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java b/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java
deleted file mode 100644
index 6ca69e23d1..0000000000
--- a/src/main/java/graphql/schema/idl/SchemaPrinterWriter.java
+++ /dev/null
@@ -1,916 +0,0 @@
-package graphql.schema.idl;
-
-import graphql.GraphQLContext;
-import graphql.Internal;
-import graphql.introspection.Introspection.DirectiveLocation;
-import graphql.language.ArrayValue;
-import graphql.language.AstPrinter;
-import graphql.language.Comment;
-import graphql.language.DescribedNode;
-import graphql.language.EnumValue;
-import graphql.language.Node;
-import graphql.language.NullValue;
-import graphql.language.ObjectField;
-import graphql.language.ObjectValue;
-import graphql.language.Value;
-import graphql.schema.ExecutableSchema;
-import graphql.schema.GraphQLAppliedDirective;
-import graphql.schema.GraphQLAppliedDirectiveArgument;
-import graphql.schema.GraphQLArgument;
-import graphql.schema.GraphQLDirective;
-import graphql.schema.GraphQLEnumValueDefinition;
-import graphql.schema.GraphQLFieldDefinition;
-import graphql.schema.GraphQLInputObjectField;
-import graphql.schema.GraphQLInterfaceType;
-import graphql.schema.GraphQLObjectType;
-import graphql.schema.GraphQLOutputType;
-import graphql.schema.GraphQLSchemaElement;
-import graphql.schema.GraphQLType;
-import graphql.schema.GraphQLUnionType;
-import graphql.schema.GraphqlTypeComparatorEnvironment;
-import graphql.schema.GraphqlTypeComparatorRegistry;
-import graphql.schema.InputValueWithState;
-import graphql.schema.SchemaAppliedDirective;
-import graphql.schema.SchemaAppliedDirectiveArgument;
-import graphql.schema.SchemaArgument;
-import graphql.schema.SchemaDirective;
-import graphql.schema.SchemaEnum;
-import graphql.schema.SchemaEnumValue;
-import graphql.schema.SchemaField;
-import graphql.schema.SchemaFieldsContainer;
-import graphql.schema.SchemaInputField;
-import graphql.schema.SchemaInputObject;
-import graphql.schema.SchemaInputType;
-import graphql.schema.SchemaInterface;
-import graphql.schema.SchemaList;
-import graphql.schema.SchemaModifiedType;
-import graphql.schema.SchemaNamedElement;
-import graphql.schema.SchemaNamedType;
-import graphql.schema.SchemaNonNull;
-import graphql.schema.SchemaObject;
-import graphql.schema.SchemaScalar;
-import graphql.schema.SchemaType;
-import graphql.schema.SchemaUnion;
-import graphql.util.FpKit;
-import org.jspecify.annotations.NullMarked;
-import org.jspecify.annotations.Nullable;
-
-import java.io.PrintWriter;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static graphql.Assert.assertNotNull;
-import static graphql.Assert.assertShouldNeverHappen;
-import static graphql.util.EscapeUtil.escapeJsonString;
-
-@Internal
-@NullMarked
-public final class SchemaPrinterWriter {
-
-    private final ExecutableSchema schema;
-    private final SchemaPrinter.Options options;
-
-    public SchemaPrinterWriter(
-            ExecutableSchema schema,
-            SchemaPrinter.Options options) {
-        this.schema = assertNotNull(schema);
-        this.options = assertNotNull(options);
-    }
-
-    public String print() {
-        StringWriter stringWriter = new StringWriter();
-        PrintWriter out = new PrintWriter(stringWriter);
-        printSchema(out);
-
-        List elements = new ArrayList<>();
-        elements.addAll(schema.getTypes());
-        elements.addAll(schema.getDirectives());
-        elements.removeIf(element -> !isIncluded(element));
-        elements = sort(null, elements, null, true);
-        for (SchemaNamedElement element : elements) {
-            printElement(out, element);
-        }
-        return trimNewLineChars(stringWriter.toString());
-    }
-
-    private void printSchema(PrintWriter out) {
-        SchemaObject queryType = schema.getQueryType();
-        SchemaObject mutationType = schema.getMutationType();
-        SchemaObject subscriptionType = schema.getSubscriptionType();
-        boolean needsSchemaPrinted = options.isIncludeSchemaDefinition()
-                || !"Query".equals(queryType.getName());
-        if (mutationType != null
-                && !"Mutation".equals(mutationType.getName())) {
-            needsSchemaPrinted = true;
-        }
-        if (subscriptionType != null
-                && !"Subscription".equals(subscriptionType.getName())) {
-            needsSchemaPrinted = true;
-        }
-        if (!needsSchemaPrinted) {
-            return;
-        }
-
-        printComments(
-                out,
-                schema.getDescription(),
-                schema.getDefinition(),
-                "");
-        out.format(
-                "schema %s{\n",
-                directivesString(null, schema.getAppliedDirectives(), true));
-        out.format("  query: %s\n", queryType.getName());
-        if (mutationType != null) {
-            out.format("  mutation: %s\n", mutationType.getName());
-        }
-        if (subscriptionType != null) {
-            out.format("  subscription: %s\n", subscriptionType.getName());
-        }
-        out.format("}\n\n");
-    }
-
-    private void printElement(
-            PrintWriter out,
-            SchemaNamedElement element) {
-        if (element instanceof SchemaObject) {
-            printObject(out, (SchemaObject) element);
-            return;
-        }
-        if (element instanceof SchemaInterface) {
-            printInterface(out, (SchemaInterface) element);
-            return;
-        }
-        if (element instanceof SchemaUnion) {
-            printUnion(out, (SchemaUnion) element);
-            return;
-        }
-        if (element instanceof SchemaEnum) {
-            printEnum(out, (SchemaEnum) element);
-            return;
-        }
-        if (element instanceof SchemaScalar) {
-            printScalar(out, (SchemaScalar) element);
-            return;
-        }
-        if (element instanceof SchemaInputObject) {
-            printInputObject(out, (SchemaInputObject) element);
-            return;
-        }
-        if (element instanceof SchemaDirective) {
-            printDirectiveDefinition(out, (SchemaDirective) element);
-            return;
-        }
-        assertShouldNeverHappen(
-                "Unsupported top-level schema print element %s",
-                element);
-    }
-
-    private void printObject(PrintWriter out, SchemaObject type) {
-        if (isIntrospectionType(type)) {
-            return;
-        }
-        printComments(out, type, "");
-        out.format("type %s", type.getName());
-        printInterfaces(out, type);
-        out.print(directivesString(
-                type,
-                schema.getAppliedDirectives(type),
-                false));
-        printFields(out, type);
-        out.format("\n\n");
-    }
-
-    private void printInterface(
-            PrintWriter out,
-            SchemaInterface type) {
-        if (isIntrospectionType(type)) {
-            return;
-        }
-        printComments(out, type, "");
-        out.format("interface %s", type.getName());
-        printInterfaces(out, type);
-        out.print(directivesString(
-                type,
-                schema.getAppliedDirectives(type),
-                false));
-        printFields(out, type);
-        out.format("\n\n");
-    }
-
-    private void printInterfaces(
-            PrintWriter out,
-            SchemaFieldsContainer type) {
-        List interfaces = sort(
-                type,
-                schema.getInterfaces(type),
-                GraphQLOutputType.class,
-                false);
-        if (interfaces.isEmpty()) {
-            return;
-        }
-        String names = interfaces.stream()
-                .map(SchemaInterface::getName)
-                .collect(Collectors.joining(" & "));
-        out.format(" implements %s", names);
-    }
-
-    private void printFields(
-            PrintWriter out,
-            SchemaFieldsContainer type) {
-        List fields = sort(
-                type,
-                schema.getFields(type),
-                GraphQLFieldDefinition.class,
-                false);
-        fields.removeIf(field -> !isIncluded(field));
-        if (fields.isEmpty()) {
-            return;
-        }
-        out.format(" {\n");
-        for (SchemaField field : fields) {
-            printComments(out, field, "  ");
-            out.format(
-                    "  %s%s: %s%s\n",
-                    field.getName(),
-                    argsString(field),
-                    typeString(field.getType()),
-                    directivesString(
-                            field,
-                            schema.getAppliedDirectives(field),
-                            false));
-        }
-        out.format("}");
-    }
-
-    private void printUnion(PrintWriter out, SchemaUnion type) {
-        if (isIntrospectionType(type)) {
-            return;
-        }
-        printComments(out, type, "");
-        out.format(
-                "union %s%s = ",
-                type.getName(),
-                directivesString(
-                        type,
-                        schema.getAppliedDirectives(type),
-                        false));
-        List members = sort(
-                type,
-                schema.getUnionMembers(type),
-                GraphQLOutputType.class,
-                false);
-        out.print(members.stream()
-                .map(SchemaObject::getName)
-                .collect(Collectors.joining(" | ")));
-        out.format("\n\n");
-    }
-
-    private void printEnum(PrintWriter out, SchemaEnum type) {
-        if (isIntrospectionType(type)) {
-            return;
-        }
-        printComments(out, type, "");
-        out.format(
-                "enum %s%s",
-                type.getName(),
-                directivesString(
-                        type,
-                        schema.getAppliedDirectives(type),
-                        false));
-        List values = sort(
-                type,
-                type.getValues(),
-                GraphQLEnumValueDefinition.class,
-                false);
-        if (!values.isEmpty()) {
-            out.format(" {\n");
-            for (SchemaEnumValue value : values) {
-                printComments(out, value, "  ");
-                out.format(
-                        "  %s%s\n",
-                        value.getName(),
-                        directivesString(
-                                value,
-                                schema.getAppliedDirectives(value),
-                                false));
-            }
-            out.format("}");
-        }
-        out.format("\n\n");
-    }
-
-    private void printScalar(PrintWriter out, SchemaScalar type) {
-        if (!options.isIncludeScalars()
-                || ScalarInfo.isGraphqlSpecifiedScalar(type.getName())) {
-            return;
-        }
-        printComments(out, type, "");
-        List directives =
-                new ArrayList<>(schema.getAppliedDirectives(type));
-        directives.removeIf(directive ->
-                "specifiedBy".equals(directive.getName()));
-        out.format(
-                "scalar %s%s%s\n\n",
-                type.getName(),
-                directivesString(type, directives, false),
-                specifiedByUrlString(type));
-    }
-
-    private String specifiedByUrlString(SchemaScalar scalar) {
-        String url = scalar.getSpecifiedByUrl();
-        if (url == null
-                || !options.getIncludeDirective().test("specifiedBy")) {
-            return "";
-        }
-        return " @specifiedBy(url : \"" + escapeJsonString(url) + "\")";
-    }
-
-    private void printInputObject(
-            PrintWriter out,
-            SchemaInputObject type) {
-        if (isIntrospectionType(type)) {
-            return;
-        }
-        printComments(out, type, "");
-        out.format(
-                "input %s%s",
-                type.getName(),
-                directivesString(
-                        type,
-                        schema.getAppliedDirectives(type),
-                        false));
-        List fields = sort(
-                type,
-                schema.getInputFields(type),
-                GraphQLInputObjectField.class,
-                false);
-        fields.removeIf(field -> !isIncluded(field));
-        if (!fields.isEmpty()) {
-            out.format(" {\n");
-            for (SchemaInputField field : fields) {
-                printInputField(out, field);
-            }
-            out.format("}");
-        }
-        out.format("\n\n");
-    }
-
-    private void printInputField(
-            PrintWriter out,
-            SchemaInputField field) {
-        printComments(out, field, "  ");
-        SchemaInputType type = field.getType();
-        out.format(
-                "  %s: %s",
-                field.getName(),
-                typeString(type));
-        InputValueWithState defaultValue =
-                field.getInputFieldDefaultValue();
-        if (defaultValue.isSet()) {
-            out.format(" = %s", printValue(defaultValue, type));
-        }
-        out.print(directivesString(
-                field,
-                schema.getAppliedDirectives(field),
-                false));
-        out.format("\n");
-    }
-
-    private void printDirectiveDefinition(
-            PrintWriter out,
-            SchemaDirective directive) {
-        if (!options.isIncludeDirectiveDefinitions()
-                || !options.getIncludeDirective().test(directive.getName())
-                || !options.getIncludeDirectiveDefinition()
-                .test(directive.getName())) {
-            return;
-        }
-        printComments(out, directive, "");
-        out.format(
-                "directive @%s%s%s",
-                directive.getName(),
-                argsString(directive),
-                directivesString(
-                        directive,
-                        schema.getAppliedDirectives(directive),
-                        false));
-        if (directive.isRepeatable()) {
-            out.print(" repeatable");
-        }
-        String locations = directive.validLocations().stream()
-                .map(DirectiveLocation::name)
-                .collect(Collectors.joining(" | "));
-        out.format(" on %s\n\n", locations);
-    }
-
-    private String argsString(SchemaNamedElement parent) {
-        List parentArguments =
-                getArguments(parent);
-        boolean hasComments = parentArguments.stream()
-                .anyMatch(this::hasAstDefinitionComments);
-        boolean hasDescriptions = parentArguments.stream()
-                .anyMatch(this::hasDescription);
-        String halfPrefix = hasComments || hasDescriptions ? "  " : "";
-        String prefix = hasComments || hasDescriptions ? "    " : "";
-        List arguments = sort(
-                parent,
-                parentArguments,
-                GraphQLArgument.class,
-                false);
-        arguments.removeIf(argument -> !isIncluded(argument));
-        if (arguments.isEmpty()) {
-            return "";
-        }
-        return argumentsString(
-                arguments,
-                halfPrefix,
-                prefix,
-                hasComments || hasDescriptions);
-    }
-
-    private List getArguments(
-            SchemaNamedElement parent) {
-        if (parent instanceof SchemaField) {
-            return ((SchemaField) parent).getArguments();
-        }
-        return ((SchemaDirective) parent).getArguments();
-    }
-
-    private String argumentsString(
-            List arguments,
-            String halfPrefix,
-            String prefix,
-            boolean multiline) {
-        StringBuilder result = new StringBuilder("(");
-        for (int i = 0; i < arguments.size(); i++) {
-            appendArgumentSeparator(result, i, multiline);
-            SchemaArgument argument = arguments.get(i);
-            result.append(commentsString(argument, prefix));
-            SchemaInputType type = argument.getType();
-            result.append(prefix)
-                    .append(argument.getName())
-                    .append(": ")
-                    .append(typeString(type));
-            appendDefaultValue(result, argument, type);
-            result.append(directivesString(
-                    argument,
-                    schema.getAppliedDirectives(argument),
-                    false));
-        }
-        if (multiline) {
-            result.append("\n");
-        }
-        return result.append(halfPrefix).append(")").toString();
-    }
-
-    private void appendArgumentSeparator(
-            StringBuilder result,
-            int index,
-            boolean multiline) {
-        if (index > 0) {
-            result.append(",");
-            if (!multiline) {
-                result.append(" ");
-            }
-        }
-        if (multiline) {
-            result.append("\n");
-        }
-    }
-
-    private void appendDefaultValue(
-            StringBuilder result,
-            SchemaArgument argument,
-            SchemaInputType type) {
-        InputValueWithState defaultValue =
-                argument.getArgumentDefaultValue();
-        if (!defaultValue.isSet()) {
-            return;
-        }
-        result.append(" = ").append(printValue(defaultValue, type));
-    }
-
-    private String directivesString(
-            @Nullable SchemaNamedElement parent,
-            List directives,
-            boolean schemaContainer) {
-        List included = directives.stream()
-                .filter(this::isIncluded)
-                .filter(directive -> options.getIncludeDirective()
-                        .test(directive.getName()))
-                .collect(Collectors.toCollection(ArrayList::new));
-        if (included.isEmpty()) {
-            return "";
-        }
-        included = sort(
-                parent,
-                included,
-                GraphQLAppliedDirective.class,
-                false);
-        String result = included.stream()
-                .map(this::directiveString)
-                .collect(Collectors.joining(" "));
-        return schemaContainer ? result : " " + result;
-    }
-
-    private String directiveString(SchemaAppliedDirective directive) {
-        StringBuilder result = new StringBuilder("@")
-                .append(directive.getName());
-        List arguments =
-                directive.getArguments().stream()
-                        .filter(argument -> argument.getArgumentValue().isSet())
-                        .collect(Collectors.toCollection(ArrayList::new));
-        arguments = sort(
-                directive,
-                arguments,
-                GraphQLAppliedDirectiveArgument.class,
-                false);
-        if (arguments.isEmpty()) {
-            return result.toString();
-        }
-        result.append("(");
-        for (int i = 0; i < arguments.size(); i++) {
-            if (i > 0) {
-                result.append(", ");
-            }
-            SchemaAppliedDirectiveArgument argument = arguments.get(i);
-            result.append(argument.getName())
-                    .append(" : ")
-                    .append(printValue(
-                            argument.getArgumentValue(),
-                            argument.getType()));
-        }
-        return result.append(")").toString();
-    }
-
-    private String typeString(SchemaType type) {
-        if (type instanceof SchemaList) {
-            return "[" + typeString(
-                    ((SchemaList) type).getWrappedType()) + "]";
-        }
-        if (type instanceof SchemaNonNull) {
-            return typeString(
-                    ((SchemaNonNull) type).getWrappedType()) + "!";
-        }
-        return ((SchemaNamedType) type).getName();
-    }
-
-    private String printValue(
-            InputValueWithState value,
-            SchemaInputType type) {
-        if (type instanceof GraphQLType) {
-            return AstPrinter.printAst(
-                    graphql.execution.ValuesResolver.valueToLiteral(
-                            value,
-                            (GraphQLType) type,
-                            GraphQLContext.getDefault(),
-                            Locale.getDefault()));
-        }
-        if (value.isLiteral()) {
-            return AstPrinter.printAst(
-                    (Value) assertNotNull(value.getValue()));
-        }
-        Value literal = valueToLiteral(
-                value.getValue(),
-                type,
-                value.isInternal());
-        return AstPrinter.printAst(literal);
-    }
-
-    private Value valueToLiteral(
-            @Nullable Object value,
-            SchemaInputType type,
-            boolean internal) {
-        SchemaInputType unwrappedType = unwrapNonNull(type);
-        if (value == null) {
-            return NullValue.of();
-        }
-        if (unwrappedType instanceof SchemaList) {
-            return listLiteral(
-                    value,
-                    (SchemaList) unwrappedType,
-                    internal);
-        }
-        if (unwrappedType instanceof SchemaInputObject) {
-            return objectLiteral(
-                    value,
-                    (SchemaInputObject) unwrappedType,
-                    internal);
-        }
-        if (unwrappedType instanceof SchemaEnum) {
-            return EnumValue.newEnumValue(String.valueOf(value)).build();
-        }
-        return scalarLiteral(
-                value,
-                (SchemaScalar) unwrappedType,
-                internal);
-    }
-
-    private SchemaInputType unwrapNonNull(SchemaInputType type) {
-        if (!(type instanceof SchemaNonNull)) {
-            return type;
-        }
-        return (SchemaInputType)
-                ((SchemaModifiedType) type).getWrappedType();
-    }
-
-    private Value listLiteral(
-            Object value,
-            SchemaList type,
-            boolean internal) {
-        SchemaInputType wrappedType =
-                (SchemaInputType) type.getWrappedType();
-        List values = new ArrayList<>();
-        for (Object item : FpKit.toListOrSingletonList(value)) {
-            values.add(valueToLiteral(item, wrappedType, internal));
-        }
-        return ArrayValue.newArrayValue().values(values).build();
-    }
-
-    private Value objectLiteral(
-            Object value,
-            SchemaInputObject type,
-            boolean internal) {
-        if (!(value instanceof Map)) {
-            return assertShouldNeverHappen(
-                    "Cannot print value '%s' for input object '%s' "
-                            + "without a map",
-                    value,
-                    type.getName());
-        }
-        Map map = (Map) value;
-        List fields = new ArrayList<>();
-        for (SchemaInputField field : schema.getInputFields(type)) {
-            if (!map.containsKey(field.getName())) {
-                continue;
-            }
-            fields.add(ObjectField.newObjectField()
-                    .name(field.getName())
-                    .value(valueToLiteral(
-                            map.get(field.getName()),
-                            field.getType(),
-                            internal))
-                    .build());
-        }
-        return ObjectValue.newObjectValue().objectFields(fields).build();
-    }
-
-    private Value scalarLiteral(
-            Object value,
-            SchemaScalar type,
-            boolean internal) {
-        Object externalValue = internal
-                ? schema.getScalarCoercing(type).serialize(
-                        value,
-                        GraphQLContext.getDefault(),
-                        Locale.getDefault())
-                : value;
-        return schema.getScalarCoercing(type).valueToLiteral(
-                assertNotNull(externalValue),
-                GraphQLContext.getDefault(),
-                Locale.getDefault());
-    }
-
-    private boolean isIntrospectionType(SchemaNamedType type) {
-        return !options.isIncludeIntrospectionTypes()
-                && type.getName().startsWith("__");
-    }
-
-    private boolean isIncluded(SchemaNamedElement element) {
-        if (!(element instanceof GraphQLSchemaElement)) {
-            return true;
-        }
-        return options.getIncludeSchemaElement()
-                .test((GraphQLSchemaElement) element);
-    }
-
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    private  List sort(
-            @Nullable SchemaNamedElement parent,
-            List elements,
-            @Nullable Class elementType,
-            boolean topLevel) {
-        List result = new ArrayList<>(elements);
-        if (result.size() < 2) {
-            return result;
-        }
-        if (result.get(0) instanceof GraphQLSchemaElement) {
-            Class parentType =
-                    graphQLParentClass(parent);
-            GraphqlTypeComparatorEnvironment environment =
-                    GraphqlTypeComparatorEnvironment.newEnvironment()
-                            .parentType(parentType)
-                            .elementType(elementType)
-                            .build();
-            Comparator comparator = options.getComparatorRegistry()
-                    .getComparator(environment);
-            result.sort(comparator);
-            return result;
-        }
-        if (options.getComparatorRegistry()
-                == GraphqlTypeComparatorRegistry.AS_IS_REGISTRY) {
-            return result;
-        }
-        Comparator comparator =
-                Comparator.comparing(SchemaNamedElement::getName);
-        if (topLevel
-                && options.getComparatorRegistry()
-                != GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) {
-            comparator = Comparator
-                    .comparingInt((T element) -> topLevelRank(element))
-                    .thenComparing(SchemaNamedElement::getName);
-        }
-        result.sort(comparator);
-        return result;
-    }
-
-    private Class graphQLParentClass(
-            @Nullable SchemaNamedElement parent) {
-        if (parent == null) {
-            return GraphQLSchemaElement.class;
-        }
-        if (parent instanceof SchemaObject) {
-            return GraphQLObjectType.class;
-        }
-        if (parent instanceof SchemaInterface) {
-            return GraphQLInterfaceType.class;
-        }
-        if (parent instanceof SchemaUnion) {
-            return GraphQLUnionType.class;
-        }
-        if (parent instanceof SchemaEnum) {
-            return graphql.schema.GraphQLEnumType.class;
-        }
-        if (parent instanceof SchemaInputObject) {
-            return graphql.schema.GraphQLInputObjectType.class;
-        }
-        if (parent instanceof SchemaField) {
-            return GraphQLFieldDefinition.class;
-        }
-        if (parent instanceof SchemaDirective) {
-            return GraphQLDirective.class;
-        }
-        if (parent instanceof SchemaAppliedDirective) {
-            return GraphQLAppliedDirective.class;
-        }
-        return GraphQLSchemaElement.class;
-    }
-
-    private int topLevelRank(SchemaNamedElement element) {
-        if (element instanceof SchemaDirective) {
-            return 1;
-        }
-        if (element instanceof SchemaInterface) {
-            return 2;
-        }
-        if (element instanceof SchemaUnion) {
-            return 3;
-        }
-        if (element instanceof SchemaObject) {
-            return 4;
-        }
-        if (element instanceof SchemaEnum) {
-            return 5;
-        }
-        if (element instanceof SchemaScalar) {
-            return 6;
-        }
-        if (element instanceof SchemaInputObject) {
-            return 7;
-        }
-        return 0;
-    }
-
-    private void printComments(
-            PrintWriter out,
-            SchemaNamedElement element,
-            String prefix) {
-        out.print(commentsString(element, prefix));
-    }
-
-    private String commentsString(
-            SchemaNamedElement element,
-            String prefix) {
-        return commentsString(
-                description(element),
-                element.getDefinition(),
-                prefix);
-    }
-
-    private void printComments(
-            PrintWriter out,
-            @Nullable String description,
-            @Nullable Node definition,
-            String prefix) {
-        out.print(commentsString(description, definition, prefix));
-    }
-
-    private String commentsString(
-            @Nullable String description,
-            @Nullable Node definition,
-            String prefix) {
-        StringWriter stringWriter = new StringWriter();
-        PrintWriter out = new PrintWriter(stringWriter);
-        if (description != null && !description.isEmpty()) {
-            printDescription(out, prefix, description);
-        }
-        if (options.isIncludeAstDefinitionComments()) {
-            printAstDefinitionComments(out, definition, prefix);
-        }
-        return stringWriter.toString();
-    }
-
-    private @Nullable String description(SchemaNamedElement element) {
-        String description = element.getDescription();
-        if (description != null && !description.isEmpty()) {
-            return description;
-        }
-        Node definition = element.getDefinition();
-        if (!(definition instanceof DescribedNode)) {
-            return description;
-        }
-        graphql.language.Description astDescription =
-                ((DescribedNode) definition).getDescription();
-        return astDescription == null
-                ? description
-                : astDescription.getContent();
-    }
-
-    private void printAstDefinitionComments(
-            PrintWriter out,
-            @Nullable Node definition,
-            String prefix) {
-        if (definition == null || definition.getComments().isEmpty()) {
-            return;
-        }
-        List comments = definition.getComments().stream()
-                .map(Comment::getContent)
-                .collect(Collectors.toList());
-        printHashDescription(out, prefix, comments);
-    }
-
-    private void printDescription(
-            PrintWriter out,
-            String prefix,
-            String description) {
-        List lines = Arrays.asList(description.split("\n"));
-        if (lines.isEmpty()) {
-            return;
-        }
-        if (options.isDescriptionsAsHashComments()) {
-            printHashDescription(out, prefix, lines);
-            return;
-        }
-        if (lines.size() > 1) {
-            out.printf("%s\"\"\"\n", prefix);
-            for (String line : lines) {
-                out.printf(
-                        "%s%s\n",
-                        prefix,
-                        line.replaceAll("\"\"\"", "\\\\\"\"\""));
-            }
-            out.printf("%s\"\"\"\n", prefix);
-            return;
-        }
-        out.printf(
-                "%s\"%s\"\n",
-                prefix,
-                escapeJsonString(lines.get(0)));
-    }
-
-    private void printHashDescription(
-            PrintWriter out,
-            String prefix,
-            List lines) {
-        for (String line : lines) {
-            out.printf("%s#%s\n", prefix, line);
-        }
-    }
-
-    private boolean hasDescription(SchemaNamedElement element) {
-        String description = description(element);
-        return description != null && !description.isEmpty();
-    }
-
-    private boolean hasAstDefinitionComments(
-            SchemaNamedElement element) {
-        if (!options.isIncludeAstDefinitionComments()) {
-            return false;
-        }
-        Node definition = element.getDefinition();
-        return definition != null && !definition.getComments().isEmpty();
-    }
-
-    private String trimNewLineChars(String value) {
-        if (value.endsWith("\n\n")) {
-            return value.substring(0, value.length() - 1);
-        }
-        return value;
-    }
-}
diff --git a/src/main/java/graphql/schema/universe/SUAppliedDirective.java b/src/main/java/graphql/schema/universe/SUAppliedDirective.java
index 4f30108b2e..41257d47f3 100644
--- a/src/main/java/graphql/schema/universe/SUAppliedDirective.java
+++ b/src/main/java/graphql/schema/universe/SUAppliedDirective.java
@@ -2,7 +2,6 @@
 
 import graphql.ExperimentalApi;
 import graphql.Internal;
-import graphql.language.Directive;
 import org.jspecify.annotations.Nullable;
 
 import java.util.Arrays;
@@ -14,7 +13,6 @@
 @ExperimentalApi
 public final class SUAppliedDirective extends SUVertex {
 
-    private final @Nullable Directive definition;
     private final SUAppliedDirectiveArgument[] arguments;
 
     @Internal
@@ -22,18 +20,12 @@ public SUAppliedDirective(
             int id,
             int nameId,
             String name,
-            @Nullable Directive definition,
             List arguments) {
         super(id, nameId, SUVertexKind.APPLIED_DIRECTIVE, name, null);
-        this.definition = definition;
         this.arguments = assertNotNull(arguments)
                 .toArray(new SUAppliedDirectiveArgument[0]);
     }
 
-    public @Nullable Directive getDefinition() {
-        return definition;
-    }
-
     public List getArguments() {
         if (arguments.length == 0) {
             return Collections.emptyList();
diff --git a/src/main/java/graphql/schema/universe/SUArgument.java b/src/main/java/graphql/schema/universe/SUArgument.java
index f63272f9a1..36479101fe 100644
--- a/src/main/java/graphql/schema/universe/SUArgument.java
+++ b/src/main/java/graphql/schema/universe/SUArgument.java
@@ -2,7 +2,6 @@
 
 import graphql.ExperimentalApi;
 import graphql.Internal;
-import graphql.language.InputValueDefinition;
 import graphql.schema.InputValueWithState;
 import org.jspecify.annotations.Nullable;
 
@@ -12,7 +11,6 @@
 public final class SUArgument extends SUVertex implements SUAppliedDirectiveContainer {
 
     private final InputValueWithState defaultValue;
-    private final @Nullable InputValueDefinition definition;
 
     @Internal
     public SUArgument(
@@ -20,18 +18,12 @@ public SUArgument(
             int nameId,
             String name,
             @Nullable String description,
-            InputValueWithState defaultValue,
-            @Nullable InputValueDefinition definition) {
+            InputValueWithState defaultValue) {
         super(id, nameId, SUVertexKind.ARGUMENT, name, description);
         this.defaultValue = assertNotNull(defaultValue);
-        this.definition = definition;
     }
 
     public InputValueWithState getArgumentDefaultValue() {
         return defaultValue;
     }
-
-    public @Nullable InputValueDefinition getDefinition() {
-        return definition;
-    }
 }
diff --git a/src/main/java/graphql/schema/universe/SUAstDefinitions.java b/src/main/java/graphql/schema/universe/SUAstDefinitions.java
new file mode 100644
index 0000000000..1adb6dcffe
--- /dev/null
+++ b/src/main/java/graphql/schema/universe/SUAstDefinitions.java
@@ -0,0 +1,36 @@
+package graphql.schema.universe;
+
+import graphql.Internal;
+import graphql.language.Node;
+import org.jspecify.annotations.Nullable;
+
+import java.util.List;
+
+import static graphql.Assert.assertNotNull;
+
+/**
+ * AST provenance for a vertex that has extension definitions.
+ */
+@Internal
+public final class SUAstDefinitions {
+
+    private final @Nullable Node definition;
+    private final List> extensionDefinitions;
+
+    @Internal
+    public SUAstDefinitions(
+            @Nullable Node definition,
+            List> extensionDefinitions) {
+        this.definition = definition;
+        this.extensionDefinitions = List.copyOf(
+                assertNotNull(extensionDefinitions));
+    }
+
+    public @Nullable Node getDefinition() {
+        return definition;
+    }
+
+    public List> getExtensionDefinitions() {
+        return extensionDefinitions;
+    }
+}
diff --git a/src/main/java/graphql/schema/universe/SUDirective.java b/src/main/java/graphql/schema/universe/SUDirective.java
index 7e95d7d1c1..c2b2bc7ab8 100644
--- a/src/main/java/graphql/schema/universe/SUDirective.java
+++ b/src/main/java/graphql/schema/universe/SUDirective.java
@@ -3,7 +3,6 @@
 import graphql.ExperimentalApi;
 import graphql.Internal;
 import graphql.introspection.Introspection.DirectiveLocation;
-import graphql.language.DirectiveDefinition;
 import org.jspecify.annotations.Nullable;
 
 import java.util.EnumSet;
@@ -17,7 +16,6 @@ public final class SUDirective extends SUVertex implements SUAppliedDirectiveCon
 
     private final boolean repeatable;
     private final long locationMask;
-    private final @Nullable DirectiveDefinition definition;
 
     @Internal
     public SUDirective(
@@ -26,12 +24,10 @@ public SUDirective(
             String name,
             @Nullable String description,
             boolean repeatable,
-            Set validLocations,
-            @Nullable DirectiveDefinition definition) {
+            Set validLocations) {
         super(id, nameId, SUVertexKind.DIRECTIVE, name, description);
         this.repeatable = repeatable;
         this.locationMask = packLocations(assertNotNull(validLocations));
-        this.definition = definition;
     }
 
     public boolean isRepeatable() {
@@ -48,10 +44,6 @@ public EnumSet validLocations() {
         return result;
     }
 
-    public @Nullable DirectiveDefinition getDefinition() {
-        return definition;
-    }
-
     private static long packLocations(Set validLocations) {
         assertTrue(DirectiveLocation.values().length <= Long.SIZE,
                 "Too many directive locations to pack into a long");
diff --git a/src/main/java/graphql/schema/universe/SUExporter.java b/src/main/java/graphql/schema/universe/SUExporter.java
index aa8309109e..9f75877e79 100644
--- a/src/main/java/graphql/schema/universe/SUExporter.java
+++ b/src/main/java/graphql/schema/universe/SUExporter.java
@@ -2,7 +2,20 @@
 
 import graphql.Directives;
 import graphql.ExperimentalApi;
+import graphql.language.Directive;
+import graphql.language.DirectiveDefinition;
+import graphql.language.EnumTypeDefinition;
+import graphql.language.EnumValueDefinition;
+import graphql.language.FieldDefinition;
+import graphql.language.InputObjectTypeDefinition;
+import graphql.language.InputValueDefinition;
+import graphql.language.InterfaceTypeDefinition;
+import graphql.language.Node;
+import graphql.language.ObjectTypeDefinition;
+import graphql.language.ScalarTypeDefinition;
+import graphql.language.SchemaDefinition;
 import graphql.language.StringValue;
+import graphql.language.UnionTypeDefinition;
 import graphql.language.Value;
 import graphql.schema.GraphQLAppliedDirective;
 import graphql.schema.GraphQLAppliedDirectiveArgument;
@@ -79,6 +92,14 @@ public GraphQLSchema exportSchema() {
                         objectType(introspectionSchemaType))
                 .description(schema.getRoot().getDescription())
                 .codeRegistry(exportCodeRegistry());
+        SchemaDefinition definition = definition(
+                schema.getRoot(),
+                SchemaDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
+        builder.extensionDefinitions(
+                extensionDefinitions(schema.getRoot()));
 
         SUObjectType mutationType = schema.getMutationType();
         if (mutationType != null) {
@@ -141,7 +162,14 @@ private GraphQLObjectType exportObject(SUObjectType type) {
         GraphQLObjectType.Builder builder = GraphQLObjectType.newObject()
                 .name(requiredName(type))
                 .description(type.getDescription())
+                .extensionDefinitions(extensionDefinitions(type))
                 .replaceAppliedDirectives(exportAppliedDirectives(type));
+        ObjectTypeDefinition definition = definition(
+                type,
+                ObjectTypeDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
         for (SUField field : schema.getFields(type)) {
             builder.field(exportField(field));
         }
@@ -155,7 +183,14 @@ private GraphQLInterfaceType exportInterface(SUInterfaceType type) {
         GraphQLInterfaceType.Builder builder = GraphQLInterfaceType.newInterface()
                 .name(requiredName(type))
                 .description(type.getDescription())
+                .extensionDefinitions(extensionDefinitions(type))
                 .replaceAppliedDirectives(exportAppliedDirectives(type));
+        InterfaceTypeDefinition definition = definition(
+                type,
+                InterfaceTypeDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
         for (SUField field : schema.getFields(type)) {
             builder.field(exportField(field));
         }
@@ -169,7 +204,14 @@ private GraphQLUnionType exportUnion(SUUnionType type) {
         GraphQLUnionType.Builder builder = GraphQLUnionType.newUnionType()
                 .name(requiredName(type))
                 .description(type.getDescription())
+                .extensionDefinitions(extensionDefinitions(type))
                 .replaceAppliedDirectives(exportAppliedDirectives(type));
+        UnionTypeDefinition definition = definition(
+                type,
+                UnionTypeDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
         for (SUObjectType member : schema.getUnionMembers(type)) {
             builder.possibleType(typeReference(member));
         }
@@ -180,7 +222,14 @@ private GraphQLEnumType exportEnum(SUEnumType type) {
         GraphQLEnumType.Builder builder = GraphQLEnumType.newEnum()
                 .name(requiredName(type))
                 .description(type.getDescription())
+                .extensionDefinitions(extensionDefinitions(type))
                 .replaceAppliedDirectives(exportAppliedDirectives(type));
+        EnumTypeDefinition definition = definition(
+                type,
+                EnumTypeDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
         for (SUEnumValue value : schema.getEnumValues(type)) {
             GraphQLEnumValueDefinition.Builder valueBuilder =
                     GraphQLEnumValueDefinition.newEnumValueDefinition()
@@ -188,6 +237,12 @@ private GraphQLEnumType exportEnum(SUEnumType type) {
                             .description(value.getDescription())
                             .value(requiredName(value))
                             .replaceAppliedDirectives(exportAppliedDirectives(value));
+            EnumValueDefinition valueDefinition = definition(
+                    value,
+                    EnumValueDefinition.class);
+            if (valueDefinition != null) {
+                valueBuilder.definition(valueDefinition);
+            }
             String deprecationReason = deprecationReason(value);
             if (deprecationReason != null) {
                 valueBuilder.deprecationReason(deprecationReason);
@@ -202,17 +257,32 @@ private GraphQLScalarType exportScalar(SUScalarType type) {
         if (source != null) {
             return source;
         }
-        return GraphQLScalarType.newScalar(EchoingWiringFactory.fakeScalar(requiredName(type)))
+        GraphQLScalarType.Builder builder = GraphQLScalarType.newScalar(
+                        EchoingWiringFactory.fakeScalar(requiredName(type)))
                 .description(type.getDescription())
-                .replaceAppliedDirectives(exportAppliedDirectives(type))
-                .build();
+                .extensionDefinitions(extensionDefinitions(type))
+                .replaceAppliedDirectives(exportAppliedDirectives(type));
+        ScalarTypeDefinition definition = definition(
+                type,
+                ScalarTypeDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
+        return builder.build();
     }
 
     private GraphQLInputObjectType exportInputObject(SUInputObjectType type) {
         GraphQLInputObjectType.Builder builder = GraphQLInputObjectType.newInputObject()
                 .name(requiredName(type))
                 .description(type.getDescription())
+                .extensionDefinitions(extensionDefinitions(type))
                 .replaceAppliedDirectives(exportAppliedDirectives(type));
+        InputObjectTypeDefinition definition = definition(
+                type,
+                InputObjectTypeDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
         for (SUInputField field : schema.getInputFields(type)) {
             builder.field(exportInputField(field));
         }
@@ -225,6 +295,12 @@ private GraphQLFieldDefinition exportField(SUField field) {
                 .description(field.getDescription())
                 .type(outputType(requiredType(field)))
                 .replaceAppliedDirectives(exportAppliedDirectives(field));
+        FieldDefinition definition = definition(
+                field,
+                FieldDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
+        }
         for (SUArgument argument : schema.getArguments(field)) {
             builder.argument(exportArgument(argument));
         }
@@ -242,8 +318,11 @@ private GraphQLArgument exportArgument(SUArgument argument) {
                 .type(inputType(requiredType(argument)))
                 .replaceAppliedDirectives(exportAppliedDirectives(argument));
         applyArgumentDefault(builder, argument.getArgumentDefaultValue());
-        if (argument.getDefinition() != null) {
-            builder.definition(argument.getDefinition());
+        InputValueDefinition definition = definition(
+                argument,
+                InputValueDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
         }
         String deprecationReason = deprecationReason(argument);
         if (deprecationReason != null) {
@@ -259,8 +338,11 @@ private GraphQLInputObjectField exportInputField(SUInputField field) {
                 .type(inputType(requiredType(field)))
                 .replaceAppliedDirectives(exportAppliedDirectives(field));
         applyInputFieldDefault(builder, field.getInputFieldDefaultValue());
-        if (field.getDefinition() != null) {
-            builder.definition(field.getDefinition());
+        InputValueDefinition definition = definition(
+                field,
+                InputValueDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
         }
         String deprecationReason = deprecationReason(field);
         if (deprecationReason != null) {
@@ -274,13 +356,17 @@ private GraphQLDirective exportDirectiveDefinition(SUDirective directive) {
                 .name(requiredName(directive))
                 .description(directive.getDescription())
                 .repeatable(directive.isRepeatable())
+                .extensionDefinitions(extensionDefinitions(directive))
                 .replaceAppliedDirectives(exportAppliedDirectives(directive));
         directive.validLocations().forEach(builder::validLocation);
         for (SUArgument argument : schema.getArguments(directive)) {
             builder.argument(exportArgument(argument));
         }
-        if (directive.getDefinition() != null) {
-            builder.definition(directive.getDefinition());
+        DirectiveDefinition definition = definition(
+                directive,
+                DirectiveDefinition.class);
+        if (definition != null) {
+            builder.definition(definition);
         }
         String deprecationReason = deprecationReason(directive);
         if (deprecationReason != null) {
@@ -298,8 +384,11 @@ private List exportAppliedDirectives(SUVertex container
     private GraphQLAppliedDirective exportAppliedDirective(SUAppliedDirective directive) {
         GraphQLAppliedDirective.Builder builder = GraphQLAppliedDirective.newDirective()
                 .name(requiredName(directive));
-        if (directive.getDefinition() != null) {
-            builder.definition(directive.getDefinition());
+        Directive definition = definition(
+                directive,
+                Directive.class);
+        if (definition != null) {
+            builder.definition(definition);
         }
         for (SUAppliedDirectiveArgument argument : schema.getArguments(directive)) {
             GraphQLAppliedDirectiveArgument.Builder argumentBuilder =
@@ -329,6 +418,21 @@ private GraphQLType exportType(SUVertex type) {
         return typeReference(type);
     }
 
+    private > @Nullable T definition(
+            SUVertex vertex,
+            Class definitionType) {
+        Node definition = schema.getDefinition(vertex);
+        return definition == null
+                ? null
+                : definitionType.cast(definition);
+    }
+
+    @SuppressWarnings("unchecked")
+    private > List extensionDefinitions(
+            SUVertex vertex) {
+        return (List) schema.getExtensionDefinitions(vertex);
+    }
+
     private GraphQLOutputType outputType(SUVertex type) {
         return (GraphQLOutputType) exportType(type);
     }
diff --git a/src/main/java/graphql/schema/universe/SUImporter.java b/src/main/java/graphql/schema/universe/SUImporter.java
index 8eb13522c6..8053a3d755 100644
--- a/src/main/java/graphql/schema/universe/SUImporter.java
+++ b/src/main/java/graphql/schema/universe/SUImporter.java
@@ -4,6 +4,7 @@
 import graphql.DirectivesUtil;
 import graphql.ExperimentalApi;
 import graphql.Internal;
+import graphql.language.Node;
 import graphql.language.StringValue;
 import graphql.schema.GraphQLAppliedDirective;
 import graphql.schema.GraphQLAppliedDirectiveArgument;
@@ -45,8 +46,9 @@
  * Imports the type-system topology of an existing {@link GraphQLSchema} into a universe.
  *
  * 

The importer preserves schema-element object identity during one import. Runtime wiring in - * {@code GraphQLCodeRegistry}, AST definitions, coercing implementations, and input values are not - * part of this topology import.

+ * {@code GraphQLCodeRegistry}, coercing implementations, and programmatic behavior are not part of + * this topology import. Source AST definitions are retained by default and can be disabled through + * {@link SUSchemaOptions}.

*/ @ExperimentalApi public final class SUImporter { @@ -58,13 +60,30 @@ public final class SUImporter { appliedArgumentValues = new IdentityHashMap<>(); private final Map directiveDefinitions = new LinkedHashMap<>(); private @Nullable SUSchemaBuilder schemaBuilder; + private boolean captureAstDefinitions = true; public SUImporter(SchemaUniverse universe) { this.universe = assertNotNull(universe); } public SUSchema importSchema(String name, GraphQLSchema schema) { + return importSchema( + name, + schema, + SUSchemaOptions.defaultOptions()); + } + + public SUSchema importSchema( + String name, + GraphQLSchema schema, + SUSchemaOptions options) { + captureAstDefinitions = + assertNotNull(options).isCaptureAstDefinitions(); schemaBuilder = universe.newSchema(name, schema.getDescription()); + captureAstDefinitions( + builder().getRoot(), + schema.getDefinition(), + schema.getExtensionDefinitions()); createNamedTypeVertices(schema.getAllTypesAsList()); addRootTypes(schema); builder().introspectionSchemaType( @@ -150,27 +169,69 @@ private void assertCompatibleNamedType( private SUNamedType createNamedTypeVertex(GraphQLNamedType type) { if (type instanceof GraphQLObjectType) { GraphQLObjectType objectType = (GraphQLObjectType) type; - return universe.newObjectType(objectType.getName(), objectType.getDescription()); + SUObjectType vertex = universe.newObjectType( + objectType.getName(), + objectType.getDescription()); + captureAstDefinitions( + vertex, + objectType.getDefinition(), + objectType.getExtensionDefinitions()); + return vertex; } if (type instanceof GraphQLInterfaceType) { GraphQLInterfaceType interfaceType = (GraphQLInterfaceType) type; - return universe.newInterfaceType(interfaceType.getName(), interfaceType.getDescription()); + SUInterfaceType vertex = universe.newInterfaceType( + interfaceType.getName(), + interfaceType.getDescription()); + captureAstDefinitions( + vertex, + interfaceType.getDefinition(), + interfaceType.getExtensionDefinitions()); + return vertex; } if (type instanceof GraphQLUnionType) { GraphQLUnionType unionType = (GraphQLUnionType) type; - return universe.newUnionType(unionType.getName(), unionType.getDescription()); + SUUnionType vertex = universe.newUnionType( + unionType.getName(), + unionType.getDescription()); + captureAstDefinitions( + vertex, + unionType.getDefinition(), + unionType.getExtensionDefinitions()); + return vertex; } if (type instanceof GraphQLEnumType) { GraphQLEnumType enumType = (GraphQLEnumType) type; - return universe.newEnumType(enumType.getName(), enumType.getDescription()); + SUEnumType vertex = universe.newEnumType( + enumType.getName(), + enumType.getDescription()); + captureAstDefinitions( + vertex, + enumType.getDefinition(), + enumType.getExtensionDefinitions()); + return vertex; } if (type instanceof GraphQLScalarType) { GraphQLScalarType scalarType = (GraphQLScalarType) type; - return universe.newScalarType(scalarType.getName(), scalarType.getDescription()); + SUScalarType vertex = universe.newScalarType( + scalarType.getName(), + scalarType.getDescription()); + captureAstDefinitions( + vertex, + scalarType.getDefinition(), + scalarType.getExtensionDefinitions()); + return vertex; } if (type instanceof GraphQLInputObjectType) { GraphQLInputObjectType inputType = (GraphQLInputObjectType) type; - return universe.newInputObjectType(inputType.getName(), inputType.getDescription()); + SUInputObjectType vertex = universe.newInputObjectType( + inputType.getName(), + inputType.getDescription()); + captureAstDefinitions( + vertex, + inputType.getDefinition(), + inputType.getExtensionDefinitions()); + return vertex; } return assertShouldNeverHappen("Unsupported schema universe named type %s", type.getClass().getName()); } @@ -195,8 +256,11 @@ private void addDirectiveDefinitions(List directives) { directive.getName(), directive.getDescription(), directive.isRepeatable(), - directive.validLocations(), - directive.getDefinition()); + directive.validLocations()); + captureAstDefinitions( + vertex, + directive.getDefinition(), + directive.getExtensionDefinitions()); elementVertices.put(directive, vertex); builder().addDirectiveDefinition(vertex); } @@ -472,6 +536,10 @@ private SUField field(GraphQLFieldDefinition field) { return (SUField) existing; } SUField vertex = universe.newField(field.getName(), field.getDescription()); + captureAstDefinitions( + vertex, + field.getDefinition(), + List.of()); elementVertices.put(field, vertex); return vertex; } @@ -484,8 +552,11 @@ private SUArgument argument(GraphQLArgument argument) { SUArgument vertex = universe.newArgument( argument.getName(), argument.getDescription(), - argument.getArgumentDefaultValue(), - argument.getDefinition()); + argument.getArgumentDefaultValue()); + captureAstDefinitions( + vertex, + argument.getDefinition(), + List.of()); elementVertices.put(argument, vertex); return vertex; } @@ -498,8 +569,11 @@ private SUInputField inputField(GraphQLInputObjectField field) { SUInputField vertex = universe.newInputField( field.getName(), field.getDescription(), - field.getInputFieldDefaultValue(), - field.getDefinition()); + field.getInputFieldDefaultValue()); + captureAstDefinitions( + vertex, + field.getDefinition(), + List.of()); elementVertices.put(field, vertex); return vertex; } @@ -510,6 +584,10 @@ private SUEnumValue enumValue(GraphQLEnumValueDefinition value) { return (SUEnumValue) existing; } SUEnumValue vertex = universe.newEnumValue(value.getName(), value.getDescription()); + captureAstDefinitions( + vertex, + value.getDefinition(), + List.of()); elementVertices.put(value, vertex); return vertex; } @@ -526,8 +604,11 @@ private SUAppliedDirective appliedDirective(GraphQLAppliedDirective directive) { } SUAppliedDirective vertex = universe.newAppliedDirective( directive.getName(), - arguments, - directive.getDefinition()); + arguments); + captureAstDefinitions( + vertex, + directive.getDefinition(), + List.of()); elementVertices.put(directive, vertex); return vertex; } @@ -541,7 +622,9 @@ private SUAppliedDirectiveArgument appliedArgument(GraphQLAppliedDirectiveArgume argument.getName(), typeVertex(argument.getType()), argument.getArgumentValue(), - argument.getDefinition()); + captureAstDefinitions + ? argument.getDefinition() + : null); appliedArgumentValues.put(argument, value); return value; } @@ -564,4 +647,16 @@ private SUSchemaRoot schemaBuilderRoot() { private SUSchemaBuilder builder() { return assertNotNull(schemaBuilder, "Schema import has not started"); } + + private void captureAstDefinitions( + SUVertex vertex, + @Nullable Node definition, + List> extensionDefinitions) { + if (captureAstDefinitions) { + universe.setAstDefinitions( + vertex, + definition, + extensionDefinitions); + } + } } diff --git a/src/main/java/graphql/schema/universe/SUInputField.java b/src/main/java/graphql/schema/universe/SUInputField.java index 3afea8512a..7cf79ca009 100644 --- a/src/main/java/graphql/schema/universe/SUInputField.java +++ b/src/main/java/graphql/schema/universe/SUInputField.java @@ -2,7 +2,6 @@ import graphql.ExperimentalApi; import graphql.Internal; -import graphql.language.InputValueDefinition; import graphql.schema.InputValueWithState; import org.jspecify.annotations.Nullable; @@ -12,7 +11,6 @@ public final class SUInputField extends SUVertex implements SUAppliedDirectiveContainer { private final InputValueWithState defaultValue; - private final @Nullable InputValueDefinition definition; @Internal public SUInputField( @@ -20,18 +18,12 @@ public SUInputField( int nameId, String name, @Nullable String description, - InputValueWithState defaultValue, - @Nullable InputValueDefinition definition) { + InputValueWithState defaultValue) { super(id, nameId, SUVertexKind.INPUT_FIELD, name, description); this.defaultValue = assertNotNull(defaultValue); - this.definition = definition; } public InputValueWithState getInputFieldDefaultValue() { return defaultValue; } - - public @Nullable InputValueDefinition getDefinition() { - return definition; - } } diff --git a/src/main/java/graphql/schema/universe/SUSchema.java b/src/main/java/graphql/schema/universe/SUSchema.java index 29cd69c59e..6b8cd4d548 100644 --- a/src/main/java/graphql/schema/universe/SUSchema.java +++ b/src/main/java/graphql/schema/universe/SUSchema.java @@ -1,8 +1,9 @@ package graphql.schema.universe; import graphql.ExperimentalApi; -import graphql.schema.GraphQLSchema; import graphql.Internal; +import graphql.language.Node; +import graphql.schema.GraphQLSchema; import org.jspecify.annotations.Nullable; import java.util.ArrayList; @@ -62,6 +63,34 @@ public SUSchemaRoot getRoot() { return root; } + /** + * Returns the source AST definition associated with a vertex. + * + *

The definition records source provenance. Transforming schema topology does not rewrite + * it, and imports with AST capture disabled return {@code null}.

+ * + * @param vertex the vertex + * + * @return the source definition, or {@code null} + */ + public @Nullable Node getDefinition(SUVertex vertex) { + assertOwned(vertex); + return universe.getAstDefinition(vertex); + } + + /** + * Returns the source AST extension definitions associated with a vertex. + * + * @param vertex the vertex + * + * @return immutable extension definitions + */ + public List> getExtensionDefinitions( + SUVertex vertex) { + assertOwned(vertex); + return universe.getAstExtensionDefinitions(vertex); + } + /** * Returns this snapshot's user-controlled metadata for a vertex. * diff --git a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java index 3204caf914..2046a03fc3 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/SUSchemaBuilder.java @@ -46,6 +46,7 @@ public SUSchemaBuilder( this.namedTypeCount = baseSchema == null ? 0 : baseSchema.getNamedTypeCount(); copyRootEdges(baseSchema); copyRootMetadata(baseSchema); + copyRootAstDefinitions(baseSchema); } public SUSchemaBuilder queryType(SUObjectType queryType) { @@ -695,6 +696,12 @@ private void copyRootMetadata(@Nullable SUSchema base) { new LinkedHashMap<>(metadata)); } + private void copyRootAstDefinitions(@Nullable SUSchema base) { + if (base != null) { + universe.copyAstDefinitions(base.getRoot(), root); + } + } + private MutablePackedEdgeSet mutableEdges(SUVertex source) { assertOwned(source); MutablePackedEdgeSet changed = changedEdges.get(source.getId()); diff --git a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java index 9061b086ce..614631c715 100644 --- a/src/main/java/graphql/schema/universe/SUSchemaGenerator.java +++ b/src/main/java/graphql/schema/universe/SUSchemaGenerator.java @@ -85,15 +85,32 @@ public final class SUSchemaGenerator { private @Nullable SUObjectType queryType; private @Nullable SUObjectType mutationType; private @Nullable SUObjectType subscriptionType; + private boolean captureAstDefinitions = true; public SUSchemaGenerator(SchemaUniverse universe) { this.universe = assertNotNull(universe); } public SUSchema generate(String name, TypeDefinitionRegistry registry) { + return generate( + name, + registry, + SUSchemaOptions.defaultOptions()); + } + + public SUSchema generate( + String name, + TypeDefinitionRegistry registry, + SUSchemaOptions options) { assertTrue(schemaBuilder == null, "This schema universe generator has already been used"); + captureAstDefinitions = + assertNotNull(options).isCaptureAstDefinitions(); ImmutableTypeDefinitionRegistry definitions = validatedDefinitions(registry); schemaBuilder = universe.newSchema(name, schemaDescription(definitions)); + captureAstDefinitions( + builder().getRoot(), + definitions.schemaDefinition().orElse(null), + definitions.getSchemaExtensionDefinitions()); createNamedTypes(definitions); createDirectiveDefinitions(definitions); @@ -129,17 +146,60 @@ private void addBuiltInDirectiveDefinitions(TypeDefinitionRegistry registry) { private void createNamedTypes(ImmutableTypeDefinitionRegistry registry) { for (TypeDefinition definition : registry.types().values()) { - namedTypes.put(definition.getName(), createNamedType(definition)); + SUNamedType type = createNamedType(definition); + namedTypes.put(definition.getName(), type); + captureAstDefinitions( + type, + definition, + extensionDefinitions(definition, registry)); } for (ScalarTypeDefinition definition : registry.scalars().values()) { - namedTypes.putIfAbsent( + if (namedTypes.containsKey(definition.getName())) { + continue; + } + SUScalarType type = universe.newScalarType( definition.getName(), - universe.newScalarType( + description( + definition, + definition.getDescription())); + namedTypes.put(definition.getName(), type); + captureAstDefinitions( + type, + definition, + registry.scalarTypeExtensions().getOrDefault( definition.getName(), - description(definition, definition.getDescription()))); + Collections.emptyList())); } } + private List> extensionDefinitions( + TypeDefinition definition, + ImmutableTypeDefinitionRegistry registry) { + String name = definition.getName(); + if (definition instanceof ObjectTypeDefinition) { + return registry.objectTypeExtensions() + .getOrDefault(name, Collections.emptyList()); + } + if (definition instanceof InterfaceTypeDefinition) { + return registry.interfaceTypeExtensions() + .getOrDefault(name, Collections.emptyList()); + } + if (definition instanceof UnionTypeDefinition) { + return registry.unionTypeExtensions() + .getOrDefault(name, Collections.emptyList()); + } + if (definition instanceof EnumTypeDefinition) { + return registry.enumTypeExtensions() + .getOrDefault(name, Collections.emptyList()); + } + if (definition instanceof InputObjectTypeDefinition) { + return registry.inputObjectTypeExtensions() + .getOrDefault(name, Collections.emptyList()); + } + return registry.scalarTypeExtensions() + .getOrDefault(name, Collections.emptyList()); + } + private SUNamedType createNamedType(TypeDefinition definition) { if (definition instanceof ObjectTypeDefinition) { ObjectTypeDefinition objectType = (ObjectTypeDefinition) definition; @@ -188,8 +248,14 @@ private void createDirectiveDefinitions(ImmutableTypeDefinitionRegistry registry definition.getName(), description(definition, definition.getDescription()), definition.isRepeatable(), - directiveLocations(definition), - definition); + directiveLocations(definition)); + captureAstDefinitions( + directive, + definition, + registry.directiveExtensions() + .getOrDefault( + definition.getName(), + Collections.emptyList())); directiveDefinitions.put(definition.getName(), directive); builder().addDirectiveDefinition(directive); } @@ -305,6 +371,10 @@ private void addFields(List definitions, SUVertex parent) { SUField field = universe.newField( definition.getName(), description(definition, definition.getDescription())); + captureAstDefinitions( + field, + definition, + Collections.emptyList()); addField(parent, field); builder().setFieldType(field, typeVertex(definition.getType())); for (InputValueDefinition argumentDefinition : @@ -327,11 +397,15 @@ private void addField(SUVertex parent, SUField field) { } private SUArgument newArgument(InputValueDefinition definition) { - return universe.newArgument( + SUArgument argument = universe.newArgument( definition.getName(), description(definition, definition.getDescription()), - inputValue(definition.getDefaultValue()), - definition); + inputValue(definition.getDefaultValue())); + captureAstDefinitions( + argument, + definition, + Collections.emptyList()); + return argument; } private void addImplementedInterfaces(List interfaces, SUVertex type) { @@ -389,6 +463,10 @@ private void addEnumValues(List definitions, SUEnumType typ SUEnumValue value = universe.newEnumValue( definition.getName(), description(definition, definition.getDescription())); + captureAstDefinitions( + value, + definition, + Collections.emptyList()); builder().addEnumValue(type, value); addAppliedDirectives(definition.getDirectives(), value); } @@ -415,8 +493,11 @@ private void addInputFields( SUInputField field = universe.newInputField( definition.getName(), description(definition, definition.getDescription()), - inputValue(definition.getDefaultValue()), - definition); + inputValue(definition.getDefaultValue())); + captureAstDefinitions( + field, + definition, + Collections.emptyList()); builder().addInputField(type, field); builder().setInputFieldType(field, typeVertex(definition.getType())); addAppliedDirectives(definition.getDirectives(), field); @@ -524,8 +605,11 @@ private void addAppliedDirectives( SUAppliedDirective applied = universe.newAppliedDirective( directive.getName(), - arguments, - directive); + arguments); + captureAstDefinitions( + applied, + directive, + Collections.emptyList()); builder().addAppliedDirective(container, applied); } } @@ -565,7 +649,9 @@ private SUAppliedDirectiveArgument appliedArgument( assertNotNull(requiredDefinition.getName()), assertNotNull(directiveArgumentTypes.get(requiredDefinition)), value, - argumentDefinition); + captureAstDefinitions + ? argumentDefinition + : null); } private SUType typeVertex(Type type) { @@ -642,4 +728,16 @@ private InputValueWithState inputValue(@Nullable Value value) { private SUSchemaBuilder builder() { return assertNotNull(schemaBuilder, "Schema generation has not started"); } + + private void captureAstDefinitions( + SUVertex vertex, + @Nullable Node definition, + List> extensionDefinitions) { + if (captureAstDefinitions) { + universe.setAstDefinitions( + vertex, + definition, + extensionDefinitions); + } + } } diff --git a/src/main/java/graphql/schema/universe/SUSchemaOptions.java b/src/main/java/graphql/schema/universe/SUSchemaOptions.java new file mode 100644 index 0000000000..be3c46c9a6 --- /dev/null +++ b/src/main/java/graphql/schema/universe/SUSchemaOptions.java @@ -0,0 +1,45 @@ +package graphql.schema.universe; + +import graphql.ExperimentalApi; + +/** + * Options controlling schema-universe imports. + */ +@ExperimentalApi +public final class SUSchemaOptions { + + private static final SUSchemaOptions DEFAULT_OPTIONS = + new SUSchemaOptions(true); + + private final boolean captureAstDefinitions; + + private SUSchemaOptions(boolean captureAstDefinitions) { + this.captureAstDefinitions = captureAstDefinitions; + } + + public static SUSchemaOptions defaultOptions() { + return DEFAULT_OPTIONS; + } + + public boolean isCaptureAstDefinitions() { + return captureAstDefinitions; + } + + /** + * Controls whether source AST definitions and extensions are retained. + * + *

Disabling capture saves memory, but AST-preserving schema printing and source provenance + * are unavailable.

+ * + * @param captureAstDefinitions whether AST provenance is retained + * + * @return updated immutable options + */ + public SUSchemaOptions captureAstDefinitions( + boolean captureAstDefinitions) { + if (this.captureAstDefinitions == captureAstDefinitions) { + return this; + } + return new SUSchemaOptions(captureAstDefinitions); + } +} diff --git a/src/main/java/graphql/schema/universe/SchemaUniverse.java b/src/main/java/graphql/schema/universe/SchemaUniverse.java index 71a671ea3d..a87d303038 100644 --- a/src/main/java/graphql/schema/universe/SchemaUniverse.java +++ b/src/main/java/graphql/schema/universe/SchemaUniverse.java @@ -8,6 +8,7 @@ import graphql.language.Directive; import graphql.language.DirectiveDefinition; import graphql.language.InputValueDefinition; +import graphql.language.Node; import graphql.schema.GraphQLSchema; import graphql.schema.InputValueWithState; import graphql.schema.idl.SchemaParser; @@ -69,14 +70,44 @@ public SUSchema importSchema(String name, GraphQLSchema schema) { return new SUImporter(this).importSchema(name, schema); } + public SUSchema importSchema( + String name, + GraphQLSchema schema, + SUSchemaOptions options) { + return new SUImporter(this).importSchema( + name, + schema, + options); + } + public SUSchema importSchema(String name, TypeDefinitionRegistry typeDefinitionRegistry) { return new SUSchemaGenerator(this).generate(name, typeDefinitionRegistry); } + public SUSchema importSchema( + String name, + TypeDefinitionRegistry typeDefinitionRegistry, + SUSchemaOptions options) { + return new SUSchemaGenerator(this).generate( + name, + typeDefinitionRegistry, + options); + } + public SUSchema parseSchema(String name, String sdl) { return importSchema(name, new SchemaParser().parse(sdl)); } + public SUSchema parseSchema( + String name, + String sdl, + SUSchemaOptions options) { + return importSchema( + name, + new SchemaParser().parse(sdl), + options); + } + public @Nullable SUSchema getSchema(String name) { return schemasByName.get(assertNotNull(name)); } @@ -272,9 +303,8 @@ public synchronized SUInputField newInputField( nameId(validName), validName, description, - defaultValue, - definition); - return append(vertex); + defaultValue); + return appendWithDefinition(vertex, definition); } public synchronized SUArgument newArgument(String name) { @@ -303,9 +333,8 @@ public synchronized SUArgument newArgument( nameId(validName), validName, description, - defaultValue, - definition); - return append(vertex); + defaultValue); + return appendWithDefinition(vertex, definition); } public synchronized SUDirective newDirective(String name) { @@ -337,9 +366,8 @@ public synchronized SUDirective newDirective( validName, description, repeatable, - validLocations, - definition); - return append(vertex); + validLocations); + return appendWithDefinition(vertex, definition); } public synchronized SUAppliedDirective newAppliedDirective(String name) { @@ -380,9 +408,8 @@ public synchronized SUAppliedDirective newAppliedDirective( nextVertexId, nameId(validName), validName, - definition, arguments); - return append(vertex); + return appendWithDefinition(vertex, definition); } public synchronized SUAppliedDirectiveArgument newAppliedDirectiveArgument( @@ -467,6 +494,59 @@ public boolean owns(SUVertex vertex) { return id >= 0 && id < nextVertexId && vertexOrNull(id) == vertex; } + /** + * Associates source AST provenance with a vertex. + * + *

AST provenance is intrinsic to the vertex and shared by every schema that uses it.

+ * + * @param vertex the vertex + * @param definition the source definition, or {@code null} + * @param extensionDefinitions source extension definitions + */ + @Internal + public synchronized void setAstDefinitions( + SUVertex vertex, + @Nullable Node definition, + List> extensionDefinitions) { + assertTrue(owns(assertNotNull(vertex)), + "Vertex %s belongs to another schema universe", + vertex); + VertexChunk chunk = vertexChunk(vertex.getId()); + chunk.setAstDefinitions( + vertex.getId() & VERTEX_CHUNK_MASK, + definition, + assertNotNull(extensionDefinitions)); + } + + @Internal + public @Nullable Node getAstDefinition(SUVertex vertex) { + assertTrue(owns(assertNotNull(vertex)), + "Vertex %s belongs to another schema universe", + vertex); + return vertexChunk(vertex.getId()).getDefinition( + vertex.getId() & VERTEX_CHUNK_MASK); + } + + @Internal + public List> getAstExtensionDefinitions( + SUVertex vertex) { + assertTrue(owns(assertNotNull(vertex)), + "Vertex %s belongs to another schema universe", + vertex); + return vertexChunk(vertex.getId()).getExtensionDefinitions( + vertex.getId() & VERTEX_CHUNK_MASK); + } + + @Internal + public synchronized void copyAstDefinitions( + SUVertex source, + SUVertex target) { + setAstDefinitions( + target, + getAstDefinition(source), + getAstExtensionDefinitions(source)); + } + @Internal public SUSchemaBuilder transformBuilder(SUSchema schema, String name) { assertTrue(schema.getUniverse() == this, "Schema belongs to another universe"); @@ -529,6 +609,17 @@ private T append(T vertex) { return vertex; } + private T appendWithDefinition( + T vertex, + @Nullable Node definition) { + T appended = append(vertex); + setAstDefinitions( + appended, + definition, + Collections.emptyList()); + return appended; + } + private void markEdgeBinding( BitSet liveVertexIds, int sourceId, @@ -594,6 +685,10 @@ private int sweepUnusedVertices(BitSet liveVertexIds) { return chunk.get(id & VERTEX_CHUNK_MASK); } + private VertexChunk vertexChunk(int id) { + return vertexChunks[id >>> VERTEX_CHUNK_SHIFT]; + } + private void ensureChunk(int chunkIndex) { VertexChunk[] chunks = vertexChunks; if (chunkIndex >= chunks.length) { diff --git a/src/main/java/graphql/schema/universe/VertexChunk.java b/src/main/java/graphql/schema/universe/VertexChunk.java index e49f0c44ab..5e5ee1692f 100644 --- a/src/main/java/graphql/schema/universe/VertexChunk.java +++ b/src/main/java/graphql/schema/universe/VertexChunk.java @@ -1,9 +1,12 @@ package graphql.schema.universe; import graphql.Internal; +import graphql.language.Node; import org.jspecify.annotations.Nullable; import java.util.BitSet; +import java.util.Collections; +import java.util.List; /** * A fixed-size arena chunk whose vertex slots may be reclaimed. @@ -14,6 +17,7 @@ public final class VertexChunk { private static final VertexChunk EMPTY = new VertexChunk(0); private final @Nullable SUVertex[] vertices; + private @Nullable Object @Nullable [] astData; private int retainedCount; /** @@ -67,10 +71,64 @@ public void set(int index, @Nullable SUVertex vertex) { } if (previous != null && vertex == null) { retainedCount--; + clearAstDefinitions(index); } vertices[index] = vertex; } + /** + * Associates source AST provenance with one occupied vertex slot. + * + * @param index the index within the chunk + * @param definition the source definition, or {@code null} + * @param extensionDefinitions source extension definitions + */ + public void setAstDefinitions( + int index, + @Nullable Node definition, + List> extensionDefinitions) { + if (definition == null && extensionDefinitions.isEmpty()) { + clearAstDefinitions(index); + return; + } + if (astData == null) { + astData = new @Nullable Object[vertices.length]; + } + astData[index] = extensionDefinitions.isEmpty() + ? definition + : new SUAstDefinitions(definition, extensionDefinitions); + } + + /** + * Returns the source definition for one slot. + * + * @param index the index within the chunk + * + * @return the definition, or {@code null} + */ + public @Nullable Node getDefinition(int index) { + Object data = astData == null ? null : astData[index]; + if (data instanceof SUAstDefinitions) { + return ((SUAstDefinitions) data).getDefinition(); + } + return (Node) data; + } + + /** + * Returns source extension definitions for one slot. + * + * @param index the index within the chunk + * + * @return immutable extension definitions + */ + public List> getExtensionDefinitions(int index) { + Object data = astData == null ? null : astData[index]; + if (data instanceof SUAstDefinitions) { + return ((SUAstDefinitions) data).getExtensionDefinitions(); + } + return Collections.emptyList(); + } + /** * Reclaims every slot not marked in the supplied live-ID set. * @@ -104,4 +162,10 @@ public int reclaimUnmarked( public boolean hasVertices() { return retainedCount != 0; } + + private void clearAstDefinitions(int index) { + if (astData != null) { + astData[index] = null; + } + } } diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java index 24eb327611..b29e9f22f6 100644 --- a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java @@ -2,10 +2,6 @@ import graphql.Internal; import graphql.language.Node; -import graphql.schema.universe.SUAppliedDirective; -import graphql.schema.universe.SUArgument; -import graphql.schema.universe.SUDirective; -import graphql.schema.universe.SUInputField; import graphql.schema.universe.SUVertex; import org.jspecify.annotations.Nullable; @@ -48,24 +44,12 @@ public final SUVertex getVertex() { } /** - * Returns the source definition stored by vertex kinds that retain one. + * Returns the source definition stored in the universe AST sidecar. * * @return the source definition, or {@code null} when unavailable */ public @Nullable Node getDefinition() { - if (vertex instanceof SUArgument) { - return ((SUArgument) vertex).getDefinition(); - } - if (vertex instanceof SUInputField) { - return ((SUInputField) vertex).getDefinition(); - } - if (vertex instanceof SUDirective) { - return ((SUDirective) vertex).getDefinition(); - } - if (vertex instanceof SUAppliedDirective) { - return ((SUAppliedDirective) vertex).getDefinition(); - } - return null; + return executableSchema.getSchema().getDefinition(vertex); } @Override diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java index f26abaefbc..cacc353d04 100644 --- a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaNamedType.java @@ -1,9 +1,12 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.language.TypeDefinition; import graphql.schema.SchemaNamedType; import graphql.schema.universe.SUNamedType; +import java.util.List; + import static graphql.Assert.assertNotNull; /** @@ -24,4 +27,13 @@ public AbstractSUSchemaNamedType( public final String getName() { return assertNotNull(getTypeVertex().getName()); } + + @Override + @SuppressWarnings("unchecked") + public final List> getExtensionDefinitions() { + return (List>) + getExecutableSchema() + .getSchema() + .getExtensionDefinitions(getTypeVertex()); + } } diff --git a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java index a334513bc4..7ef4cd461d 100644 --- a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java @@ -5,6 +5,7 @@ import graphql.Scalars; import graphql.introspection.Introspection; import graphql.language.SchemaDefinition; +import graphql.language.SchemaExtensionDefinition; import graphql.schema.Coercing; import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLNamedType; @@ -121,7 +122,12 @@ public SUSchema getSchema() { @Override public @Nullable SchemaDefinition getDefinition() { - return null; + return (SchemaDefinition) schema.getDefinition(schema.getRoot()); + } + + @Override + public List getExtensionDefinitions() { + return astExtensionDefinitions(schema.getRoot()); } @Override @@ -223,6 +229,11 @@ private List adaptAppliedDirectives( return Collections.unmodifiableList(result); } + @SuppressWarnings("unchecked") + private List astExtensionDefinitions(SUVertex vertex) { + return (List) schema.getExtensionDefinitions(vertex); + } + @Override public SchemaObject getIntrospectionSchemaType() { return new SUSchemaObject( diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java b/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java index 1891a56420..3cb273b6df 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaDirective.java @@ -2,6 +2,7 @@ import graphql.Internal; import graphql.introspection.Introspection.DirectiveLocation; +import graphql.language.DirectiveExtensionDefinition; import graphql.schema.SchemaArgument; import graphql.schema.SchemaDirective; import graphql.schema.universe.SUArgument; @@ -31,6 +32,15 @@ public String getName() { return assertNotNull(getVertex().getName()); } + @Override + @SuppressWarnings("unchecked") + public List getExtensionDefinitions() { + return (List) + getExecutableSchema() + .getSchema() + .getExtensionDefinitions(getDirectiveVertex()); + } + @Override public boolean isRepeatable() { return getDirectiveVertex().isRepeatable(); diff --git a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy index 35ef8ab026..6e09a7fe08 100644 --- a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy @@ -563,6 +563,59 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) printer.argsString(null, field.arguments) == '''(bb: Int, a: Int)''' } + def "executable schema printing derives GraphQL comparator types internally"() { + given: + def firstField = newFieldDefinition() + .name("a") + .type(GraphQLString) + .arguments(mockArguments("a", "bb")) + .withAppliedDirectives( + mockDirectivesWithArguments("a", "bb")) + .build() + def secondField = newFieldDefinition() + .name("bb") + .type(GraphQLString) + .build() + def query = newObject() + .name("Query") + .fields([firstField, secondField]) + .withAppliedDirectives( + mockDirectivesWithArguments("a", "bb")) + .build() + def schema = GraphQLSchema.newSchema().query(query).build() + def environments = [] + GraphqlTypeComparatorRegistry registry = { environment -> + environments.add([ + environment.parentType, + environment.elementType]) + DEFAULT_COMPARATOR + } + def options = defaultOptions().setComparators(registry) + + when: + new SchemaPrinter(options).print(schema) + + then: + environments.contains([ + GraphQLSchemaElement.class, + null]) + environments.contains([ + GraphQLObjectType.class, + GraphQLFieldDefinition.class]) + environments.contains([ + GraphQLFieldDefinition.class, + GraphQLArgument.class]) + environments.contains([ + GraphQLObjectType.class, + GraphQLAppliedDirective.class]) + environments.contains([ + GraphQLFieldDefinition.class, + GraphQLAppliedDirective.class]) + environments.contains([ + GraphQLAppliedDirective.class, + GraphQLAppliedDirectiveArgument.class]) + } + def "directivesString uses most specific registered comparators"() { given: @@ -791,4 +844,4 @@ scalar TestScalar @a @bb then: sortedList == [nonNullA, nonNullB] } -} \ No newline at end of file +} diff --git a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy index ec96904e14..d74248285b 100644 --- a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy @@ -6,6 +6,7 @@ import graphql.schema.GraphQLFieldDefinition import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema +import graphql.schema.GraphqlTypeComparatorRegistry import graphql.schema.idl.SchemaParser import graphql.schema.idl.SchemaPrinter import graphql.schema.idl.TypeDefinitionRegistry @@ -13,6 +14,7 @@ import graphql.schema.idl.UnExecutableSchemaGenerator import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification import spock.lang.Requires +import spock.lang.Unroll class SUExecutableSchemaPrinterTest extends Specification { @@ -102,6 +104,38 @@ class SUExecutableSchemaPrinterTest extends Specification { 'internal: Date = "2026-08-06"') } + @Unroll + def "native executable schema honors #orderingName ordering"() { + given: + def sdl = ''' + type Query { + z(z: Int, a: Int): String + a: String + } + ''' + def registry = new SchemaParser().parse(sdl) + def graphQLSchema = + UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + def universeSchema = + new SchemaUniverse().importSchema(orderingName, registry) + def options = SchemaPrinter.Options.defaultOptions() + .setComparators(comparatorRegistry) + def printer = new SchemaPrinter(options) + + when: + def nativeSdl = printer.print(executable(universeSchema)) + def graphQLSdl = printer.print(graphQLSchema) + + then: + nativeSdl.contains(expectedFields) + graphQLSdl.contains(expectedFields) + + where: + orderingName | comparatorRegistry | expectedFields + "as_is" | GraphqlTypeComparatorRegistry.AS_IS_REGISTRY | " z(z: Int, a: Int): String\n a: String" + "by_name" | GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY | " a: String\n z(a: Int, z: Int): String" + } + @Requires({ Boolean.getBoolean("graphql.largeSchemaSdlTest") }) def "large schema native import and printing match GraphQLSchema"() { given: diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaAstDefinitionsTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaAstDefinitionsTest.groovy new file mode 100644 index 0000000000..755e927980 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUSchemaAstDefinitionsTest.groovy @@ -0,0 +1,405 @@ +package graphql.schema.universe + +import graphql.Scalars +import graphql.language.Argument +import graphql.language.Directive +import graphql.language.DirectiveDefinition +import graphql.language.DirectiveExtensionDefinition +import graphql.language.EnumTypeDefinition +import graphql.language.EnumTypeExtensionDefinition +import graphql.language.EnumValueDefinition +import graphql.language.FieldDefinition +import graphql.language.InputObjectTypeDefinition +import graphql.language.InputObjectTypeExtensionDefinition +import graphql.language.InputValueDefinition +import graphql.language.InterfaceTypeDefinition +import graphql.language.InterfaceTypeExtensionDefinition +import graphql.language.ObjectTypeDefinition +import graphql.language.ObjectTypeExtensionDefinition +import graphql.language.ScalarTypeDefinition +import graphql.language.ScalarTypeExtensionDefinition +import graphql.language.SchemaDefinition +import graphql.language.SchemaExtensionDefinition +import graphql.language.UnionTypeDefinition +import graphql.language.UnionTypeExtensionDefinition +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import graphql.schema.SchemaEnum +import graphql.schema.idl.SchemaParser +import graphql.schema.idl.SchemaPrinter +import graphql.schema.idl.TypeDefinitionRegistry +import graphql.schema.idl.UnExecutableSchemaGenerator +import graphql.schema.universe.view.SUExecutableSchema +import spock.lang.Specification +import spock.lang.Unroll + +class SUSchemaAstDefinitionsTest extends Specification { + + @Unroll + def "captures definitions and extensions from #source import"() { + when: + def schema = importSchema(source) + + then: + schema.getDefinition(schema.root) instanceof SchemaDefinition + schema.getExtensionDefinitions(schema.root)*.class == + [SchemaExtensionDefinition] + + and: + assertAst( + schema, + schema.getObjectType("Query"), + ObjectTypeDefinition, + [ObjectTypeExtensionDefinition]) + assertAst( + schema, + schema.getInterfaceType("Node"), + InterfaceTypeDefinition, + [InterfaceTypeExtensionDefinition]) + assertAst( + schema, + schema.getUnionType("Search"), + UnionTypeDefinition, + [UnionTypeExtensionDefinition]) + assertAst( + schema, + schema.getEnumType("Status"), + EnumTypeDefinition, + [EnumTypeExtensionDefinition]) + assertAst( + schema, + schema.getScalarType("Date"), + ScalarTypeDefinition, + source == "registry" + ? [ScalarTypeExtensionDefinition] + : []) + assertAst( + schema, + schema.getInputObjectType("Filter"), + InputObjectTypeDefinition, + [InputObjectTypeExtensionDefinition]) + assertAst( + schema, + schema.getDirectiveDefinition("tag"), + DirectiveDefinition, + [DirectiveExtensionDefinition]) + + and: + def query = schema.getObjectType("Query") + def baseField = schema.getField(query, "node") + def extensionField = schema.getField(query, "search") + assertAst(schema, baseField, FieldDefinition) + assertAst(schema, extensionField, FieldDefinition) + assertAst( + schema, + schema.getArgument(extensionField, "filter"), + InputValueDefinition) + + and: + def status = schema.getEnumType("Status") + assertAst( + schema, + schema.getEnumValue(status, "ACTIVE"), + EnumValueDefinition) + assertAst( + schema, + schema.getEnumValue(status, "INACTIVE"), + EnumValueDefinition) + def filter = schema.getInputObjectType("Filter") + assertAst( + schema, + schema.getInputField(filter, "term"), + InputValueDefinition) + assertAst( + schema, + schema.getInputField(filter, "limit"), + InputValueDefinition) + + and: + def schemaTag = schema.getSchemaAppliedDirectives("tag").first() + schema.getDefinition(schemaTag) instanceof Directive + schema.getExtensionDefinitions(schemaTag).isEmpty() + schemaTag.arguments.first().definition instanceof Argument + + where: + source << ["registry", "graphql"] + } + + @Unroll + def "executable view exposes captured AST from #source import"() { + given: + def schema = importSchema(source) + def executable = SUExecutableSchema.newExecutableSchema(schema).build() + + expect: + executable.definition.is(schema.getDefinition(schema.root)) + executable.extensionDefinitions == + schema.getExtensionDefinitions(schema.root) + + and: + def query = executable.getType("Query") + query.definition instanceof ObjectTypeDefinition + query.extensionDefinitions*.class == + [ObjectTypeExtensionDefinition] + executable.getField(query, "node").definition instanceof FieldDefinition + executable.getField(query, "search") + .getArgument("filter") + .definition instanceof InputValueDefinition + + and: + SchemaEnum status = executable.getType("Status") as SchemaEnum + status.definition instanceof EnumTypeDefinition + status.extensionDefinitions*.class == + [EnumTypeExtensionDefinition] + status.getValue("INACTIVE").definition instanceof EnumValueDefinition + + and: + def directive = executable.getDirective("tag") + directive.definition instanceof DirectiveDefinition + directive.extensionDefinitions*.class == + [DirectiveExtensionDefinition] + + where: + source << ["registry", "graphql"] + } + + @Unroll + def "capture can be disabled for #source import"() { + given: + def options = SUSchemaOptions.defaultOptions() + .captureAstDefinitions(false) + + when: + def schema = importSchema(source, options) + + then: + astVertices(schema).every { + schema.getDefinition(it) == null && + schema.getExtensionDefinitions(it).isEmpty() + } + + and: + appliedDirectives(schema).every { directive -> + schema.getDefinition(directive) == null && + directive.arguments.every { it.definition == null } + } + + where: + source << ["registry", "graphql"] + } + + def "export and transformed roots retain normalized AST provenance"() { + given: + def schema = importSchema("registry") + + when: + def transformed = schema.transform("transformed", builder -> {}) + def exported = schema.toGraphQLSchema() + + then: + transformed.getDefinition(transformed.root) + .is(schema.getDefinition(schema.root)) + transformed.getExtensionDefinitions(transformed.root) == + schema.getExtensionDefinitions(schema.root) + + and: + exported.definition.is(schema.getDefinition(schema.root)) + exported.extensionDefinitions == + schema.getExtensionDefinitions(schema.root) + exported.getObjectType("Query").definition.is( + schema.getDefinition(schema.getObjectType("Query"))) + exported.getObjectType("Query").extensionDefinitions == + schema.getExtensionDefinitions(schema.getObjectType("Query")) + exported.getDirective("tag").extensionDefinitions == + schema.getExtensionDefinitions( + schema.getDirectiveDefinition("tag")) + } + + def "GraphQLSchema import captures wired scalar AST provenance"() { + given: + def registry = new SchemaParser().parse(''' + directive @marker on SCALAR + scalar Date + extend scalar Date @marker + ''') + def scalarDefinition = registry.getType("Date").get() + def scalarExtensions = registry.scalarTypeExtensions()["Date"] + def date = GraphQLScalarType.newScalar() + .name("Date") + .coercing(Scalars.GraphQLString.coercing) + .definition(scalarDefinition) + .extensionDefinitions(scalarExtensions) + .build() + def query = GraphQLObjectType.newObject() + .name("Query") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("date") + .type(date)) + .build() + GraphQLSchema graphQLSchema = GraphQLSchema.newSchema() + .query(query) + .additionalType(date) + .build() + + when: + def schema = new SchemaUniverse() + .importSchema("schema", graphQLSchema) + + then: + schema.getDefinition(schema.getScalarType("Date")) + .is(scalarDefinition) + schema.getExtensionDefinitions(schema.getScalarType("Date")) == + scalarExtensions + } + + def "AST printer preserves schema and type extension declarations"() { + given: + def schema = importSchema("registry") + def executable = SUExecutableSchema.newExecutableSchema(schema).build() + def options = SchemaPrinter.Options.defaultOptions() + .includeSchemaDefinition(true) + .useAstDefinitions(true) + + when: + def printed = new SchemaPrinter(options).print(executable) + + then: + printed.contains("extend schema") + printed.contains("extend type Query") + printed.contains("extend interface Node") + printed.contains("extend union Search") + printed.contains("extend enum Status") + printed.contains("extend scalar Date") + printed.contains("extend input Filter") + } + + private static void assertAst( + SUSchema schema, + SUVertex vertex, + Class definitionType, + List> extensionTypes = []) { + assert definitionType.isInstance(schema.getDefinition(vertex)) + assert schema.getExtensionDefinitions(vertex)*.class == extensionTypes + } + + private static SUSchema importSchema( + String source, + SUSchemaOptions options = SUSchemaOptions.defaultOptions()) { + TypeDefinitionRegistry registry = + new SchemaParser().parse(sdl()) + def universe = new SchemaUniverse() + if (source == "registry") { + return universe.importSchema("schema", registry, options) + } + def graphQLSchema = + UnExecutableSchemaGenerator.makeUnExecutableSchema(registry) + return universe.importSchema("schema", graphQLSchema, options) + } + + private static List astVertices(SUSchema schema) { + def query = schema.getObjectType("Query") + def status = schema.getEnumType("Status") + def filter = schema.getInputObjectType("Filter") + def extensionField = schema.getField(query, "search") + return [ + schema.root, + query, + schema.getInterfaceType("Node"), + schema.getUnionType("Search"), + status, + schema.getScalarType("Date"), + filter, + schema.getDirectiveDefinition("tag"), + schema.getField(query, "node"), + extensionField, + schema.getArgument(extensionField, "filter"), + schema.getEnumValue(status, "ACTIVE"), + schema.getEnumValue(status, "INACTIVE"), + schema.getInputField(filter, "term"), + schema.getInputField(filter, "limit") + ] + } + + private static List appliedDirectives( + SUSchema schema) { + def directives = [] + directives.addAll(schema.schemaAppliedDirectives) + directives.addAll( + schema.getAppliedDirectives(schema.getObjectType("Query"))) + directives.addAll( + schema.getAppliedDirectives( + schema.getDirectiveDefinition("tag"))) + return directives + } + + private static String sdl() { + return ''' + directive @tag(value: String = "default") repeatable on + SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | + ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | + ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION | + DIRECTIVE_DEFINITION + + directive @marker(value: String) repeatable on + DIRECTIVE_DEFINITION + + extend directive @tag @marker(value: "directive extension") + + schema @tag(value: "schema") { + query: Query + } + + extend schema @tag(value: "schema extension") + + scalar Date + extend scalar Date @tag + + interface Node { + id: ID! + } + + extend interface Node @tag { + label: String + } + + type Item implements Node { + id: ID! + label: String + } + + type Other implements Node { + id: ID! + label: String + } + + union Search = Item + extend union Search @tag = Other + + enum Status { + ACTIVE + } + + extend enum Status @tag { + INACTIVE @tag + } + + input Filter { + term: String + } + + extend input Filter @tag { + limit: Int = 10 + } + + type Query @tag { + node: Node + } + + extend type Query @tag(value: "type extension") { + search(filter: Filter = {limit: 5}): Search @tag + } + ''' + } +} diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy index 04d66da114..86413447fb 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaGeneratorTest.groovy @@ -77,7 +77,7 @@ class SUSchemaGeneratorTest extends Specification { and: def schemaTag = schema.schemaAppliedDirectives[0] schemaTag.name == "tag" - schemaTag.definition != null + schema.getDefinition(schemaTag) != null schemaTag.arguments*.name == ["value"] schemaTag.arguments[0].argumentValue.value instanceof StringValue schemaTag.arguments[0].argumentValue.value.value == "schema" diff --git a/src/test/groovy/graphql/schema/universe/SUTest.groovy b/src/test/groovy/graphql/schema/universe/SUTest.groovy index cc1ae63dac..d5627ef0d2 100644 --- a/src/test/groovy/graphql/schema/universe/SUTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUTest.groovy @@ -683,10 +683,10 @@ class SUTest extends Specification { directive instanceof SUDirective directive.repeatable directive.validLocations() == EnumSet.of(OBJECT, FIELD_DEFINITION) - directive.definition.name == "tag" + schema.getDefinition(directive).name == "tag" directiveArgument.argumentDefaultValue.isLiteral() directiveArgument.argumentDefaultValue.value.value == "fallback" - directiveArgument.definition.name == "label" + schema.getDefinition(directiveArgument).name == "label" and: inputField.inputFieldDefaultValue.isLiteral() @@ -695,7 +695,7 @@ class SUTest extends Specification { fieldArgument.argumentDefaultValue.value.value == 5 and: - queryTag.definition.name == "tag" + schema.getDefinition(queryTag).name == "tag" queryTagArgument.argumentValue.isLiteral() queryTagArgument.argumentValue.value.value == "query" queryTagArgument.definition.name == "label" diff --git a/src/test/groovy/graphql/schema/universe/VertexChunkAstTest.groovy b/src/test/groovy/graphql/schema/universe/VertexChunkAstTest.groovy new file mode 100644 index 0000000000..66e55bfd37 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/VertexChunkAstTest.groovy @@ -0,0 +1,79 @@ +package graphql.schema.universe + +import graphql.language.ObjectTypeDefinition +import graphql.language.ObjectTypeExtensionDefinition +import spock.lang.Specification + +class VertexChunkAstTest extends Specification { + + def "AST sidecar supports empty definition-only and extension slots"() { + given: + def chunk = new VertexChunk(4) + def definition = ObjectTypeDefinition.newObjectTypeDefinition() + .name("Query") + .build() + def extension = ObjectTypeExtensionDefinition + .newObjectTypeExtensionDefinition() + .name("Query") + .build() + + expect: + chunk.getDefinition(0) == null + chunk.getExtensionDefinitions(0).isEmpty() + + when: + chunk.setAstDefinitions(0, definition, []) + chunk.setAstDefinitions(1, definition, [extension]) + chunk.setAstDefinitions(2, null, [extension]) + chunk.setAstDefinitions(3, null, []) + + then: + chunk.getDefinition(0).is(definition) + chunk.getExtensionDefinitions(0).isEmpty() + chunk.getDefinition(1).is(definition) + chunk.getExtensionDefinitions(1) == [extension] + chunk.getDefinition(2) == null + chunk.getExtensionDefinitions(2) == [extension] + chunk.getDefinition(3) == null + chunk.getExtensionDefinitions(3).isEmpty() + + when: + chunk.setAstDefinitions(1, null, []) + + then: + chunk.getDefinition(1) == null + chunk.getExtensionDefinitions(1).isEmpty() + } + + def "reclaiming a vertex clears its AST slot"() { + given: + def chunk = new VertexChunk(1) + def vertex = new SUObjectType(0, 1, "Query", null) + def definition = ObjectTypeDefinition.newObjectTypeDefinition() + .name("Query") + .build() + chunk.set(0, vertex) + chunk.setAstDefinitions(0, definition, []) + + when: + def removed = chunk.reclaimUnmarked(new BitSet(), 0, 1) + + then: + removed == 1 + chunk.get(0) == null + chunk.getDefinition(0) == null + chunk.getExtensionDefinitions(0).isEmpty() + !chunk.hasVertices() + } + + def "schema options are immutable and reuse unchanged values"() { + given: + def defaults = SUSchemaOptions.defaultOptions() + + expect: + defaults.captureAstDefinitions(true).is(defaults) + defaults.captureAstDefinitions(false) + .captureAstDefinitions(false) + .isCaptureAstDefinitions() == false + } +} From d7c21bfc3fd98b25a7259fbd73902cf81fbb0581 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 6 Aug 2026 03:45:06 +0000 Subject: [PATCH 18/32] Rename schema printer AST behavior test --- ...rLegacyTest.groovy => SchemaPrinterAstBehaviorTest.groovy} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename src/test/groovy/graphql/schema/idl/{SchemaPrinterLegacyTest.groovy => SchemaPrinterAstBehaviorTest.groovy} (98%) diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterLegacyTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy similarity index 98% rename from src/test/groovy/graphql/schema/idl/SchemaPrinterLegacyTest.groovy rename to src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy index ce917f4ac9..fcdb061e36 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterLegacyTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy @@ -31,7 +31,7 @@ import static graphql.schema.GraphQLUnionType.newUnionType import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring import static graphql.schema.idl.SchemaPrinter.Options.defaultOptions -class SchemaPrinterLegacyTest extends Specification { +class SchemaPrinterAstBehaviorTest extends Specification { def "AST-aware semantic fallback remains equivalent to the semantic writer"() { given: @@ -91,7 +91,7 @@ class SchemaPrinterLegacyTest extends Specification { } } - def "direct legacy printing retains AST comments for every schema element kind"() { + def "direct schema element printing retains AST comments for every schema element kind"() { given: def registry = new SchemaParser().parse(commentsEverywhereSdl()) def wiring = newRuntimeWiring() From 4a2a30f6b66f94d4c25585b4db7cb0526ebd96e2 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 6 Aug 2026 04:07:05 +0000 Subject: [PATCH 19/32] Move schema relationships onto schema types --- .../java/graphql/schema/ExecutableSchema.java | 15 ----- .../schema/GraphQLImplementingType.java | 3 +- .../java/graphql/schema/GraphQLSchema.java | 33 ----------- .../java/graphql/schema/GraphQLUnionType.java | 1 + .../schema/SchemaImplementingType.java | 24 ++++++++ .../java/graphql/schema/SchemaInterface.java | 2 +- .../java/graphql/schema/SchemaObject.java | 2 +- src/main/java/graphql/schema/SchemaUnion.java | 12 ++++ .../graphql/schema/idl/SchemaPrinter.java | 49 ++++----------- .../universe/view/SUExecutableSchema.java | 36 ----------- .../universe/view/SUSchemaInterface.java | 24 ++++++++ .../schema/universe/view/SUSchemaObject.java | 25 ++++++++ .../schema/universe/view/SUSchemaUnion.java | 22 +++++++ .../schema/ExecutableSchemaTest.groovy | 59 +++++++++++++++++-- 14 files changed, 176 insertions(+), 131 deletions(-) create mode 100644 src/main/java/graphql/schema/SchemaImplementingType.java diff --git a/src/main/java/graphql/schema/ExecutableSchema.java b/src/main/java/graphql/schema/ExecutableSchema.java index 952b7fa293..c77467324c 100644 --- a/src/main/java/graphql/schema/ExecutableSchema.java +++ b/src/main/java/graphql/schema/ExecutableSchema.java @@ -128,21 +128,6 @@ List getAppliedDirectives( */ List getInputFields(SchemaInputObject parentType); - /** - * @param implementingType an object or interface - * - * @return the interfaces directly implemented by the type - */ - List getInterfaces( - SchemaFieldsContainer implementingType); - - /** - * @param unionType the union - * - * @return the object types declared by the union - */ - List getUnionMembers(SchemaUnion unionType); - /** * @param compositeType an object, interface, or union * diff --git a/src/main/java/graphql/schema/GraphQLImplementingType.java b/src/main/java/graphql/schema/GraphQLImplementingType.java index f8d394567f..dae348b131 100644 --- a/src/main/java/graphql/schema/GraphQLImplementingType.java +++ b/src/main/java/graphql/schema/GraphQLImplementingType.java @@ -8,7 +8,8 @@ * A GraphQLType which can implement interfaces */ @PublicApi -public interface GraphQLImplementingType extends GraphQLFieldsContainer { +public interface GraphQLImplementingType + extends GraphQLFieldsContainer, SchemaImplementingType { /** * @return This returns GraphQLInterface or GraphQLTypeReference instances, if the type * references are not resolved yet. After they are resolved it contains only GraphQLInterface. diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 409bb4b540..c681102bc5 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -521,39 +521,6 @@ public List getInputFields( .getFieldDefinitions((GraphQLInputObjectType) parentType); } - @Override - public List getInterfaces( - SchemaFieldsContainer implementingType) { - assertTrue( - implementingType instanceof GraphQLImplementingType, - "The implementing type must belong to this GraphQLSchema"); - List result = new ArrayList<>(); - for (GraphQLNamedOutputType interfaceType : - ((GraphQLImplementingType) implementingType).getInterfaces()) { - assertTrue( - interfaceType instanceof GraphQLInterfaceType, - "Interface type references must be resolved"); - result.add((GraphQLInterfaceType) interfaceType); - } - return ImmutableList.copyOf(result); - } - - @Override - public List getUnionMembers(SchemaUnion unionType) { - assertTrue( - unionType instanceof GraphQLUnionType, - "The union type must belong to this GraphQLSchema"); - List result = new ArrayList<>(); - for (GraphQLNamedOutputType member : - ((GraphQLUnionType) unionType).getTypes()) { - assertTrue( - member instanceof GraphQLObjectType, - "Union type references must be resolved"); - result.add((GraphQLObjectType) member); - } - return ImmutableList.copyOf(result); - } - /** * @return all the named types in the scheme as a map from name to named type */ diff --git a/src/main/java/graphql/schema/GraphQLUnionType.java b/src/main/java/graphql/schema/GraphQLUnionType.java index 9577dc5329..36132345c8 100644 --- a/src/main/java/graphql/schema/GraphQLUnionType.java +++ b/src/main/java/graphql/schema/GraphQLUnionType.java @@ -86,6 +86,7 @@ void replaceTypes(List types) { * references are not resolved yet. After they are resolved it contains only GraphQLObjectType. * Reference resolving happens when a full schema is built. */ + @Override public List getTypes() { if (replacedTypes != null) { return replacedTypes; diff --git a/src/main/java/graphql/schema/SchemaImplementingType.java b/src/main/java/graphql/schema/SchemaImplementingType.java new file mode 100644 index 0000000000..c9c19b6e32 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaImplementingType.java @@ -0,0 +1,24 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +import java.util.List; + +/** + * An object or interface type that directly implements interfaces. + */ +@ExperimentalApi +@NullMarked +public interface SchemaImplementingType extends SchemaFieldsContainer { + + /** + * Returns the interfaces directly implemented by this type. + * + *

Elements obtained from an {@link ExecutableSchema} return + * {@link SchemaInterface} instances.

+ * + * @return the implemented interface types + */ + List getInterfaces(); +} diff --git a/src/main/java/graphql/schema/SchemaInterface.java b/src/main/java/graphql/schema/SchemaInterface.java index a1f2429d7b..eb6ef0476e 100644 --- a/src/main/java/graphql/schema/SchemaInterface.java +++ b/src/main/java/graphql/schema/SchemaInterface.java @@ -8,5 +8,5 @@ */ @ExperimentalApi @NullMarked -public interface SchemaInterface extends SchemaFieldsContainer { +public interface SchemaInterface extends SchemaImplementingType { } diff --git a/src/main/java/graphql/schema/SchemaObject.java b/src/main/java/graphql/schema/SchemaObject.java index 361cc1ab74..bd3854a774 100644 --- a/src/main/java/graphql/schema/SchemaObject.java +++ b/src/main/java/graphql/schema/SchemaObject.java @@ -8,5 +8,5 @@ */ @ExperimentalApi @NullMarked -public interface SchemaObject extends SchemaFieldsContainer { +public interface SchemaObject extends SchemaImplementingType { } diff --git a/src/main/java/graphql/schema/SchemaUnion.java b/src/main/java/graphql/schema/SchemaUnion.java index 4140e67f3e..76d47fc462 100644 --- a/src/main/java/graphql/schema/SchemaUnion.java +++ b/src/main/java/graphql/schema/SchemaUnion.java @@ -3,10 +3,22 @@ import graphql.ExperimentalApi; import org.jspecify.annotations.NullMarked; +import java.util.List; + /** * A GraphQL union type. */ @ExperimentalApi @NullMarked public interface SchemaUnion extends SchemaComposite { + + /** + * Returns the types declared as members of this union. + * + *

Elements obtained from an {@link ExecutableSchema} return + * {@link SchemaObject} instances.

+ * + * @return the union member types + */ + List getTypes(); } diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index 7274e36f9b..f9bc5ec343 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -700,8 +700,8 @@ private SchemaElementPrinter interfacePrinter() { printAsAst(out, type.getDefinition(), type.getExtensionDefinitions()); } else { printComments(out, type, ""); - List interfaces = - getInterfaces(schema, type); + List interfaces = + type.getInterfaces(); if (interfaces.isEmpty()) { out.format( "interface %s%s", @@ -712,12 +712,12 @@ private SchemaElementPrinter interfacePrinter() { getAppliedDirectives(schema, type), false)); } else { - List sortedInterfaces = sort( + List sortedInterfaces = sort( type, interfaces, false); Stream interfaceNames = sortedInterfaces.stream() - .map(SchemaInterface::getName); + .map(SchemaNamedType::getName); out.format("interface %s implements %s%s", type.getName(), interfaceNames.collect(joining(" & ")), @@ -758,12 +758,12 @@ private SchemaElementPrinter unionPrinter() { type, getAppliedDirectives(schema, type), false)); - List types = sort( + List types = sort( type, - getUnionMembers(schema, type), + type.getTypes(), false); for (int i = 0; i < types.size(); i++) { - SchemaObject objectType = types.get(i); + SchemaNamedType objectType = types.get(i); if (i > 0) { out.format(" | "); } @@ -799,8 +799,8 @@ private SchemaElementPrinter objectPrinter() { printAsAst(out, type.getDefinition(), type.getExtensionDefinitions()); } else { printComments(out, type, ""); - List interfaces = - getInterfaces(schema, type); + List interfaces = + type.getInterfaces(); if (interfaces.isEmpty()) { out.format( "type %s%s", @@ -811,12 +811,12 @@ private SchemaElementPrinter objectPrinter() { getAppliedDirectives(schema, type), false)); } else { - List sortedInterfaces = sort( + List sortedInterfaces = sort( type, interfaces, false); Stream interfaceNames = sortedInterfaces.stream() - .map(SchemaInterface::getName); + .map(SchemaNamedType::getName); out.format("type %s implements %s%s", type.getName(), interfaceNames.collect(joining(" & ")), @@ -1589,33 +1589,6 @@ private List getInputFields( return ((GraphQLInputObjectType) type).getFieldDefinitions(); } - private List getInterfaces( - @Nullable ExecutableSchema schema, - SchemaFieldsContainer type) { - if (schema != null) { - return schema.getInterfaces(type); - } - if (type instanceof GraphQLObjectType) { - return ((GraphQLObjectType) type).getInterfaces().stream() - .map(interfaceType -> (SchemaInterface) interfaceType) - .collect(toList()); - } - return ((GraphQLInterfaceType) type).getInterfaces().stream() - .map(interfaceType -> (SchemaInterface) interfaceType) - .collect(toList()); - } - - private List getUnionMembers( - @Nullable ExecutableSchema schema, - SchemaUnion type) { - if (schema != null) { - return schema.getUnionMembers(type); - } - return ((GraphQLUnionType) type).getTypes().stream() - .map(member -> (SchemaObject) member) - .collect(toList()); - } - private String printValue( @Nullable ExecutableSchema schema, InputValueWithState value, diff --git a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java index 7ef4cd461d..a384e4d6b8 100644 --- a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java @@ -21,13 +21,11 @@ import graphql.schema.SchemaInputField; import graphql.schema.SchemaInputObject; import graphql.schema.SchemaInputType; -import graphql.schema.SchemaInterface; import graphql.schema.SchemaNamedType; import graphql.schema.SchemaObject; import graphql.schema.SchemaOutputType; import graphql.schema.SchemaScalar; import graphql.schema.SchemaType; -import graphql.schema.SchemaUnion; import graphql.schema.universe.PersistentIntMap; import graphql.schema.universe.SUAppliedDirective; import graphql.schema.universe.SUCompositeType; @@ -374,40 +372,6 @@ public List getInputFields( return Collections.unmodifiableList(result); } - @Override - public List getInterfaces( - SchemaFieldsContainer implementingType) { - SUVertex vertex = elementVertex(implementingType); - List interfaces; - if (vertex instanceof SUObjectType) { - interfaces = schema.getInterfaces((SUObjectType) vertex); - } else { - assertTrue( - vertex instanceof SUInterfaceType, - "Expected an object or interface type"); - interfaces = schema.getInterfaces((SUInterfaceType) vertex); - } - List result = - new ArrayList<>(interfaces.size()); - for (SUInterfaceType interfaceType : interfaces) { - result.add(new SUSchemaInterface(this, interfaceType)); - } - return Collections.unmodifiableList(result); - } - - @Override - public List getUnionMembers(SchemaUnion unionType) { - SUVertex vertex = elementVertex(unionType); - assertTrue(vertex instanceof SUUnionType, "Expected a union type"); - List members = - schema.getUnionMembers((SUUnionType) vertex); - List result = new ArrayList<>(members.size()); - for (SUObjectType member : members) { - result.add(new SUSchemaObject(this, member)); - } - return Collections.unmodifiableList(result); - } - @Override public List getPossibleTypes( SchemaComposite compositeType) { diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java index b093870cd6..7ee0a24ec8 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java @@ -4,6 +4,10 @@ import graphql.schema.SchemaInterface; import graphql.schema.universe.SUInterfaceType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + @Internal public final class SUSchemaInterface extends AbstractSUSchemaNamedType implements SchemaInterface { @@ -14,4 +18,24 @@ public SUSchemaInterface( SUInterfaceType type) { super(executableSchema, type); } + + @Override + public List getInterfaces() { + List interfaces = getExecutableSchema() + .getSchema() + .getInterfaces(getInterfaceTypeVertex()); + List result = + new ArrayList<>(interfaces.size()); + for (SUInterfaceType interfaceType : interfaces) { + result.add(new SUSchemaInterface( + getExecutableSchema(), + interfaceType)); + } + return Collections.unmodifiableList(result); + } + + @Internal + public SUInterfaceType getInterfaceTypeVertex() { + return (SUInterfaceType) getTypeVertex(); + } } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaObject.java b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java index 73a943880a..88bfb7f47a 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaObject.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java @@ -2,8 +2,13 @@ import graphql.Internal; import graphql.schema.SchemaObject; +import graphql.schema.universe.SUInterfaceType; import graphql.schema.universe.SUObjectType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + @Internal public final class SUSchemaObject extends AbstractSUSchemaNamedType implements SchemaObject { @@ -14,4 +19,24 @@ public SUSchemaObject( SUObjectType type) { super(executableSchema, type); } + + @Override + public List getInterfaces() { + List interfaces = getExecutableSchema() + .getSchema() + .getInterfaces(getObjectTypeVertex()); + List result = + new ArrayList<>(interfaces.size()); + for (SUInterfaceType interfaceType : interfaces) { + result.add(new SUSchemaInterface( + getExecutableSchema(), + interfaceType)); + } + return Collections.unmodifiableList(result); + } + + @Internal + public SUObjectType getObjectTypeVertex() { + return (SUObjectType) getTypeVertex(); + } } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java b/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java index b69472bba8..726d3e7561 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaUnion.java @@ -2,8 +2,13 @@ import graphql.Internal; import graphql.schema.SchemaUnion; +import graphql.schema.universe.SUObjectType; import graphql.schema.universe.SUUnionType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + @Internal public final class SUSchemaUnion extends AbstractSUSchemaNamedType implements SchemaUnion { @@ -14,4 +19,21 @@ public SUSchemaUnion( SUUnionType type) { super(executableSchema, type); } + + @Override + public List getTypes() { + List types = getExecutableSchema() + .getSchema() + .getUnionMembers(getUnionTypeVertex()); + List result = new ArrayList<>(types.size()); + for (SUObjectType type : types) { + result.add(new SUSchemaObject(getExecutableSchema(), type)); + } + return Collections.unmodifiableList(result); + } + + @Internal + public SUUnionType getUnionTypeVertex() { + return (SUUnionType) getTypeVertex(); + } } diff --git a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy index cfe4628de0..ff35ed0751 100644 --- a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy @@ -18,9 +18,11 @@ class ExecutableSchemaTest extends Specification { expect: schema.queryType instanceof SchemaObject + schema.queryType instanceof SchemaImplementingType schema.mutationType.name == "Mutation" schema.subscriptionType.name == "Subscription" schema.getType("Node") instanceof SchemaInterface + schema.getType("Node") instanceof SchemaImplementingType schema.getType("Search") instanceof SchemaUnion schema.getType("Date") instanceof SchemaScalar schema.getType("Color") instanceof SchemaEnum @@ -43,7 +45,7 @@ class ExecutableSchemaTest extends Specification { schema.appliedDirectives.isEmpty() and: - def node = schema.getType("Node") as SchemaFieldsContainer + def node = schema.getType("Node") as SchemaInterface schema.getFields(node)*.name == ["id"] schema.getField(node, "id").type instanceof SchemaNonNull schema.getField( @@ -78,11 +80,13 @@ class ExecutableSchemaTest extends Specification { color.getValue("BLUE") == null and: - def user = schema.getType("User") as SchemaFieldsContainer - schema.getInterfaces(user)*.name == ["Node"] - schema.getInterfaces(node).isEmpty() - schema.getUnionMembers( - schema.getType("Search") as SchemaUnion)*.name == ["User"] + def user = schema.getType("User") as SchemaObject + user.interfaces*.name == ["Node"] + user.interfaces.every { it instanceof SchemaInterface } + node.interfaces.isEmpty() + def search = schema.getType("Search") as SchemaUnion + search.types*.name == ["User"] + search.types.every { it instanceof SchemaObject } schema.getAppliedDirectives(choice)*.name == ["oneOf"] and: @@ -161,6 +165,49 @@ class ExecutableSchemaTest extends Specification { executableSchema << executableSchemas() } + def "SUSchema element relationships follow their schema snapshot"() { + given: + def universe = new SchemaUniverse() + def query = universe.newObjectType("Query") + def node = universe.newInterfaceType("Node") + def resource = universe.newInterfaceType("Resource") + def search = universe.newUnionType("Search") + def alpha = universe.newObjectType("Alpha") + def beta = universe.newObjectType("Beta") + def baseSchema = universe.newSchema("base") + .queryType(query) + .addInterface(node, resource) + .addInterface(alpha, node) + .addUnionMember(search, alpha) + .addType(beta) + .build() + def changedSchema = baseSchema.transform("changed", builder -> builder + .removeInterface(alpha, node) + .addInterface(beta, node) + .removeUnionMember(search, alpha) + .addUnionMember(search, beta)) + def base = SUExecutableSchema + .newExecutableSchema(baseSchema) + .build() + def changed = SUExecutableSchema + .newExecutableSchema(changedSchema) + .build() + + expect: + (base.getType("Alpha") as SchemaObject).interfaces*.name == ["Node"] + (base.getType("Beta") as SchemaObject).interfaces.isEmpty() + (base.getType("Node") as SchemaInterface).interfaces*.name == + ["Resource"] + (base.getType("Search") as SchemaUnion).types*.name == ["Alpha"] + + and: + (changed.getType("Alpha") as SchemaObject).interfaces.isEmpty() + (changed.getType("Beta") as SchemaObject).interfaces*.name == ["Node"] + (changed.getType("Node") as SchemaInterface).interfaces*.name == + ["Resource"] + (changed.getType("Search") as SchemaUnion).types*.name == ["Beta"] + } + def "GraphQLSchema executable lookups apply output and input field visibility"() { given: def source = TestUtil.schema(''' From fba559e6c33f410c387735474e96b02527f0c864 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 6 Aug 2026 05:18:47 +0000 Subject: [PATCH 20/32] Support validation with executable schema views --- src/main/java/graphql/ParseAndValidate.java | 159 +++++++- .../graphql/execution/FieldCollector.java | 47 ++- .../execution/FieldCollectorParameters.java | 28 +- .../conditional/ConditionalNodes.java | 13 +- .../java/graphql/schema/GraphQLTypeUtil.java | 88 ++++- .../validation/ArgumentValidationUtil.java | 26 +- .../validation/OperationValidator.java | 347 ++++++++---------- .../graphql/validation/ValidationContext.java | 10 +- .../graphql/validation/ValidationUtil.java | 206 ++++++++--- .../java/graphql/validation/Validator.java | 8 +- .../validation/VariablesTypesMatcher.java | 94 +++-- .../execution/ConditionalNodesTest.groovy | 29 ++ .../execution/FieldCollectorTest.groovy | 26 ++ .../graphql/schema/GraphQLTypeUtilTest.groovy | 52 +++ .../SUExecutableSchemaValidationTest.groovy | 232 ++++++++++++ .../validation/ValidationUtilTest.groovy | 73 ++++ .../VariablesTypesMatcherTest.groovy | 42 ++- 17 files changed, 1130 insertions(+), 350 deletions(-) create mode 100644 src/test/groovy/graphql/schema/universe/SUExecutableSchemaValidationTest.groovy diff --git a/src/main/java/graphql/ParseAndValidate.java b/src/main/java/graphql/ParseAndValidate.java index a092f28fc0..0245458ebb 100644 --- a/src/main/java/graphql/ParseAndValidate.java +++ b/src/main/java/graphql/ParseAndValidate.java @@ -5,6 +5,7 @@ import graphql.parser.Parser; import graphql.parser.ParserEnvironment; import graphql.parser.ParserOptions; +import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLSchema; import graphql.validation.OperationValidationRule; import graphql.validation.QueryComplexityLimits; @@ -49,9 +50,31 @@ public class ParseAndValidate { * @return a result object that indicates how this operation went */ public static ParseAndValidateResult parseAndValidate(@NonNull GraphQLSchema graphQLSchema, @NonNull ExecutionInput executionInput) { + return parseAndValidate( + (ExecutableSchema) graphQLSchema, + executionInput); + } + + /** + * This can be called to parse and validate a GraphQL query against an executable schema view. + * + * @param schema the schema to validate against + * @param executionInput the execution input containing the query + * + * @return a result object that indicates how this operation went + */ + @ExperimentalApi + public static ParseAndValidateResult parseAndValidate( + ExecutableSchema schema, + ExecutionInput executionInput) { ParseAndValidateResult result = parse(executionInput); if (!result.isFailure()) { - List errors = validate(graphQLSchema, assertNotNull(result.getDocument(), "Parse result document cannot be null when parse succeeded"), executionInput.getLocale()); + List errors = validate( + schema, + assertNotNull( + result.getDocument(), + "Parse result document cannot be null when parse succeeded"), + executionInput.getLocale()); return result.transform(builder -> builder.validationErrors(errors)); } return result; @@ -94,7 +117,27 @@ public static ParseAndValidateResult parse(@NonNull ExecutionInput executionInpu * @return a result object that indicates how this operation went */ public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Locale locale) { - return validate(graphQLSchema, parsedDocument, rule -> true, locale); + return validate( + (ExecutableSchema) graphQLSchema, + parsedDocument, + locale); + } + + /** + * This can be called to validate a parsed GraphQL query. + * + * @param schema the executable schema view to validate against + * @param parsedDocument the previously parsed document + * @param locale the current locale + * + * @return validation errors + */ + @ExperimentalApi + public static List validate( + ExecutableSchema schema, + Document parsedDocument, + Locale locale) { + return validate(schema, parsedDocument, rule -> true, locale); } /** @@ -106,7 +149,28 @@ public static List validate(@NonNull GraphQLSchema graphQLSchem * @return a result object that indicates how this operation went */ public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument) { - return validate(graphQLSchema, parsedDocument, rule -> true, Locale.getDefault()); + return validate( + (ExecutableSchema) graphQLSchema, + parsedDocument); + } + + /** + * This can be called to validate a parsed GraphQL query, with the JVM default locale. + * + * @param schema the executable schema view to validate against + * @param parsedDocument the previously parsed document + * + * @return validation errors + */ + @ExperimentalApi + public static List validate( + ExecutableSchema schema, + Document parsedDocument) { + return validate( + schema, + parsedDocument, + rule -> true, + Locale.getDefault()); } /** @@ -120,7 +184,35 @@ public static List validate(@NonNull GraphQLSchema graphQLSchem * @return a result object that indicates how this operation went */ public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Predicate rulePredicate, @NonNull Locale locale) { - return validate(graphQLSchema, parsedDocument, rulePredicate, locale, null); + return validate( + (ExecutableSchema) graphQLSchema, + parsedDocument, + rulePredicate, + locale); + } + + /** + * This can be called to validate a parsed GraphQL query with selected rules. + * + * @param schema the executable schema view to validate against + * @param parsedDocument the previously parsed document + * @param rulePredicate selects validation rules + * @param locale the current locale + * + * @return validation errors + */ + @ExperimentalApi + public static List validate( + ExecutableSchema schema, + Document parsedDocument, + Predicate rulePredicate, + Locale locale) { + return validate( + schema, + parsedDocument, + rulePredicate, + locale, + null); } /** @@ -135,8 +227,39 @@ public static List validate(@NonNull GraphQLSchema graphQLSchem * @return a result object that indicates how this operation went */ public static List validate(GraphQLSchema graphQLSchema, Document parsedDocument, Predicate rulePredicate, Locale locale, @Nullable QueryComplexityLimits limits) { + return validate( + (ExecutableSchema) graphQLSchema, + parsedDocument, + rulePredicate, + locale, + limits); + } + + /** + * This can be called to validate a parsed GraphQL query with selected rules and complexity limits. + * + * @param schema the executable schema view to validate against + * @param parsedDocument the previously parsed document + * @param rulePredicate selects validation rules + * @param locale the current locale + * @param limits optional query complexity limits + * + * @return validation errors + */ + @ExperimentalApi + public static List validate( + ExecutableSchema schema, + Document parsedDocument, + Predicate rulePredicate, + Locale locale, + @Nullable QueryComplexityLimits limits) { Validator validator = new Validator(); - return validator.validateDocument(graphQLSchema, parsedDocument, rulePredicate, locale, limits); + return validator.validateDocument( + schema, + parsedDocument, + rulePredicate, + locale, + limits); } /** @@ -149,7 +272,31 @@ public static List validate(GraphQLSchema graphQLSchema, Docume * @return a result object that indicates how this operation went */ public static List validate(@NonNull GraphQLSchema graphQLSchema, @NonNull Document parsedDocument, @NonNull Predicate rulePredicate) { + return validate( + (ExecutableSchema) graphQLSchema, + parsedDocument, + rulePredicate); + } + + /** + * This can be called to validate a parsed GraphQL query with selected rules and the JVM default locale. + * + * @param schema the executable schema view to validate against + * @param parsedDocument the previously parsed document + * @param rulePredicate selects validation rules + * + * @return validation errors + */ + @ExperimentalApi + public static List validate( + ExecutableSchema schema, + Document parsedDocument, + Predicate rulePredicate) { Validator validator = new Validator(); - return validator.validateDocument(graphQLSchema, parsedDocument, rulePredicate, Locale.getDefault()); + return validator.validateDocument( + schema, + parsedDocument, + rulePredicate, + Locale.getDefault()); } } diff --git a/src/main/java/graphql/execution/FieldCollector.java b/src/main/java/graphql/execution/FieldCollector.java index 9a63e009b3..6c303a2aba 100644 --- a/src/main/java/graphql/execution/FieldCollector.java +++ b/src/main/java/graphql/execution/FieldCollector.java @@ -11,10 +11,9 @@ import graphql.language.InlineFragment; import graphql.language.Selection; import graphql.language.SelectionSet; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLUnionType; +import graphql.schema.SchemaComposite; +import graphql.schema.SchemaObject; +import graphql.schema.SchemaType; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -23,8 +22,6 @@ import java.util.Set; import static graphql.execution.MergedSelectionSet.newMergedSelectionSet; -import static graphql.execution.TypeFromAST.getTypeFromAST; - /** * A field collector can iterate over field selection sets and build out the sub fields that have been selected, * expanding named and inline fragments as it goes. @@ -88,7 +85,7 @@ private void collectFragmentSpread(FieldCollectorParameters parameters, Set visitedFragments, Map fields, InlineFragment inlineFragment, boolean incrementalSupport) { if (!conditionalNodes.shouldInclude(inlineFragment, parameters.getVariables(), - parameters.getGraphQLSchema(), + parameters.getSchema(), parameters.getGraphQLContext()) || !doesFragmentConditionMatch(parameters, inlineFragment)) { return; @@ -135,7 +132,7 @@ private void collectInlineFragment(FieldCollectorParameters parameters, Set fields, Field field, DeferredExecution deferredExecution) { if (!conditionalNodes.shouldInclude(field, parameters.getVariables(), - parameters.getGraphQLSchema(), + parameters.getSchema(), parameters.getGraphQLContext())) { return; } @@ -152,30 +149,28 @@ private boolean doesFragmentConditionMatch(FieldCollectorParameters parameters, if (inlineFragment.getTypeCondition() == null) { return true; } - GraphQLType conditionType; - conditionType = getTypeFromAST(parameters.getGraphQLSchema(), inlineFragment.getTypeCondition()); + SchemaType conditionType = parameters.getSchema() + .getType(inlineFragment.getTypeCondition().getName()); return checkTypeCondition(parameters, conditionType); } private boolean doesFragmentConditionMatch(FieldCollectorParameters parameters, FragmentDefinition fragmentDefinition) { - GraphQLType conditionType; - conditionType = getTypeFromAST(parameters.getGraphQLSchema(), fragmentDefinition.getTypeCondition()); + SchemaType conditionType = parameters.getSchema() + .getType(fragmentDefinition.getTypeCondition().getName()); return checkTypeCondition(parameters, conditionType); } - private boolean checkTypeCondition(FieldCollectorParameters parameters, GraphQLType conditionType) { - GraphQLObjectType type = parameters.getObjectType(); - if (conditionType.equals(type)) { - return true; - } - - if (conditionType instanceof GraphQLInterfaceType) { - List implementations = parameters.getGraphQLSchema().getImplementations((GraphQLInterfaceType) conditionType); - return implementations.contains(type); - } else if (conditionType instanceof GraphQLUnionType) { - return ((GraphQLUnionType) conditionType).getTypes().contains(type); + private boolean checkTypeCondition( + FieldCollectorParameters parameters, + SchemaType conditionType) { + SchemaObject type = parameters.getObjectType(); + if (!(conditionType instanceof SchemaComposite) + || type == null) { + return false; } - return false; + return parameters.getSchema().isPossibleType( + (SchemaComposite) conditionType, + type); } diff --git a/src/main/java/graphql/execution/FieldCollectorParameters.java b/src/main/java/graphql/execution/FieldCollectorParameters.java index 75dafc1eff..0b8b646601 100644 --- a/src/main/java/graphql/execution/FieldCollectorParameters.java +++ b/src/main/java/graphql/execution/FieldCollectorParameters.java @@ -4,8 +4,8 @@ import graphql.GraphQLContext; import graphql.Internal; import graphql.language.FragmentDefinition; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLSchema; +import graphql.schema.ExecutableSchema; +import graphql.schema.SchemaObject; import java.util.Map; @@ -14,14 +14,14 @@ */ @Internal public class FieldCollectorParameters { - private final GraphQLSchema graphQLSchema; + private final ExecutableSchema schema; private final Map fragmentsByName; private final Map variables; - private final GraphQLObjectType objectType; + private final SchemaObject objectType; private final GraphQLContext graphQLContext; - public GraphQLSchema getGraphQLSchema() { - return graphQLSchema; + public ExecutableSchema getSchema() { + return schema; } public Map getFragmentsByName() { @@ -32,7 +32,7 @@ public Map getVariables() { return variables; } - public GraphQLObjectType getObjectType() { + public SchemaObject getObjectType() { return objectType; } @@ -42,7 +42,7 @@ public GraphQLContext getGraphQLContext() { private FieldCollectorParameters(Builder builder) { this.fragmentsByName = builder.fragmentsByName; - this.graphQLSchema = builder.graphQLSchema; + this.schema = builder.schema; this.variables = builder.variables; this.objectType = builder.objectType; this.graphQLContext = builder.graphQLContext; @@ -53,10 +53,10 @@ public static Builder newParameters() { } public static class Builder { - private GraphQLSchema graphQLSchema; + private ExecutableSchema schema; private Map fragmentsByName; private Map variables; - private GraphQLObjectType objectType; + private SchemaObject objectType; private GraphQLContext graphQLContext = GraphQLContext.getDefault(); /** @@ -66,12 +66,12 @@ private Builder() { } - public Builder schema(GraphQLSchema graphQLSchema) { - this.graphQLSchema = graphQLSchema; + public Builder schema(ExecutableSchema schema) { + this.schema = schema; return this; } - public Builder objectType(GraphQLObjectType objectType) { + public Builder objectType(SchemaObject objectType) { this.objectType = objectType; return this; } @@ -92,7 +92,7 @@ public Builder variables(Map variables) { } public FieldCollectorParameters build() { - Assert.assertNotNull(graphQLSchema, "You must provide a schema"); + Assert.assertNotNull(schema, "You must provide a schema"); return new FieldCollectorParameters(this); } diff --git a/src/main/java/graphql/execution/conditional/ConditionalNodes.java b/src/main/java/graphql/execution/conditional/ConditionalNodes.java index e922eeb427..286a751bfa 100644 --- a/src/main/java/graphql/execution/conditional/ConditionalNodes.java +++ b/src/main/java/graphql/execution/conditional/ConditionalNodes.java @@ -10,6 +10,7 @@ import graphql.language.DirectivesContainer; import graphql.language.NodeUtil; import graphql.language.VariableReference; +import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLSchema; import org.jspecify.annotations.Nullable; @@ -31,7 +32,7 @@ public Boolean shouldIncludeWithoutVariables(DirectivesContainer element) { public boolean shouldInclude(DirectivesContainer element, Map variables, - GraphQLSchema graphQLSchema, + ExecutableSchema schema, @Nullable GraphQLContext graphQLContext ) { // @@ -45,7 +46,15 @@ public boolean shouldInclude(DirectivesContainer element, if (graphQLContext != null) { ConditionalNodeDecision conditionalDecision = graphQLContext.get(ConditionalNodeDecision.class); if (conditionalDecision != null) { - return customShouldInclude(variables, element, graphQLSchema, graphQLContext, conditionalDecision); + Assert.assertTrue( + schema instanceof GraphQLSchema, + "Custom conditional node decisions require a GraphQLSchema"); + return customShouldInclude( + variables, + element, + (GraphQLSchema) schema, + graphQLContext, + conditionalDecision); } } // if no one says otherwise, the node is considered included diff --git a/src/main/java/graphql/schema/GraphQLTypeUtil.java b/src/main/java/graphql/schema/GraphQLTypeUtil.java index abca3fcc42..b4160401ab 100644 --- a/src/main/java/graphql/schema/GraphQLTypeUtil.java +++ b/src/main/java/graphql/schema/GraphQLTypeUtil.java @@ -1,9 +1,10 @@ package graphql.schema; import graphql.Assert; +import graphql.Directives; +import graphql.ExperimentalApi; import graphql.PublicApi; import graphql.introspection.Introspection; -import graphql.Directives; import graphql.schema.idl.ScalarInfo; import org.jspecify.annotations.NullMarked; @@ -14,7 +15,7 @@ import static graphql.Assert.assertShouldNeverHappen; /** - * A utility class that helps work with {@link graphql.schema.GraphQLType}s + * A utility class that helps work with GraphQL schema types. */ @PublicApi @NullMarked @@ -48,6 +49,89 @@ public static String simplePrint(GraphQLSchemaElement schemaElement) { return assertShouldNeverHappen("unexpected schema element: " + schemaElement); } + /** + * This will return an abstract schema type in GraphQL SDL format, eg {@code [typeName!]!}. + * + * @param type the type in play + * + * @return the type in GraphQL SDL format + */ + @ExperimentalApi + public static String simplePrint(SchemaType type) { + Assert.assertNotNull(type, "type can't be null"); + if (isNonNull(type)) { + return simplePrint(unwrapOne(type)) + "!"; + } + if (isList(type)) { + return "[" + simplePrint(unwrapOne(type)) + "]"; + } + return ((SchemaNamedType) type).getName(); + } + + @ExperimentalApi + public static boolean isNonNull(SchemaType type) { + return type instanceof SchemaNonNull; + } + + @ExperimentalApi + public static boolean isNullable(SchemaType type) { + return !isNonNull(type); + } + + @ExperimentalApi + public static boolean isList(SchemaType type) { + return type instanceof SchemaList; + } + + @ExperimentalApi + public static boolean isWrapped(SchemaType type) { + return isList(type) || isNonNull(type); + } + + @ExperimentalApi + public static boolean isNotWrapped(SchemaType type) { + return !isWrapped(type); + } + + @ExperimentalApi + public static boolean isScalar(SchemaType type) { + return type instanceof SchemaScalar; + } + + @ExperimentalApi + public static boolean isEnum(SchemaType type) { + return type instanceof SchemaEnum; + } + + @ExperimentalApi + public static boolean isLeaf(SchemaType type) { + SchemaType unmodifiedType = unwrapAll(type); + return unmodifiedType instanceof SchemaScalar + || unmodifiedType instanceof SchemaEnum; + } + + @ExperimentalApi + public static boolean isInput(SchemaType type) { + return unwrapAll(type) instanceof SchemaInputType; + } + + @ExperimentalApi + public static SchemaType unwrapOne(SchemaType type) { + if (type instanceof SchemaModifiedType) { + return ((SchemaModifiedType) type).getWrappedType(); + } + return type; + } + + @ExperimentalApi + public static SchemaType unwrapAll(SchemaType type) { + SchemaType current = type; + while (isWrapped(current)) { + current = unwrapOne(current); + } + return current; + } + /** * Returns true if the given type is a non null type * diff --git a/src/main/java/graphql/validation/ArgumentValidationUtil.java b/src/main/java/graphql/validation/ArgumentValidationUtil.java index a378f40b63..ab0b546182 100644 --- a/src/main/java/graphql/validation/ArgumentValidationUtil.java +++ b/src/main/java/graphql/validation/ArgumentValidationUtil.java @@ -6,10 +6,10 @@ import graphql.language.Argument; import graphql.language.ObjectField; import graphql.language.Value; -import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLScalarType; -import graphql.schema.GraphQLType; +import graphql.schema.SchemaEnum; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaScalar; +import graphql.schema.SchemaType; import java.util.ArrayList; import java.util.List; @@ -33,13 +33,13 @@ public ArgumentValidationUtil(Argument argument) { } @Override - protected void handleNullError(Value value, GraphQLType type) { + protected void handleNullError(Value value, SchemaType type) { errMsgKey = "ArgumentValidationUtil.handleNullError"; argumentValue = value; } @Override - protected void handleScalarError(Value value, GraphQLScalarType type, GraphQLError invalid) { + protected void handleScalarError(Value value, SchemaScalar type, GraphQLError invalid) { if (invalid.getMessage() == null) { errMsgKey = "ArgumentValidationUtil.handleScalarError"; } else { @@ -52,7 +52,7 @@ protected void handleScalarError(Value value, GraphQLScalarType type, GraphQL } @Override - protected void handleEnumError(Value value, GraphQLEnumType type, GraphQLError invalid) { + protected void handleEnumError(Value value, SchemaEnum type, GraphQLError invalid) { if (invalid.getMessage() == null) { errMsgKey = "ArgumentValidationUtil.handleEnumError"; } else { @@ -64,35 +64,35 @@ protected void handleEnumError(Value value, GraphQLEnumType type, GraphQLErro } @Override - protected void handleNotObjectError(Value value, GraphQLInputObjectType type) { + protected void handleNotObjectError(Value value, SchemaInputObject type) { errMsgKey = "ArgumentValidationUtil.handleNotObjectError"; } @Override - protected void handleMissingFieldsError(Value value, GraphQLInputObjectType type, Set missingFields) { + protected void handleMissingFieldsError(Value value, SchemaInputObject type, Set missingFields) { errMsgKey = "ArgumentValidationUtil.handleMissingFieldsError"; arguments.add(missingFields); } @Override - protected void handleExtraFieldError(Value value, GraphQLInputObjectType type, ObjectField objectField) { + protected void handleExtraFieldError(Value value, SchemaInputObject type, ObjectField objectField) { errMsgKey = "ArgumentValidationUtil.handleExtraFieldError"; arguments.add(type.getName()); arguments.add(objectField.getName()); } @Override - protected void handleFieldNotValidError(ObjectField objectField, GraphQLInputObjectType type) { + protected void handleFieldNotValidError(ObjectField objectField, SchemaInputObject type) { argumentNames.add(0, objectField.getName()); } @Override - protected void handleFieldNotValidError(Value value, GraphQLType type, int index) { + protected void handleFieldNotValidError(Value value, SchemaType type, int index) { argumentNames.add(0, String.format("[%s]", index)); } @Override - protected void handleExtraOneOfFieldsError(GraphQLInputObjectType type, Value value) { + protected void handleExtraOneOfFieldsError(SchemaInputObject type, Value value) { errMsgKey = "ArgumentValidationUtil.extraOneOfFieldsError"; arguments.add(type.getName()); } diff --git a/src/main/java/graphql/validation/OperationValidator.java b/src/main/java/graphql/validation/OperationValidator.java index fdb0cf04bd..e8b65c3c3f 100644 --- a/src/main/java/graphql/validation/OperationValidator.java +++ b/src/main/java/graphql/validation/OperationValidator.java @@ -11,8 +11,6 @@ import graphql.execution.FieldCollectorParameters; import graphql.execution.MergedField; import graphql.execution.MergedSelectionSet; -import graphql.execution.TypeFromAST; -import graphql.execution.ValuesResolver; import graphql.i18n.I18nMsg; import graphql.introspection.GoodFaithIntrospection; import graphql.introspection.Introspection; @@ -45,22 +43,19 @@ import graphql.language.Value; import graphql.language.VariableDefinition; import graphql.language.VariableReference; -import graphql.schema.GraphQLArgument; -import graphql.schema.GraphQLCompositeType; -import graphql.schema.GraphQLDirective; -import graphql.schema.GraphQLFieldDefinition; -import graphql.schema.GraphQLFieldsContainer; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLInterfaceType; -import graphql.schema.GraphQLNullableType; -import graphql.schema.GraphQLObjectType; -import graphql.schema.GraphQLOutputType; -import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; -import graphql.schema.GraphQLUnionType; -import graphql.schema.GraphQLUnmodifiedType; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaComposite; +import graphql.schema.SchemaDirective; +import graphql.schema.SchemaField; +import graphql.schema.SchemaFieldsContainer; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaInputType; +import graphql.schema.SchemaInterface; +import graphql.schema.SchemaObject; +import graphql.schema.SchemaOutputType; +import graphql.schema.SchemaType; +import graphql.schema.SchemaUnion; import graphql.schema.InputValueWithState; import graphql.util.StringKit; import org.jspecify.annotations.NullMarked; @@ -69,7 +64,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.EnumSet; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -280,12 +274,12 @@ public class OperationValidator implements DocumentVisitor { private final Predicate rulePredicate; // --- Traversal context --- - private final List<@Nullable GraphQLCompositeType> parentTypeStack = new ArrayList<>(); - private final List<@Nullable GraphQLFieldDefinition> fieldDefinitionStack = new ArrayList<>(); - private final List<@Nullable GraphQLInputType> inputTypeStack = new ArrayList<>(); + private final List<@Nullable SchemaComposite> parentTypeStack = new ArrayList<>(); + private final List<@Nullable SchemaField> fieldDefinitionStack = new ArrayList<>(); + private final List<@Nullable SchemaInputType> inputTypeStack = new ArrayList<>(); private final List<@Nullable InputValueWithState> defaultValueStack = new ArrayList<>(); private final List queryPath = new ArrayList<>(); - private @Nullable GraphQLDirective directiveDefinition; + private @Nullable SchemaDirective directiveDefinition; /** * The operation currently being processed, including during fragment retraversal. @@ -394,7 +388,7 @@ private void enterTraversal(Node node, List ancestors) { } else if (node instanceof FragmentDefinition) { enterName(((FragmentDefinition) node).getName()); } else if (node instanceof VariableDefinition) { - enterVariableDefinition((VariableDefinition) node); + enterVariableDefinition(); } else if (node instanceof Argument) { enterArgument((Argument) node); } else if (node instanceof ArrayValue) { @@ -405,51 +399,46 @@ private void enterTraversal(Node node, List ancestors) { } private void enterField(Field field) { - GraphQLCompositeType parentType = getParentType(); - GraphQLFieldDefinition fieldDefinition = parentType == null + SchemaComposite parentType = getParentType(); + SchemaField fieldDefinition = parentType == null ? null : getFieldDefinition(parentType, field.getName()); fieldDefinitionStack.add(fieldDefinition); enterName(field.getName()); } - private void enterVariableDefinition(VariableDefinition variableDefinition) { - GraphQLType type = TypeFromAST.getTypeFromAST( - validationContext.getSchema(), - variableDefinition.getType()); - inputTypeStack.add(type instanceof GraphQLInputType ? (GraphQLInputType) type : null); + private void enterVariableDefinition() { + inputTypeStack.add(null); defaultValueStack.add(null); } private void enterArgument(Argument argument) { - GraphQLArgument argumentDefinition = getArgumentDefinition(argument.getName()); + SchemaArgument argumentDefinition = getArgumentDefinition(argument.getName()); inputTypeStack.add(argumentDefinition == null ? null : argumentDefinition.getType()); defaultValueStack.add( argumentDefinition == null ? null : argumentDefinition.getArgumentDefaultValue()); } private void enterArrayValue() { - GraphQLNullableType nullableType = getNullableType(getInputType()); - GraphQLInputType inputType = null; + SchemaInputType nullableType = getNullableType(getInputType()); + SchemaInputType inputType = null; if (nullableType != null && isList(nullableType)) { - inputType = (GraphQLInputType) unwrapOne(nullableType); + inputType = (SchemaInputType) unwrapOne(nullableType); } inputTypeStack.add(inputType); defaultValueStack.add(null); } private void enterObjectField(ObjectField objectField) { - GraphQLInputType currentInputType = getInputType(); - GraphQLUnmodifiedType unwrappedType = + SchemaInputType currentInputType = getInputType(); + SchemaType unwrappedType = currentInputType == null ? null : unwrapAll(currentInputType); - GraphQLInputObjectField inputField = null; - if (unwrappedType instanceof GraphQLInputObjectType) { + SchemaInputField inputField = null; + if (unwrappedType instanceof SchemaInputObject) { inputField = validationContext.getSchema() - .getCodeRegistry() - .getFieldVisibility() - .getFieldDefinition( - (GraphQLInputObjectType) unwrappedType, - objectField.getName()); + .getInputField( + (SchemaInputObject) unwrappedType, + objectField.getName()); } inputTypeStack.add(inputField == null ? null : inputField.getType()); defaultValueStack.add( @@ -475,22 +464,22 @@ private void leaveTraversal(Node node) { } } - private @Nullable GraphQLCompositeType resolveSelectionSetParent(List ancestors) { + private @Nullable SchemaComposite resolveSelectionSetParent(List ancestors) { Assert.assertNotEmpty(ancestors, "A selection set must have an owner"); Node owner = ancestors.get(ancestors.size() - 1); - GraphQLType type = getSelectionSetOwnerType(owner); - GraphQLUnmodifiedType unwrappedType = type == null ? null : unwrapAll(type); - return unwrappedType instanceof GraphQLCompositeType - ? (GraphQLCompositeType) unwrappedType + SchemaType type = getSelectionSetOwnerType(owner); + SchemaType unwrappedType = type == null ? null : unwrapAll(type); + return unwrappedType instanceof SchemaComposite + ? (SchemaComposite) unwrappedType : null; } - private @Nullable GraphQLType getSelectionSetOwnerType(Node owner) { + private @Nullable SchemaType getSelectionSetOwnerType(Node owner) { if (owner instanceof OperationDefinition) { return getOperationType((OperationDefinition) owner); } if (owner instanceof Field) { - GraphQLFieldDefinition fieldDefinition = getFieldDefinition(); + SchemaField fieldDefinition = getFieldDefinition(); return fieldDefinition == null ? null : fieldDefinition.getType(); } if (owner instanceof InlineFragment) { @@ -504,7 +493,7 @@ private void leaveTraversal(Node node) { return validationContext.getSchema().getType(typeCondition.getName()); } - private @Nullable GraphQLObjectType getOperationType( + private @Nullable SchemaObject getOperationType( OperationDefinition operationDefinition) { if (operationDefinition.getOperation() == OperationDefinition.Operation.QUERY) { return validationContext.getSchema().getQueryType(); @@ -518,62 +507,39 @@ private void leaveTraversal(Node node) { return validationContext.getSchema().getSubscriptionType(); } - private @Nullable GraphQLFieldDefinition getFieldDefinition( - GraphQLCompositeType parentType, + private @Nullable SchemaField getFieldDefinition( + SchemaComposite parentType, String fieldName) { - if (validationContext.getSchema().getQueryType().equals(parentType)) { - if (fieldName.equals( - validationContext.getSchema() - .getIntrospectionSchemaFieldDefinition() - .getName())) { - return validationContext.getSchema().getIntrospectionSchemaFieldDefinition(); - } - if (fieldName.equals( - validationContext.getSchema() - .getIntrospectionTypeFieldDefinition() - .getName())) { - return validationContext.getSchema().getIntrospectionTypeFieldDefinition(); - } - } - if (fieldName.equals( - validationContext.getSchema() - .getIntrospectionTypenameFieldDefinition() - .getName())) { - return validationContext.getSchema().getIntrospectionTypenameFieldDefinition(); - } - if (parentType instanceof GraphQLFieldsContainer) { - return validationContext.getSchema() - .getCodeRegistry() - .getFieldVisibility() - .getFieldDefinition((GraphQLFieldsContainer) parentType, fieldName); - } - return null; + return validationContext.getSchema().getField( + parentType, + fieldName); } - private @Nullable GraphQLArgument getArgumentDefinition(String name) { + private @Nullable SchemaArgument getArgumentDefinition(String name) { if (directiveDefinition != null) { return directiveDefinition.getArgument(name); } - GraphQLFieldDefinition fieldDefinition = getFieldDefinition(); + SchemaField fieldDefinition = getFieldDefinition(); return fieldDefinition == null ? null : fieldDefinition.getArgument(name); } - private @Nullable GraphQLNullableType getNullableType(@Nullable GraphQLType type) { + private @Nullable SchemaInputType getNullableType( + @Nullable SchemaInputType type) { if (type == null) { return null; } - return (GraphQLNullableType) (isNonNull(type) ? unwrapOne(type) : type); + return (SchemaInputType) (isNonNull(type) ? unwrapOne(type) : type); } - private @Nullable GraphQLCompositeType getParentType() { + private @Nullable SchemaComposite getParentType() { return lastElement(parentTypeStack); } - private @Nullable GraphQLFieldDefinition getFieldDefinition() { + private @Nullable SchemaField getFieldDefinition() { return lastElement(fieldDefinitionStack); } - private @Nullable GraphQLInputType getInputType() { + private @Nullable SchemaInputType getInputType() { return lastElement(inputTypeStack); } @@ -581,7 +547,7 @@ private void leaveTraversal(Node node) { return lastElement(defaultValueStack); } - private @Nullable GraphQLDirective getDirectiveDefinition() { + private @Nullable SchemaDirective getDirectiveDefinition() { return directiveDefinition; } @@ -859,7 +825,7 @@ private void checkField(Field field) { // --- GoodFaithIntrospection --- private void checkGoodFaithIntrospection(Field field) { - GraphQLCompositeType parentType = getParentType(); + SchemaComposite parentType = getParentType(); if (parentType == null) { return; } @@ -870,7 +836,7 @@ private void checkGoodFaithIntrospection(Field field) { // Only counted at the structural level (not during fragment traversal) to match ENO merging // behavior where the same field from a direct selection and a fragment spread merge into one. if (shouldRunDocumentLevelRules()) { - GraphQLObjectType queryType = validationContext.getSchema().getQueryType(); + SchemaObject queryType = validationContext.getSchema().getQueryType(); if (parentType.getName().equals(queryType.getName())) { if (Introspection.SchemaMetaFieldDef.getName().equals(fieldName) || Introspection.TypeMetaFieldDef.getName().equals(fieldName)) { key = parentType.getName() + "." + fieldName; @@ -1160,7 +1126,7 @@ private String nonExecutableDefinitionMessage(Definition definition) { // --- ArgumentsOfCorrectType --- private void validateArgumentsOfCorrectType(Argument argument) { - GraphQLArgument fieldArgument = getArgumentDefinition(argument.getName()); + SchemaArgument fieldArgument = getArgumentDefinition(argument.getName()); if (fieldArgument == null) { return; } @@ -1178,11 +1144,11 @@ private void validateArgumentsOfCorrectType(Argument argument) { // --- FieldsOnCorrectType --- private void validateFieldsOnCorrectType(Field field) { - GraphQLCompositeType parentType = getParentType(); + SchemaComposite parentType = getParentType(); if (parentType == null) { return; } - GraphQLFieldDefinition fieldDef = getFieldDefinition(); + SchemaField fieldDef = getFieldDefinition(); if (fieldDef == null) { String message = i18n(FieldUndefined, "FieldsOnCorrectType.unknownField", field.getName(), parentType.getName()); addError(FieldUndefined, field.getSourceLocation(), message); @@ -1194,22 +1160,22 @@ private void validateFragmentsOnCompositeType_inline(InlineFragment inlineFragme if (inlineFragment.getTypeCondition() == null) { return; } - GraphQLType type = validationContext.getSchema().getType(inlineFragment.getTypeCondition().getName()); + SchemaType type = validationContext.getSchema().getType(inlineFragment.getTypeCondition().getName()); if (type == null) { return; } - if (!(type instanceof GraphQLCompositeType)) { + if (!(type instanceof SchemaComposite)) { String message = i18n(InlineFragmentTypeConditionInvalid, "FragmentsOnCompositeType.invalidInlineTypeCondition"); addError(InlineFragmentTypeConditionInvalid, inlineFragment.getSourceLocation(), message); } } private void validateFragmentsOnCompositeType_definition(FragmentDefinition fragmentDefinition) { - GraphQLType type = validationContext.getSchema().getType(fragmentDefinition.getTypeCondition().getName()); + SchemaType type = validationContext.getSchema().getType(fragmentDefinition.getTypeCondition().getName()); if (type == null) { return; } - if (!(type instanceof GraphQLCompositeType)) { + if (!(type instanceof SchemaComposite)) { String message = i18n(FragmentTypeConditionInvalid, "FragmentsOnCompositeType.invalidFragmentTypeCondition"); addError(FragmentTypeConditionInvalid, fragmentDefinition.getSourceLocation(), message); } @@ -1217,20 +1183,20 @@ private void validateFragmentsOnCompositeType_definition(FragmentDefinition frag // --- KnownArgumentNames --- private void validateKnownArgumentNames(Argument argument) { - GraphQLDirective directiveDef = getDirectiveDefinition(); + SchemaDirective directiveDef = getDirectiveDefinition(); if (directiveDef != null) { - GraphQLArgument directiveArgument = directiveDef.getArgument(argument.getName()); + SchemaArgument directiveArgument = directiveDef.getArgument(argument.getName()); if (directiveArgument == null) { String message = i18n(UnknownDirective, "KnownArgumentNames.unknownDirectiveArg", argument.getName()); addError(UnknownDirective, argument.getSourceLocation(), message); } return; } - GraphQLFieldDefinition fieldDef = getFieldDefinition(); + SchemaField fieldDef = getFieldDefinition(); if (fieldDef == null) { return; } - GraphQLArgument fieldArgument = fieldDef.getArgument(argument.getName()); + SchemaArgument fieldArgument = fieldDef.getArgument(argument.getName()); if (fieldArgument == null) { String message = i18n(UnknownArgument, "KnownArgumentNames.unknownFieldArg", argument.getName()); addError(UnknownArgument, argument.getSourceLocation(), message); @@ -1239,7 +1205,7 @@ private void validateKnownArgumentNames(Argument argument) { // --- KnownDirectives --- private void validateKnownDirectives(Directive directive, List ancestors) { - GraphQLDirective graphQLDirective = validationContext.getSchema().getDirective(directive.getName()); + SchemaDirective graphQLDirective = validationContext.getSchema().getDirective(directive.getName()); if (graphQLDirective == null) { String message = i18n(UnknownDirective, "KnownDirectives.unknownDirective", directive.getName()); addError(UnknownDirective, directive.getSourceLocation(), message); @@ -1252,8 +1218,8 @@ private void validateKnownDirectives(Directive directive, List ancestors) } } - private boolean hasInvalidLocation(GraphQLDirective directive, Node ancestor) { - EnumSet validLocations = directive.validLocations(); + private boolean hasInvalidLocation(SchemaDirective directive, Node ancestor) { + Set validLocations = directive.validLocations(); if (ancestor instanceof OperationDefinition) { OperationDefinition.Operation operation = ((OperationDefinition) ancestor).getOperation(); if (OperationDefinition.Operation.QUERY.equals(operation)) { @@ -1432,7 +1398,7 @@ private void validateOverlappingFieldsCanBeMerged(OperationDefinition operationD getOperationType(operationDefinition)); } - private void overlappingFieldsImpl(SelectionSet selectionSet, @Nullable GraphQLOutputType graphQLOutputType) { + private void overlappingFieldsImpl(SelectionSet selectionSet, @Nullable SchemaOutputType graphQLOutputType) { Map> fieldMap = new LinkedHashMap<>(selectionSet.getSelections().size()); Set visitedFragments = new LinkedHashSet<>(); overlappingFields_collectFields(fieldMap, selectionSet, graphQLOutputType, visitedFragments); @@ -1446,7 +1412,7 @@ private void overlappingFieldsImpl(SelectionSet selectionSet, @Nullable GraphQLO } } - private void overlappingFields_collectFields(Map> fieldMap, SelectionSet selectionSet, @Nullable GraphQLType parentType, Set visitedFragments) { + private void overlappingFields_collectFields(Map> fieldMap, SelectionSet selectionSet, @Nullable SchemaType parentType, Set visitedFragments) { for (Selection selection : selectionSet.getSelections()) { if (selection instanceof Field) { overlappingFields_collectFieldsForField(fieldMap, parentType, (Field) selection); @@ -1467,27 +1433,30 @@ private void overlappingFields_collectFieldsForFragmentSpread(Map> fieldMap, Set visitedFragments, @Nullable GraphQLType parentType, InlineFragment inlineFragment) { - GraphQLType graphQLType; + private void overlappingFields_collectFieldsForInlineFragment(Map> fieldMap, Set visitedFragments, @Nullable SchemaType parentType, InlineFragment inlineFragment) { + SchemaType graphQLType; if (inlineFragment.getTypeCondition() == null) { graphQLType = parentType; } else { - graphQLType = TypeFromAST.getTypeFromAST(validationContext.getSchema(), inlineFragment.getTypeCondition()); + graphQLType = validationContext.getSchema() + .getType(inlineFragment.getTypeCondition().getName()); } overlappingFields_collectFields(fieldMap, inlineFragment.getSelectionSet(), graphQLType, visitedFragments); } - private void overlappingFields_collectFieldsForField(Map> fieldMap, @Nullable GraphQLType parentType, Field field) { + private void overlappingFields_collectFieldsForField(Map> fieldMap, @Nullable SchemaType parentType, Field field) { String responseName = field.getResultKey(); - GraphQLOutputType fieldType = null; - GraphQLUnmodifiedType unwrappedParent = parentType != null ? unwrapAll(parentType) : null; - if (unwrappedParent instanceof GraphQLFieldsContainer) { - GraphQLFieldsContainer fieldsContainer = (GraphQLFieldsContainer) unwrappedParent; - GraphQLFieldDefinition fieldDefinition = validationContext.getSchema().getCodeRegistry().getFieldVisibility().getFieldDefinition(fieldsContainer, field.getName()); + SchemaOutputType fieldType = null; + SchemaType unwrappedParent = parentType != null ? unwrapAll(parentType) : null; + if (unwrappedParent instanceof SchemaFieldsContainer) { + SchemaFieldsContainer fieldsContainer = (SchemaFieldsContainer) unwrappedParent; + SchemaField fieldDefinition = validationContext.getSchema() + .getField(fieldsContainer, field.getName()); fieldType = fieldDefinition != null ? fieldDefinition.getType() : null; } fieldMap.computeIfAbsent(responseName, k -> new LinkedHashSet<>()).add(new FieldAndType(field, fieldType, unwrappedParent)); @@ -1551,7 +1520,7 @@ private void sameForCommonParentsByName(Map> fieldMap, private List> groupByCommonParents(Set fields) { // Single-pass: partition into abstract types and concrete groups simultaneously List abstractTypes = null; - Map> concreteGroups = null; + Map> concreteGroups = null; for (FieldAndType fieldAndType : fields) { if (isInterfaceOrUnion(fieldAndType.parentType)) { @@ -1559,7 +1528,7 @@ private List> groupByCommonParents(Set fields) { abstractTypes = new ArrayList<>(); } abstractTypes.add(fieldAndType); - } else if (fieldAndType.parentType instanceof GraphQLObjectType) { + } else if (fieldAndType.parentType instanceof SchemaObject) { if (concreteGroups == null) { concreteGroups = new LinkedHashMap<>(); } @@ -1585,8 +1554,8 @@ private List> groupByCommonParents(Set fields) { return result; } - private boolean isInterfaceOrUnion(@Nullable GraphQLType type) { - return type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType; + private boolean isInterfaceOrUnion(@Nullable SchemaType type) { + return type instanceof SchemaInterface || type instanceof SchemaUnion; } private @Nullable Conflict requireSameNameAndArguments(ImmutableList path, Set fieldAndTypes) { @@ -1650,15 +1619,15 @@ private boolean sameArguments(List arguments1, @Nullable List fields = new ArrayList<>(); - GraphQLType typeAOriginal = null; + SchemaType typeAOriginal = null; for (FieldAndType fieldAndType : fieldAndTypes) { fields.add(fieldAndType.field); if (typeAOriginal == null) { typeAOriginal = fieldAndType.graphQLType; continue; } - GraphQLType typeA = typeAOriginal; - GraphQLType typeB = fieldAndType.graphQLType; + SchemaType typeA = typeAOriginal; + SchemaType typeB = fieldAndType.graphQLType; if (typeA == null || typeB == null) { continue; } @@ -1695,14 +1664,14 @@ private boolean sameArguments(List arguments1, @Nullable List path, List fields, @Nullable GraphQLType typeA, @Nullable GraphQLType typeB) { + private Conflict mkNotSameTypeError(ImmutableList path, List fields, @Nullable SchemaType typeA, @Nullable SchemaType typeB) { String name1 = typeA != null ? simplePrint(typeA) : "null"; String name2 = typeB != null ? simplePrint(typeB) : "null"; String reason = i18n(FieldsConflict, "OverlappingFieldsCanBeMerged.differentReturnTypes", pathToString(path), name1, name2); return new Conflict(reason, fields); } - private boolean notSameType(@Nullable GraphQLType type1, @Nullable GraphQLType type2) { + private boolean notSameType(@Nullable SchemaType type1, @Nullable SchemaType type2) { if (type1 == null || type2 == null) { return false; } @@ -1711,10 +1680,10 @@ private boolean notSameType(@Nullable GraphQLType type1, @Nullable GraphQLType t private static class FieldAndType { final Field field; - final @Nullable GraphQLType graphQLType; - final @Nullable GraphQLType parentType; + final @Nullable SchemaType graphQLType; + final @Nullable SchemaType parentType; - public FieldAndType(Field field, @Nullable GraphQLType graphQLType, @Nullable GraphQLType parentType) { + public FieldAndType(Field field, @Nullable SchemaType graphQLType, @Nullable SchemaType parentType) { this.field = field; this.graphQLType = graphQLType; this.parentType = parentType; @@ -1759,8 +1728,8 @@ public Conflict(String reason, List fields) { // --- PossibleFragmentSpreads --- private void validatePossibleFragmentSpreads_inline(InlineFragment inlineFragment) { - GraphQLOutputType fragType = getInlineFragmentType(inlineFragment); - GraphQLCompositeType parentType = getParentType(); + SchemaOutputType fragType = getInlineFragmentType(inlineFragment); + SchemaComposite parentType = getParentType(); if (fragType == null || parentType == null) { return; } @@ -1776,8 +1745,9 @@ private void validatePossibleFragmentSpreads_spread(FragmentSpread fragmentSprea if (fragment == null) { return; } - GraphQLType typeCondition = TypeFromAST.getTypeFromAST(validationContext.getSchema(), fragment.getTypeCondition()); - GraphQLCompositeType parentType = getParentType(); + SchemaType typeCondition = validationContext.getSchema() + .getType(fragment.getTypeCondition().getName()); + SchemaComposite parentType = getParentType(); if (typeCondition == null || parentType == null) { return; } @@ -1788,52 +1758,41 @@ private void validatePossibleFragmentSpreads_spread(FragmentSpread fragmentSprea } } - private boolean typesDoNotOverlap(GraphQLType type, GraphQLCompositeType parent) { - if (type == parent) { + private boolean typesDoNotOverlap(SchemaType type, SchemaComposite parent) { + if (type.equals(parent)) { return false; } - List possibleParentTypes = getPossibleType(parent); - List possibleConditionTypes = getPossibleType(type); + SchemaComposite compositeType = (SchemaComposite) type; + List possibleParentTypes = + validationContext.getSchema().getPossibleTypes(parent); + List possibleConditionTypes = + validationContext.getSchema().getPossibleTypes(compositeType); return Collections.disjoint(possibleParentTypes, possibleConditionTypes); } - private List getPossibleType(GraphQLType type) { - if (type instanceof GraphQLObjectType) { - return Collections.singletonList(type); - } else if (type instanceof GraphQLInterfaceType) { - List implementations = validationContext.getSchema().getImplementations((GraphQLInterfaceType) type); - return implementations != null ? implementations : Collections.emptyList(); - } else if (type instanceof GraphQLUnionType) { - return ((GraphQLUnionType) type).getTypes(); - } else { - Assert.assertShouldNeverHappen(); - } - return Collections.emptyList(); - } - - private boolean isValidTargetCompositeType(GraphQLType type) { - return type instanceof GraphQLCompositeType; + private boolean isValidTargetCompositeType(SchemaType type) { + return type instanceof SchemaComposite; } - private @Nullable GraphQLOutputType getInlineFragmentType( + private @Nullable SchemaOutputType getInlineFragmentType( InlineFragment inlineFragment) { TypeName typeCondition = inlineFragment.getTypeCondition(); if (typeCondition == null) { return getParentType(); } - GraphQLType type = validationContext.getSchema().getType(typeCondition.getName()); - return type instanceof GraphQLOutputType ? (GraphQLOutputType) type : null; + SchemaType type = validationContext.getSchema().getType(typeCondition.getName()); + return type instanceof SchemaOutputType ? (SchemaOutputType) type : null; } // --- ProvidedNonNullArguments --- private void validateProvidedNonNullArguments_field(Field field) { - GraphQLFieldDefinition fieldDef = getFieldDefinition(); + SchemaField fieldDef = getFieldDefinition(); if (fieldDef == null) { return; } List providedArguments = field.getArguments(); - for (GraphQLArgument graphQLArgument : fieldDef.getArguments()) { + for (SchemaArgument graphQLArgument : fieldDef.getArguments()) { Argument argument = findArgumentByName(providedArguments, graphQLArgument.getName()); boolean nonNullType = isNonNull(graphQLArgument.getType()); boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); @@ -1852,13 +1811,13 @@ private void validateProvidedNonNullArguments_field(Field field) { } private void validateProvidedNonNullArguments_directive(Directive directive) { - GraphQLDirective graphQLDirective = getDirectiveDefinition(); + SchemaDirective graphQLDirective = getDirectiveDefinition(); if (graphQLDirective == null) { return; } List providedArguments = directive.getArguments(); - for (GraphQLArgument graphQLArgument : graphQLDirective.getArguments()) { + for (SchemaArgument graphQLArgument : graphQLDirective.getArguments()) { Argument argument = findArgumentByName(providedArguments, graphQLArgument.getName()); boolean nonNullType = isNonNull(graphQLArgument.getType()); boolean noDefaultValue = graphQLArgument.getArgumentDefaultValue().isNotSet(); @@ -1880,11 +1839,11 @@ private void validateProvidedNonNullArguments_directive(Directive directive) { // --- ScalarLeaves --- private void validateScalarLeaves(Field field) { - GraphQLFieldDefinition fieldDefinition = getFieldDefinition(); + SchemaField fieldDefinition = getFieldDefinition(); if (fieldDefinition == null) { return; } - GraphQLOutputType type = fieldDefinition.getType(); + SchemaOutputType type = fieldDefinition.getType(); if (isLeaf(type)) { if (field.getSelectionSet() != null) { String message = i18n(SubselectionNotAllowed, "ScalarLeaves.subselectionOnLeaf", simplePrint(type), field.getName()); @@ -1900,14 +1859,22 @@ private void validateScalarLeaves(Field field) { // --- VariableDefaultValuesOfCorrectType --- private void validateVariableDefaultValuesOfCorrectType(VariableDefinition variableDefinition) { - GraphQLInputType inputType = getInputType(); - if (inputType == null) { + TypeName typeName = + validationUtil.getUnmodifiedType(variableDefinition.getType()); + SchemaType inputType = + validationContext.getSchema().getType(typeName.getName()); + if (!(inputType instanceof SchemaInputType)) { return; } if (variableDefinition.getDefaultValue() != null - && !validationUtil.isValidLiteralValue(variableDefinition.getDefaultValue(), inputType, + && !validationUtil.isValidLiteralValue( + variableDefinition.getDefaultValue(), + variableDefinition.getType(), validationContext.getSchema(), validationContext.getGraphQLContext(), validationContext.getI18n().getLocale())) { - String message = i18n(BadValueForDefaultArg, "VariableDefaultValuesOfCorrectType.badDefault", variableDefinition.getDefaultValue(), simplePrint(inputType)); + String typeString = variablesTypesMatcher.effectiveTypeName( + variableDefinition.getType(), + null); + String message = i18n(BadValueForDefaultArg, "VariableDefaultValuesOfCorrectType.badDefault", variableDefinition.getDefaultValue(), typeString); addError(BadValueForDefaultArg, variableDefinition.getSourceLocation(), message); } } @@ -1915,7 +1882,7 @@ private void validateVariableDefaultValuesOfCorrectType(VariableDefinition varia // --- VariablesAreInputTypes --- private void validateVariablesAreInputTypes(VariableDefinition variableDefinition) { TypeName unmodifiedAstType = validationUtil.getUnmodifiedType(variableDefinition.getType()); - GraphQLType type = validationContext.getSchema().getType(unmodifiedAstType.getName()); + SchemaType type = validationContext.getSchema().getType(unmodifiedAstType.getName()); if (type == null) { return; } @@ -1934,29 +1901,34 @@ private void validateVariableTypesMatch(VariableReference variableReference) { if (variableDefinition == null) { return; } - GraphQLType variableType = TypeFromAST.getTypeFromAST(validationContext.getSchema(), variableDefinition.getType()); - if (variableType == null) { + TypeName variableTypeName = + validationUtil.getUnmodifiedType(variableDefinition.getType()); + if (validationContext.getSchema().getType( + variableTypeName.getName()) == null) { return; } - GraphQLInputType locationType = getInputType(); + SchemaInputType locationType = getInputType(); if (locationType == null) { return; } InputValueWithState locationDefault = getDefaultValue(); - Value locationDefaultValue = null; - if (locationDefault != null && locationDefault.isLiteral()) { - locationDefaultValue = (Value) locationDefault.getValue(); - } else if (locationDefault != null && locationDefault.isSet()) { - locationDefaultValue = ValuesResolver.valueToLiteral(locationDefault, locationType, - validationContext.getGraphQLContext(), validationContext.getI18n().getLocale()); - } - boolean variableDefMatches = variablesTypesMatcher.doesVariableTypesMatch(variableType, variableDefinition.getDefaultValue(), locationType, locationDefaultValue); + boolean hasLocationDefault = + locationDefault != null && locationDefault.isSet(); + boolean variableDefMatches = + variablesTypesMatcher.doesVariableTypesMatch( + variableDefinition.getType(), + variableDefinition.getDefaultValue(), + locationType, + hasLocationDefault); if (!variableDefMatches) { - GraphQLType effectiveType = variablesTypesMatcher.effectiveType(variableType, variableDefinition.getDefaultValue()); + String effectiveType = + variablesTypesMatcher.effectiveTypeName( + variableDefinition.getType(), + variableDefinition.getDefaultValue()); String message = i18n(VariableTypeMismatch, "VariableTypesMatchRule.unexpectedType", variableDefinition.getName(), - GraphQLTypeUtil.simplePrint(effectiveType), - GraphQLTypeUtil.simplePrint(locationType)); + effectiveType, + simplePrint(locationType)); addError(VariableTypeMismatch, variableReference.getSourceLocation(), message); } } @@ -2012,8 +1984,9 @@ private void validateUniqueDirectiveNamesPerLocation(Node directivesContainer Set directiveNames = new LinkedHashSet<>(); for (Directive directive : directives) { String name = directive.getName(); - GraphQLDirective graphQLDirective = validationContext.getSchema().getDirective(name); - boolean nonRepeatable = graphQLDirective != null && graphQLDirective.isNonRepeatable(); + SchemaDirective graphQLDirective = validationContext.getSchema().getDirective(name); + boolean nonRepeatable = + graphQLDirective != null && !graphQLDirective.isRepeatable(); if (directiveNames.contains(name) && nonRepeatable) { String message = i18n(DuplicateDirectiveName, "UniqueDirectiveNamesPerLocation.uniqueDirectives", name, directivesContainer.getClass().getSimpleName()); addError(DuplicateDirectiveName, directive.getSourceLocation(), message); @@ -2067,7 +2040,7 @@ private void validateUniqueVariableNames(OperationDefinition operationDefinition // --- SubscriptionUniqueRootField --- private void validateSubscriptionUniqueRootField(OperationDefinition operationDef) { if (operationDef.getOperation() == SUBSCRIPTION) { - GraphQLObjectType subscriptionType = validationContext.getSchema().getSubscriptionType(); + SchemaObject subscriptionType = validationContext.getSchema().getSubscriptionType(); FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() .schema(validationContext.getSchema()) .fragments(NodeUtil.getFragmentsByName(validationContext.getDocument())) @@ -2115,9 +2088,9 @@ private void validateDeferDirectiveOnRootLevel(Directive directive) { if (!Directives.DeferDirective.getName().equals(directive.getName())) { return; } - GraphQLObjectType mutationType = validationContext.getSchema().getMutationType(); - GraphQLObjectType subscriptionType = validationContext.getSchema().getSubscriptionType(); - GraphQLCompositeType parentType = getParentType(); + SchemaObject mutationType = validationContext.getSchema().getMutationType(); + SchemaObject subscriptionType = validationContext.getSchema().getSubscriptionType(); + SchemaComposite parentType = getParentType(); if (mutationType != null && parentType != null && parentType.getName().equals(mutationType.getName())) { String message = i18n(MisplacedDirective, "DeferDirective.notAllowedOperationRootLevelMutation", parentType.getName()); addError(MisplacedDirective, directive.getSourceLocation(), message); diff --git a/src/main/java/graphql/validation/ValidationContext.java b/src/main/java/graphql/validation/ValidationContext.java index d5d982d816..32d9f8a846 100644 --- a/src/main/java/graphql/validation/ValidationContext.java +++ b/src/main/java/graphql/validation/ValidationContext.java @@ -6,7 +6,7 @@ import graphql.language.Definition; import graphql.language.Document; import graphql.language.FragmentDefinition; -import graphql.schema.GraphQLSchema; +import graphql.schema.ExecutableSchema; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -18,7 +18,7 @@ @NullMarked public class ValidationContext { - private final GraphQLSchema schema; + private final ExecutableSchema schema; private final Document document; private final Map fragmentDefinitionMap = new LinkedHashMap<>(); @@ -26,11 +26,11 @@ public class ValidationContext { private final GraphQLContext graphQLContext; private final QueryComplexityLimits queryComplexityLimits; - public ValidationContext(GraphQLSchema schema, Document document, I18n i18n) { + public ValidationContext(ExecutableSchema schema, Document document, I18n i18n) { this(schema, document, i18n, null); } - public ValidationContext(GraphQLSchema schema, Document document, I18n i18n, @Nullable QueryComplexityLimits limits) { + public ValidationContext(ExecutableSchema schema, Document document, I18n i18n, @Nullable QueryComplexityLimits limits) { this.schema = schema; this.document = document; this.i18n = i18n; @@ -49,7 +49,7 @@ private void buildFragmentMap() { } } - public GraphQLSchema getSchema() { + public ExecutableSchema getSchema() { return schema; } diff --git a/src/main/java/graphql/validation/ValidationUtil.java b/src/main/java/graphql/validation/ValidationUtil.java index 1bff274d92..80d785fbed 100644 --- a/src/main/java/graphql/validation/ValidationUtil.java +++ b/src/main/java/graphql/validation/ValidationUtil.java @@ -3,12 +3,12 @@ import com.google.common.collect.ImmutableSet; import graphql.Assert; -import graphql.Directives; import graphql.GraphQLContext; import graphql.GraphQLError; import graphql.Internal; import graphql.execution.CoercedVariables; import graphql.language.ArrayValue; +import graphql.language.EnumValue; import graphql.language.ListType; import graphql.language.NonNullType; import graphql.language.NullValue; @@ -20,22 +20,24 @@ import graphql.language.VariableReference; import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; -import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLScalarType; -import graphql.schema.GraphQLSchema; -import graphql.schema.GraphQLType; -import graphql.schema.visibility.GraphqlFieldVisibility; +import graphql.schema.ExecutableSchema; +import graphql.schema.SchemaEnum; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaInputType; +import graphql.schema.SchemaList; +import graphql.schema.SchemaScalar; +import graphql.schema.SchemaType; +import org.jspecify.annotations.Nullable; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Optional; import java.util.Set; +import static graphql.scalar.CoercingUtil.i18nMsg; +import static graphql.scalar.CoercingUtil.typeName; import static graphql.schema.GraphQLTypeUtil.isList; import static graphql.schema.GraphQLTypeUtil.isNonNull; import static graphql.schema.GraphQLTypeUtil.unwrapOne; @@ -54,34 +56,34 @@ public TypeName getUnmodifiedType(Type type) { return Assert.assertShouldNeverHappen(); } - protected void handleNullError(Value value, GraphQLType type) { + protected void handleNullError(Value value, SchemaType type) { } - protected void handleScalarError(Value value, GraphQLScalarType type, GraphQLError invalid) { + protected void handleScalarError(Value value, SchemaScalar type, GraphQLError invalid) { } - protected void handleEnumError(Value value, GraphQLEnumType type, GraphQLError invalid) { + protected void handleEnumError(Value value, SchemaEnum type, GraphQLError invalid) { } - protected void handleNotObjectError(Value value, GraphQLInputObjectType type) { + protected void handleNotObjectError(Value value, SchemaInputObject type) { } - protected void handleMissingFieldsError(Value value, GraphQLInputObjectType type, Set missingFields) { + protected void handleMissingFieldsError(Value value, SchemaInputObject type, Set missingFields) { } - protected void handleExtraFieldError(Value value, GraphQLInputObjectType type, ObjectField objectField) { + protected void handleExtraFieldError(Value value, SchemaInputObject type, ObjectField objectField) { } - protected void handleFieldNotValidError(ObjectField objectField, GraphQLInputObjectType type) { + protected void handleFieldNotValidError(ObjectField objectField, SchemaInputObject type) { } - protected void handleFieldNotValidError(Value value, GraphQLType type, int index) { + protected void handleFieldNotValidError(Value value, SchemaType type, int index) { } - protected void handleExtraOneOfFieldsError(GraphQLInputObjectType type, Value value) { + protected void handleExtraOneOfFieldsError(SchemaInputObject type, Value value) { } - public boolean isValidLiteralValue(Value value, GraphQLType type, GraphQLSchema schema, GraphQLContext graphQLContext, Locale locale) { + public boolean isValidLiteralValue(Value value, SchemaType type, ExecutableSchema schema, GraphQLContext graphQLContext, Locale locale) { if (value == null || value instanceof NullValue) { boolean valid = !(isNonNull(type)); if (!valid) { @@ -96,52 +98,147 @@ public boolean isValidLiteralValue(Value value, GraphQLType type, GraphQLSche return isValidLiteralValue(value, unwrapOne(type), schema, graphQLContext, locale); } - if (type instanceof GraphQLScalarType) { - Optional invalid = parseLiteral(value, ((GraphQLScalarType) type).getCoercing(), graphQLContext, locale); - invalid.ifPresent(graphQLError -> handleScalarError(value, (GraphQLScalarType) type, graphQLError)); - return invalid.isEmpty(); + if (type instanceof SchemaScalar) { + SchemaScalar scalar = (SchemaScalar) type; + GraphQLError invalid = parseLiteral( + value, + schema.getScalarCoercing(scalar), + graphQLContext, + locale); + if (invalid != null) { + handleScalarError(value, scalar, invalid); + return false; + } + return true; } - if (type instanceof GraphQLEnumType) { - Optional invalid = parseLiteralEnum(value, (GraphQLEnumType) type, graphQLContext, locale); - invalid.ifPresent(graphQLError -> handleEnumError(value, (GraphQLEnumType) type, graphQLError)); - return invalid.isEmpty(); + if (type instanceof SchemaEnum) { + SchemaEnum enumType = (SchemaEnum) type; + GraphQLError invalid = parseLiteralEnum(value, enumType, locale); + if (invalid != null) { + handleEnumError(value, enumType, invalid); + return false; + } + return true; } if (isList(type)) { - return isValidLiteralValue(value, (GraphQLList) type, schema, graphQLContext, locale); + return isValidLiteralValue(value, (SchemaList) type, schema, graphQLContext, locale); } - return type instanceof GraphQLInputObjectType && isValidLiteralValueForInputObjectType(value, (GraphQLInputObjectType) type, schema, graphQLContext, locale); + return type instanceof SchemaInputObject + && isValidLiteralValueForInputObjectType( + value, + (SchemaInputObject) type, + schema, + graphQLContext, + locale); + } + public boolean isValidLiteralValue( + Value value, + Type type, + ExecutableSchema schema, + GraphQLContext graphQLContext, + Locale locale) { + if (type instanceof NonNullType) { + if (value == null || value instanceof NullValue) { + return false; + } + return isValidLiteralValue( + value, + ((NonNullType) type).getType(), + schema, + graphQLContext, + locale); + } + if (value == null || value instanceof NullValue + || value instanceof VariableReference) { + return true; + } + if (type instanceof ListType) { + Type wrappedType = ((ListType) type).getType(); + if (!(value instanceof ArrayValue)) { + return isValidLiteralValue( + value, + wrappedType, + schema, + graphQLContext, + locale); + } + for (Value item : ((ArrayValue) value).getValues()) { + if (!isValidLiteralValue( + item, + wrappedType, + schema, + graphQLContext, + locale)) { + return false; + } + } + return true; + } + SchemaType schemaType = + schema.getType(((TypeName) type).getName()); + if (!(schemaType instanceof SchemaInputType)) { + return false; + } + return isValidLiteralValue( + value, + schemaType, + schema, + graphQLContext, + locale); } - private Optional parseLiteralEnum(Value value, GraphQLEnumType graphQLEnumType, GraphQLContext graphQLContext, Locale locale) { - try { - graphQLEnumType.parseLiteral(value, graphQLContext, locale); - return Optional.empty(); - } catch (CoercingParseLiteralException e) { - return Optional.of(e); + private @Nullable GraphQLError parseLiteralEnum( + Value value, + SchemaEnum enumType, + Locale locale) { + if (!(value instanceof EnumValue)) { + return new CoercingParseLiteralException( + i18nMsg( + locale, + "Scalar.unexpectedAstType", + "EnumValue", + typeName(value))); + } + if (enumType.getValue(((EnumValue) value).getName()) == null) { + return new CoercingParseLiteralException( + i18nMsg( + locale, + "Enum.unallowableValue", + enumType.getName(), + value)); } + return null; } - private Optional parseLiteral(Value value, Coercing coercing, GraphQLContext graphQLContext, Locale locale) { + private @Nullable GraphQLError parseLiteral( + Value value, + Coercing coercing, + GraphQLContext graphQLContext, + Locale locale) { try { coercing.parseLiteral(value, CoercedVariables.emptyVariables(), graphQLContext, locale); - return Optional.empty(); + return null; } catch (CoercingParseLiteralException e) { - return Optional.of(e); + return e; } } - boolean isValidLiteralValueForInputObjectType(Value value, GraphQLInputObjectType type, GraphQLSchema schema, GraphQLContext graphQLContext, Locale locale) { + boolean isValidLiteralValueForInputObjectType( + Value value, + SchemaInputObject type, + ExecutableSchema schema, + GraphQLContext graphQLContext, + Locale locale) { if (!(value instanceof ObjectValue)) { handleNotObjectError(value, type); return false; } - GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); ObjectValue objectValue = (ObjectValue) value; Map objectFieldMap = fieldMap(objectValue); - Set missingFields = getMissingFields(type, objectFieldMap, fieldVisibility); + Set missingFields = getMissingFields(type, objectFieldMap, schema); if (!missingFields.isEmpty()) { handleMissingFieldsError(value, type, missingFields); return false; @@ -149,7 +246,8 @@ boolean isValidLiteralValueForInputObjectType(Value value, GraphQLInputObject for (ObjectField objectField : objectValue.getObjectFields()) { - GraphQLInputObjectField inputObjectField = fieldVisibility.getFieldDefinition(type, objectField.getName()); + SchemaInputField inputObjectField = + schema.getInputField(type, objectField.getName()); if (inputObjectField == null) { handleExtraFieldError(value, type, objectField); return false; @@ -160,9 +258,9 @@ boolean isValidLiteralValueForInputObjectType(Value value, GraphQLInputObject } } - if (type.hasAppliedDirective(Directives.OneOfDirective.getName())) { + if (type.isOneOf()) { if (objectFieldMap.keySet().size() != 1) { - handleExtraOneOfFieldsError(type,value); + handleExtraOneOfFieldsError(type, value); return false; } } @@ -170,11 +268,14 @@ boolean isValidLiteralValueForInputObjectType(Value value, GraphQLInputObject } - private Set getMissingFields(GraphQLInputObjectType type, Map objectFieldMap, GraphqlFieldVisibility fieldVisibility) { - return fieldVisibility.getFieldDefinitions(type).stream() + private Set getMissingFields( + SchemaInputObject type, + Map objectFieldMap, + ExecutableSchema schema) { + return schema.getInputFields(type).stream() .filter(field -> isNonNull(field.getType())) .filter(value -> (value.getInputFieldDefaultValue().isNotSet()) && !objectFieldMap.containsKey(value.getName())) - .map(GraphQLInputObjectField::getName) + .map(SchemaInputField::getName) .collect(ImmutableSet.toImmutableSet()); } @@ -186,8 +287,13 @@ private Map fieldMap(ObjectValue objectValue) { return result; } - private boolean isValidLiteralValue(Value value, GraphQLList type, GraphQLSchema schema, GraphQLContext graphQLContext, Locale locale) { - GraphQLType wrappedType = type.getWrappedType(); + private boolean isValidLiteralValue( + Value value, + SchemaList type, + ExecutableSchema schema, + GraphQLContext graphQLContext, + Locale locale) { + SchemaType wrappedType = type.getWrappedType(); if (value instanceof ArrayValue) { List values = ((ArrayValue) value).getValues(); for (int i = 0; i < values.size(); i++) { diff --git a/src/main/java/graphql/validation/Validator.java b/src/main/java/graphql/validation/Validator.java index d6237bf131..b83de5c859 100644 --- a/src/main/java/graphql/validation/Validator.java +++ b/src/main/java/graphql/validation/Validator.java @@ -4,7 +4,7 @@ import graphql.Internal; import graphql.i18n.I18n; import graphql.language.Document; -import graphql.schema.GraphQLSchema; +import graphql.schema.ExecutableSchema; import java.util.List; import java.util.Locale; @@ -32,15 +32,15 @@ public static int getMaxValidationErrors() { return MAX_VALIDATION_ERRORS; } - public List validateDocument(GraphQLSchema schema, Document document, Locale locale) { + public List validateDocument(ExecutableSchema schema, Document document, Locale locale) { return validateDocument(schema, document, rule -> true, locale); } - public List validateDocument(GraphQLSchema schema, Document document, Predicate rulePredicate, Locale locale) { + public List validateDocument(ExecutableSchema schema, Document document, Predicate rulePredicate, Locale locale) { return validateDocument(schema, document, rulePredicate, locale, null); } - public List validateDocument(GraphQLSchema schema, Document document, Predicate rulePredicate, Locale locale, QueryComplexityLimits limits) { + public List validateDocument(ExecutableSchema schema, Document document, Predicate rulePredicate, Locale locale, QueryComplexityLimits limits) { I18n i18n = I18n.i18n(I18n.BundleType.Validation, locale); ValidationContext validationContext = new ValidationContext(schema, document, i18n, limits); diff --git a/src/main/java/graphql/validation/VariablesTypesMatcher.java b/src/main/java/graphql/validation/VariablesTypesMatcher.java index 854f553f9d..ea9f63ade9 100644 --- a/src/main/java/graphql/validation/VariablesTypesMatcher.java +++ b/src/main/java/graphql/validation/VariablesTypesMatcher.java @@ -2,15 +2,17 @@ import graphql.Internal; +import graphql.language.ListType; +import graphql.language.NonNullType; import graphql.language.NullValue; +import graphql.language.Type; +import graphql.language.TypeName; import graphql.language.Value; -import graphql.schema.GraphQLType; - -import static graphql.schema.GraphQLNonNull.nonNull; -import static graphql.schema.GraphQLTypeUtil.isList; -import static graphql.schema.GraphQLTypeUtil.isNonNull; -import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; -import static graphql.schema.GraphQLTypeUtil.unwrapOne; +import graphql.schema.SchemaInputType; +import graphql.schema.SchemaList; +import graphql.schema.SchemaNamedType; +import graphql.schema.SchemaNonNull; +import graphql.schema.SchemaType; @Internal public class VariablesTypesMatcher { @@ -18,57 +20,91 @@ public class VariablesTypesMatcher { /** * This method and variable naming was inspired from the reference graphql-js implementation * - * @param varType the variable type - * @param varDefaultValue the default value for the variable - * @param locationType the location type where the variable was encountered - * @param locationDefaultValue the default value for that location + * @param varType the variable type + * @param varDefaultValue the default value for the variable + * @param locationType the location type where the variable was encountered + * @param hasLocationDefaultValue whether that location has a default value * * @return true if the variable matches ok */ - public boolean doesVariableTypesMatch(GraphQLType varType, Value varDefaultValue, GraphQLType locationType, Value locationDefaultValue) { - if (isNonNull(locationType) && !isNonNull(varType)) { + public boolean doesVariableTypesMatch( + Type varType, + Value varDefaultValue, + SchemaInputType locationType, + boolean hasLocationDefaultValue) { + if (locationType instanceof SchemaNonNull + && !(varType instanceof NonNullType)) { boolean hasNonNullVariableDefaultValue = varDefaultValue != null && !(varDefaultValue instanceof NullValue); - boolean hasLocationDefaultValue = locationDefaultValue != null; if (!hasNonNullVariableDefaultValue && !hasLocationDefaultValue) { return false; } - GraphQLType nullableLocationType = unwrapNonNull(locationType); + SchemaType nullableLocationType = + ((SchemaNonNull) locationType).getWrappedType(); return checkType(varType, nullableLocationType); } return checkType(varType, locationType); } - public GraphQLType effectiveType(GraphQLType variableType, Value defaultValue) { + public String effectiveTypeName( + Type variableType, + Value defaultValue) { if (defaultValue == null || defaultValue instanceof NullValue) { - return variableType; + return simplePrint(variableType); } - if (isNonNull(variableType)) { - return variableType; + if (variableType instanceof NonNullType) { + return simplePrint(variableType); } - return nonNull(variableType); + return simplePrint(variableType) + "!"; } @SuppressWarnings("SimplifiableIfStatement") - private boolean checkType(GraphQLType actualType, GraphQLType expectedType) { + private boolean checkType( + Type actualType, + SchemaType expectedType) { - if (isNonNull(expectedType)) { - if (isNonNull(actualType)) { - return checkType(unwrapOne(actualType), unwrapOne(expectedType)); + if (expectedType instanceof SchemaNonNull) { + if (actualType instanceof NonNullType) { + return checkType( + ((NonNullType) actualType).getType(), + ((SchemaNonNull) expectedType).getWrappedType()); } return false; } - if (isNonNull(actualType)) { - return checkType(unwrapOne(actualType), expectedType); + if (actualType instanceof NonNullType) { + return checkType( + ((NonNullType) actualType).getType(), + expectedType); } + if (actualType instanceof ListType + && expectedType instanceof SchemaList) { + return checkType( + ((ListType) actualType).getType(), + ((SchemaList) expectedType).getWrappedType()); + } + if (actualType instanceof ListType + || expectedType instanceof SchemaList) { + return false; + } + if (!(actualType instanceof TypeName) + || !(expectedType instanceof SchemaNamedType)) { + return false; + } + return ((TypeName) actualType).getName().equals( + ((SchemaNamedType) expectedType).getName()); + } - if (isList(actualType) && isList(expectedType)) { - return checkType(unwrapOne(actualType), unwrapOne(expectedType)); + private String simplePrint(Type type) { + if (type instanceof NonNullType) { + return simplePrint(((NonNullType) type).getType()) + "!"; + } + if (type instanceof ListType) { + return "[" + simplePrint(((ListType) type).getType()) + "]"; } - return actualType == expectedType; + return ((TypeName) type).getName(); } } diff --git a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy index 3b75b05b0b..64a31088c2 100644 --- a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy +++ b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy @@ -1,5 +1,6 @@ package graphql.execution +import graphql.AssertException import graphql.ExecutionInput import graphql.GraphQLContext import graphql.TestUtil @@ -13,6 +14,8 @@ import graphql.language.Field import graphql.language.NodeUtil import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment +import graphql.schema.universe.SchemaUniverse +import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification class ConditionalNodesTest extends Specification { @@ -102,6 +105,32 @@ class ConditionalNodesTest extends Specification { called == true } + def "custom conditional decisions remain GraphQLSchema specific"() { + given: + def source = TestUtil.schema("type Query { f: String }") + def imported = new SchemaUniverse() + .importSchema("conditional_nodes", source) + def schema = SUExecutableSchema.fromGraphQLSchema( + imported, + source) + def context = GraphQLContext.getDefault() + context.put( + ConditionalNodeDecision.class, + { true } as ConditionalNodeDecision) + + when: + new ConditionalNodes().shouldInclude( + mkField([]), + [:], + schema, + context) + + then: + def exception = thrown(AssertException) + exception.message == + "Custom conditional node decisions require a GraphQLSchema" + } + def "integration test showing conditional nodes can be custom included"() { def sdl = """ diff --git a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy index 6460512543..6b25679a98 100644 --- a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy +++ b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy @@ -75,6 +75,32 @@ class FieldCollectorTest extends Specification { result.getSubField('fieldOnInterface').getFields() == [interfaceField] } + def "ignores inline fragments with non composite type conditions"() { + given: + def schema = TestUtil.schema(""" + type Query { + value: String + } + """) + def object = schema.getType("Query") as GraphQLObjectType + def parameters = newParameters() + .schema(schema) + .objectType(object) + .build() + Document document = new Parser().parseDocument( + "{ ... on String { value } }") + def selectionSet = ((OperationDefinition) document.children[0]) + .selectionSet + + when: + def result = new FieldCollector().collectFields( + parameters, + selectionSet) + + then: + result.empty + } + def "collect fields that are merged together - one of the fields is on an inline fragment "() { def schema = TestUtil.schema(""" type Query { diff --git a/src/test/groovy/graphql/schema/GraphQLTypeUtilTest.groovy b/src/test/groovy/graphql/schema/GraphQLTypeUtilTest.groovy index bc8a08aff1..34e267a18b 100644 --- a/src/test/groovy/graphql/schema/GraphQLTypeUtilTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLTypeUtilTest.groovy @@ -1,5 +1,8 @@ package graphql.schema +import graphql.TestUtil +import graphql.schema.universe.SchemaUniverse +import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification import static graphql.Scalars.GraphQLString @@ -233,4 +236,53 @@ class GraphQLTypeUtilTest extends Specification { (type as GraphQLNamedType).getName() == "String" } + + def "schema view types can be inspected without GraphQLType"() { + given: + def source = TestUtil.schema(''' + enum Choice { + FIRST + } + + input Filter { + choice: Choice + } + + type Query { + values: [String!]! + choice: Choice + filter(value: Filter): String + } + ''') + def imported = new SchemaUniverse() + .importSchema("type_util", source) + def schema = SUExecutableSchema.fromGraphQLSchema( + imported, + source) + SchemaType valuesType = schema.getField( + schema.queryType, + "values").type + SchemaType scalarType = schema.getType("String") + SchemaType enumSchemaType = schema.getType("Choice") + SchemaType inputSchemaType = schema.getType("Filter") + + expect: + GraphQLTypeUtil.simplePrint(valuesType) == "[String!]!" + GraphQLTypeUtil.isNonNull(valuesType) + !GraphQLTypeUtil.isNullable(valuesType) + GraphQLTypeUtil.isWrapped(valuesType) + !GraphQLTypeUtil.isNotWrapped(valuesType) + !GraphQLTypeUtil.isList(valuesType) + GraphQLTypeUtil.isList(GraphQLTypeUtil.unwrapOne(valuesType)) + GraphQLTypeUtil.unwrapAll(valuesType) == scalarType + GraphQLTypeUtil.unwrapOne(scalarType) == scalarType + + and: + GraphQLTypeUtil.isScalar(scalarType) + !GraphQLTypeUtil.isEnum(scalarType) + GraphQLTypeUtil.isEnum(enumSchemaType) + GraphQLTypeUtil.isLeaf(enumSchemaType) + GraphQLTypeUtil.isInput(inputSchemaType) + !GraphQLTypeUtil.isInput(schema.queryType) + } } diff --git a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValidationTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValidationTest.groovy new file mode 100644 index 0000000000..a9ed57fd93 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValidationTest.groovy @@ -0,0 +1,232 @@ +package graphql.schema.universe + +import graphql.ExecutionInput +import graphql.ParseAndValidate +import graphql.Scalars +import graphql.TestUtil +import graphql.language.Document +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import graphql.schema.universe.view.SUExecutableSchema +import graphql.validation.OperationValidationRule +import graphql.validation.QueryComplexityLimits +import graphql.validation.ValidationError +import graphql.validation.ValidationErrorType +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +class SUExecutableSchemaValidationTest extends Specification { + + @Shared + GraphQLSchema graphQLSchema + + @Shared + SUExecutableSchema universeSchema + + def setupSpec() { + graphQLSchema = TestUtil.schema(schemaSdl()) + def imported = new SchemaUniverse() + .importSchema("validation", graphQLSchema) + universeSchema = SUExecutableSchema + .fromGraphQLSchema(imported, graphQLSchema) + } + + @Unroll + def "GraphQLSchema and universe validation agree for #scenario"() { + given: + Document document = TestUtil.parseQuery(query) + + when: + List graphQLErrors = ParseAndValidate.validate( + graphQLSchema, + document, + Locale.ENGLISH) + List universeErrors = ParseAndValidate.validate( + universeSchema, + document, + Locale.ENGLISH) + + then: + errorSnapshot(universeErrors) == errorSnapshot(graphQLErrors) + universeErrors*.validationErrorType == expectedErrors + + where: + scenario | query || expectedErrors + "valid composite selections"| 'query($id: ID!, $filter: Filter) { node(id: $id, filter: $filter) { id ... on User { name } } search { ... on Photo { url } } }' || [] + "scalar and enum literals" | '{ count(value: "wrong") choose(mode: UNKNOWN) }' || [ValidationErrorType.WrongType, ValidationErrorType.WrongType] + "required input fields" | '{ node(id: "1", filter: { mode: FAST }) { id } }' || [ValidationErrorType.WrongType] + "extra input fields" | '{ node(id: "1", filter: { required: true, extra: true }) { id } }' || [ValidationErrorType.WrongType] + "oneOf input objects" | '{ chooseOne(input: { name: "Ada", id: "1" }) }' || [ValidationErrorType.WrongType] + "field and argument names" | '{ missing node(id: "1", unknown: true) { missing } }' || [ValidationErrorType.FieldUndefined, ValidationErrorType.UnknownArgument, ValidationErrorType.FieldUndefined] + "incompatible fragments" | '{ node(id: "1") { ... on Query { search { __typename } } } }' || [ValidationErrorType.InvalidFragmentType] + "overlapping fields" | '{ same: count(value: 1) same: choose(mode: FAST) }' || [ValidationErrorType.FieldsConflict] + "overlapping enum fields" | '{ same: currentMode same: currentState }' || [ValidationErrorType.FieldsConflict] + "enum subselections" | '{ currentMode { __typename } }' || [ValidationErrorType.SubselectionNotAllowed] + "variable type matching" | 'query($id: String) { node(id: $id) { id } }' || [ValidationErrorType.VariableTypeMismatch] + "bad variable defaults" | 'query($value: Int = "wrong") { count(value: $value) }' || [ValidationErrorType.BadValueForDefaultArg] + "list variable defaults" | 'query($values: [Int!] = [1, "wrong"]) { counts(values: $values) }' || [ValidationErrorType.BadValueForDefaultArg] + "variable input types" | 'query($value: Query) { __typename }' || [ValidationErrorType.NonInputTypeOnVariable, ValidationErrorType.UnusedVariable] + "argument defaults" | 'query($value: String) { withDefault(value: $value) }' || [] + "directives" | '{ count(value: 1) @mark @unknown }' || [ValidationErrorType.MissingDirectiveArgument, ValidationErrorType.UnknownDirective] + "subscription roots" | 'subscription { first { id } second { id } }' || [ValidationErrorType.SubscriptionMultipleRootFields] + "subscription fragments" | 'subscription { ... on Subscription { first { id } second { id } } }' || [ValidationErrorType.SubscriptionMultipleRootFields] + "fields on unions" | '{ search { id } }' || [ValidationErrorType.FieldUndefined] + "introspection fields" | '{ __schema { queryType { name } } __type(name: "User") { name } }' || [] + } + + def "all ExecutableSchema parse and validation entry points accept a universe view"() { + given: + Document validDocument = TestUtil.parseQuery( + '{ count(value: 1) }') + Document invalidDocument = TestUtil.parseQuery( + '{ missing }') + def skipUnknownFields = { + it != OperationValidationRule.FIELDS_ON_CORRECT_TYPE + } + + expect: + ParseAndValidate.validate( + universeSchema, + validDocument).isEmpty() + ParseAndValidate.validate( + universeSchema, + invalidDocument, + skipUnknownFields).isEmpty() + ParseAndValidate.validate( + universeSchema, + invalidDocument, + skipUnknownFields, + Locale.ENGLISH).isEmpty() + ParseAndValidate.validate( + universeSchema, + invalidDocument, + { true }, + Locale.ENGLISH, + QueryComplexityLimits.NONE)*.validationErrorType == + [ValidationErrorType.FieldUndefined] + + and: + !ParseAndValidate.parseAndValidate( + universeSchema, + ExecutionInput.newExecutionInput( + '{ count(value: 1) }').build()).isFailure() + ParseAndValidate.parseAndValidate( + universeSchema, + ExecutionInput.newExecutionInput( + '{ count(').build()).isFailure() + } + + def "custom scalar coercing is used through the universe view"() { + given: + GraphQLScalarType customScalar = GraphQLScalarType.newScalar() + .name("Custom") + .coercing(Scalars.GraphQLInt.coercing) + .build() + GraphQLObjectType query = GraphQLObjectType.newObject() + .name("Query") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("value") + .type(Scalars.GraphQLString) + .argument(GraphQLArgument.newArgument() + .name("input") + .type(customScalar))) + .build() + GraphQLSchema source = GraphQLSchema.newSchema() + .query(query) + .build() + def imported = new SchemaUniverse() + .importSchema("custom_scalar_validation", source) + def universe = SUExecutableSchema.fromGraphQLSchema( + imported, + source) + Document document = TestUtil.parseQuery( + '{ value(input: "not an int") }') + + expect: + errorSnapshot(ParseAndValidate.validate( + universe, + document, + Locale.ENGLISH)) == + errorSnapshot(ParseAndValidate.validate( + source, + document, + Locale.ENGLISH)) + } + + private static List> errorSnapshot( + List errors) { + return errors.collect { + [ + type : it.validationErrorType, + message : it.message, + queryPath : it.queryPath, + locations : it.locations, + extensions: it.extensions + ] + } + } + + private static String schemaSdl() { + return ''' + directive @mark(required: Boolean!, mode: Mode) + repeatable on FIELD | INLINE_FRAGMENT + + interface Node { + id: ID! + } + + type User implements Node { + id: ID! + name: String + } + + type Photo implements Node { + id: ID! + url: String + } + + union SearchResult = User | Photo + + enum Mode { + FAST + SLOW + } + + enum State { + ON + OFF + } + + input Filter { + required: Boolean! + mode: Mode + } + + input Choice @oneOf { + name: String + id: ID + } + + type Query { + node(id: ID!, filter: Filter): Node + search: [SearchResult!]! + choose(mode: Mode!): String + chooseOne(input: Choice): String + count(value: Int): Int + counts(values: [Int!]): Int + currentMode: Mode + currentState: State + withDefault(value: String! = "fallback"): String + } + + type Subscription { + first: User + second: Photo + } + ''' + } +} diff --git a/src/test/groovy/graphql/validation/ValidationUtilTest.groovy b/src/test/groovy/graphql/validation/ValidationUtilTest.groovy index 251362b09a..bfc7d2a492 100644 --- a/src/test/groovy/graphql/validation/ValidationUtilTest.groovy +++ b/src/test/groovy/graphql/validation/ValidationUtilTest.groovy @@ -88,6 +88,79 @@ class ValidationUtilTest extends Specification { validationUtil.isValidLiteralValue(new VariableReference("var"), GraphQLBoolean, schema, graphQLContext, locale) } + def "AST variable types preserve wrapper semantics"() { + given: + def stringType = new TypeName("String") + def nonNullStringType = new NonNullType(stringType) + def stringListType = new ListType(nonNullStringType) + + expect: + !validationUtil.isValidLiteralValue( + NullValue.newNullValue().build(), + nonNullStringType, + schema, + graphQLContext, + locale) + validationUtil.isValidLiteralValue( + new StringValue("value"), + nonNullStringType, + schema, + graphQLContext, + locale) + validationUtil.isValidLiteralValue( + null, + stringType, + schema, + graphQLContext, + locale) + validationUtil.isValidLiteralValue( + new VariableReference("value"), + stringType, + schema, + graphQLContext, + locale) + + and: + validationUtil.isValidLiteralValue( + new StringValue("value"), + stringListType, + schema, + graphQLContext, + locale) + validationUtil.isValidLiteralValue( + new ArrayValue([ + new StringValue("first"), + new StringValue("second") + ]), + stringListType, + schema, + graphQLContext, + locale) + !validationUtil.isValidLiteralValue( + new ArrayValue([ + new StringValue("first"), + NullValue.newNullValue().build() + ]), + stringListType, + schema, + graphQLContext, + locale) + + and: + !validationUtil.isValidLiteralValue( + new StringValue("value"), + new TypeName("Missing"), + schema, + graphQLContext, + locale) + !validationUtil.isValidLiteralValue( + new StringValue("value"), + new TypeName("QueryType"), + schema, + graphQLContext, + locale) + } + def "ArrayValue and ListType is invalid when one entry is invalid"() { given: def arrayValue = new ArrayValue([new BooleanValue(true)]) diff --git a/src/test/groovy/graphql/validation/VariablesTypesMatcherTest.groovy b/src/test/groovy/graphql/validation/VariablesTypesMatcherTest.groovy index 97eebfd612..bc097be074 100644 --- a/src/test/groovy/graphql/validation/VariablesTypesMatcherTest.groovy +++ b/src/test/groovy/graphql/validation/VariablesTypesMatcherTest.groovy @@ -1,7 +1,11 @@ package graphql.validation import graphql.language.BooleanValue +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.NullValue import graphql.language.StringValue +import graphql.language.TypeName import spock.lang.Specification import spock.lang.Unroll @@ -19,27 +23,41 @@ class VariablesTypesMatcherTest extends Specification { def "#variableType with default value #defaultValue and expected #expectedType should result: #result "() { expect: - typesMatcher.doesVariableTypesMatch(variableType, defaultValue, expectedType, null) == result + typesMatcher.doesVariableTypesMatch(variableType, defaultValue, expectedType, false) == result where: variableType | defaultValue | expectedType || result - GraphQLString | null | GraphQLString || true - list(GraphQLString) | null | list(GraphQLString) || true - nonNull(GraphQLBoolean) | new BooleanValue(true) | GraphQLBoolean || true - nonNull(GraphQLString) | null | list(GraphQLString) || false + new TypeName("String") | null | GraphQLString || true + new ListType(new TypeName("String")) | null | list(GraphQLString) || true + new NonNullType(new TypeName("Boolean")) | new BooleanValue(true) | GraphQLBoolean || true + new NonNullType(new TypeName("String")) | null | list(GraphQLString) || false } @Unroll - def "issue 3276 - #variableType with default value #defaultValue and expected #expectedType with #locationDefaultValue should result: #result "() { + def "issue 3276 - #variableType with default value #defaultValue and expected #expectedType with location default #hasLocationDefault should result: #result "() { expect: - typesMatcher.doesVariableTypesMatch(variableType, defaultValue, expectedType, locationDefaultValue) == result + typesMatcher.doesVariableTypesMatch(variableType, defaultValue, expectedType, hasLocationDefault) == result where: - variableType | defaultValue | expectedType | locationDefaultValue || result - GraphQLString | null | nonNull(GraphQLString) | null || false - GraphQLString | null | nonNull(GraphQLString) | StringValue.of("x") || true - GraphQLString | StringValue.of("x") | nonNull(GraphQLString) | StringValue.of("x") || true - GraphQLString | StringValue.of("x") | nonNull(GraphQLString) | null || true + variableType | defaultValue | expectedType | hasLocationDefault || result + new TypeName("String") | null | nonNull(GraphQLString) | false || false + new TypeName("String") | null | nonNull(GraphQLString) | true || true + new TypeName("String") | StringValue.of("x") | nonNull(GraphQLString) | true || true + new TypeName("String") | StringValue.of("x") | nonNull(GraphQLString) | false || true + } + + @Unroll + def "effective variable type for #variableType and default #defaultValue is #effectiveType"() { + expect: + typesMatcher.effectiveTypeName(variableType, defaultValue) == effectiveType + + where: + variableType | defaultValue || effectiveType + new TypeName("String") | null || "String" + new TypeName("String") | NullValue.newNullValue().build() || "String" + new TypeName("String") | StringValue.of("x") || "String!" + new NonNullType(new TypeName("String")) | StringValue.of("x") || "String!" + new ListType(new TypeName("String")) | null || "[String]" } } From 014821d2a6c21f6156f507e5fb936e6a28d6f848 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Thu, 6 Aug 2026 21:59:00 +0000 Subject: [PATCH 21/32] Support input value resolution with executable schema views --- .../execution/ExecutableInputValueSchema.java | 43 ++ .../execution/GraphQLInputValueSchema.java | 61 ++ ...InputMapDefinesTooManyFieldsException.java | 10 + .../graphql/execution/InputValueSchema.java | 23 + ...onNullableValueCoercedAsNullException.java | 38 ++ .../execution/ResolvedSchemaListType.java | 24 + .../execution/ResolvedSchemaNonNullType.java | 24 + .../java/graphql/execution/TypeFromAST.java | 25 + .../graphql/execution/ValuesResolver.java | 109 +++- .../execution/ValuesResolverConversion.java | 589 ++++++++++++++---- .../ValuesResolverOneOfValidation.java | 80 ++- .../execution/values/InputInterceptor.java | 22 + .../java/graphql/schema/ExecutableSchema.java | 9 + .../java/graphql/schema/GraphQLSchema.java | 12 + .../universe/view/SUExecutableSchema.java | 27 +- .../view/SUExecutableSchemaBuilder.java | 86 ++- ...UExecutableSchemaValuesResolverTest.groovy | 535 ++++++++++++++++ 17 files changed, 1554 insertions(+), 163 deletions(-) create mode 100644 src/main/java/graphql/execution/ExecutableInputValueSchema.java create mode 100644 src/main/java/graphql/execution/GraphQLInputValueSchema.java create mode 100644 src/main/java/graphql/execution/InputValueSchema.java create mode 100644 src/main/java/graphql/execution/ResolvedSchemaListType.java create mode 100644 src/main/java/graphql/execution/ResolvedSchemaNonNullType.java create mode 100644 src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy diff --git a/src/main/java/graphql/execution/ExecutableInputValueSchema.java b/src/main/java/graphql/execution/ExecutableInputValueSchema.java new file mode 100644 index 0000000000..b982cb7c50 --- /dev/null +++ b/src/main/java/graphql/execution/ExecutableInputValueSchema.java @@ -0,0 +1,43 @@ +package graphql.execution; + +import graphql.Internal; +import graphql.schema.Coercing; +import graphql.schema.ExecutableSchema; +import graphql.schema.SchemaEnumValue; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaScalar; +import org.jspecify.annotations.NullMarked; + +import java.util.List; + +import static graphql.Assert.assertNotNull; + +@Internal +@NullMarked +public final class ExecutableInputValueSchema + implements InputValueSchema { + + private final ExecutableSchema schema; + + public ExecutableInputValueSchema(ExecutableSchema schema) { + this.schema = assertNotNull(schema); + } + + @Override + public List getInputFields( + SchemaInputObject inputObjectType) { + return schema.getInputFields(inputObjectType); + } + + @Override + public Coercing getScalarCoercing(SchemaScalar scalarType) { + return schema.getScalarCoercing(scalarType); + } + + @Override + public Object getEnumRuntimeValue( + SchemaEnumValue enumValue) { + return schema.getEnumRuntimeValue(enumValue); + } +} diff --git a/src/main/java/graphql/execution/GraphQLInputValueSchema.java b/src/main/java/graphql/execution/GraphQLInputValueSchema.java new file mode 100644 index 0000000000..ae92656e29 --- /dev/null +++ b/src/main/java/graphql/execution/GraphQLInputValueSchema.java @@ -0,0 +1,61 @@ +package graphql.execution; + +import graphql.Internal; +import graphql.schema.Coercing; +import graphql.schema.GraphQLEnumValueDefinition; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLScalarType; +import graphql.schema.SchemaEnumValue; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaScalar; +import graphql.schema.visibility.GraphqlFieldVisibility; +import org.jspecify.annotations.NullMarked; + +import java.util.List; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertTrue; + +@Internal +@NullMarked +public final class GraphQLInputValueSchema + implements InputValueSchema { + + private final GraphqlFieldVisibility fieldVisibility; + + public GraphQLInputValueSchema( + GraphqlFieldVisibility fieldVisibility) { + this.fieldVisibility = assertNotNull(fieldVisibility); + } + + @Override + public List getInputFields( + SchemaInputObject inputObjectType) { + assertTrue( + inputObjectType instanceof GraphQLInputObjectType, + "Expected a GraphQLInputObjectType"); + return fieldVisibility.getFieldDefinitions( + (GraphQLInputObjectType) inputObjectType); + } + + @Override + public Coercing getScalarCoercing(SchemaScalar scalarType) { + assertTrue( + scalarType instanceof GraphQLScalarType, + "Expected a GraphQLScalarType"); + return ((GraphQLScalarType) scalarType).getCoercing(); + } + + @Override + public Object getEnumRuntimeValue( + SchemaEnumValue enumValue) { + assertTrue( + enumValue instanceof GraphQLEnumValueDefinition, + "Expected a GraphQLEnumValueDefinition"); + return assertNotNull( + ((GraphQLEnumValueDefinition) enumValue).getValue(), + "The enum value '%s' must have a runtime value", + enumValue.getName()); + } +} diff --git a/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java b/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java index e4aef7e2c1..f24c2bf9ea 100644 --- a/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java +++ b/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java @@ -7,6 +7,7 @@ import graphql.language.SourceLocation; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; +import graphql.schema.SchemaType; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -25,6 +26,15 @@ public InputMapDefinesTooManyFieldsException(GraphQLType graphQLType, String fie super(String.format("The variables input contains a field name '%s' that is not defined for input object type '%s' ", fieldName, GraphQLTypeUtil.simplePrint(graphQLType))); } + public InputMapDefinesTooManyFieldsException( + SchemaType schemaType, + String fieldName) { + super(String.format( + "The variables input contains a field name '%s' that is not defined for input object type '%s' ", + fieldName, + GraphQLTypeUtil.simplePrint(schemaType))); + } + @Override public @Nullable List getLocations() { return null; diff --git a/src/main/java/graphql/execution/InputValueSchema.java b/src/main/java/graphql/execution/InputValueSchema.java new file mode 100644 index 0000000000..dae0e6298d --- /dev/null +++ b/src/main/java/graphql/execution/InputValueSchema.java @@ -0,0 +1,23 @@ +package graphql.execution; + +import graphql.Internal; +import graphql.schema.Coercing; +import graphql.schema.SchemaEnumValue; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaScalar; +import org.jspecify.annotations.NullMarked; + +import java.util.List; + +@Internal +@NullMarked +public interface InputValueSchema { + + List getInputFields( + SchemaInputObject inputObjectType); + + Coercing getScalarCoercing(SchemaScalar scalarType); + + Object getEnumRuntimeValue(SchemaEnumValue enumValue); +} diff --git a/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java index 9192d50802..14d7e0aad4 100644 --- a/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java +++ b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java @@ -10,6 +10,8 @@ import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaType; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -33,6 +35,17 @@ public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinit this.sourceLocations = Collections.singletonList(variableDefinition.getSourceLocation()); } + public NonNullableValueCoercedAsNullException( + VariableDefinition variableDefinition, + SchemaType schemaType) { + super(format( + "Variable '%s' has coerced Null value for NonNull type '%s'", + variableDefinition.getName(), + GraphQLTypeUtil.simplePrint(schemaType))); + this.sourceLocations = Collections.singletonList( + variableDefinition.getSourceLocation()); + } + public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinition, String fieldName, GraphQLType graphQLType) { super(format("Field '%s' of variable '%s' has coerced Null value for NonNull type '%s'", fieldName, variableDefinition.getName(), GraphQLTypeUtil.simplePrint(graphQLType))); @@ -50,6 +63,12 @@ public NonNullableValueCoercedAsNullException(GraphQLType graphQLType) { super(format("Coerced Null value for NonNull type '%s'", GraphQLTypeUtil.simplePrint(graphQLType))); } + public NonNullableValueCoercedAsNullException(SchemaType schemaType) { + super(format( + "Coerced Null value for NonNull type '%s'", + GraphQLTypeUtil.simplePrint(schemaType))); + } + public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinition, String causeMessage) { super(format("Variable '%s' has an invalid value: %s", variableDefinition.getName(), causeMessage)); this.sourceLocations = Collections.singletonList(variableDefinition.getSourceLocation()); @@ -61,6 +80,17 @@ public NonNullableValueCoercedAsNullException(String fieldName, List pat this.path = path; } + public NonNullableValueCoercedAsNullException( + String fieldName, + List path, + SchemaType schemaType) { + super(format( + "Field '%s' has coerced Null value for NonNull type '%s'", + fieldName, + GraphQLTypeUtil.simplePrint(schemaType))); + this.path = path; + } + public NonNullableValueCoercedAsNullException(GraphQLInputObjectField inputTypeField) { super(format("Input field '%s' has coerced Null value for NonNull type '%s'", inputTypeField.getName(), GraphQLTypeUtil.simplePrint(inputTypeField.getType()))); @@ -76,6 +106,14 @@ public NonNullableValueCoercedAsNullException(GraphQLArgument graphQLArgument) { super(format("Argument '%s' has coerced Null value for NonNull type '%s'", graphQLArgument.getName(), graphQLArgument.getType())); } + public NonNullableValueCoercedAsNullException( + SchemaArgument argument) { + super(format( + "Argument '%s' has coerced Null value for NonNull type '%s'", + argument.getName(), + GraphQLTypeUtil.simplePrint(argument.getType()))); + } + @Override public @Nullable List getLocations() { return sourceLocations; diff --git a/src/main/java/graphql/execution/ResolvedSchemaListType.java b/src/main/java/graphql/execution/ResolvedSchemaListType.java new file mode 100644 index 0000000000..a2075e5ef8 --- /dev/null +++ b/src/main/java/graphql/execution/ResolvedSchemaListType.java @@ -0,0 +1,24 @@ +package graphql.execution; + +import graphql.Internal; +import graphql.schema.SchemaList; +import graphql.schema.SchemaType; +import org.jspecify.annotations.NullMarked; + +import static graphql.Assert.assertNotNull; + +@Internal +@NullMarked +public final class ResolvedSchemaListType implements SchemaList { + + private final SchemaType wrappedType; + + public ResolvedSchemaListType(SchemaType wrappedType) { + this.wrappedType = assertNotNull(wrappedType); + } + + @Override + public SchemaType getWrappedType() { + return wrappedType; + } +} diff --git a/src/main/java/graphql/execution/ResolvedSchemaNonNullType.java b/src/main/java/graphql/execution/ResolvedSchemaNonNullType.java new file mode 100644 index 0000000000..2f1965c098 --- /dev/null +++ b/src/main/java/graphql/execution/ResolvedSchemaNonNullType.java @@ -0,0 +1,24 @@ +package graphql.execution; + +import graphql.Internal; +import graphql.schema.SchemaNonNull; +import graphql.schema.SchemaType; +import org.jspecify.annotations.NullMarked; + +import static graphql.Assert.assertNotNull; + +@Internal +@NullMarked +public final class ResolvedSchemaNonNullType implements SchemaNonNull { + + private final SchemaType wrappedType; + + public ResolvedSchemaNonNullType(SchemaType wrappedType) { + this.wrappedType = assertNotNull(wrappedType); + } + + @Override + public SchemaType getWrappedType() { + return wrappedType; + } +} diff --git a/src/main/java/graphql/execution/TypeFromAST.java b/src/main/java/graphql/execution/TypeFromAST.java index bbde44552f..b2d155369a 100644 --- a/src/main/java/graphql/execution/TypeFromAST.java +++ b/src/main/java/graphql/execution/TypeFromAST.java @@ -6,8 +6,10 @@ import graphql.language.NonNullType; import graphql.language.Type; import graphql.language.TypeName; +import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; +import graphql.schema.SchemaType; import static graphql.schema.GraphQLList.list; import static graphql.schema.GraphQLNonNull.nonNull; @@ -28,4 +30,27 @@ public static GraphQLType getTypeFromAST(GraphQLSchema schema, Type type) { return schema.getType(((TypeName) type).getName()); } + + public static SchemaType getSchemaTypeFromAST( + ExecutableSchema schema, + Type type) { + SchemaType innerType; + if (type instanceof ListType) { + innerType = getSchemaTypeFromAST( + schema, + ((ListType) type).getType()); + return innerType == null + ? null + : new ResolvedSchemaListType(innerType); + } + if (type instanceof NonNullType) { + innerType = getSchemaTypeFromAST( + schema, + ((NonNullType) type).getType()); + return innerType == null + ? null + : new ResolvedSchemaNonNullType(innerType); + } + return schema.getType(((TypeName) type).getName()); + } } diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index 357e19d64b..f738bf7c63 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -17,6 +17,7 @@ import graphql.normalized.NormalizedInputValue; import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; +import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLArgument; import graphql.schema.GraphQLCodeRegistry; import graphql.schema.GraphQLEnumType; @@ -27,6 +28,9 @@ import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; import graphql.schema.InputValueWithState; +import graphql.schema.SchemaArgument; +import graphql.schema.SchemaInputType; +import graphql.schema.SchemaType; import graphql.schema.visibility.GraphqlFieldVisibility; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -79,7 +83,22 @@ public static CoercedVariables coerceVariableValues(GraphQLSchema schema, RawVariables rawVariables, GraphQLContext graphqlContext, Locale locale) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { + return coerceVariableValues( + (ExecutableSchema) schema, + variableDefinitions, + rawVariables, + graphqlContext, + locale); + } + public static CoercedVariables coerceVariableValues( + ExecutableSchema schema, + List variableDefinitions, + RawVariables rawVariables, + GraphQLContext graphqlContext, + Locale locale) + throws CoercingParseValueException, + NonNullableValueCoercedAsNullException { InputInterceptor inputInterceptor = graphqlContext.get(InputInterceptor.class); return ValuesResolverConversion.externalValueToInternalValueForVariables( inputInterceptor, @@ -109,12 +128,30 @@ public static NormalizedVariables getNormalizedVariableValues( GraphQLContext graphqlContext, Locale locale ) { - GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); + return getNormalizedVariableValues( + (ExecutableSchema) schema, + variableDefinitions, + rawVariables, + graphqlContext, + locale); + } + + public static NormalizedVariables getNormalizedVariableValues( + ExecutableSchema schema, + List variableDefinitions, + RawVariables rawVariables, + GraphQLContext graphqlContext, + Locale locale + ) { + InputValueSchema inputValueSchema = + new ExecutableInputValueSchema(schema); Map result = Maps.newLinkedHashMapWithExpectedSize(variableDefinitions.size()); for (VariableDefinition variableDefinition : variableDefinitions) { String variableName = variableDefinition.getName(); - GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); - assertTrue(variableType instanceof GraphQLInputType); + SchemaType variableType = TypeFromAST.getSchemaTypeFromAST( + schema, + variableDefinition.getType()); + assertTrue(variableType instanceof SchemaInputType); // can be NullValue Value defaultValue = variableDefinition.getDefaultValue(); boolean hasValue = rawVariables.containsKey(variableName); @@ -127,7 +164,13 @@ public static NormalizedVariables getNormalizedVariableValues( if (value == null) { result.put(variableName, new NormalizedInputValue(simplePrint(variableType), null)); } else { - Object literal = ValuesResolverConversion.externalValueToLiteral(fieldVisibility, value, (GraphQLInputType) variableType, NORMALIZED, graphqlContext, locale); + Object literal = ValuesResolverConversion.externalValueToLiteral( + inputValueSchema, + value, + (SchemaInputType) variableType, + NORMALIZED, + graphqlContext, + locale); result.put(variableName, new NormalizedInputValue(simplePrint(variableType), literal)); } } @@ -158,6 +201,26 @@ public static Map getArgumentValues( return getArgumentValuesImpl(inputInterceptor, DEFAULT_FIELD_VISIBILITY, argumentTypes, arguments, coercedVariables, graphqlContext, locale); } + public static Map getArgumentValues( + ExecutableSchema schema, + List argumentTypes, + List arguments, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale + ) { + InputInterceptor inputInterceptor = + graphqlContext.get(InputInterceptor.class); + return getArgumentValuesImpl( + inputInterceptor, + new ExecutableInputValueSchema(schema), + argumentTypes, + arguments, + coercedVariables, + graphqlContext, + locale); + } + /** * No validation as the arguments are assumed valid * @@ -326,6 +389,26 @@ private static Map getArgumentValuesImpl( CoercedVariables coercedVariables, GraphQLContext graphqlContext, Locale locale + ) { + return getArgumentValuesImpl( + inputInterceptor, + new GraphQLInputValueSchema(fieldVisibility), + argumentTypes, + arguments, + coercedVariables, + graphqlContext, + locale); + } + + @NonNull + private static Map getArgumentValuesImpl( + InputInterceptor inputInterceptor, + InputValueSchema schema, + List argumentTypes, + List arguments, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale ) { if (argumentTypes.isEmpty()) { return ImmutableKit.emptyMap(); @@ -333,8 +416,8 @@ private static Map getArgumentValuesImpl( Map coercedValues = Maps.newLinkedHashMapWithExpectedSize(arguments.size()); Map argumentMap = argumentMap(arguments); - for (GraphQLArgument argumentDefinition : argumentTypes) { - GraphQLInputType argumentType = argumentDefinition.getType(); + for (SchemaArgument argumentDefinition : argumentTypes) { + SchemaInputType argumentType = argumentDefinition.getType(); String argumentName = argumentDefinition.getName(); Argument argument = argumentMap.get(argumentName); InputValueWithState defaultValue = argumentDefinition.getArgumentDefaultValue(); @@ -348,10 +431,10 @@ private static Map getArgumentValuesImpl( } else { value = argumentValue; } - if (!hasValue && argumentDefinition.hasSetDefaultValue()) { + if (!hasValue && defaultValue.isSet()) { Object coercedDefaultValue = ValuesResolverConversion.defaultValueToInternalValue( inputInterceptor, - fieldVisibility, + schema, defaultValue, argumentType, graphqlContext, @@ -366,7 +449,7 @@ private static Map getArgumentValuesImpl( coercedValues.put(argumentName, value); } else { value = ValuesResolverConversion.literalToInternalValue(inputInterceptor, - fieldVisibility, + schema, argumentType, argument.getValue(), coercedVariables, @@ -375,7 +458,13 @@ private static Map getArgumentValuesImpl( coercedValues.put(argumentName, value); } - ValuesResolverOneOfValidation.validateOneOfInputTypes(argumentType, value, argumentValue, argumentName, locale); + ValuesResolverOneOfValidation.validateOneOfInputTypes( + schema, + argumentType, + value, + argumentValue, + argumentName, + locale); } } diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index 20e9feb552..2dbbc54e3a 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -5,6 +5,7 @@ import graphql.Internal; import graphql.execution.values.InputInterceptor; import graphql.language.ArrayValue; +import graphql.language.EnumValue; import graphql.language.NullValue; import graphql.language.ObjectField; import graphql.language.ObjectValue; @@ -12,17 +13,22 @@ import graphql.language.VariableDefinition; import graphql.language.VariableReference; import graphql.normalized.NormalizedInputValue; +import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; -import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; +import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLScalarType; -import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; import graphql.schema.InputValueWithState; +import graphql.schema.SchemaEnum; +import graphql.schema.SchemaEnumValue; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaInputType; +import graphql.schema.SchemaList; +import graphql.schema.SchemaNonNull; +import graphql.schema.SchemaScalar; +import graphql.schema.SchemaType; import graphql.schema.visibility.DefaultGraphqlFieldVisibility; import graphql.schema.visibility.GraphqlFieldVisibility; import graphql.util.FpKit; @@ -43,11 +49,11 @@ import static graphql.execution.ValuesResolver.ValueMode.NORMALIZED; import static graphql.language.NullValue.newNullValue; import static graphql.language.ObjectField.newObjectField; +import static graphql.scalar.CoercingUtil.i18nMsg; +import static graphql.scalar.CoercingUtil.typeName; import static graphql.schema.GraphQLTypeUtil.isList; import static graphql.schema.GraphQLTypeUtil.isNonNull; import static graphql.schema.GraphQLTypeUtil.simplePrint; -import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; -import static graphql.schema.GraphQLTypeUtil.unwrapOneAs; import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; /** @@ -64,30 +70,49 @@ static Object valueToLiteralImpl(GraphqlFieldVisibility fieldVisibility, ValuesResolver.ValueMode valueMode, GraphQLContext graphqlContext, Locale locale) { + return valueToLiteralImpl( + new GraphQLInputValueSchema(fieldVisibility), + inputValueWithState, + (SchemaType) type, + valueMode, + graphqlContext, + locale); + } + + static Object valueToLiteralImpl( + InputValueSchema schema, + InputValueWithState inputValueWithState, + SchemaType type, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, + Locale locale) { if (inputValueWithState.isInternal()) { if (valueMode == NORMALIZED) { return assertShouldNeverHappen("can't infer normalized structure"); } - Value value = ValuesResolverLegacy.valueToLiteralLegacy( + if (type instanceof GraphQLType) { + Value value = ValuesResolverLegacy.valueToLiteralLegacy( + inputValueWithState.getValue(), + (GraphQLType) type, + graphqlContext, + locale); + return value == null ? NullValue.of() : value; + } + return internalValueToLiteral( + schema, inputValueWithState.getValue(), type, graphqlContext, locale); - // - // the valueToLiteralLegacy() nominally cant know if null means never set or is set to a null value - // but this code can know - its is SET to a value so, it MUST be a Null Literal - // this method would assert at the end of it if inputValueWithState.isNotSet() were true - // - return value == null ? NullValue.of() : value; } if (inputValueWithState.isLiteral()) { return inputValueWithState.getValue(); } if (inputValueWithState.isExternal()) { return externalValueToLiteral( - fieldVisibility, + schema, inputValueWithState.getValue(), - (GraphQLInputType) type, + (SchemaInputType) type, valueMode, graphqlContext, locale); @@ -166,43 +191,61 @@ static Object externalValueToLiteral( ValuesResolver.ValueMode valueMode, GraphQLContext graphqlContext, Locale locale + ) { + return externalValueToLiteral( + new GraphQLInputValueSchema(fieldVisibility), + value, + type, + valueMode, + graphqlContext, + locale); + } + + static Object externalValueToLiteral( + InputValueSchema schema, + @Nullable Object value, + SchemaInputType type, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, + Locale locale ) { if (value == null) { return newNullValue().build(); } if (GraphQLTypeUtil.isNonNull(type)) { return externalValueToLiteral( - fieldVisibility, + schema, value, - (GraphQLInputType) unwrapNonNull(type), + (SchemaInputType) ((SchemaNonNull) type) + .getWrappedType(), valueMode, graphqlContext, locale); } - if (type instanceof GraphQLScalarType) { + if (type instanceof SchemaScalar) { return externalValueToLiteralForScalar( - (GraphQLScalarType) type, + schema, + (SchemaScalar) type, value, graphqlContext, locale); - } else if (type instanceof GraphQLEnumType) { + } else if (type instanceof SchemaEnum) { return externalValueToLiteralForEnum( - (GraphQLEnumType) type, + (SchemaEnum) type, value, - graphqlContext, locale); - } else if (type instanceof GraphQLList) { + } else if (type instanceof SchemaList) { return externalValueToLiteralForList( - fieldVisibility, - (GraphQLList) type, + schema, + (SchemaList) type, value, valueMode, graphqlContext, locale); - } else if (type instanceof GraphQLInputObjectType) { + } else if (type instanceof SchemaInputObject) { return externalValueToLiteralForObject( - fieldVisibility, - (GraphQLInputObjectType) type, + schema, + (SchemaInputObject) type, value, valueMode, graphqlContext, @@ -216,12 +259,14 @@ static Object externalValueToLiteral( * No validation */ private static Value externalValueToLiteralForScalar( - GraphQLScalarType scalarType, + InputValueSchema schema, + SchemaScalar scalarType, Object value, GraphQLContext graphqlContext, @NonNull Locale locale ) { - return scalarType.getCoercing().valueToLiteral(value, graphqlContext, locale); + return schema.getScalarCoercing(scalarType) + .valueToLiteral(value, graphqlContext, locale); } @@ -229,14 +274,18 @@ private static Value externalValueToLiteralForScalar( * No validation */ private static Value externalValueToLiteralForEnum( - GraphQLEnumType enumType, + SchemaEnum enumType, Object value, - GraphQLContext graphqlContext, Locale locale) { - return enumType.valueToLiteral( - value, - graphqlContext, - locale); + SchemaEnumValue enumValue = enumType.getValue(value.toString()); + if (enumValue == null) { + return assertShouldNeverHappen( + "Invalid external enum value '%s' for '%s' in locale '%s'", + value, + enumType.getName(), + locale); + } + return EnumValue.newEnumValue(enumValue.getName()).build(); } /** @@ -244,19 +293,20 @@ private static Value externalValueToLiteralForEnum( */ @SuppressWarnings("unchecked") private static Object externalValueToLiteralForList( - GraphqlFieldVisibility fieldVisibility, - GraphQLList listType, + InputValueSchema schema, + SchemaList listType, Object value, ValuesResolver.ValueMode valueMode, GraphQLContext graphqlContext, Locale locale ) { - GraphQLInputType wrappedType = (GraphQLInputType) listType.getWrappedType(); + SchemaInputType wrappedType = + (SchemaInputType) listType.getWrappedType(); List valueList = FpKit.toListOrSingletonList(value); ImmutableList.Builder resultBuilder = ImmutableList.builderWithExpectedSize(valueList.size()); for (Object item : valueList) { resultBuilder.add(externalValueToLiteral( - fieldVisibility, + schema, item, wrappedType, valueMode, @@ -277,8 +327,8 @@ private static Object externalValueToLiteralForList( */ @SuppressWarnings("unchecked") private static Object externalValueToLiteralForObject( - GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType inputObjectType, + InputValueSchema schema, + SchemaInputObject inputObjectType, Object inputValue, ValuesResolver.ValueMode valueMode, GraphQLContext graphqlContext, @@ -286,26 +336,33 @@ private static Object externalValueToLiteralForObject( ) { assertTrue(inputValue instanceof Map, "Expect Map as input"); Map inputMap = (Map) inputValue; - List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); + List fieldDefinitions = + schema.getInputFields(inputObjectType); Map normalizedResult = new LinkedHashMap<>(); ImmutableList.Builder objectFields = ImmutableList.builder(); - for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); + for (SchemaInputField inputFieldDefinition : fieldDefinitions) { + SchemaInputType fieldType = inputFieldDefinition.getType(); String fieldName = inputFieldDefinition.getName(); boolean hasValue = inputMap.containsKey(fieldName); Object fieldValue = inputMap.getOrDefault(fieldName, null); - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { - //TODO: consider valueMode + if (!hasValue + && inputFieldDefinition + .getInputFieldDefaultValue() + .isSet()) { Object defaultValueLiteral = valueToLiteralImpl( - fieldVisibility, + schema, inputFieldDefinition.getInputFieldDefaultValue(), fieldType, ValuesResolver.ValueMode.LITERAL, graphqlContext, locale); - if (valueMode == ValuesResolver.ValueMode.LITERAL) { - normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), defaultValueLiteral)); + if (valueMode == NORMALIZED) { + normalizedResult.put( + fieldName, + new NormalizedInputValue( + simplePrint(fieldType), + defaultValueLiteral)); } else { objectFields.add(newObjectField().name(fieldName).value((Value) defaultValueLiteral).build()); } @@ -318,7 +375,7 @@ private static Object externalValueToLiteralForObject( } } else { Object literal = externalValueToLiteral( - fieldVisibility, + schema, fieldValue, fieldType, valueMode, @@ -337,24 +394,153 @@ private static Object externalValueToLiteralForObject( return ObjectValue.newObjectValue().objectFields(objectFields.build()).build(); } + private static Value internalValueToLiteral( + InputValueSchema schema, + @Nullable Object value, + SchemaType type, + GraphQLContext graphqlContext, + Locale locale) { + if (value == null) { + return NullValue.of(); + } + if (type instanceof SchemaNonNull) { + return internalValueToLiteral( + schema, + value, + ((SchemaNonNull) type).getWrappedType(), + graphqlContext, + locale); + } + if (type instanceof SchemaScalar) { + return schema.getScalarCoercing((SchemaScalar) type) + .valueToLiteral(value, graphqlContext, locale); + } + if (type instanceof SchemaEnum) { + return internalEnumValueToLiteral( + schema, + value, + (SchemaEnum) type); + } + if (type instanceof SchemaList) { + return internalListValueToLiteral( + schema, + value, + (SchemaList) type, + graphqlContext, + locale); + } + if (type instanceof SchemaInputObject) { + return internalObjectValueToLiteral( + schema, + value, + (SchemaInputObject) type, + graphqlContext, + locale); + } + return assertShouldNeverHappen("unexpected type %s", type); + } + + private static Value internalEnumValueToLiteral( + InputValueSchema schema, + Object value, + SchemaEnum enumType) { + for (SchemaEnumValue enumValue : enumType.getValues()) { + Object runtimeValue = schema.getEnumRuntimeValue(enumValue); + if (value.equals(runtimeValue)) { + return EnumValue.newEnumValue(enumValue.getName()).build(); + } + if (runtimeValue instanceof Enum + && value instanceof String + && value.equals(((Enum) runtimeValue).name())) { + return EnumValue.newEnumValue(enumValue.getName()).build(); + } + if (value instanceof Enum + && ((Enum) value).name() + .equals(String.valueOf(runtimeValue))) { + return EnumValue.newEnumValue(enumValue.getName()).build(); + } + } + return assertShouldNeverHappen( + "Invalid internal enum value '%s' for '%s'", + value, + enumType.getName()); + } + + @SuppressWarnings("unchecked") + private static Value internalListValueToLiteral( + InputValueSchema schema, + Object value, + SchemaList listType, + GraphQLContext graphqlContext, + Locale locale) { + List values = FpKit.toListOrSingletonList(value); + ImmutableList.Builder result = + ImmutableList.builderWithExpectedSize(values.size()); + for (Object item : values) { + result.add(internalValueToLiteral( + schema, + item, + listType.getWrappedType(), + graphqlContext, + locale)); + } + return ArrayValue.newArrayValue().values(result.build()).build(); + } + + @SuppressWarnings("unchecked") + private static Value internalObjectValueToLiteral( + InputValueSchema schema, + Object value, + SchemaInputObject inputObjectType, + GraphQLContext graphqlContext, + Locale locale) { + assertTrue(value instanceof Map, "Expect Map as input"); + Map values = (Map) value; + ImmutableList.Builder fields = ImmutableList.builder(); + for (SchemaInputField field : + schema.getInputFields(inputObjectType)) { + if (!values.containsKey(field.getName())) { + continue; + } + Value fieldValue = internalValueToLiteral( + schema, + values.get(field.getName()), + field.getType(), + graphqlContext, + locale); + fields.add(newObjectField() + .name(field.getName()) + .value(fieldValue) + .build()); + } + return ObjectValue.newObjectValue() + .objectFields(fields.build()) + .build(); + } + /** * performs validation too */ static CoercedVariables externalValueToInternalValueForVariables( InputInterceptor inputInterceptor, - GraphQLSchema schema, + ExecutableSchema schema, List variableDefinitions, RawVariables rawVariables, GraphQLContext graphqlContext, Locale locale ) { - GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); + InputValueSchema inputValueSchema = + new ExecutableInputValueSchema(schema); Map coercedValues = new LinkedHashMap<>(); for (VariableDefinition variableDefinition : variableDefinitions) { try { String variableName = variableDefinition.getName(); - GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); - assertTrue(variableType instanceof GraphQLInputType); - GraphQLInputType variableInputType = (GraphQLInputType) variableType; + SchemaType variableType = + TypeFromAST.getSchemaTypeFromAST( + schema, + variableDefinition.getType()); + assertTrue(variableType instanceof SchemaInputType); + SchemaInputType variableInputType = + (SchemaInputType) variableType; // can be NullValue Value defaultValue = variableDefinition.getDefaultValue(); boolean hasValue = rawVariables.containsKey(variableName); @@ -362,7 +548,7 @@ static CoercedVariables externalValueToInternalValueForVariables( if (!hasValue && defaultValue != null) { Object coercedDefaultValue = literalToInternalValue( inputInterceptor, - fieldVisibility, + inputValueSchema, variableInputType, defaultValue, CoercedVariables.emptyVariables(), @@ -378,7 +564,7 @@ static CoercedVariables externalValueToInternalValueForVariables( Object coercedValue = externalValueToInternalValueImpl( variableName, inputInterceptor, - fieldVisibility, + inputValueSchema, variableInputType, value, graphqlContext, @@ -409,9 +595,10 @@ static Object externalValueToInternalValueImpl( GraphQLContext graphqlContext, Locale locale ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - return externalValueToInternalValueImpl("externalValue", + return externalValueToInternalValueImpl( + "externalValue", inputInterceptor, - fieldVisibility, + new GraphQLInputValueSchema(fieldVisibility), graphQLType, originalValue, graphqlContext, @@ -430,17 +617,57 @@ static Object externalValueToInternalValueImpl( GraphQLContext graphqlContext, Locale locale ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - if (isNonNull(graphQLType)) { + return externalValueToInternalValueImpl( + variableName, + inputInterceptor, + new GraphQLInputValueSchema(fieldVisibility), + graphQLType, + originalValue, + graphqlContext, + locale); + } + + static Object externalValueToInternalValueImpl( + InputInterceptor inputInterceptor, + InputValueSchema schema, + SchemaInputType type, + Object originalValue, + GraphQLContext graphqlContext, + Locale locale + ) throws NonNullableValueCoercedAsNullException, + CoercingParseValueException { + return externalValueToInternalValueImpl( + "externalValue", + inputInterceptor, + schema, + type, + originalValue, + graphqlContext, + locale); + } + + static Object externalValueToInternalValueImpl( + String variableName, + InputInterceptor inputInterceptor, + InputValueSchema schema, + SchemaInputType type, + Object originalValue, + GraphQLContext graphqlContext, + Locale locale + ) throws NonNullableValueCoercedAsNullException, + CoercingParseValueException { + if (isNonNull(type)) { Object returnValue = externalValueToInternalValueImpl( variableName, inputInterceptor, - fieldVisibility, - unwrapOneAs(graphQLType), + schema, + (SchemaInputType) ((SchemaNonNull) type) + .getWrappedType(), originalValue, graphqlContext, locale); if (returnValue == null) { - throw new NonNullableValueCoercedAsNullException(graphQLType); + throw new NonNullableValueCoercedAsNullException(type); } return returnValue; } @@ -451,45 +678,57 @@ static Object externalValueToInternalValueImpl( // Object value = originalValue; if (inputInterceptor != null) { - value = inputInterceptor.intercept(originalValue, graphQLType, graphqlContext, locale); + value = inputInterceptor.intercept( + originalValue, + type, + graphqlContext, + locale); } if (value == null) { return null; } - if (graphQLType instanceof GraphQLScalarType) { + if (type instanceof SchemaScalar) { return externalValueToInternalValueForScalar( - (GraphQLScalarType) graphQLType, + schema, + (SchemaScalar) type, value, graphqlContext, locale); - } else if (graphQLType instanceof GraphQLEnumType) { + } else if (type instanceof SchemaEnum) { return externalValueToInternalValueForEnum( - (GraphQLEnumType) graphQLType, + schema, + (SchemaEnum) type, value, - graphqlContext, locale); - } else if (graphQLType instanceof GraphQLList) { + } else if (type instanceof SchemaList) { return externalValueToInternalValueForList( inputInterceptor, - fieldVisibility, - (GraphQLList) graphQLType, + schema, + (SchemaList) type, value, graphqlContext, locale); - } else if (graphQLType instanceof GraphQLInputObjectType) { + } else if (type instanceof SchemaInputObject) { if (value instanceof Map) { - GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) graphQLType; + SchemaInputObject inputObjectType = + (SchemaInputObject) type; //noinspection unchecked Map coercedMap = externalValueToInternalValueForObject( inputInterceptor, - fieldVisibility, + schema, inputObjectType, (Map) value, graphqlContext, locale); - ValuesResolverOneOfValidation.validateOneOfInputTypes(inputObjectType, coercedMap, null, variableName, locale); + ValuesResolverOneOfValidation.validateOneOfInputTypes( + schema, + inputObjectType, + coercedMap, + null, + variableName, + locale); return coercedMap; } else { throw CoercingParseValueException.newCoercingParseValueException() @@ -498,7 +737,7 @@ static Object externalValueToInternalValueImpl( .build(); } } else { - return assertShouldNeverHappen("unhandled type %s", graphQLType); + return assertShouldNeverHappen("unhandled type %s", type); } } @@ -507,14 +746,17 @@ static Object externalValueToInternalValueImpl( */ private static Map externalValueToInternalValueForObject( InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType inputObjectType, + InputValueSchema schema, + SchemaInputObject inputObjectType, Map inputMap, GraphQLContext graphqlContext, Locale locale ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); - List fieldNames = map(fieldDefinitions, GraphQLInputObjectField::getName); + List fieldDefinitions = + schema.getInputFields(inputObjectType); + List fieldNames = map( + fieldDefinitions, + SchemaInputField::getName); for (String providedFieldName : inputMap.keySet()) { if (!fieldNames.contains(providedFieldName)) { throw new InputMapDefinesTooManyFieldsException(inputObjectType, providedFieldName); @@ -523,16 +765,16 @@ private static Map externalValueToInternalValueForObject( Map coercedValues = new LinkedHashMap<>(); - for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); + for (SchemaInputField inputFieldDefinition : fieldDefinitions) { + SchemaInputType fieldType = inputFieldDefinition.getType(); String fieldName = inputFieldDefinition.getName(); InputValueWithState defaultValue = inputFieldDefinition.getInputFieldDefaultValue(); boolean hasValue = inputMap.containsKey(fieldName); Object value = inputMap.getOrDefault(fieldName, null); - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + if (!hasValue && defaultValue.isSet()) { Object coercedDefaultValue = defaultValueToInternalValue( inputInterceptor, - fieldVisibility, + schema, defaultValue, fieldType, graphqlContext, @@ -546,7 +788,7 @@ private static Map externalValueToInternalValueForObject( } else { value = externalValueToInternalValueImpl( inputInterceptor, - fieldVisibility, + schema, fieldType, value, graphqlContext, @@ -562,12 +804,13 @@ private static Map externalValueToInternalValueForObject( * including validation */ private static Object externalValueToInternalValueForScalar( - GraphQLScalarType graphQLScalarType, + InputValueSchema schema, + SchemaScalar scalarType, Object value, GraphQLContext graphqlContext, Locale locale ) throws CoercingParseValueException { - return graphQLScalarType.getCoercing().parseValue( + return schema.getScalarCoercing(scalarType).parseValue( value, graphqlContext, locale); @@ -577,15 +820,22 @@ private static Object externalValueToInternalValueForScalar( * including validation */ private static Object externalValueToInternalValueForEnum( - GraphQLEnumType graphQLEnumType, + InputValueSchema schema, + SchemaEnum enumType, Object value, - GraphQLContext graphqlContext, Locale locale ) throws CoercingParseValueException { - return graphQLEnumType.parseValue( - value, - graphqlContext, - locale); + SchemaEnumValue enumValue = + enumType.getValue(value.toString()); + if (enumValue != null) { + return schema.getEnumRuntimeValue(enumValue); + } + throw new CoercingParseValueException( + i18nMsg( + locale, + "Enum.badName", + enumType.getName(), + value.toString())); } /** @@ -593,20 +843,21 @@ private static Object externalValueToInternalValueForEnum( */ private static List externalValueToInternalValueForList( InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLList graphQLList, + InputValueSchema schema, + SchemaList listType, Object value, GraphQLContext graphqlContext, Locale locale ) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { - GraphQLInputType wrappedType = (GraphQLInputType) graphQLList.getWrappedType(); + SchemaInputType wrappedType = + (SchemaInputType) listType.getWrappedType(); List listOrSingletonList = FpKit.toListOrSingletonList(value); List list = FpKit.arrayListSizedTo(listOrSingletonList); for (Object val : listOrSingletonList) { list.add(externalValueToInternalValueImpl( inputInterceptor, - fieldVisibility, + schema, wrappedType, val, graphqlContext, @@ -635,10 +886,29 @@ static Object literalToInternalValue( CoercedVariables coercedVariables, GraphQLContext graphqlContext, Locale locale + ) { + return literalToInternalValue( + inputInterceptor, + new GraphQLInputValueSchema(fieldVisibility), + type, + inputValue, + coercedVariables, + graphqlContext, + locale); + } + + static Object literalToInternalValue( + InputInterceptor inputInterceptor, + InputValueSchema schema, + SchemaInputType type, + Value inputValue, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale ) { return literalToInternalValueImpl( inputInterceptor, - fieldVisibility, + schema, type, inputValue, coercedVariables, @@ -649,8 +919,8 @@ static Object literalToInternalValue( @Nullable private static Object literalToInternalValueImpl( InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLType type, + InputValueSchema schema, + SchemaType type, Value inputValue, CoercedVariables coercedVariables, GraphQLContext graphqlContext, @@ -663,10 +933,11 @@ private static Object literalToInternalValueImpl( if (inputValue instanceof NullValue) { return null; } - if (type instanceof GraphQLScalarType) { + if (type instanceof SchemaScalar) { return literalToInternalValueForScalar( + schema, inputValue, - (GraphQLScalarType) type, + (SchemaScalar) type, coercedVariables, graphqlContext, locale); @@ -674,31 +945,36 @@ private static Object literalToInternalValueImpl( if (isNonNull(type)) { return literalToInternalValue( inputInterceptor, - fieldVisibility, - unwrapOneAs(type), + schema, + (SchemaInputType) ((SchemaNonNull) type) + .getWrappedType(), inputValue, coercedVariables, graphqlContext, locale); } - if (type instanceof GraphQLInputObjectType) { + if (type instanceof SchemaInputObject) { return literalToInternalValueForInputObject( inputInterceptor, - fieldVisibility, - (GraphQLInputObjectType) type, + schema, + (SchemaInputObject) type, (ObjectValue) inputValue, coercedVariables, graphqlContext, locale); } - if (type instanceof GraphQLEnumType) { - return ((GraphQLEnumType) type).parseLiteral(inputValue, graphqlContext, locale); + if (type instanceof SchemaEnum) { + return literalToInternalValueForEnum( + schema, + inputValue, + (SchemaEnum) type, + locale); } if (isList(type)) { return literalToInternalValueForList( inputInterceptor, - fieldVisibility, - (GraphQLList) type, + schema, + (SchemaList) type, inputValue, coercedVariables, graphqlContext, @@ -711,14 +987,15 @@ private static Object literalToInternalValueImpl( * no validation */ private static Object literalToInternalValueForScalar( + InputValueSchema schema, Value inputValue, - GraphQLScalarType scalarType, + SchemaScalar scalarType, CoercedVariables coercedVariables, GraphQLContext graphqlContext, @NonNull Locale locale ) { // the CoercingParseLiteralException exception that could happen here has been validated earlier via ValidationUtil - return scalarType.getCoercing().parseLiteral( + return schema.getScalarCoercing(scalarType).parseLiteral( inputValue, coercedVariables, graphqlContext, @@ -730,22 +1007,23 @@ private static Object literalToInternalValueForScalar( */ private static Object literalToInternalValueForList( InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLList graphQLList, + InputValueSchema schema, + SchemaList listType, Value value, CoercedVariables coercedVariables, GraphQLContext graphqlContext, Locale locale ) { - GraphQLInputType inputType = (GraphQLInputType) graphQLList.getWrappedType(); + SchemaInputType inputType = + (SchemaInputType) listType.getWrappedType(); if (value instanceof ArrayValue) { ArrayValue arrayValue = (ArrayValue) value; List result = new ArrayList<>(); for (Value singleValue : arrayValue.getValues()) { result.add(literalToInternalValue( inputInterceptor, - fieldVisibility, + schema, inputType, singleValue, coercedVariables, @@ -757,7 +1035,7 @@ private static Object literalToInternalValueForList( return Collections.singletonList( literalToInternalValue( inputInterceptor, - fieldVisibility, + schema, inputType, value, coercedVariables, @@ -771,8 +1049,8 @@ private static Object literalToInternalValueForList( */ private static Object literalToInternalValueForInputObject( InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType type, + InputValueSchema schema, + SchemaInputObject type, ObjectValue inputValue, CoercedVariables coercedVariables, GraphQLContext graphqlContext, @@ -783,9 +1061,10 @@ private static Object literalToInternalValueForInputObject( Map inputFieldsByName = mapObjectValueFieldsByName(inputValue); - List inputFieldTypes = fieldVisibility.getFieldDefinitions(type); - for (GraphQLInputObjectField inputFieldDefinition : inputFieldTypes) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); + List inputFieldTypes = + schema.getInputFields(type); + for (SchemaInputField inputFieldDefinition : inputFieldTypes) { + SchemaInputType fieldType = inputFieldDefinition.getType(); String fieldName = inputFieldDefinition.getName(); ObjectField field = inputFieldsByName.get(fieldName); boolean hasValue = field != null; @@ -798,10 +1077,13 @@ private static Object literalToInternalValueForInputObject( } else { value = fieldValue; } - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + if (!hasValue + && inputFieldDefinition + .getInputFieldDefaultValue() + .isSet()) { Object coercedDefaultValue = defaultValueToInternalValue( inputInterceptor, - fieldVisibility, + schema, inputFieldDefinition.getInputFieldDefaultValue(), fieldType, graphqlContext, @@ -817,7 +1099,7 @@ private static Object literalToInternalValueForInputObject( } else { value = literalToInternalValue( inputInterceptor, - fieldVisibility, + schema, fieldType, fieldValue, coercedVariables, @@ -830,6 +1112,32 @@ private static Object literalToInternalValueForInputObject( return coercedValues; } + private static Object literalToInternalValueForEnum( + InputValueSchema schema, + Value inputValue, + SchemaEnum enumType, + Locale locale) { + if (!(inputValue instanceof EnumValue)) { + throw new CoercingParseLiteralException( + i18nMsg( + locale, + "Scalar.unexpectedAstType", + "EnumValue", + typeName(inputValue))); + } + SchemaEnumValue enumValue = enumType.getValue( + ((EnumValue) inputValue).getName()); + if (enumValue != null) { + return schema.getEnumRuntimeValue(enumValue); + } + throw new CoercingParseLiteralException( + i18nMsg( + locale, + "Enum.unallowableValue", + enumType.getName(), + inputValue)); + } + static boolean isNullValue(Object value) { if (value == null) { return true; @@ -855,6 +1163,23 @@ static Object defaultValueToInternalValue( GraphQLInputType type, GraphQLContext graphqlContext, Locale locale + ) { + return defaultValueToInternalValue( + inputInterceptor, + new GraphQLInputValueSchema(fieldVisibility), + defaultValue, + type, + graphqlContext, + locale); + } + + static Object defaultValueToInternalValue( + InputInterceptor inputInterceptor, + InputValueSchema schema, + InputValueWithState defaultValue, + SchemaInputType type, + GraphQLContext graphqlContext, + Locale locale ) { if (defaultValue.isInternal()) { return defaultValue.getValue(); @@ -863,7 +1188,7 @@ static Object defaultValueToInternalValue( // default value literals can't reference variables, this is why the variables are empty return literalToInternalValue( inputInterceptor, - fieldVisibility, + schema, type, (Value) defaultValue.getValue(), CoercedVariables.emptyVariables(), @@ -874,7 +1199,7 @@ static Object defaultValueToInternalValue( // performs validation too return externalValueToInternalValueImpl( inputInterceptor, - fieldVisibility, + schema, type, defaultValue.getValue(), graphqlContext, diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index 9955ce050d..01d7d41983 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -7,12 +7,12 @@ import graphql.language.ObjectField; import graphql.language.ObjectValue; import graphql.language.Value; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; +import graphql.schema.SchemaInputField; +import graphql.schema.SchemaInputObject; +import graphql.schema.SchemaList; +import graphql.schema.SchemaNonNull; +import graphql.schema.SchemaType; import java.util.List; import java.util.Locale; @@ -25,34 +25,55 @@ final class ValuesResolverOneOfValidation { @SuppressWarnings("unchecked") - static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value argumentValue, String argumentName, Locale locale) { - GraphQLType unwrappedNonNullType = GraphQLTypeUtil.unwrapNonNull(type); + static void validateOneOfInputTypes( + InputValueSchema schema, + SchemaType type, + Object inputValue, + Value argumentValue, + String argumentName, + Locale locale) { + SchemaType unwrappedNonNullType = type instanceof SchemaNonNull + ? GraphQLTypeUtil.unwrapOne(type) + : type; if (isList(unwrappedNonNullType) && !ValuesResolverConversion.isNullValue(inputValue) && inputValue instanceof List && argumentValue instanceof ArrayValue) { - GraphQLType elementType = ((GraphQLList) unwrappedNonNullType).getWrappedType(); + SchemaType elementType = + ((SchemaList) unwrappedNonNullType).getWrappedType(); List inputList = (List) inputValue; List argumentList = ((ArrayValue) argumentValue).getValues(); for (int i = 0; i < argumentList.size(); i++) { - validateOneOfInputTypes(elementType, inputList.get(i), argumentList.get(i), argumentName, locale); + validateOneOfInputTypes( + schema, + elementType, + inputList.get(i), + argumentList.get(i), + argumentName, + locale); } } - if (unwrappedNonNullType instanceof GraphQLInputObjectType && !ValuesResolverConversion.isNullValue(inputValue)) { - Assert.assertTrue(inputValue instanceof Map, "The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName); + if (unwrappedNonNullType instanceof SchemaInputObject + && !ValuesResolverConversion.isNullValue(inputValue)) { + Assert.assertTrue( + inputValue instanceof Map, + "The coerced argument %s SchemaInputObject is unexpectedly not a map", + argumentName); Map objectMap = (Map) inputValue; - GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedNonNullType; + SchemaInputObject inputObjectType = + (SchemaInputObject) unwrappedNonNullType; if (inputObjectType.isOneOf()) { validateOneOfInputTypesInternal(inputObjectType, argumentValue, objectMap, locale); } - for (GraphQLInputObjectField fieldDefinition : inputObjectType.getFields()) { - GraphQLInputType childFieldType = fieldDefinition.getType(); + for (SchemaInputField fieldDefinition : + schema.getInputFields(inputObjectType)) { + SchemaType childFieldType = fieldDefinition.getType(); String childFieldName = fieldDefinition.getName(); Object childFieldInputValue = objectMap.get(childFieldName); @@ -65,16 +86,32 @@ static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value 1) { Assert.assertShouldNeverHappen("argument %s has %s object fields with the same name: '%s'. A maximum of 1 is expected", argumentName, values.size(), childFieldName); } else if (!values.isEmpty()) { - validateOneOfInputTypes(childFieldType, childFieldInputValue, values.get(0), argumentName, locale); + validateOneOfInputTypes( + schema, + childFieldType, + childFieldInputValue, + values.get(0), + argumentName, + locale); } } else { - validateOneOfInputTypes(childFieldType, childFieldInputValue, argumentValue, argumentName, locale); + validateOneOfInputTypes( + schema, + childFieldType, + childFieldInputValue, + argumentValue, + argumentName, + locale); } } } } - private static void validateOneOfInputTypesInternal(GraphQLInputObjectType oneOfInputType, Value argumentValue, Map objectMap, Locale locale) { + private static void validateOneOfInputTypesInternal( + SchemaInputObject oneOfInputType, + Value argumentValue, + Map objectMap, + Locale locale) { final String fieldName; if (argumentValue instanceof ObjectValue) { List objectFields = ((ObjectValue) argumentValue).getObjectFields(); @@ -96,13 +133,18 @@ private static void validateOneOfInputTypesInternal(GraphQLInputObjectType oneOf } } - private static void throwValueIsNullError(GraphQLInputObjectType oneOfInputType, Locale locale, String fieldName) { + private static void throwValueIsNullError( + SchemaInputObject oneOfInputType, + Locale locale, + String fieldName) { String msg = I18n.i18n(I18n.BundleType.Execution, locale) .msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName); throw new OneOfNullValueException(msg); } - private static void throwNotOneFieldError(GraphQLInputObjectType oneOfInputType, Locale locale) { + private static void throwNotOneFieldError( + SchemaInputObject oneOfInputType, + Locale locale) { String msg = I18n.i18n(I18n.BundleType.Execution, locale) .msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName()); throw new OneOfTooManyKeysException(msg); diff --git a/src/main/java/graphql/execution/values/InputInterceptor.java b/src/main/java/graphql/execution/values/InputInterceptor.java index 0fb7534d59..7bd8ca98bc 100644 --- a/src/main/java/graphql/execution/values/InputInterceptor.java +++ b/src/main/java/graphql/execution/values/InputInterceptor.java @@ -3,6 +3,7 @@ import graphql.GraphQLContext; import graphql.Internal; import graphql.schema.GraphQLInputType; +import graphql.schema.SchemaInputType; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -39,4 +40,25 @@ Object intercept(@Nullable Object value, @NonNull GraphQLInputType graphQLType, @NonNull GraphQLContext graphqlContext, @NonNull Locale locale); + + /** + * Intercepts a value described by a schema-neutral input type. + * + *

The default preserves existing interceptors for GraphQL schema types. Implementations + * that need to intercept values from other executable schema views can override this method.

+ */ + default Object intercept( + @Nullable Object value, + @NonNull SchemaInputType inputType, + @NonNull GraphQLContext graphqlContext, + @NonNull Locale locale) { + if (inputType instanceof GraphQLInputType) { + return intercept( + value, + (GraphQLInputType) inputType, + graphqlContext, + locale); + } + return value; + } } diff --git a/src/main/java/graphql/schema/ExecutableSchema.java b/src/main/java/graphql/schema/ExecutableSchema.java index c77467324c..809b2691ed 100644 --- a/src/main/java/graphql/schema/ExecutableSchema.java +++ b/src/main/java/graphql/schema/ExecutableSchema.java @@ -145,6 +145,15 @@ boolean isPossibleType( SchemaComposite compositeType, SchemaObject objectType); + /** + * Returns the runtime value represented by an enum value definition. + * + * @param enumValue the enum value definition + * + * @return the schema-specific runtime value + */ + Object getEnumRuntimeValue(SchemaEnumValue enumValue); + /** * @param scalarType the scalar * diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index c681102bc5..256ff00dad 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -650,6 +650,18 @@ public boolean isPossibleType( (GraphQLObjectType) objectType); } + @Override + public Object getEnumRuntimeValue( + SchemaEnumValue enumValue) { + assertTrue( + enumValue instanceof GraphQLEnumValueDefinition, + "The enum value must belong to this GraphQLSchema"); + return assertNotNull( + ((GraphQLEnumValueDefinition) enumValue).getValue(), + "The enum value '%s' must have a runtime value", + enumValue.getName()); + } + @Override public Coercing getScalarCoercing(SchemaScalar scalarType) { assertTrue( diff --git a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java index a384e4d6b8..976341d348 100644 --- a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java @@ -16,6 +16,7 @@ import graphql.schema.SchemaComposite; import graphql.schema.SchemaDirective; import graphql.schema.SchemaDirectiveContainer; +import graphql.schema.SchemaEnumValue; import graphql.schema.SchemaField; import graphql.schema.SchemaFieldsContainer; import graphql.schema.SchemaInputField; @@ -31,6 +32,7 @@ import graphql.schema.universe.SUCompositeType; import graphql.schema.universe.SUDirective; import graphql.schema.universe.SUEnumType; +import graphql.schema.universe.SUEnumValue; import graphql.schema.universe.SUField; import graphql.schema.universe.SUInputField; import graphql.schema.universe.SUInputObjectType; @@ -66,13 +68,16 @@ public final class SUExecutableSchema implements ExecutableSchema { private final SUSchema schema; private final PersistentIntMap> coercingByScalarId; + private final PersistentIntMap enumRuntimeValueById; @Internal public SUExecutableSchema( SUSchema schema, - PersistentIntMap> coercingByScalarId) { + PersistentIntMap> coercingByScalarId, + PersistentIntMap enumRuntimeValueById) { this.schema = assertNotNull(schema); this.coercingByScalarId = assertNotNull(coercingByScalarId); + this.enumRuntimeValueById = assertNotNull(enumRuntimeValueById); } public static SUExecutableSchemaBuilder newExecutableSchema( @@ -98,6 +103,7 @@ public static SUExecutableSchema fromGraphQLSchema( GraphQLSchema graphQLSchema) { return newExecutableSchema(schema) .scalarCoercings(graphQLSchema) + .enumRuntimeValues(graphQLSchema) .build(); } @@ -394,6 +400,20 @@ public boolean isPossibleType( objectVertex(objectType)); } + @Override + public Object getEnumRuntimeValue( + SchemaEnumValue enumValue) { + SUVertex vertex = elementVertex(enumValue); + assertTrue( + vertex instanceof SUEnumValue, + "The enum value must belong to this SUSchema"); + Object runtimeValue = enumRuntimeValueById.get(vertex.getId()); + if (runtimeValue != null) { + return runtimeValue; + } + return assertNotNull(vertex.getName()); + } + @Override public Coercing getScalarCoercing(SchemaScalar scalarType) { SUVertex vertex = elementVertex(scalarType); @@ -562,4 +582,9 @@ private SUInputObjectType inputObjectVertex( public PersistentIntMap> getCoercingByScalarId() { return coercingByScalarId; } + + @Internal + public PersistentIntMap getEnumRuntimeValueById() { + return enumRuntimeValueById; + } } diff --git a/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java index 5053a27fd1..fe4ed97b74 100644 --- a/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java @@ -2,10 +2,14 @@ import graphql.ExperimentalApi; import graphql.schema.Coercing; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLEnumValueDefinition; import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; import graphql.schema.universe.PersistentIntMap; +import graphql.schema.universe.SUEnumType; +import graphql.schema.universe.SUEnumValue; import graphql.schema.universe.SUNamedType; import graphql.schema.universe.SUScalarType; import graphql.schema.universe.SUSchema; @@ -23,16 +27,20 @@ public final class SUExecutableSchemaBuilder { private final SUSchema schema; private PersistentIntMap> coercingByScalarId; + private PersistentIntMap enumRuntimeValueById; public SUExecutableSchemaBuilder(SUSchema schema) { this.schema = assertNotNull(schema); this.coercingByScalarId = PersistentIntMap.empty(); + this.enumRuntimeValueById = PersistentIntMap.empty(); } public SUExecutableSchemaBuilder( SUExecutableSchema executableSchema) { this.schema = assertNotNull(executableSchema).getSchema(); this.coercingByScalarId = executableSchema.getCoercingByScalarId(); + this.enumRuntimeValueById = + executableSchema.getEnumRuntimeValueById(); } public SUExecutableSchemaBuilder scalarCoercing( @@ -96,9 +104,85 @@ private void copyScalarCoercing( ((GraphQLScalarType) sourceType).getCoercing()); } + public SUExecutableSchemaBuilder enumRuntimeValue( + SUEnumType enumType, + String enumValueName, + Object runtimeValue) { + SUEnumType type = assertNotNull(enumType); + String typeName = assertNotNull(type.getName()); + Object value = assertNotNull( + runtimeValue, + "runtimeValue can't be null"); + assertTrue( + schema.getEnumType(typeName) == type, + "The enum type must belong to this SUSchema"); + SUEnumValue enumValue = schema.getEnumValue( + type, + assertNotNull(enumValueName)); + assertTrue( + enumValue != null, + "No enum value named '%s.%s' exists in this SUSchema", + typeName, + enumValueName); + if (enumValueName.equals(value)) { + enumRuntimeValueById = enumRuntimeValueById.remove( + assertNotNull(enumValue).getId()); + return this; + } + enumRuntimeValueById = enumRuntimeValueById.put( + assertNotNull(enumValue).getId(), + value); + return this; + } + + /** + * Copies enum runtime values from a matching {@link GraphQLSchema}. + * + * @param graphQLSchema the source executable schema + * + * @return this builder + */ + public SUExecutableSchemaBuilder enumRuntimeValues( + GraphQLSchema graphQLSchema) { + GraphQLSchema source = assertNotNull(graphQLSchema); + for (SUNamedType type : schema.getTypes()) { + copyEnumRuntimeValues(source, type); + } + return this; + } + + private void copyEnumRuntimeValues( + GraphQLSchema source, + SUNamedType type) { + if (!(type instanceof SUEnumType)) { + return; + } + GraphQLType sourceType = source.getType( + assertNotNull(type.getName())); + assertTrue( + sourceType instanceof GraphQLEnumType, + "The GraphQLSchema has no matching enum named '%s'", + type.getName()); + GraphQLEnumType sourceEnum = (GraphQLEnumType) sourceType; + for (SUEnumValue enumValue : + schema.getEnumValues((SUEnumType) type)) { + GraphQLEnumValueDefinition sourceValue = assertNotNull( + sourceEnum.getValue( + assertNotNull(enumValue.getName())), + "The GraphQLSchema has no matching enum value named '%s.%s'", + type.getName(), + enumValue.getName()); + enumRuntimeValue( + (SUEnumType) type, + assertNotNull(enumValue.getName()), + source.getEnumRuntimeValue(sourceValue)); + } + } + public SUExecutableSchema build() { return new SUExecutableSchema( schema, - coercingByScalarId); + coercingByScalarId, + enumRuntimeValueById); } } diff --git a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy new file mode 100644 index 0000000000..1644fa93f3 --- /dev/null +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy @@ -0,0 +1,535 @@ +package graphql.schema.universe + +import graphql.AssertException +import graphql.Directives +import graphql.GraphQLContext +import graphql.Scalars +import graphql.TestUtil +import graphql.execution.CoercedVariables +import graphql.execution.OneOfTooManyKeysException +import graphql.execution.RawVariables +import graphql.execution.TypeFromAST +import graphql.execution.ValuesResolver +import graphql.execution.values.InputInterceptor +import graphql.execution.values.legacycoercing.LegacyCoercingInputInterceptor +import graphql.language.AstPrinter +import graphql.language.Field +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.OperationDefinition +import graphql.language.TypeName +import graphql.language.Value +import graphql.normalized.NormalizedInputValue +import graphql.schema.GraphQLArgument +import graphql.schema.GraphQLEnumType +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLInputObjectField +import graphql.schema.GraphQLInputObjectType +import graphql.schema.GraphQLInputType +import graphql.schema.GraphQLList +import graphql.schema.GraphQLNonNull +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLScalarType +import graphql.schema.GraphQLSchema +import graphql.schema.GraphQLType +import graphql.schema.InputValueWithState +import graphql.schema.SchemaEnum +import graphql.schema.SchemaField +import graphql.schema.universe.view.SUExecutableSchema +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +class SUExecutableSchemaValuesResolverTest extends Specification { + + @Shared + GraphQLSchema graphQLSchema + + @Shared + SUExecutableSchema universeSchema + + def setupSpec() { + graphQLSchema = schema() + def imported = new SchemaUniverse() + .importSchema("values_resolver", graphQLSchema) + universeSchema = SUExecutableSchema.fromGraphQLSchema( + imported, + graphQLSchema) + } + + def "variable coercion and normalization match GraphQLSchema"() { + given: + def operation = operation(''' + query Resolve( + $filter: Filter! + $modes: [Mode!]! + $custom: Custom! + $choice: Choice = { id: "default" } + ) { + resolve( + filter: $filter + modes: $modes + custom: $custom + choice: $choice + ) + } + ''') + def rawVariables = RawVariables.of([ + filter: [ + required: 3, + numbers : [4, 5], + choice : [name: "Ada"] + ], + modes : ["FAST", "SLOW"], + custom: 6 + ]) + def context = GraphQLContext.newContext() + .put( + InputInterceptor.class, + LegacyCoercingInputInterceptor.migratesValues()) + .build() + + when: + def graphQLValues = ValuesResolver.coerceVariableValues( + graphQLSchema, + operation.variableDefinitions, + rawVariables, + context, + Locale.ENGLISH) + def universeValues = ValuesResolver.coerceVariableValues( + universeSchema, + operation.variableDefinitions, + rawVariables, + context, + Locale.ENGLISH) + + then: + universeValues.toMap() == graphQLValues.toMap() + universeValues.toMap() == [ + filter: [ + required : 3, + mode : 22, + legacyMode : 11, + numbers : [4, 5], + choice : [name: "Ada"], + legacyNumbers : [7, 8], + legacyChoice : [id: "legacy"], + legacyRequired: 10, + legacyNull : null + ], + modes : [11, 22], + custom: 6, + choice: [id: "default"] + ] + + when: + def graphQLNormalized = ValuesResolver.getNormalizedVariableValues( + graphQLSchema, + operation.variableDefinitions, + rawVariables, + context, + Locale.ENGLISH) + def universeNormalized = ValuesResolver.getNormalizedVariableValues( + universeSchema, + operation.variableDefinitions, + rawVariables, + context, + Locale.ENGLISH) + + then: + normalizedSnapshot(universeNormalized.toMap()) == + normalizedSnapshot(graphQLNormalized.toMap()) + universeNormalized.toMap().filter.value.mode.value.name == "SLOW" + universeNormalized.toMap() + .filter.value.legacyMode.value.name == "FAST" + universeNormalized.toMap() + .filter.value.legacyNumbers.value.values*.value == [7, 8] + universeNormalized.toMap() + .filter.value.legacyChoice.value + .objectFields[0].value.value == "legacy" + universeNormalized.toMap() + .filter.value.legacyRequired.value.value == 10 + universeNormalized.toMap() + .filter.value.legacyNull.value.class.simpleName == "NullValue" + } + + def "literal argument coercion matches GraphQLSchema"() { + given: + def operation = operation(''' + query Resolve($custom: Custom!) { + resolve( + filter: { + required: 3 + numbers: [4, 5] + choice: { id: "literal" } + } + modes: [FAST, SLOW] + custom: $custom + choice: { name: "Ada" } + ) + } + ''') + Field field = operation.selectionSet.selections[0] as Field + def variables = CoercedVariables.of([custom: 6]) + + when: + def graphQLValues = ValuesResolver.getArgumentValues( + graphQLSchema, + graphQLSchema.queryType.getFieldDefinition("resolve").arguments, + field.arguments, + variables, + GraphQLContext.getDefault(), + Locale.ENGLISH) + SchemaField universeField = universeSchema.getField( + universeSchema.queryType, + "resolve") + def universeValues = ValuesResolver.getArgumentValues( + universeSchema, + universeField.arguments, + field.arguments, + variables, + GraphQLContext.getDefault(), + Locale.ENGLISH) + + then: + universeValues == graphQLValues + universeValues == [ + filter : [ + required : 3, + mode : 22, + legacyMode : 11, + numbers : [4, 5], + choice : [id: "literal"], + legacyNumbers : [7, 8], + legacyChoice : [id: "legacy"], + legacyRequired: 10, + legacyNull : null + ], + modes : [11, 22], + custom : 6, + choice : [name: "Ada"], + optional: 9 + ] + } + + def "oneOf variable and literal failures match GraphQLSchema"() { + given: + def variableOperation = operation(''' + query Resolve($choice: Choice) { + resolve(choice: $choice) + } + ''') + + when: + ValuesResolver.coerceVariableValues( + universeSchema, + variableOperation.variableDefinitions, + RawVariables.of([choice: [name: "Ada", id: "1"]]), + GraphQLContext.getDefault(), + Locale.ENGLISH) + + then: + def universeVariableError = thrown(OneOfTooManyKeysException) + + when: + ValuesResolver.coerceVariableValues( + graphQLSchema, + variableOperation.variableDefinitions, + RawVariables.of([choice: [name: "Ada", id: "1"]]), + GraphQLContext.getDefault(), + Locale.ENGLISH) + + then: + def graphQLVariableError = thrown(OneOfTooManyKeysException) + universeVariableError.message == graphQLVariableError.message + + when: + def literalOperation = operation(''' + { + resolve( + filter: { required: 1 } + choice: { name: "Ada", id: "1" } + ) + } + ''') + Field field = literalOperation.selectionSet.selections[0] as Field + SchemaField universeField = universeSchema.getField( + universeSchema.queryType, + "resolve") + ValuesResolver.getArgumentValues( + universeSchema, + universeField.arguments, + field.arguments, + CoercedVariables.emptyVariables(), + GraphQLContext.getDefault(), + Locale.ENGLISH) + + then: + def universeLiteralError = thrown(OneOfTooManyKeysException) + + when: + ValuesResolver.getArgumentValues( + graphQLSchema, + graphQLSchema.queryType.getFieldDefinition("resolve").arguments, + field.arguments, + CoercedVariables.emptyVariables(), + GraphQLContext.getDefault(), + Locale.ENGLISH) + + then: + def graphQLLiteralError = thrown(OneOfTooManyKeysException) + universeLiteralError.message == graphQLLiteralError.message + } + + @Unroll + def "variable coercion failure matches GraphQLSchema for #scenario"() { + given: + def operation = operation(''' + query Resolve($filter: Filter) { + resolve(filter: $filter) + } + ''') + def variables = RawVariables.of([filter: filter]) + + expect: + failureSnapshot { + ValuesResolver.coerceVariableValues( + universeSchema, + operation.variableDefinitions, + variables, + GraphQLContext.getDefault(), + Locale.ENGLISH) + } == failureSnapshot { + ValuesResolver.coerceVariableValues( + graphQLSchema, + operation.variableDefinitions, + variables, + GraphQLContext.getDefault(), + Locale.ENGLISH) + } + + where: + scenario | filter + "unknown input field" | [required: 1, unknown: true] + "missing required field" | [numbers: [1]] + "invalid enum name" | [required: 1, mode: "UNKNOWN"] + "invalid custom scalar" | [required: 1, numbers: ["wrong"]] + "null non-null element" | [required: 1, numbers: [null]] + } + + def "enum runtime values are view-specific and structurally persistent"() { + given: + SchemaEnum originalEnum = universeSchema.getType("Mode") as SchemaEnum + def modeVertex = universeSchema.schema.getEnumType("Mode") + def fastVertex = universeSchema.schema.getEnumValue(modeVertex, "FAST") + def slowVertex = universeSchema.schema.getEnumValue(modeVertex, "SLOW") + + expect: + universeSchema.getEnumRuntimeValue( + originalEnum.getValue("FAST")) == 11 + universeSchema.getEnumRuntimeValue( + originalEnum.getValue("SLOW")) == 22 + + when: + def changed = universeSchema.transform { + it.enumRuntimeValue(modeVertex, "FAST", 99) + it.enumRuntimeValue(modeVertex, "SLOW", "SLOW") + } + SchemaEnum changedEnum = changed.getType("Mode") as SchemaEnum + + then: + universeSchema.getEnumRuntimeValue( + originalEnum.getValue("FAST")) == 11 + universeSchema.getEnumRuntimeValue( + originalEnum.getValue("SLOW")) == 22 + changed.getEnumRuntimeValue(changedEnum.getValue("FAST")) == 99 + changed.getEnumRuntimeValue(changedEnum.getValue("SLOW")) == "SLOW" + changed.getEnumRuntimeValueById().get(fastVertex.id) == 99 + changed.getEnumRuntimeValueById().get(slowVertex.id) == null + } + + def "enum runtime values reject null"() { + given: + def modeVertex = universeSchema.schema.getEnumType("Mode") + + when: + universeSchema.transform { + it.enumRuntimeValue(modeVertex, "FAST", null) + } + + then: + thrown(AssertException) + } + + def "wrapped missing AST types remain unresolved"() { + expect: + TypeFromAST.getSchemaTypeFromAST( + universeSchema, + new ListType(new TypeName("Missing"))) == null + TypeFromAST.getSchemaTypeFromAST( + universeSchema, + new NonNullType(new TypeName("Missing"))) == null + } + + def "literal conversion includes input field defaults"() { + given: + GraphQLType filter = graphQLSchema.getType("Filter") + + when: + def literal = ValuesResolver.valueToLiteral( + InputValueWithState.newExternalValue([required: 1]), + filter, + GraphQLContext.getDefault(), + Locale.ENGLISH) + + then: + literal.objectFields*.name as Set == [ + "required", + "mode", + "legacyMode", + "legacyNumbers", + "legacyChoice", + "legacyRequired", + "legacyNull" + ] as Set + } + + private static OperationDefinition operation(String query) { + return TestUtil.parseQuery(query) + .getDefinitionsOfType(OperationDefinition)[0] + } + + private static Object normalizedSnapshot(Object value) { + if (value instanceof NormalizedInputValue) { + return [ + type : value.typeName, + value: normalizedSnapshot(value.value) + ] + } + if (value instanceof Value) { + return AstPrinter.printAst(value) + } + if (value instanceof Map) { + return value.collectEntries { + [(it.key): normalizedSnapshot(it.value)] + } + } + if (value instanceof List) { + return value.collect { + normalizedSnapshot(it) + } + } + return value + } + + private static Map failureSnapshot(Closure action) { + try { + action.call() + } catch (RuntimeException exception) { + return [ + type : exception.class, + message: exception.message + ] + } + throw new AssertionError("Expected value coercion to fail") + } + + private static GraphQLSchema schema() { + GraphQLScalarType custom = GraphQLScalarType.newScalar() + .name("Custom") + .coercing(Scalars.GraphQLInt.coercing) + .build() + GraphQLEnumType mode = GraphQLEnumType.newEnum() + .name("Mode") + .value("FAST", 11) + .value("SLOW", 22) + .build() + GraphQLInputObjectType choice = GraphQLInputObjectType + .newInputObject() + .name("Choice") + .withAppliedDirective( + Directives.OneOfDirective.toAppliedDirective()) + .field(inputField("name", Scalars.GraphQLString)) + .field(inputField("id", Scalars.GraphQLID)) + .build() + GraphQLInputObjectType filter = GraphQLInputObjectType + .newInputObject() + .name("Filter") + .field(inputField( + "required", + GraphQLNonNull.nonNull(Scalars.GraphQLInt))) + .field(GraphQLInputObjectField.newInputObjectField() + .name("mode") + .type(mode) + .defaultValueProgrammatic("SLOW")) + .field(GraphQLInputObjectField.newInputObjectField() + .name("legacyMode") + .type(mode) + .defaultValue(11)) + .field(inputField( + "numbers", + GraphQLList.list( + GraphQLNonNull.nonNull(custom)))) + .field(inputField("choice", choice)) + .field(GraphQLInputObjectField.newInputObjectField() + .name("legacyNumbers") + .type(GraphQLList.list(custom)) + .defaultValue([7, 8])) + .field(GraphQLInputObjectField.newInputObjectField() + .name("legacyChoice") + .type(choice) + .defaultValue([id: "legacy"])) + .field(GraphQLInputObjectField.newInputObjectField() + .name("legacyRequired") + .type(GraphQLNonNull.nonNull(custom)) + .defaultValue(10)) + .field(GraphQLInputObjectField.newInputObjectField() + .name("legacyNull") + .type(Scalars.GraphQLString) + .defaultValue(null)) + .build() + GraphQLFieldDefinition resolve = GraphQLFieldDefinition + .newFieldDefinition() + .name("resolve") + .type(Scalars.GraphQLString) + .argument(argument( + "filter", + GraphQLNonNull.nonNull(filter))) + .argument(argument( + "modes", + GraphQLList.list( + GraphQLNonNull.nonNull(mode)))) + .argument(argument("custom", custom)) + .argument(argument("choice", choice)) + .argument(GraphQLArgument.newArgument() + .name("optional") + .type(Scalars.GraphQLInt) + .defaultValueProgrammatic(9)) + .build() + GraphQLObjectType query = GraphQLObjectType.newObject() + .name("Query") + .field(resolve) + .build() + return GraphQLSchema.newSchema() + .query(query) + .build() + } + + private static GraphQLInputObjectField inputField( + String name, + GraphQLInputType type) { + return GraphQLInputObjectField.newInputObjectField() + .name(name) + .type(type) + .build() + } + + private static GraphQLArgument argument( + String name, + GraphQLInputType type) { + return GraphQLArgument.newArgument() + .name(name) + .type(type) + .build() + } +} From 78bab3ff1c7a09094e8e3d59d1aac0b4e1fb6f0b Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 7 Aug 2026 00:11:55 +0000 Subject: [PATCH 22/32] Separate executable schema value resolution --- .../execution/ExecutableInputValueSchema.java | 43 - .../execution/GraphQLInputValueSchema.java | 61 -- .../graphql/execution/InputValueSchema.java | 23 - .../graphql/execution/ValuesResolver.java | 108 ++- .../execution/ValuesResolverConversion.java | 771 ++++++++++++++++-- .../ValuesResolverOneOfValidation.java | 92 ++- ...UExecutableSchemaValuesResolverTest.groovy | 39 +- 7 files changed, 880 insertions(+), 257 deletions(-) delete mode 100644 src/main/java/graphql/execution/ExecutableInputValueSchema.java delete mode 100644 src/main/java/graphql/execution/GraphQLInputValueSchema.java delete mode 100644 src/main/java/graphql/execution/InputValueSchema.java diff --git a/src/main/java/graphql/execution/ExecutableInputValueSchema.java b/src/main/java/graphql/execution/ExecutableInputValueSchema.java deleted file mode 100644 index b982cb7c50..0000000000 --- a/src/main/java/graphql/execution/ExecutableInputValueSchema.java +++ /dev/null @@ -1,43 +0,0 @@ -package graphql.execution; - -import graphql.Internal; -import graphql.schema.Coercing; -import graphql.schema.ExecutableSchema; -import graphql.schema.SchemaEnumValue; -import graphql.schema.SchemaInputField; -import graphql.schema.SchemaInputObject; -import graphql.schema.SchemaScalar; -import org.jspecify.annotations.NullMarked; - -import java.util.List; - -import static graphql.Assert.assertNotNull; - -@Internal -@NullMarked -public final class ExecutableInputValueSchema - implements InputValueSchema { - - private final ExecutableSchema schema; - - public ExecutableInputValueSchema(ExecutableSchema schema) { - this.schema = assertNotNull(schema); - } - - @Override - public List getInputFields( - SchemaInputObject inputObjectType) { - return schema.getInputFields(inputObjectType); - } - - @Override - public Coercing getScalarCoercing(SchemaScalar scalarType) { - return schema.getScalarCoercing(scalarType); - } - - @Override - public Object getEnumRuntimeValue( - SchemaEnumValue enumValue) { - return schema.getEnumRuntimeValue(enumValue); - } -} diff --git a/src/main/java/graphql/execution/GraphQLInputValueSchema.java b/src/main/java/graphql/execution/GraphQLInputValueSchema.java deleted file mode 100644 index ae92656e29..0000000000 --- a/src/main/java/graphql/execution/GraphQLInputValueSchema.java +++ /dev/null @@ -1,61 +0,0 @@ -package graphql.execution; - -import graphql.Internal; -import graphql.schema.Coercing; -import graphql.schema.GraphQLEnumValueDefinition; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLScalarType; -import graphql.schema.SchemaEnumValue; -import graphql.schema.SchemaInputField; -import graphql.schema.SchemaInputObject; -import graphql.schema.SchemaScalar; -import graphql.schema.visibility.GraphqlFieldVisibility; -import org.jspecify.annotations.NullMarked; - -import java.util.List; - -import static graphql.Assert.assertNotNull; -import static graphql.Assert.assertTrue; - -@Internal -@NullMarked -public final class GraphQLInputValueSchema - implements InputValueSchema { - - private final GraphqlFieldVisibility fieldVisibility; - - public GraphQLInputValueSchema( - GraphqlFieldVisibility fieldVisibility) { - this.fieldVisibility = assertNotNull(fieldVisibility); - } - - @Override - public List getInputFields( - SchemaInputObject inputObjectType) { - assertTrue( - inputObjectType instanceof GraphQLInputObjectType, - "Expected a GraphQLInputObjectType"); - return fieldVisibility.getFieldDefinitions( - (GraphQLInputObjectType) inputObjectType); - } - - @Override - public Coercing getScalarCoercing(SchemaScalar scalarType) { - assertTrue( - scalarType instanceof GraphQLScalarType, - "Expected a GraphQLScalarType"); - return ((GraphQLScalarType) scalarType).getCoercing(); - } - - @Override - public Object getEnumRuntimeValue( - SchemaEnumValue enumValue) { - assertTrue( - enumValue instanceof GraphQLEnumValueDefinition, - "Expected a GraphQLEnumValueDefinition"); - return assertNotNull( - ((GraphQLEnumValueDefinition) enumValue).getValue(), - "The enum value '%s' must have a runtime value", - enumValue.getName()); - } -} diff --git a/src/main/java/graphql/execution/InputValueSchema.java b/src/main/java/graphql/execution/InputValueSchema.java deleted file mode 100644 index dae0e6298d..0000000000 --- a/src/main/java/graphql/execution/InputValueSchema.java +++ /dev/null @@ -1,23 +0,0 @@ -package graphql.execution; - -import graphql.Internal; -import graphql.schema.Coercing; -import graphql.schema.SchemaEnumValue; -import graphql.schema.SchemaInputField; -import graphql.schema.SchemaInputObject; -import graphql.schema.SchemaScalar; -import org.jspecify.annotations.NullMarked; - -import java.util.List; - -@Internal -@NullMarked -public interface InputValueSchema { - - List getInputFields( - SchemaInputObject inputObjectType); - - Coercing getScalarCoercing(SchemaScalar scalarType); - - Object getEnumRuntimeValue(SchemaEnumValue enumValue); -} diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index f738bf7c63..258d727f01 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -83,8 +83,11 @@ public static CoercedVariables coerceVariableValues(GraphQLSchema schema, RawVariables rawVariables, GraphQLContext graphqlContext, Locale locale) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { - return coerceVariableValues( - (ExecutableSchema) schema, + + InputInterceptor inputInterceptor = graphqlContext.get(InputInterceptor.class); + return ValuesResolverConversion.externalValueToInternalValueForVariables( + inputInterceptor, + schema, variableDefinitions, rawVariables, graphqlContext, @@ -128,12 +131,30 @@ public static NormalizedVariables getNormalizedVariableValues( GraphQLContext graphqlContext, Locale locale ) { - return getNormalizedVariableValues( - (ExecutableSchema) schema, - variableDefinitions, - rawVariables, - graphqlContext, - locale); + GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); + Map result = Maps.newLinkedHashMapWithExpectedSize(variableDefinitions.size()); + for (VariableDefinition variableDefinition : variableDefinitions) { + String variableName = variableDefinition.getName(); + GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); + assertTrue(variableType instanceof GraphQLInputType); + // can be NullValue + Value defaultValue = variableDefinition.getDefaultValue(); + boolean hasValue = rawVariables.containsKey(variableName); + Object value = rawVariables.get(variableName); + if (!hasValue && defaultValue != null) { + result.put(variableName, new NormalizedInputValue(simplePrint(variableType), defaultValue)); + } else if (isNonNull(variableType) && (!hasValue || value == null)) { + return assertShouldNeverHappen("variable values are expected to be valid"); + } else if (hasValue) { + if (value == null) { + result.put(variableName, new NormalizedInputValue(simplePrint(variableType), null)); + } else { + Object literal = ValuesResolverConversion.externalValueToLiteral(fieldVisibility, value, (GraphQLInputType) variableType, NORMALIZED, graphqlContext, locale); + result.put(variableName, new NormalizedInputValue(simplePrint(variableType), literal)); + } + } + } + return NormalizedVariables.of(result); } public static NormalizedVariables getNormalizedVariableValues( @@ -143,8 +164,6 @@ public static NormalizedVariables getNormalizedVariableValues( GraphQLContext graphqlContext, Locale locale ) { - InputValueSchema inputValueSchema = - new ExecutableInputValueSchema(schema); Map result = Maps.newLinkedHashMapWithExpectedSize(variableDefinitions.size()); for (VariableDefinition variableDefinition : variableDefinitions) { String variableName = variableDefinition.getName(); @@ -165,7 +184,7 @@ public static NormalizedVariables getNormalizedVariableValues( result.put(variableName, new NormalizedInputValue(simplePrint(variableType), null)); } else { Object literal = ValuesResolverConversion.externalValueToLiteral( - inputValueSchema, + schema, value, (SchemaInputType) variableType, NORMALIZED, @@ -213,7 +232,7 @@ public static Map getArgumentValues( graphqlContext.get(InputInterceptor.class); return getArgumentValuesImpl( inputInterceptor, - new ExecutableInputValueSchema(schema), + schema, argumentTypes, arguments, coercedVariables, @@ -390,20 +409,67 @@ private static Map getArgumentValuesImpl( GraphQLContext graphqlContext, Locale locale ) { - return getArgumentValuesImpl( - inputInterceptor, - new GraphQLInputValueSchema(fieldVisibility), - argumentTypes, - arguments, - coercedVariables, - graphqlContext, - locale); + if (argumentTypes.isEmpty()) { + return ImmutableKit.emptyMap(); + } + + Map coercedValues = Maps.newLinkedHashMapWithExpectedSize(arguments.size()); + Map argumentMap = argumentMap(arguments); + for (GraphQLArgument argumentDefinition : argumentTypes) { + GraphQLInputType argumentType = argumentDefinition.getType(); + String argumentName = argumentDefinition.getName(); + Argument argument = argumentMap.get(argumentName); + InputValueWithState defaultValue = argumentDefinition.getArgumentDefaultValue(); + boolean hasValue = argument != null; + Object value; + Value argumentValue = argument != null ? argument.getValue() : null; + if (argumentValue instanceof VariableReference) { + String variableName = ((VariableReference) argumentValue).getName(); + hasValue = coercedVariables.containsKey(variableName); + value = coercedVariables.get(variableName); + } else { + value = argumentValue; + } + if (!hasValue && argumentDefinition.hasSetDefaultValue()) { + Object coercedDefaultValue = ValuesResolverConversion.defaultValueToInternalValue( + inputInterceptor, + fieldVisibility, + defaultValue, + argumentType, + graphqlContext, + locale); + coercedValues.put(argumentName, coercedDefaultValue); + } else if (isNonNull(argumentType) && (!hasValue || ValuesResolverConversion.isNullValue(value))) { + throw new NonNullableValueCoercedAsNullException(argumentDefinition); + } else if (hasValue) { + if (ValuesResolverConversion.isNullValue(value)) { + coercedValues.put(argumentName, value); + } else if (argumentValue instanceof VariableReference) { + coercedValues.put(argumentName, value); + } else { + value = ValuesResolverConversion.literalToInternalValue(inputInterceptor, + fieldVisibility, + argumentType, + argument.getValue(), + coercedVariables, + graphqlContext, + locale); + coercedValues.put(argumentName, value); + } + + ValuesResolverOneOfValidation.validateOneOfInputTypes(argumentType, value, argumentValue, argumentName, locale); + + } + } + + + return coercedValues; } @NonNull private static Map getArgumentValuesImpl( InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, List argumentTypes, List arguments, CoercedVariables coercedVariables, diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index 2dbbc54e3a..a859d47ec8 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -16,7 +16,13 @@ import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.ExecutableSchema; +import graphql.schema.GraphQLEnumType; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLScalarType; +import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; import graphql.schema.InputValueWithState; @@ -54,6 +60,8 @@ import static graphql.schema.GraphQLTypeUtil.isList; import static graphql.schema.GraphQLTypeUtil.isNonNull; import static graphql.schema.GraphQLTypeUtil.simplePrint; +import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; +import static graphql.schema.GraphQLTypeUtil.unwrapOneAs; import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; /** @@ -70,17 +78,39 @@ static Object valueToLiteralImpl(GraphqlFieldVisibility fieldVisibility, ValuesResolver.ValueMode valueMode, GraphQLContext graphqlContext, Locale locale) { - return valueToLiteralImpl( - new GraphQLInputValueSchema(fieldVisibility), - inputValueWithState, - (SchemaType) type, - valueMode, - graphqlContext, - locale); + if (inputValueWithState.isInternal()) { + if (valueMode == NORMALIZED) { + return assertShouldNeverHappen("can't infer normalized structure"); + } + Value value = ValuesResolverLegacy.valueToLiteralLegacy( + inputValueWithState.getValue(), + type, + graphqlContext, + locale); + // + // the valueToLiteralLegacy() nominally cant know if null means never set or is set to a null value + // but this code can know - its is SET to a value so, it MUST be a Null Literal + // this method would assert at the end of it if inputValueWithState.isNotSet() were true + // + return value == null ? NullValue.of() : value; + } + if (inputValueWithState.isLiteral()) { + return inputValueWithState.getValue(); + } + if (inputValueWithState.isExternal()) { + return externalValueToLiteral( + fieldVisibility, + inputValueWithState.getValue(), + (GraphQLInputType) type, + valueMode, + graphqlContext, + locale); + } + return assertShouldNeverHappen("unexpected value state " + inputValueWithState); } static Object valueToLiteralImpl( - InputValueSchema schema, + ExecutableSchema schema, InputValueWithState inputValueWithState, SchemaType type, ValuesResolver.ValueMode valueMode, @@ -192,17 +222,53 @@ static Object externalValueToLiteral( GraphQLContext graphqlContext, Locale locale ) { - return externalValueToLiteral( - new GraphQLInputValueSchema(fieldVisibility), - value, - type, - valueMode, - graphqlContext, - locale); + if (value == null) { + return newNullValue().build(); + } + if (GraphQLTypeUtil.isNonNull(type)) { + return externalValueToLiteral( + fieldVisibility, + value, + (GraphQLInputType) unwrapNonNull(type), + valueMode, + graphqlContext, + locale); + } + if (type instanceof GraphQLScalarType) { + return externalValueToLiteralForScalar( + (GraphQLScalarType) type, + value, + graphqlContext, + locale); + } else if (type instanceof GraphQLEnumType) { + return externalValueToLiteralForEnum( + (GraphQLEnumType) type, + value, + graphqlContext, + locale); + } else if (type instanceof GraphQLList) { + return externalValueToLiteralForList( + fieldVisibility, + (GraphQLList) type, + value, + valueMode, + graphqlContext, + locale); + } else if (type instanceof GraphQLInputObjectType) { + return externalValueToLiteralForObject( + fieldVisibility, + (GraphQLInputObjectType) type, + value, + valueMode, + graphqlContext, + locale); + } else { + return assertShouldNeverHappen("unexpected type %s", type); + } } static Object externalValueToLiteral( - InputValueSchema schema, + ExecutableSchema schema, @Nullable Object value, SchemaInputType type, ValuesResolver.ValueMode valueMode, @@ -259,7 +325,132 @@ static Object externalValueToLiteral( * No validation */ private static Value externalValueToLiteralForScalar( - InputValueSchema schema, + GraphQLScalarType scalarType, + Object value, + GraphQLContext graphqlContext, + @NonNull Locale locale + ) { + return scalarType.getCoercing().valueToLiteral(value, graphqlContext, locale); + + } + + /** + * No validation + */ + private static Value externalValueToLiteralForEnum( + GraphQLEnumType enumType, + Object value, + GraphQLContext graphqlContext, + Locale locale) { + return enumType.valueToLiteral( + value, + graphqlContext, + locale); + } + + /** + * No validation + */ + @SuppressWarnings("unchecked") + private static Object externalValueToLiteralForList( + GraphqlFieldVisibility fieldVisibility, + GraphQLList listType, + Object value, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, + Locale locale + ) { + GraphQLInputType wrappedType = (GraphQLInputType) listType.getWrappedType(); + List valueList = FpKit.toListOrSingletonList(value); + ImmutableList.Builder resultBuilder = ImmutableList.builderWithExpectedSize(valueList.size()); + for (Object item : valueList) { + resultBuilder.add(externalValueToLiteral( + fieldVisibility, + item, + wrappedType, + valueMode, + graphqlContext, + locale)); + } + ImmutableList result = resultBuilder.build(); + + if (valueMode == NORMALIZED) { + return result; + } else { + return ArrayValue.newArrayValue().values((ImmutableList) result).build(); + } + } + + /** + * No validation + */ + @SuppressWarnings("unchecked") + private static Object externalValueToLiteralForObject( + GraphqlFieldVisibility fieldVisibility, + GraphQLInputObjectType inputObjectType, + Object inputValue, + ValuesResolver.ValueMode valueMode, + GraphQLContext graphqlContext, + Locale locale + ) { + assertTrue(inputValue instanceof Map, "Expect Map as input"); + Map inputMap = (Map) inputValue; + List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); + + Map normalizedResult = new LinkedHashMap<>(); + ImmutableList.Builder objectFields = ImmutableList.builder(); + for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { + GraphQLInputType fieldType = inputFieldDefinition.getType(); + String fieldName = inputFieldDefinition.getName(); + boolean hasValue = inputMap.containsKey(fieldName); + Object fieldValue = inputMap.getOrDefault(fieldName, null); + if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + //TODO: consider valueMode + Object defaultValueLiteral = valueToLiteralImpl( + fieldVisibility, + inputFieldDefinition.getInputFieldDefaultValue(), + fieldType, + ValuesResolver.ValueMode.LITERAL, + graphqlContext, + locale); + if (valueMode == ValuesResolver.ValueMode.LITERAL) { + normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), defaultValueLiteral)); + } else { + objectFields.add(newObjectField().name(fieldName).value((Value) defaultValueLiteral).build()); + } + } else if (hasValue) { + if (fieldValue == null) { + if (valueMode == NORMALIZED) { + normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), null)); + } else { + objectFields.add(newObjectField().name(fieldName).value(newNullValue().build()).build()); + } + } else { + Object literal = externalValueToLiteral( + fieldVisibility, + fieldValue, + fieldType, + valueMode, + graphqlContext, locale); + if (valueMode == NORMALIZED) { + normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), literal)); + } else { + objectFields.add(newObjectField().name(fieldName).value((Value) literal).build()); + } + } + } + } + if (valueMode == NORMALIZED) { + return normalizedResult; + } + return ObjectValue.newObjectValue().objectFields(objectFields.build()).build(); + } + + /** + * No validation + */ + private static Value externalValueToLiteralForScalar( + ExecutableSchema schema, SchemaScalar scalarType, Object value, GraphQLContext graphqlContext, @@ -293,7 +484,7 @@ private static Value externalValueToLiteralForEnum( */ @SuppressWarnings("unchecked") private static Object externalValueToLiteralForList( - InputValueSchema schema, + ExecutableSchema schema, SchemaList listType, Object value, ValuesResolver.ValueMode valueMode, @@ -327,7 +518,7 @@ private static Object externalValueToLiteralForList( */ @SuppressWarnings("unchecked") private static Object externalValueToLiteralForObject( - InputValueSchema schema, + ExecutableSchema schema, SchemaInputObject inputObjectType, Object inputValue, ValuesResolver.ValueMode valueMode, @@ -357,7 +548,7 @@ private static Object externalValueToLiteralForObject( ValuesResolver.ValueMode.LITERAL, graphqlContext, locale); - if (valueMode == NORMALIZED) { + if (valueMode == ValuesResolver.ValueMode.LITERAL) { normalizedResult.put( fieldName, new NormalizedInputValue( @@ -395,7 +586,7 @@ private static Object externalValueToLiteralForObject( } private static Value internalValueToLiteral( - InputValueSchema schema, + ExecutableSchema schema, @Nullable Object value, SchemaType type, GraphQLContext graphqlContext, @@ -441,7 +632,7 @@ private static Value internalValueToLiteral( } private static Value internalEnumValueToLiteral( - InputValueSchema schema, + ExecutableSchema schema, Object value, SchemaEnum enumType) { for (SchemaEnumValue enumValue : enumType.getValues()) { @@ -468,7 +659,7 @@ private static Value internalEnumValueToLiteral( @SuppressWarnings("unchecked") private static Value internalListValueToLiteral( - InputValueSchema schema, + ExecutableSchema schema, Object value, SchemaList listType, GraphQLContext graphqlContext, @@ -489,7 +680,7 @@ private static Value internalListValueToLiteral( @SuppressWarnings("unchecked") private static Value internalObjectValueToLiteral( - InputValueSchema schema, + ExecutableSchema schema, Object value, SchemaInputObject inputObjectType, GraphQLContext graphqlContext, @@ -518,6 +709,70 @@ private static Value internalObjectValueToLiteral( .build(); } + /** + * performs validation too + */ + static CoercedVariables externalValueToInternalValueForVariables( + InputInterceptor inputInterceptor, + GraphQLSchema schema, + List variableDefinitions, + RawVariables rawVariables, + GraphQLContext graphqlContext, Locale locale + ) { + GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); + Map coercedValues = new LinkedHashMap<>(); + for (VariableDefinition variableDefinition : variableDefinitions) { + try { + String variableName = variableDefinition.getName(); + GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); + assertTrue(variableType instanceof GraphQLInputType); + GraphQLInputType variableInputType = (GraphQLInputType) variableType; + // can be NullValue + Value defaultValue = variableDefinition.getDefaultValue(); + boolean hasValue = rawVariables.containsKey(variableName); + Object value = rawVariables.get(variableName); + if (!hasValue && defaultValue != null) { + Object coercedDefaultValue = literalToInternalValue( + inputInterceptor, + fieldVisibility, + variableInputType, + defaultValue, + CoercedVariables.emptyVariables(), + graphqlContext, + locale); + coercedValues.put(variableName, coercedDefaultValue); + } else if (isNonNull(variableType) && (!hasValue || value == null)) { + throw new NonNullableValueCoercedAsNullException(variableDefinition, variableType); + } else if (hasValue) { + if (value == null) { + coercedValues.put(variableName, null); + } else { + Object coercedValue = externalValueToInternalValueImpl( + variableName, + inputInterceptor, + fieldVisibility, + variableInputType, + value, + graphqlContext, + locale); + coercedValues.put(variableName, coercedValue); + } + } + } catch (CoercingParseValueException e) { + throw CoercingParseValueException.newCoercingParseValueException() + .message(String.format("Variable '%s' has an invalid value: %s", variableDefinition.getName(), e.getMessage())) + .extensions(e.getExtensions()) + .cause(e.getCause()) + .sourceLocation(variableDefinition.getSourceLocation()) + .build(); + } catch (NonNullableValueCoercedAsNullException e) { + throw new NonNullableValueCoercedAsNullException(variableDefinition, e.getMessage()); + } + } + + return CoercedVariables.of(coercedValues); + } + /** * performs validation too */ @@ -528,8 +783,6 @@ static CoercedVariables externalValueToInternalValueForVariables( RawVariables rawVariables, GraphQLContext graphqlContext, Locale locale ) { - InputValueSchema inputValueSchema = - new ExecutableInputValueSchema(schema); Map coercedValues = new LinkedHashMap<>(); for (VariableDefinition variableDefinition : variableDefinitions) { try { @@ -548,7 +801,7 @@ static CoercedVariables externalValueToInternalValueForVariables( if (!hasValue && defaultValue != null) { Object coercedDefaultValue = literalToInternalValue( inputInterceptor, - inputValueSchema, + schema, variableInputType, defaultValue, CoercedVariables.emptyVariables(), @@ -564,7 +817,7 @@ static CoercedVariables externalValueToInternalValueForVariables( Object coercedValue = externalValueToInternalValueImpl( variableName, inputInterceptor, - inputValueSchema, + schema, variableInputType, value, graphqlContext, @@ -595,10 +848,9 @@ static Object externalValueToInternalValueImpl( GraphQLContext graphqlContext, Locale locale ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - return externalValueToInternalValueImpl( - "externalValue", + return externalValueToInternalValueImpl("externalValue", inputInterceptor, - new GraphQLInputValueSchema(fieldVisibility), + fieldVisibility, graphQLType, originalValue, graphqlContext, @@ -617,25 +869,87 @@ static Object externalValueToInternalValueImpl( GraphQLContext graphqlContext, Locale locale ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - return externalValueToInternalValueImpl( - variableName, - inputInterceptor, - new GraphQLInputValueSchema(fieldVisibility), - graphQLType, - originalValue, - graphqlContext, - locale); - } - - static Object externalValueToInternalValueImpl( - InputInterceptor inputInterceptor, - InputValueSchema schema, - SchemaInputType type, - Object originalValue, - GraphQLContext graphqlContext, - Locale locale - ) throws NonNullableValueCoercedAsNullException, - CoercingParseValueException { + if (isNonNull(graphQLType)) { + Object returnValue = externalValueToInternalValueImpl( + variableName, + inputInterceptor, + fieldVisibility, + unwrapOneAs(graphQLType), + originalValue, + graphqlContext, + locale); + if (returnValue == null) { + throw new NonNullableValueCoercedAsNullException(graphQLType); + } + return returnValue; + } + // + // we have a @Internal hook that allows input values to be changed before they are + // presented to scalars and enums - if it's not present then the cost is an extra `if` + // statement. We expect this to be NOT present most of the time + // + Object value = originalValue; + if (inputInterceptor != null) { + value = inputInterceptor.intercept(originalValue, graphQLType, graphqlContext, locale); + } + if (value == null) { + return null; + } + + if (graphQLType instanceof GraphQLScalarType) { + return externalValueToInternalValueForScalar( + (GraphQLScalarType) graphQLType, + value, + graphqlContext, + locale); + } else if (graphQLType instanceof GraphQLEnumType) { + return externalValueToInternalValueForEnum( + (GraphQLEnumType) graphQLType, + value, + graphqlContext, + locale); + } else if (graphQLType instanceof GraphQLList) { + return externalValueToInternalValueForList( + inputInterceptor, + fieldVisibility, + (GraphQLList) graphQLType, + value, + graphqlContext, + locale); + } else if (graphQLType instanceof GraphQLInputObjectType) { + if (value instanceof Map) { + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) graphQLType; + //noinspection unchecked + Map coercedMap = externalValueToInternalValueForObject( + inputInterceptor, + fieldVisibility, + inputObjectType, + (Map) value, + graphqlContext, + locale); + + ValuesResolverOneOfValidation.validateOneOfInputTypes(inputObjectType, coercedMap, null, variableName, locale); + return coercedMap; + } else { + throw CoercingParseValueException.newCoercingParseValueException() + .message("Expected type 'Map' but was '" + value.getClass().getSimpleName() + + "'. Variables for input objects must be an instance of type 'Map'.") + .build(); + } + } else { + return assertShouldNeverHappen("unhandled type %s", graphQLType); + } + } + + static Object externalValueToInternalValueImpl( + InputInterceptor inputInterceptor, + ExecutableSchema schema, + SchemaInputType type, + Object originalValue, + GraphQLContext graphqlContext, + Locale locale + ) throws NonNullableValueCoercedAsNullException, + CoercingParseValueException { return externalValueToInternalValueImpl( "externalValue", inputInterceptor, @@ -649,7 +963,7 @@ static Object externalValueToInternalValueImpl( static Object externalValueToInternalValueImpl( String variableName, InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, SchemaInputType type, Object originalValue, GraphQLContext graphqlContext, @@ -746,7 +1060,120 @@ static Object externalValueToInternalValueImpl( */ private static Map externalValueToInternalValueForObject( InputInterceptor inputInterceptor, - InputValueSchema schema, + GraphqlFieldVisibility fieldVisibility, + GraphQLInputObjectType inputObjectType, + Map inputMap, + GraphQLContext graphqlContext, + Locale locale + ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { + List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); + List fieldNames = map(fieldDefinitions, GraphQLInputObjectField::getName); + for (String providedFieldName : inputMap.keySet()) { + if (!fieldNames.contains(providedFieldName)) { + throw new InputMapDefinesTooManyFieldsException(inputObjectType, providedFieldName); + } + } + + Map coercedValues = new LinkedHashMap<>(); + + for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { + GraphQLInputType fieldType = inputFieldDefinition.getType(); + String fieldName = inputFieldDefinition.getName(); + InputValueWithState defaultValue = inputFieldDefinition.getInputFieldDefaultValue(); + boolean hasValue = inputMap.containsKey(fieldName); + Object value = inputMap.getOrDefault(fieldName, null); + if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + Object coercedDefaultValue = defaultValueToInternalValue( + inputInterceptor, + fieldVisibility, + defaultValue, + fieldType, + graphqlContext, + locale); + coercedValues.put(fieldName, coercedDefaultValue); + } else if (isNonNull(fieldType) && (!hasValue || value == null)) { + throw new NonNullableValueCoercedAsNullException(fieldName, emptyList(), fieldType); + } else if (hasValue) { + if (value == null) { + coercedValues.put(fieldName, null); + } else { + value = externalValueToInternalValueImpl( + inputInterceptor, + fieldVisibility, + fieldType, + value, + graphqlContext, + locale); + coercedValues.put(fieldName, value); + } + } + } + return coercedValues; + } + + /** + * including validation + */ + private static Object externalValueToInternalValueForScalar( + GraphQLScalarType graphQLScalarType, + Object value, + GraphQLContext graphqlContext, + Locale locale + ) throws CoercingParseValueException { + return graphQLScalarType.getCoercing().parseValue( + value, + graphqlContext, + locale); + } + + /** + * including validation + */ + private static Object externalValueToInternalValueForEnum( + GraphQLEnumType graphQLEnumType, + Object value, + GraphQLContext graphqlContext, + Locale locale + ) throws CoercingParseValueException { + return graphQLEnumType.parseValue( + value, + graphqlContext, + locale); + } + + /** + * including validation + */ + private static List externalValueToInternalValueForList( + InputInterceptor inputInterceptor, + GraphqlFieldVisibility fieldVisibility, + GraphQLList graphQLList, + Object value, + GraphQLContext graphqlContext, + Locale locale + ) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { + + GraphQLInputType wrappedType = (GraphQLInputType) graphQLList.getWrappedType(); + List listOrSingletonList = FpKit.toListOrSingletonList(value); + List list = FpKit.arrayListSizedTo(listOrSingletonList); + for (Object val : listOrSingletonList) { + list.add(externalValueToInternalValueImpl( + inputInterceptor, + fieldVisibility, + wrappedType, + val, + graphqlContext, + locale)); + } + return list; + } + + /** + * performs validation + */ + private static Map externalValueToInternalValueForObject( + InputInterceptor inputInterceptor, + ExecutableSchema schema, SchemaInputObject inputObjectType, Map inputMap, GraphQLContext graphqlContext, @@ -804,7 +1231,7 @@ private static Map externalValueToInternalValueForObject( * including validation */ private static Object externalValueToInternalValueForScalar( - InputValueSchema schema, + ExecutableSchema schema, SchemaScalar scalarType, Object value, GraphQLContext graphqlContext, @@ -820,7 +1247,7 @@ private static Object externalValueToInternalValueForScalar( * including validation */ private static Object externalValueToInternalValueForEnum( - InputValueSchema schema, + ExecutableSchema schema, SchemaEnum enumType, Object value, Locale locale @@ -843,7 +1270,7 @@ private static Object externalValueToInternalValueForEnum( */ private static List externalValueToInternalValueForList( InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, SchemaList listType, Object value, GraphQLContext graphqlContext, @@ -887,9 +1314,9 @@ static Object literalToInternalValue( GraphQLContext graphqlContext, Locale locale ) { - return literalToInternalValue( + return literalToInternalValueImpl( inputInterceptor, - new GraphQLInputValueSchema(fieldVisibility), + fieldVisibility, type, inputValue, coercedVariables, @@ -897,9 +1324,193 @@ static Object literalToInternalValue( locale); } + @Nullable + private static Object literalToInternalValueImpl( + InputInterceptor inputInterceptor, + GraphqlFieldVisibility fieldVisibility, + GraphQLType type, + Value inputValue, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale + ) { + if (inputValue instanceof VariableReference) { + String variableName = ((VariableReference) inputValue).getName(); + return coercedVariables.get(variableName); + } + if (inputValue instanceof NullValue) { + return null; + } + if (type instanceof GraphQLScalarType) { + return literalToInternalValueForScalar( + inputValue, + (GraphQLScalarType) type, + coercedVariables, + graphqlContext, + locale); + } + if (isNonNull(type)) { + return literalToInternalValue( + inputInterceptor, + fieldVisibility, + unwrapOneAs(type), + inputValue, + coercedVariables, + graphqlContext, + locale); + } + if (type instanceof GraphQLInputObjectType) { + return literalToInternalValueForInputObject( + inputInterceptor, + fieldVisibility, + (GraphQLInputObjectType) type, + (ObjectValue) inputValue, + coercedVariables, + graphqlContext, + locale); + } + if (type instanceof GraphQLEnumType) { + return ((GraphQLEnumType) type).parseLiteral(inputValue, graphqlContext, locale); + } + if (isList(type)) { + return literalToInternalValueForList( + inputInterceptor, + fieldVisibility, + (GraphQLList) type, + inputValue, + coercedVariables, + graphqlContext, + locale); + } + return null; + } + + /** + * no validation + */ + private static Object literalToInternalValueForScalar( + Value inputValue, + GraphQLScalarType scalarType, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + @NonNull Locale locale + ) { + // the CoercingParseLiteralException exception that could happen here has been validated earlier via ValidationUtil + return scalarType.getCoercing().parseLiteral( + inputValue, + coercedVariables, + graphqlContext, + locale); + } + + /** + * no validation + */ + private static Object literalToInternalValueForList( + InputInterceptor inputInterceptor, + GraphqlFieldVisibility fieldVisibility, + GraphQLList graphQLList, + Value value, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale + ) { + + GraphQLInputType inputType = (GraphQLInputType) graphQLList.getWrappedType(); + if (value instanceof ArrayValue) { + ArrayValue arrayValue = (ArrayValue) value; + List result = new ArrayList<>(); + for (Value singleValue : arrayValue.getValues()) { + result.add(literalToInternalValue( + inputInterceptor, + fieldVisibility, + inputType, + singleValue, + coercedVariables, + graphqlContext, + locale)); + } + return result; + } else { + return Collections.singletonList( + literalToInternalValue( + inputInterceptor, + fieldVisibility, + inputType, + value, + coercedVariables, + graphqlContext, + locale)); + } + } + + /** + * no validation + */ + private static Object literalToInternalValueForInputObject( + InputInterceptor inputInterceptor, + GraphqlFieldVisibility fieldVisibility, + GraphQLInputObjectType type, + ObjectValue inputValue, + CoercedVariables coercedVariables, + GraphQLContext graphqlContext, + Locale locale + ) { + Map coercedValues = new LinkedHashMap<>(); + + Map inputFieldsByName = mapObjectValueFieldsByName(inputValue); + + + List inputFieldTypes = fieldVisibility.getFieldDefinitions(type); + for (GraphQLInputObjectField inputFieldDefinition : inputFieldTypes) { + GraphQLInputType fieldType = inputFieldDefinition.getType(); + String fieldName = inputFieldDefinition.getName(); + ObjectField field = inputFieldsByName.get(fieldName); + boolean hasValue = field != null; + Object value; + Value fieldValue = field != null ? field.getValue() : null; + if (fieldValue instanceof VariableReference) { + String variableName = ((VariableReference) fieldValue).getName(); + hasValue = coercedVariables.containsKey(variableName); + value = coercedVariables.get(variableName); + } else { + value = fieldValue; + } + if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { + Object coercedDefaultValue = defaultValueToInternalValue( + inputInterceptor, + fieldVisibility, + inputFieldDefinition.getInputFieldDefaultValue(), + fieldType, + graphqlContext, + locale); + coercedValues.put(fieldName, coercedDefaultValue); + } else if (isNonNull(fieldType) && (!hasValue || isNullValue(value))) { + return assertShouldNeverHappen("Should have been validated before"); + } else if (hasValue) { + if (isNullValue(value)) { + coercedValues.put(fieldName, value); + } else if (fieldValue instanceof VariableReference) { + coercedValues.put(fieldName, value); + } else { + value = literalToInternalValue( + inputInterceptor, + fieldVisibility, + fieldType, + fieldValue, + coercedVariables, + graphqlContext, + locale); + coercedValues.put(fieldName, value); + } + } + } + return coercedValues; + } + static Object literalToInternalValue( InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, SchemaInputType type, Value inputValue, CoercedVariables coercedVariables, @@ -919,7 +1530,7 @@ static Object literalToInternalValue( @Nullable private static Object literalToInternalValueImpl( InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, SchemaType type, Value inputValue, CoercedVariables coercedVariables, @@ -987,7 +1598,7 @@ private static Object literalToInternalValueImpl( * no validation */ private static Object literalToInternalValueForScalar( - InputValueSchema schema, + ExecutableSchema schema, Value inputValue, SchemaScalar scalarType, CoercedVariables coercedVariables, @@ -1007,7 +1618,7 @@ private static Object literalToInternalValueForScalar( */ private static Object literalToInternalValueForList( InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, SchemaList listType, Value value, CoercedVariables coercedVariables, @@ -1049,7 +1660,7 @@ private static Object literalToInternalValueForList( */ private static Object literalToInternalValueForInputObject( InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, SchemaInputObject type, ObjectValue inputValue, CoercedVariables coercedVariables, @@ -1113,7 +1724,7 @@ private static Object literalToInternalValueForInputObject( } private static Object literalToInternalValueForEnum( - InputValueSchema schema, + ExecutableSchema schema, Value inputValue, SchemaEnum enumType, Locale locale) { @@ -1164,18 +1775,36 @@ static Object defaultValueToInternalValue( GraphQLContext graphqlContext, Locale locale ) { - return defaultValueToInternalValue( - inputInterceptor, - new GraphQLInputValueSchema(fieldVisibility), - defaultValue, - type, - graphqlContext, - locale); + if (defaultValue.isInternal()) { + return defaultValue.getValue(); + } + if (defaultValue.isLiteral()) { + // default value literals can't reference variables, this is why the variables are empty + return literalToInternalValue( + inputInterceptor, + fieldVisibility, + type, + (Value) defaultValue.getValue(), + CoercedVariables.emptyVariables(), + graphqlContext, + locale); + } + if (defaultValue.isExternal()) { + // performs validation too + return externalValueToInternalValueImpl( + inputInterceptor, + fieldVisibility, + type, + defaultValue.getValue(), + graphqlContext, + locale); + } + return assertShouldNeverHappen(); } static Object defaultValueToInternalValue( InputInterceptor inputInterceptor, - InputValueSchema schema, + ExecutableSchema schema, InputValueWithState defaultValue, SchemaInputType type, GraphQLContext graphqlContext, diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index 01d7d41983..9ce9a18c25 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -7,6 +7,12 @@ import graphql.language.ObjectField; import graphql.language.ObjectValue; import graphql.language.Value; +import graphql.schema.ExecutableSchema; +import graphql.schema.GraphQLInputObjectField; +import graphql.schema.GraphQLInputObjectType; +import graphql.schema.GraphQLInputType; +import graphql.schema.GraphQLList; +import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; import graphql.schema.SchemaInputField; import graphql.schema.SchemaInputObject; @@ -24,9 +30,93 @@ @Internal final class ValuesResolverOneOfValidation { + @SuppressWarnings("unchecked") + static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value argumentValue, String argumentName, Locale locale) { + GraphQLType unwrappedNonNullType = GraphQLTypeUtil.unwrapNonNull(type); + + if (isList(unwrappedNonNullType) + && !ValuesResolverConversion.isNullValue(inputValue) + && inputValue instanceof List + && argumentValue instanceof ArrayValue) { + GraphQLType elementType = ((GraphQLList) unwrappedNonNullType).getWrappedType(); + List inputList = (List) inputValue; + List argumentList = ((ArrayValue) argumentValue).getValues(); + + for (int i = 0; i < argumentList.size(); i++) { + validateOneOfInputTypes(elementType, inputList.get(i), argumentList.get(i), argumentName, locale); + } + } + + if (unwrappedNonNullType instanceof GraphQLInputObjectType && !ValuesResolverConversion.isNullValue(inputValue)) { + Assert.assertTrue(inputValue instanceof Map, "The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName); + Map objectMap = (Map) inputValue; + + GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedNonNullType; + + if (inputObjectType.isOneOf()) { + validateOneOfInputTypesInternal(inputObjectType, argumentValue, objectMap, locale); + } + + for (GraphQLInputObjectField fieldDefinition : inputObjectType.getFields()) { + GraphQLInputType childFieldType = fieldDefinition.getType(); + String childFieldName = fieldDefinition.getName(); + Object childFieldInputValue = objectMap.get(childFieldName); + + if (argumentValue instanceof ObjectValue) { + List values = ((ObjectValue) argumentValue).getObjectFields().stream() + .filter(of -> of.getName().equals(childFieldName)) + .map(ObjectField::getValue) + .collect(Collectors.toList()); + + if (values.size() > 1) { + Assert.assertShouldNeverHappen("argument %s has %s object fields with the same name: '%s'. A maximum of 1 is expected", argumentName, values.size(), childFieldName); + } else if (!values.isEmpty()) { + validateOneOfInputTypes(childFieldType, childFieldInputValue, values.get(0), argumentName, locale); + } + } else { + validateOneOfInputTypes(childFieldType, childFieldInputValue, argumentValue, argumentName, locale); + } + } + } + } + + private static void validateOneOfInputTypesInternal(GraphQLInputObjectType oneOfInputType, Value argumentValue, Map objectMap, Locale locale) { + final String fieldName; + if (argumentValue instanceof ObjectValue) { + List objectFields = ((ObjectValue) argumentValue).getObjectFields(); + if (objectFields.size() != 1) { + throwNotOneFieldError(oneOfInputType, locale); + } + + fieldName = objectFields.iterator().next().getName(); + } else { + if (objectMap.size() != 1) { + throwNotOneFieldError(oneOfInputType, locale); + } + + fieldName = objectMap.keySet().iterator().next(); + } + + if (objectMap.get(fieldName) == null) { + throwValueIsNullError(oneOfInputType, locale, fieldName); + } + } + + private static void throwValueIsNullError(GraphQLInputObjectType oneOfInputType, Locale locale, String fieldName) { + String msg = I18n.i18n(I18n.BundleType.Execution, locale) + .msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName); + throw new OneOfNullValueException(msg); + } + + private static void throwNotOneFieldError(GraphQLInputObjectType oneOfInputType, Locale locale) { + String msg = I18n.i18n(I18n.BundleType.Execution, locale) + .msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName()); + throw new OneOfTooManyKeysException(msg); + } + @SuppressWarnings("unchecked") static void validateOneOfInputTypes( - InputValueSchema schema, + ExecutableSchema schema, SchemaType type, Object inputValue, Value argumentValue, diff --git a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy index 1644fa93f3..e09b83ecc9 100644 --- a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy @@ -31,8 +31,6 @@ import graphql.schema.GraphQLNonNull import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema -import graphql.schema.GraphQLType -import graphql.schema.InputValueWithState import graphql.schema.SchemaEnum import graphql.schema.SchemaField import graphql.schema.universe.view.SUExecutableSchema @@ -139,18 +137,8 @@ class SUExecutableSchemaValuesResolverTest extends Specification { then: normalizedSnapshot(universeNormalized.toMap()) == normalizedSnapshot(graphQLNormalized.toMap()) - universeNormalized.toMap().filter.value.mode.value.name == "SLOW" - universeNormalized.toMap() - .filter.value.legacyMode.value.name == "FAST" - universeNormalized.toMap() - .filter.value.legacyNumbers.value.values*.value == [7, 8] - universeNormalized.toMap() - .filter.value.legacyChoice.value - .objectFields[0].value.value == "legacy" - universeNormalized.toMap() - .filter.value.legacyRequired.value.value == 10 - universeNormalized.toMap() - .filter.value.legacyNull.value.class.simpleName == "NullValue" + universeNormalized.toMap().filter.value.keySet() == + ["required", "numbers", "choice"] as Set } def "literal argument coercion matches GraphQLSchema"() { @@ -371,29 +359,6 @@ class SUExecutableSchemaValuesResolverTest extends Specification { new NonNullType(new TypeName("Missing"))) == null } - def "literal conversion includes input field defaults"() { - given: - GraphQLType filter = graphQLSchema.getType("Filter") - - when: - def literal = ValuesResolver.valueToLiteral( - InputValueWithState.newExternalValue([required: 1]), - filter, - GraphQLContext.getDefault(), - Locale.ENGLISH) - - then: - literal.objectFields*.name as Set == [ - "required", - "mode", - "legacyMode", - "legacyNumbers", - "legacyChoice", - "legacyRequired", - "legacyNull" - ] as Set - } - private static OperationDefinition operation(String query) { return TestUtil.parseQuery(query) .getDefinitionsOfType(OperationDefinition)[0] From f9888dfc2190032fa5365c39e95c22c36d7ec5cd Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 7 Aug 2026 04:55:15 +0000 Subject: [PATCH 23/32] Support schema-neutral enum coercion and printing --- .../execution/ValuesResolverConversion.java | 89 ++--- .../java/graphql/schema/ExecutableSchema.java | 9 - .../java/graphql/schema/GraphQLEnumType.java | 6 +- .../java/graphql/schema/GraphQLSchema.java | 12 - .../graphql/schema/GraphQLSchemaElement.java | 2 +- .../java/graphql/schema/SchemaElement.java | 12 + .../SchemaElementComparatorEnvironment.java | 74 +++++ .../SchemaElementComparatorRegistry.java | 43 +++ .../schema/SchemaElementComparators.java | 83 +++++ src/main/java/graphql/schema/SchemaEnum.java | 23 ++ .../graphql/schema/SchemaNamedElement.java | 2 +- src/main/java/graphql/schema/SchemaType.java | 2 +- .../graphql/schema/idl/SchemaPrinter.java | 311 ++++++++++++++++-- .../view/AbstractSUSchemaElement.java | 3 +- .../universe/view/SUExecutableSchema.java | 15 - .../view/SUExecutableSchemaBuilder.java | 4 +- .../schema/universe/view/SUSchemaEnum.java | 130 +++++++- .../SchemaElementComparatorsTest.groovy | 127 +++++++ .../SchemaPrinterComparatorsTest.groovy | 43 ++- .../idl/AbstractSchemaPrintingTest.groovy | 19 ++ .../idl/SchemaPrinterAstBehaviorTest.groovy | 19 +- .../schema/idl/SchemaPrinterTest.groovy | 166 ++++++---- .../SUExecutableSchemaPrinterTest.groovy | 8 +- ...UExecutableSchemaValuesResolverTest.groovy | 79 ++++- 24 files changed, 1045 insertions(+), 236 deletions(-) create mode 100644 src/main/java/graphql/schema/SchemaElement.java create mode 100644 src/main/java/graphql/schema/SchemaElementComparatorEnvironment.java create mode 100644 src/main/java/graphql/schema/SchemaElementComparatorRegistry.java create mode 100644 src/main/java/graphql/schema/SchemaElementComparators.java create mode 100644 src/test/groovy/graphql/schema/SchemaElementComparatorsTest.groovy create mode 100644 src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index a859d47ec8..f4cd080b5a 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -27,7 +27,6 @@ import graphql.schema.GraphQLTypeUtil; import graphql.schema.InputValueWithState; import graphql.schema.SchemaEnum; -import graphql.schema.SchemaEnumValue; import graphql.schema.SchemaInputField; import graphql.schema.SchemaInputObject; import graphql.schema.SchemaInputType; @@ -299,6 +298,7 @@ static Object externalValueToLiteral( return externalValueToLiteralForEnum( (SchemaEnum) type, value, + graphqlContext, locale); } else if (type instanceof SchemaList) { return externalValueToLiteralForList( @@ -467,16 +467,9 @@ private static Value externalValueToLiteralForScalar( private static Value externalValueToLiteralForEnum( SchemaEnum enumType, Object value, + GraphQLContext graphqlContext, Locale locale) { - SchemaEnumValue enumValue = enumType.getValue(value.toString()); - if (enumValue == null) { - return assertShouldNeverHappen( - "Invalid external enum value '%s' for '%s' in locale '%s'", - value, - enumType.getName(), - locale); - } - return EnumValue.newEnumValue(enumValue.getName()).build(); + return enumType.valueToLiteral(value, graphqlContext, locale); } /** @@ -608,9 +601,10 @@ private static Value internalValueToLiteral( } if (type instanceof SchemaEnum) { return internalEnumValueToLiteral( - schema, value, - (SchemaEnum) type); + (SchemaEnum) type, + graphqlContext, + locale); } if (type instanceof SchemaList) { return internalListValueToLiteral( @@ -632,29 +626,15 @@ private static Value internalValueToLiteral( } private static Value internalEnumValueToLiteral( - ExecutableSchema schema, Object value, - SchemaEnum enumType) { - for (SchemaEnumValue enumValue : enumType.getValues()) { - Object runtimeValue = schema.getEnumRuntimeValue(enumValue); - if (value.equals(runtimeValue)) { - return EnumValue.newEnumValue(enumValue.getName()).build(); - } - if (runtimeValue instanceof Enum - && value instanceof String - && value.equals(((Enum) runtimeValue).name())) { - return EnumValue.newEnumValue(enumValue.getName()).build(); - } - if (value instanceof Enum - && ((Enum) value).name() - .equals(String.valueOf(runtimeValue))) { - return EnumValue.newEnumValue(enumValue.getName()).build(); - } - } - return assertShouldNeverHappen( - "Invalid internal enum value '%s' for '%s'", + SchemaEnum enumType, + GraphQLContext graphqlContext, + Locale locale) { + Object serialized = enumType.serialize( value, - enumType.getName()); + graphqlContext, + locale); + return EnumValue.newEnumValue(serialized.toString()).build(); } @SuppressWarnings("unchecked") @@ -1011,9 +991,9 @@ static Object externalValueToInternalValueImpl( locale); } else if (type instanceof SchemaEnum) { return externalValueToInternalValueForEnum( - schema, (SchemaEnum) type, value, + graphqlContext, locale); } else if (type instanceof SchemaList) { return externalValueToInternalValueForList( @@ -1247,22 +1227,12 @@ private static Object externalValueToInternalValueForScalar( * including validation */ private static Object externalValueToInternalValueForEnum( - ExecutableSchema schema, SchemaEnum enumType, Object value, + GraphQLContext graphqlContext, Locale locale ) throws CoercingParseValueException { - SchemaEnumValue enumValue = - enumType.getValue(value.toString()); - if (enumValue != null) { - return schema.getEnumRuntimeValue(enumValue); - } - throw new CoercingParseValueException( - i18nMsg( - locale, - "Enum.badName", - enumType.getName(), - value.toString())); + return enumType.parseValue(value, graphqlContext, locale); } /** @@ -1576,9 +1546,9 @@ private static Object literalToInternalValueImpl( } if (type instanceof SchemaEnum) { return literalToInternalValueForEnum( - schema, inputValue, (SchemaEnum) type, + graphqlContext, locale); } if (isList(type)) { @@ -1724,29 +1694,14 @@ private static Object literalToInternalValueForInputObject( } private static Object literalToInternalValueForEnum( - ExecutableSchema schema, Value inputValue, SchemaEnum enumType, + GraphQLContext graphqlContext, Locale locale) { - if (!(inputValue instanceof EnumValue)) { - throw new CoercingParseLiteralException( - i18nMsg( - locale, - "Scalar.unexpectedAstType", - "EnumValue", - typeName(inputValue))); - } - SchemaEnumValue enumValue = enumType.getValue( - ((EnumValue) inputValue).getName()); - if (enumValue != null) { - return schema.getEnumRuntimeValue(enumValue); - } - throw new CoercingParseLiteralException( - i18nMsg( - locale, - "Enum.unallowableValue", - enumType.getName(), - inputValue)); + return enumType.parseLiteral( + inputValue, + graphqlContext, + locale); } static boolean isNullValue(Object value) { diff --git a/src/main/java/graphql/schema/ExecutableSchema.java b/src/main/java/graphql/schema/ExecutableSchema.java index 809b2691ed..c77467324c 100644 --- a/src/main/java/graphql/schema/ExecutableSchema.java +++ b/src/main/java/graphql/schema/ExecutableSchema.java @@ -145,15 +145,6 @@ boolean isPossibleType( SchemaComposite compositeType, SchemaObject objectType); - /** - * Returns the runtime value represented by an enum value definition. - * - * @param enumValue the enum value definition - * - * @return the schema-specific runtime value - */ - Object getEnumRuntimeValue(SchemaEnumValue enumValue); - /** * @param scalarType the scalar * diff --git a/src/main/java/graphql/schema/GraphQLEnumType.java b/src/main/java/graphql/schema/GraphQLEnumType.java index 692d1ab49d..84f62ce9bd 100644 --- a/src/main/java/graphql/schema/GraphQLEnumType.java +++ b/src/main/java/graphql/schema/GraphQLEnumType.java @@ -80,6 +80,7 @@ public Object serialize(Object input) { } @Internal + @Override public Object serialize(Object input, GraphQLContext graphQLContext, Locale locale) { return getNameByValue(input, graphQLContext, locale); } @@ -91,6 +92,7 @@ public Object parseValue(Object input) { } @Internal + @Override public Object parseValue(Object input, GraphQLContext graphQLContext, Locale locale) { return getValueByName(input, graphQLContext, locale); } @@ -103,6 +105,7 @@ public Object parseLiteral(Object input) { } @Internal + @Override public Object parseLiteral(Value input, GraphQLContext graphQLContext, Locale locale) { return parseLiteralImpl(input, graphQLContext, locale); } @@ -131,6 +134,7 @@ public Value valueToLiteral(Object input) { } @Internal + @Override public Value valueToLiteral(Object input, GraphQLContext graphQLContext, Locale locale) { GraphQLEnumValueDefinition enumValueDefinition = valueDefinitionMap.get(input.toString()); if (enumValueDefinition == null) { @@ -209,7 +213,7 @@ public List getDirectives() { return directivesHolder.getDirectives(); } - @Override + public Map getDirectivesByName() { return directivesHolder.getDirectivesByName(); } diff --git a/src/main/java/graphql/schema/GraphQLSchema.java b/src/main/java/graphql/schema/GraphQLSchema.java index 256ff00dad..c681102bc5 100644 --- a/src/main/java/graphql/schema/GraphQLSchema.java +++ b/src/main/java/graphql/schema/GraphQLSchema.java @@ -650,18 +650,6 @@ public boolean isPossibleType( (GraphQLObjectType) objectType); } - @Override - public Object getEnumRuntimeValue( - SchemaEnumValue enumValue) { - assertTrue( - enumValue instanceof GraphQLEnumValueDefinition, - "The enum value must belong to this GraphQLSchema"); - return assertNotNull( - ((GraphQLEnumValueDefinition) enumValue).getValue(), - "The enum value '%s' must have a runtime value", - enumValue.getName()); - } - @Override public Coercing getScalarCoercing(SchemaScalar scalarType) { assertTrue( diff --git a/src/main/java/graphql/schema/GraphQLSchemaElement.java b/src/main/java/graphql/schema/GraphQLSchemaElement.java index df6a15cc0e..60f73e2d7f 100644 --- a/src/main/java/graphql/schema/GraphQLSchemaElement.java +++ b/src/main/java/graphql/schema/GraphQLSchemaElement.java @@ -14,7 +14,7 @@ * this interface. */ @PublicApi -public interface GraphQLSchemaElement { +public interface GraphQLSchemaElement extends SchemaElement { default List getChildren() { return ImmutableKit.emptyList(); diff --git a/src/main/java/graphql/schema/SchemaElement.java b/src/main/java/graphql/schema/SchemaElement.java new file mode 100644 index 0000000000..cfaaa014e5 --- /dev/null +++ b/src/main/java/graphql/schema/SchemaElement.java @@ -0,0 +1,12 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +/** + * An element in a GraphQL schema. + */ +@ExperimentalApi +@NullMarked +public interface SchemaElement { +} diff --git a/src/main/java/graphql/schema/SchemaElementComparatorEnvironment.java b/src/main/java/graphql/schema/SchemaElementComparatorEnvironment.java new file mode 100644 index 0000000000..5ecd8fc31e --- /dev/null +++ b/src/main/java/graphql/schema/SchemaElementComparatorEnvironment.java @@ -0,0 +1,74 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +import java.util.Objects; + +/** + * Identifies the parent and element kinds for a schema-element comparator. + */ +@ExperimentalApi +@NullMarked +public final class SchemaElementComparatorEnvironment { + + private final @Nullable Class parentType; + private final @Nullable Class elementType; + + private SchemaElementComparatorEnvironment( + @Nullable Class parentType, + @Nullable Class elementType) { + this.parentType = parentType; + this.elementType = elementType; + } + + /** + * Creates an environment describing the semantic schema kinds being sorted. + * + * @param parentType the containing element kind, or {@code null} + * @param elementType the element kind, or {@code null} for top-level elements + * + * @return a comparator environment + */ + public static SchemaElementComparatorEnvironment newEnvironment( + @Nullable Class parentType, + @Nullable Class elementType) { + return new SchemaElementComparatorEnvironment( + parentType, + elementType); + } + + /** + * @return the containing element kind, or {@code null} + */ + public @Nullable Class getParentType() { + return parentType; + } + + /** + * @return the element kind, or {@code null} for top-level elements + */ + public @Nullable Class getElementType() { + return elementType; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof SchemaElementComparatorEnvironment)) { + return false; + } + SchemaElementComparatorEnvironment that = + (SchemaElementComparatorEnvironment) other; + return Objects.equals(parentType, that.parentType) + && Objects.equals(elementType, that.elementType); + } + + @Override + public int hashCode() { + return Objects.hash(parentType, elementType); + } +} diff --git a/src/main/java/graphql/schema/SchemaElementComparatorRegistry.java b/src/main/java/graphql/schema/SchemaElementComparatorRegistry.java new file mode 100644 index 0000000000..d7f8e75fac --- /dev/null +++ b/src/main/java/graphql/schema/SchemaElementComparatorRegistry.java @@ -0,0 +1,43 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +import java.util.Comparator; + +/** + * Supplies schema-neutral comparators for schema elements. + */ +@ExperimentalApi +@NullMarked +@FunctionalInterface +public interface SchemaElementComparatorRegistry { + + /** + * A registry that applies GraphQL's default grouped and name ordering. + */ + SchemaElementComparatorRegistry DEFAULT_REGISTRY = + environment -> SchemaElementComparators.sensibleGroupedOrder(); + + /** + * A registry that preserves the current order. + */ + SchemaElementComparatorRegistry AS_IS_REGISTRY = + environment -> SchemaElementComparators.asIsOrder(); + + /** + * A registry that orders elements by name. + */ + SchemaElementComparatorRegistry BY_NAME_REGISTRY = + environment -> SchemaElementComparators.byNameAsc(); + + /** + * Returns the comparator for the supplied semantic schema context. + * + * @param environment the parent and element kinds being sorted + * + * @return the comparator to apply + */ + Comparator getComparator( + SchemaElementComparatorEnvironment environment); +} diff --git a/src/main/java/graphql/schema/SchemaElementComparators.java b/src/main/java/graphql/schema/SchemaElementComparators.java new file mode 100644 index 0000000000..afa6987ffd --- /dev/null +++ b/src/main/java/graphql/schema/SchemaElementComparators.java @@ -0,0 +1,83 @@ +package graphql.schema; + +import graphql.ExperimentalApi; +import org.jspecify.annotations.NullMarked; + +import java.util.Comparator; +import java.util.Objects; + +/** + * Common comparators for schema-neutral elements. + */ +@ExperimentalApi +@NullMarked +public final class SchemaElementComparators { + + private SchemaElementComparators() { + } + + /** + * @return a comparator that preserves the current order + */ + public static Comparator asIsOrder() { + return (first, second) -> 0; + } + + /** + * @return a comparator that orders named elements by ascending name + */ + public static Comparator byNameAsc() { + return Comparator.comparing( + SchemaElementComparators::elementName); + } + + /** + * @return the default grouped schema-printer ordering + */ + public static Comparator sensibleGroupedOrder() { + return Comparator + .comparingInt(SchemaElementComparators::topLevelRank) + .thenComparing(SchemaElementComparators::elementName); + } + + private static String elementName(SchemaElement element) { + element = unwrapElement(element); + if (element instanceof SchemaNamedElement) { + return ((SchemaNamedElement) element).getName(); + } + return Objects.toString(element); + } + + private static int topLevelRank(SchemaElement element) { + element = unwrapElement(element); + if (element instanceof SchemaDirective) { + return 1; + } + if (element instanceof SchemaInterface) { + return 2; + } + if (element instanceof SchemaUnion) { + return 3; + } + if (element instanceof SchemaObject) { + return 4; + } + if (element instanceof SchemaEnum) { + return 5; + } + if (element instanceof SchemaScalar) { + return 6; + } + if (element instanceof SchemaInputObject) { + return 7; + } + return 0; + } + + private static SchemaElement unwrapElement(SchemaElement element) { + if (element instanceof SchemaType) { + return GraphQLTypeUtil.unwrapAll((SchemaType) element); + } + return element; + } +} diff --git a/src/main/java/graphql/schema/SchemaEnum.java b/src/main/java/graphql/schema/SchemaEnum.java index 743b2c640f..07c61b25f3 100644 --- a/src/main/java/graphql/schema/SchemaEnum.java +++ b/src/main/java/graphql/schema/SchemaEnum.java @@ -1,10 +1,13 @@ package graphql.schema; import graphql.ExperimentalApi; +import graphql.GraphQLContext; +import graphql.language.Value; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import java.util.List; +import java.util.Locale; /** * A GraphQL enum type. @@ -14,6 +17,26 @@ public interface SchemaEnum extends SchemaNamedType, SchemaInputType, SchemaOutputType { + Object serialize( + Object input, + GraphQLContext graphQLContext, + Locale locale); + + Object parseValue( + Object input, + GraphQLContext graphQLContext, + Locale locale); + + Object parseLiteral( + Value input, + GraphQLContext graphQLContext, + Locale locale); + + Value valueToLiteral( + Object input, + GraphQLContext graphQLContext, + Locale locale); + List getValues(); @Nullable SchemaEnumValue getValue(String name); diff --git a/src/main/java/graphql/schema/SchemaNamedElement.java b/src/main/java/graphql/schema/SchemaNamedElement.java index d716c8c6ae..8c5089b20b 100644 --- a/src/main/java/graphql/schema/SchemaNamedElement.java +++ b/src/main/java/graphql/schema/SchemaNamedElement.java @@ -10,7 +10,7 @@ */ @ExperimentalApi @NullMarked -public interface SchemaNamedElement { +public interface SchemaNamedElement extends SchemaElement { /** * Returns the name of this schema element. diff --git a/src/main/java/graphql/schema/SchemaType.java b/src/main/java/graphql/schema/SchemaType.java index e4da1d716b..4bc8df23f0 100644 --- a/src/main/java/graphql/schema/SchemaType.java +++ b/src/main/java/graphql/schema/SchemaType.java @@ -8,5 +8,5 @@ */ @ExperimentalApi @NullMarked -public interface SchemaType { +public interface SchemaType extends SchemaElement { } diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index f9bc5ec343..3da59432a1 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -65,6 +65,9 @@ import graphql.schema.SchemaArgument; import graphql.schema.SchemaDirective; import graphql.schema.SchemaDirectiveContainer; +import graphql.schema.SchemaElement; +import graphql.schema.SchemaElementComparatorEnvironment; +import graphql.schema.SchemaElementComparatorRegistry; import graphql.schema.SchemaEnum; import graphql.schema.SchemaEnumValue; import graphql.schema.SchemaField; @@ -79,6 +82,7 @@ import graphql.schema.SchemaNamedType; import graphql.schema.SchemaNonNull; import graphql.schema.SchemaObject; +import graphql.schema.SchemaOutputType; import graphql.schema.SchemaScalar; import graphql.schema.SchemaType; import graphql.schema.SchemaUnion; @@ -140,10 +144,34 @@ public static class Options { private final Predicate includeDirective; + /** + * The original GraphQL-specific element predicate configured by + * {@link #includeSchemaElement(Predicate)}. It is evaluated only for + * {@link GraphQLSchemaElement} instances and before {@link #schemaElementFilter}. + */ private final Predicate includeSchemaElement; + /** + * The schema-neutral element predicate configured by + * {@link #filterSchemaElements(Predicate)}. It is evaluated for every + * {@link SchemaElement} after the GraphQL-specific predicate, when applicable, accepts it. + */ + private final Predicate schemaElementFilter; + + /** + * The original GraphQL-specific comparator registry configured by + * {@link #setComparators(GraphqlTypeComparatorRegistry)}. It orders only + * {@link GraphQLSchemaElement} instances. + */ private final GraphqlTypeComparatorRegistry comparatorRegistry; + /** + * The schema-neutral comparator registry configured by + * {@link #sortSchemaElements(SchemaElementComparatorRegistry)}. It orders schema elements + * that are not {@link GraphQLSchemaElement} instances. + */ + private final SchemaElementComparatorRegistry schemaElementComparatorRegistry; + private final boolean includeAstDefinitionComments; private Options(boolean includeIntrospectionTypes, @@ -155,7 +183,9 @@ private Options(boolean includeIntrospectionTypes, boolean descriptionsAsHashComments, Predicate includeDirective, Predicate includeSchemaElement, + Predicate schemaElementFilter, GraphqlTypeComparatorRegistry comparatorRegistry, + SchemaElementComparatorRegistry schemaElementComparatorRegistry, boolean includeAstDefinitionComments) { this.includeIntrospectionTypes = includeIntrospectionTypes; this.includeScalars = includeScalars; @@ -167,6 +197,9 @@ private Options(boolean includeIntrospectionTypes, this.descriptionsAsHashComments = descriptionsAsHashComments; this.comparatorRegistry = comparatorRegistry; this.includeSchemaElement = includeSchemaElement; + this.schemaElementFilter = schemaElementFilter; + this.schemaElementComparatorRegistry = + schemaElementComparatorRegistry; this.includeAstDefinitionComments = includeAstDefinitionComments; } @@ -194,18 +227,58 @@ public Predicate getIncludeDirective() { return includeDirective; } + /** + * Returns the original GraphQL-specific element predicate. + * + *

This predicate is evaluated only for {@link GraphQLSchemaElement} instances. + * If it rejects an element, {@link #getSchemaElementFilter()} is not evaluated.

+ * + * @return the GraphQL-specific element predicate + */ public Predicate getIncludeSchemaElement() { return includeSchemaElement; } + /** + * Returns the schema-neutral element predicate. + * + *

This predicate is evaluated for all schema implementations. For a + * {@link GraphQLSchemaElement}, it runs after {@link #getIncludeSchemaElement()}.

+ * + * @return the schema-neutral element predicate + */ + public Predicate getSchemaElementFilter() { + return schemaElementFilter; + } + public boolean isDescriptionsAsHashComments() { return descriptionsAsHashComments; } + /** + * Returns the original GraphQL-specific comparator registry. + * + *

This registry is used only for {@link GraphQLSchemaElement} instances.

+ * + * @return the GraphQL-specific comparator registry + */ public GraphqlTypeComparatorRegistry getComparatorRegistry() { return comparatorRegistry; } + /** + * Returns the schema-neutral comparator registry. + * + *

This registry is used for schema elements that are not + * {@link GraphQLSchemaElement} instances.

+ * + * @return the schema-neutral comparator registry + */ + public SchemaElementComparatorRegistry + getSchemaElementComparatorRegistry() { + return schemaElementComparatorRegistry; + } + public boolean isUseAstDefinitions() { return useAstDefinitions; } @@ -223,7 +296,9 @@ public static Options defaultOptions() { false, directive -> true, element -> true, + element -> true, DefaultGraphqlTypeComparatorRegistry.defaultComparators(), + SchemaElementComparatorRegistry.DEFAULT_REGISTRY, false); } @@ -243,7 +318,9 @@ public Options includeIntrospectionTypes(boolean flag) { this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -263,7 +340,9 @@ public Options includeScalarTypes(boolean flag) { this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -287,7 +366,9 @@ public Options includeSchemaDefinition(boolean flag) { this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -313,7 +394,9 @@ public Options includeDirectiveDefinitions(boolean flag) { this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -335,7 +418,9 @@ public Options includeDirectiveDefinition(Predicate includeDirectiveDefi this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -357,7 +442,9 @@ public Options includeDirectives(boolean flag) { this.descriptionsAsHashComments, directive -> flag, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -378,15 +465,23 @@ public Options includeDirectives(Predicate includeDirective) { this.descriptionsAsHashComments, includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } /** - * This is a general purpose Predicate that decides whether a schema element is printed ever. + * Sets the original GraphQL-specific predicate that decides whether a schema element is + * printed. + * + *

This option remains specific to {@link GraphQLSchemaElement} for compatibility. It is + * not evaluated for other {@link SchemaElement} implementations. For GraphQL elements it + * runs before the predicate configured by {@link #filterSchemaElements(Predicate)}, and + * both predicates must accept the element.

* - * @param includeSchemaElement the predicate to decide of a schema is printed + * @param includeSchemaElement the GraphQL-specific inclusion predicate * * @return new instance of options */ @@ -401,7 +496,39 @@ public Options includeSchemaElement(Predicate includeSchem this.descriptionsAsHashComments, this.includeDirective, includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, + this.includeAstDefinitionComments); + } + + /** + * Sets the schema-neutral predicate that decides whether a schema element is printed. + * + *

This predicate is evaluated for every {@link SchemaElement}. For a + * {@link GraphQLSchemaElement}, the predicate configured by + * {@link #includeSchemaElement(Predicate)} runs first and both predicates must accept the + * element. The default predicate accepts every element.

+ * + * @param schemaElementFilter the schema-neutral inclusion predicate + * + * @return new instance of options + */ + public Options filterSchemaElements( + Predicate schemaElementFilter) { + Assert.assertNotNull(schemaElementFilter); + return new Options(this.includeIntrospectionTypes, + this.includeScalars, + this.includeSchemaDefinition, + this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, + this.useAstDefinitions, + this.descriptionsAsHashComments, + this.includeDirective, + this.includeSchemaElement, + schemaElementFilter, + this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -423,7 +550,9 @@ public Options useAstDefinitions(boolean flag) { this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } @@ -447,12 +576,18 @@ public Options descriptionsAsHashComments(boolean flag) { flag, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, this.comparatorRegistry, + this.schemaElementComparatorRegistry, this.includeAstDefinitionComments); } /** - * The comparator registry controls the printing order for registered {@code GraphQLType}s. + * Sets the original GraphQL-specific comparator registry. + * + *

This option remains specific to {@link GraphQLSchemaElement} for compatibility. + * It does not affect other {@link SchemaElement} implementations; configure those through + * {@link #sortSchemaElements(SchemaElementComparatorRegistry)}.

*

* The default is to sort elements by name but you can put in your own code to decide on the field order * @@ -470,7 +605,40 @@ public Options setComparators(GraphqlTypeComparatorRegistry comparatorRegistry) this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, comparatorRegistry, + this.schemaElementComparatorRegistry, + this.includeAstDefinitionComments); + } + + /** + * Sets the schema-neutral comparator registry. + * + *

This registry orders schema elements that are not {@link GraphQLSchemaElement} + * instances. It does not affect GraphQL elements; configure those through + * {@link #setComparators(GraphqlTypeComparatorRegistry)}. Its default applies the same + * grouped and name ordering as the GraphQL-specific default registry.

+ * + * @param schemaElementComparatorRegistry the schema-neutral comparator registry + * + * @return options + */ + public Options sortSchemaElements( + SchemaElementComparatorRegistry + schemaElementComparatorRegistry) { + return new Options(this.includeIntrospectionTypes, + this.includeScalars, + this.includeSchemaDefinition, + this.includeDirectiveDefinitions, + this.includeDirectiveDefinition, + this.useAstDefinitions, + this.descriptionsAsHashComments, + this.includeDirective, + this.includeSchemaElement, + this.schemaElementFilter, + this.comparatorRegistry, + Assert.assertNotNull( + schemaElementComparatorRegistry), this.includeAstDefinitionComments); } @@ -493,7 +661,9 @@ public Options includeAstDefinitionComments(boolean flag) { this.descriptionsAsHashComments, this.includeDirective, this.includeSchemaElement, + this.schemaElementFilter, comparatorRegistry, + this.schemaElementComparatorRegistry, flag); } } @@ -1718,12 +1888,17 @@ private Value scalarLiteral( Locale.getDefault()); } - private boolean isIncluded(SchemaNamedElement element) { - if (!(element instanceof GraphQLSchemaElement)) { - return true; + /** + * Applies the GraphQL-specific predicate first, when applicable, followed by the + * schema-neutral predicate. + */ + private boolean isIncluded(SchemaElement element) { + if (element instanceof GraphQLSchemaElement + && !options.getIncludeSchemaElement() + .test((GraphQLSchemaElement) element)) { + return false; } - return options.getIncludeSchemaElement() - .test((GraphQLSchemaElement) element); + return options.getSchemaElementFilter().test(element); } private List sort( @@ -1739,20 +1914,22 @@ private List sort( graphQLElementClass(elements.get(0), topLevel), elements); } + return sortSchemaElements(parent, elements, topLevel); + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private List sortSchemaElements( + @Nullable SchemaNamedElement parent, + List elements, + boolean topLevel) { List result = new ArrayList<>(elements); - if (options.getComparatorRegistry() - == GraphqlTypeComparatorRegistry.AS_IS_REGISTRY) { - return result; - } - Comparator comparator = - Comparator.comparing(SchemaNamedElement::getName); - if (topLevel - && options.getComparatorRegistry() - != GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) { - comparator = Comparator - .comparingInt((T element) -> topLevelRank(element)) - .thenComparing(SchemaNamedElement::getName); - } + SchemaElementComparatorEnvironment environment = + SchemaElementComparatorEnvironment.newEnvironment( + schemaParentClass(parent), + schemaElementClass(elements.get(0), topLevel)); + Comparator comparator = options + .getSchemaElementComparatorRegistry() + .getComparator(environment); result.sort(comparator); return result; } @@ -1869,29 +2046,95 @@ private Class graphQLParentClass( return GraphQLSchemaElement.class; } - private int topLevelRank(SchemaNamedElement element) { - if (element instanceof SchemaDirective) { - return 1; + private Class schemaParentClass( + @Nullable SchemaNamedElement parent) { + if (parent == null) { + return SchemaElement.class; } - if (element instanceof SchemaInterface) { - return 2; + if (parent instanceof SchemaObject) { + return SchemaObject.class; } - if (element instanceof SchemaUnion) { - return 3; + if (parent instanceof SchemaInterface) { + return SchemaInterface.class; + } + if (parent instanceof SchemaUnion) { + return SchemaUnion.class; + } + if (parent instanceof SchemaEnum) { + return SchemaEnum.class; + } + if (parent instanceof SchemaEnumValue) { + return SchemaEnumValue.class; + } + if (parent instanceof SchemaScalar) { + return SchemaScalar.class; + } + if (parent instanceof SchemaInputObject) { + return SchemaInputObject.class; + } + if (parent instanceof SchemaInputField) { + return SchemaInputField.class; } - if (element instanceof SchemaObject) { - return 4; + if (parent instanceof SchemaField) { + return SchemaField.class; + } + if (parent instanceof SchemaArgument) { + return SchemaArgument.class; + } + if (parent instanceof SchemaDirective) { + return SchemaDirective.class; + } + if (parent instanceof SchemaAppliedDirective) { + return SchemaAppliedDirective.class; + } + return SchemaElement.class; + } + + private @Nullable Class + schemaElementClass( + SchemaNamedElement element, + boolean topLevel) { + if (topLevel) { + return null; + } + if (element instanceof SchemaObject + || element instanceof SchemaInterface) { + return SchemaOutputType.class; + } + if (element instanceof SchemaEnumValue) { + return SchemaEnumValue.class; + } + if (element instanceof SchemaInputField) { + return SchemaInputField.class; + } + if (element instanceof SchemaField) { + return SchemaField.class; + } + if (element instanceof SchemaArgument) { + return SchemaArgument.class; + } + if (element instanceof SchemaAppliedDirectiveArgument) { + return SchemaAppliedDirectiveArgument.class; + } + if (element instanceof SchemaAppliedDirective) { + return SchemaAppliedDirective.class; + } + if (element instanceof SchemaDirective) { + return SchemaDirective.class; + } + if (element instanceof SchemaUnion) { + return SchemaUnion.class; } if (element instanceof SchemaEnum) { - return 5; + return SchemaEnum.class; } if (element instanceof SchemaScalar) { - return 6; + return SchemaScalar.class; } if (element instanceof SchemaInputObject) { - return 7; + return SchemaInputObject.class; } - return 0; + return SchemaElement.class; } private Comparator getComparator( diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java index b29e9f22f6..7eb8d0ee56 100644 --- a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java @@ -2,6 +2,7 @@ import graphql.Internal; import graphql.language.Node; +import graphql.schema.SchemaElement; import graphql.schema.universe.SUVertex; import org.jspecify.annotations.Nullable; @@ -11,7 +12,7 @@ * Shared identity for schema-bound universe adapters. */ @Internal -public abstract class AbstractSUSchemaElement { +public abstract class AbstractSUSchemaElement implements SchemaElement { private final SUExecutableSchema executableSchema; private final SUVertex vertex; diff --git a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java index 976341d348..91f7b5e24b 100644 --- a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java @@ -16,7 +16,6 @@ import graphql.schema.SchemaComposite; import graphql.schema.SchemaDirective; import graphql.schema.SchemaDirectiveContainer; -import graphql.schema.SchemaEnumValue; import graphql.schema.SchemaField; import graphql.schema.SchemaFieldsContainer; import graphql.schema.SchemaInputField; @@ -400,20 +399,6 @@ public boolean isPossibleType( objectVertex(objectType)); } - @Override - public Object getEnumRuntimeValue( - SchemaEnumValue enumValue) { - SUVertex vertex = elementVertex(enumValue); - assertTrue( - vertex instanceof SUEnumValue, - "The enum value must belong to this SUSchema"); - Object runtimeValue = enumRuntimeValueById.get(vertex.getId()); - if (runtimeValue != null) { - return runtimeValue; - } - return assertNotNull(vertex.getName()); - } - @Override public Coercing getScalarCoercing(SchemaScalar scalarType) { SUVertex vertex = elementVertex(scalarType); diff --git a/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java index fe4ed97b74..9309a85074 100644 --- a/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchemaBuilder.java @@ -175,7 +175,9 @@ private void copyEnumRuntimeValues( enumRuntimeValue( (SUEnumType) type, assertNotNull(enumValue.getName()), - source.getEnumRuntimeValue(sourceValue)); + sourceValue.getValue() == null + ? assertNotNull(sourceValue.getName()) + : sourceValue.getValue()); } } diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java b/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java index e30cfe7762..4fdcc6328c 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaEnum.java @@ -1,6 +1,12 @@ package graphql.schema.universe.view; +import graphql.GraphQLContext; import graphql.Internal; +import graphql.language.EnumValue; +import graphql.language.Value; +import graphql.schema.CoercingParseLiteralException; +import graphql.schema.CoercingParseValueException; +import graphql.schema.CoercingSerializeException; import graphql.schema.SchemaEnum; import graphql.schema.universe.SUEnumType; import graphql.schema.universe.SUEnumValue; @@ -9,6 +15,12 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Locale; + +import static graphql.Assert.assertNotNull; +import static graphql.Assert.assertShouldNeverHappen; +import static graphql.scalar.CoercingUtil.i18nMsg; +import static graphql.scalar.CoercingUtil.typeName; @Internal public final class SUSchemaEnum @@ -21,11 +33,97 @@ public SUSchemaEnum( super(executableSchema, type); } + @Override + public Object serialize( + Object input, + GraphQLContext graphQLContext, + Locale locale) { + for (SUEnumValue value : enumValues()) { + Object runtimeValue = runtimeValue(value); + if (input.equals(runtimeValue)) { + return assertNotNull(value.getName()); + } + if (runtimeValue instanceof Enum + && input instanceof String + && input.equals(((Enum) runtimeValue).name())) { + return assertNotNull(value.getName()); + } + } + if (input instanceof Enum) { + return serializeJavaEnum((Enum) input, locale); + } + throw new CoercingSerializeException( + i18nMsg(locale, "Enum.badInput", getName(), input)); + } + + private Object serializeJavaEnum( + Enum input, + Locale locale) { + String enumName = input.name(); + for (SUEnumValue value : enumValues()) { + if (enumName.equals(String.valueOf(runtimeValue(value)))) { + return assertNotNull(value.getName()); + } + } + throw new CoercingSerializeException( + i18nMsg(locale, "Enum.badInput", getName(), input)); + } + + @Override + public Object parseValue( + Object input, + GraphQLContext graphQLContext, + Locale locale) { + SUEnumValue value = enumValue(input.toString()); + if (value != null) { + return runtimeValue(value); + } + throw new CoercingParseValueException( + i18nMsg(locale, "Enum.badName", getName(), input.toString())); + } + + @Override + public Object parseLiteral( + Value input, + GraphQLContext graphQLContext, + Locale locale) { + if (!(input instanceof EnumValue)) { + throw new CoercingParseLiteralException( + i18nMsg( + locale, + "Scalar.unexpectedAstType", + "EnumValue", + typeName(input))); + } + SUEnumValue value = enumValue(((EnumValue) input).getName()); + if (value != null) { + return runtimeValue(value); + } + throw new CoercingParseLiteralException( + i18nMsg(locale, "Enum.unallowableValue", getName(), input)); + } + + @Override + public Value valueToLiteral( + Object input, + GraphQLContext graphQLContext, + Locale locale) { + SUEnumValue value = enumValue(input.toString()); + if (value == null) { + return assertShouldNeverHappen( + i18nMsg( + locale, + "Enum.badName", + getName(), + input.toString())); + } + return EnumValue.newEnumValue( + assertNotNull(value.getName())).build(); + } + @Override public List getValues() { - List values = getExecutableSchema() - .getSchema() - .getEnumValues(getEnumTypeVertex()); + List values = enumValues(); List result = new ArrayList<>(values.size()); for (SUEnumValue value : values) { result.add(new SUSchemaEnumValue(getExecutableSchema(), value)); @@ -35,15 +133,35 @@ public List getValues() { @Override public @Nullable SUSchemaEnumValue getValue(String name) { - SUEnumValue value = getExecutableSchema() - .getSchema() - .getEnumValue(getEnumTypeVertex(), name); + SUEnumValue value = enumValue(name); if (value == null) { return null; } return new SUSchemaEnumValue(getExecutableSchema(), value); } + private List enumValues() { + return getExecutableSchema() + .getSchema() + .getEnumValues(getEnumTypeVertex()); + } + + private @Nullable SUEnumValue enumValue(String name) { + return getExecutableSchema() + .getSchema() + .getEnumValue(getEnumTypeVertex(), name); + } + + private Object runtimeValue(SUEnumValue value) { + Object runtimeValue = getExecutableSchema() + .getEnumRuntimeValueById() + .get(value.getId()); + if (runtimeValue != null) { + return runtimeValue; + } + return assertNotNull(value.getName()); + } + @Internal public SUEnumType getEnumTypeVertex() { return (SUEnumType) getTypeVertex(); diff --git a/src/test/groovy/graphql/schema/SchemaElementComparatorsTest.groovy b/src/test/groovy/graphql/schema/SchemaElementComparatorsTest.groovy new file mode 100644 index 0000000000..d020356260 --- /dev/null +++ b/src/test/groovy/graphql/schema/SchemaElementComparatorsTest.groovy @@ -0,0 +1,127 @@ +package graphql.schema + +import graphql.introspection.Introspection +import spock.lang.Specification + +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLEnumType.newEnum +import static graphql.schema.GraphQLInputObjectType.newInputObject +import static graphql.schema.GraphQLInterfaceType.newInterface +import static graphql.schema.GraphQLNonNull.nonNull +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLUnionType.newUnionType +import static graphql.schema.SchemaElementComparatorEnvironment.newEnvironment + +class SchemaElementComparatorsTest extends Specification { + + def "comparator environment identifies semantic schema kinds"() { + given: + def environment = newEnvironment( + SchemaObject, + SchemaField) + def equalEnvironment = newEnvironment( + SchemaObject, + SchemaField) + def differentEnvironment = newEnvironment( + SchemaObject, + SchemaArgument) + + expect: + environment.parentType == SchemaObject + environment.elementType == SchemaField + environment.is(environment) + environment == equalEnvironment + environment.hashCode() == equalEnvironment.hashCode() + environment != differentEnvironment + environment != "not an environment" + !environment.equals(null) + } + + def "schema element comparators provide as-is and by-name ordering"() { + given: + SchemaElement first = newObject().name("a").build() + SchemaElement second = newObject().name("b").build() + def environment = newEnvironment( + SchemaElement, + SchemaObject) + def directlySorted = [second, first] + def registrySorted = [second, first] + + when: + directlySorted.sort( + SchemaElementComparators.byNameAsc()) + registrySorted.sort( + SchemaElementComparatorRegistry.BY_NAME_REGISTRY + .getComparator(environment)) + + then: + SchemaElementComparators.asIsOrder() + .compare(second, first) == 0 + directlySorted == [first, second] + SchemaElementComparatorRegistry.AS_IS_REGISTRY + .getComparator(environment) + .compare(second, first) == 0 + registrySorted == [first, second] + } + + def "default schema element registry matches grouped and name ordering"() { + given: + SchemaElement modified = nonNull(GraphQLString) + SchemaElement directive = GraphQLDirective.newDirective() + .name("Directive") + .validLocation(Introspection.DirectiveLocation.FIELD) + .build() + SchemaElement interfaceType = newInterface() + .name("Interface") + .build() + SchemaElement possibleType = newObject() + .name("Possible") + .build() + SchemaElement unionType = newUnionType() + .name("Union") + .possibleType((GraphQLObjectType) possibleType) + .build() + SchemaElement objectType = newObject() + .name("Object") + .build() + SchemaElement enumType = newEnum() + .name("Enum") + .value("VALUE") + .build() + SchemaElement inputType = newInputObject() + .name("Input") + .build() + def elements = [ + inputType, + GraphQLString, + enumType, + objectType, + unionType, + interfaceType, + directive, + modified + ] + def graphQLSorted = elements.toList() + + when: + graphQLSorted.sort( + DefaultGraphqlTypeComparatorRegistry.DEFAULT_COMPARATOR) + elements.sort(SchemaElementComparatorRegistry.DEFAULT_REGISTRY + .getComparator(newEnvironment( + SchemaElement, + null))) + + then: + elements == graphQLSorted + elements == [ + directive, + interfaceType, + unionType, + objectType, + enumType, + GraphQLString, + modified, + inputType + ] + } +} diff --git a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy index 6e09a7fe08..c3be4af6aa 100644 --- a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy @@ -1,8 +1,8 @@ package graphql.schema import graphql.TestUtil +import graphql.schema.idl.AbstractSchemaPrintingTest import graphql.schema.idl.SchemaPrinter -import spock.lang.Specification import java.util.stream.Collectors @@ -23,7 +23,7 @@ import static graphql.schema.GraphQLUnionType.newUnionType import static graphql.schema.GraphqlTypeComparatorEnvironment.newEnvironment import static graphql.schema.idl.SchemaPrinter.Options.defaultOptions -class SchemaPrinterComparatorsTest extends Specification { +class SchemaPrinterComparatorsTest extends AbstractSchemaPrintingTest { def "scalarPrinter default comparator"() { given: @@ -584,21 +584,32 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .build() def schema = GraphQLSchema.newSchema().query(query).build() def environments = [] + def schemaEnvironments = [] GraphqlTypeComparatorRegistry registry = { environment -> environments.add([ environment.parentType, environment.elementType]) DEFAULT_COMPARATOR } - def options = defaultOptions().setComparators(registry) + SchemaElementComparatorRegistry schemaRegistry = { environment -> + schemaEnvironments.add([ + environment.parentType, + environment.elementType]) + SchemaElementComparators.sensibleGroupedOrder() + } + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) when: - new SchemaPrinter(options).print(schema) + printSchema(new SchemaPrinter(options), schema) then: - environments.contains([ + environments.count { + it == [ GraphQLSchemaElement.class, - null]) + null] + } == 1 environments.contains([ GraphQLObjectType.class, GraphQLFieldDefinition.class]) @@ -614,6 +625,26 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) environments.contains([ GraphQLAppliedDirective.class, GraphQLAppliedDirectiveArgument.class]) + schemaEnvironments.count { + it == [ + SchemaElement.class, + null] + } == 1 + schemaEnvironments.contains([ + SchemaObject.class, + SchemaField.class]) + schemaEnvironments.contains([ + SchemaField.class, + SchemaArgument.class]) + schemaEnvironments.contains([ + SchemaObject.class, + SchemaAppliedDirective.class]) + schemaEnvironments.contains([ + SchemaField.class, + SchemaAppliedDirective.class]) + schemaEnvironments.contains([ + SchemaAppliedDirective.class, + SchemaAppliedDirectiveArgument.class]) } diff --git a/src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy b/src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy new file mode 100644 index 0000000000..b60a266463 --- /dev/null +++ b/src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy @@ -0,0 +1,19 @@ +package graphql.schema.idl + +import graphql.schema.GraphQLSchema +import graphql.schema.universe.SchemaUniverse +import graphql.schema.universe.view.SUExecutableSchema +import spock.lang.Specification + +abstract class AbstractSchemaPrintingTest extends Specification { + + String printSchema(SchemaPrinter printer, GraphQLSchema schema) { + def expected = printer.print(schema) + def imported = new SchemaUniverse() + .importSchema("schema_printer_test", schema) + def executable = SUExecutableSchema.fromGraphQLSchema(imported, schema) + + assert printer.print(executable) == expected + return expected + } +} diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy index fcdb061e36..dbca52b2bd 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy @@ -17,7 +17,6 @@ import graphql.schema.GraphQLSchema import graphql.schema.GraphQLSchemaElement import graphql.schema.GraphQLUnionType import graphql.schema.TypeResolver -import spock.lang.Specification import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLEnumType.newEnum @@ -31,7 +30,7 @@ import static graphql.schema.GraphQLUnionType.newUnionType import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring import static graphql.schema.idl.SchemaPrinter.Options.defaultOptions -class SchemaPrinterAstBehaviorTest extends Specification { +class SchemaPrinterAstBehaviorTest extends AbstractSchemaPrintingTest { def "AST-aware semantic fallback remains equivalent to the semantic writer"() { given: @@ -54,10 +53,10 @@ class SchemaPrinterAstBehaviorTest extends Specification { expect: optionSets.every { options -> - def semantic = new SchemaPrinter( - options.useAstDefinitions(false)).print(schema) - def astAwareFallback = new SchemaPrinter( - options.useAstDefinitions(true)).print(schema) + def semantic = printSchema(new SchemaPrinter( + options.useAstDefinitions(false)), schema) + def astAwareFallback = printSchema(new SchemaPrinter( + options.useAstDefinitions(true)), schema) astAwareFallback == semantic } } @@ -83,10 +82,10 @@ class SchemaPrinterAstBehaviorTest extends Specification { expect: optionSets.every { options -> - def semantic = new SchemaPrinter( - options.useAstDefinitions(false)).print(schema) - def astAwareFallback = new SchemaPrinter( - options.useAstDefinitions(true)).print(schema) + def semantic = printSchema(new SchemaPrinter( + options.useAstDefinitions(false)), schema) + def astAwareFallback = printSchema(new SchemaPrinter( + options.useAstDefinitions(true)), schema) astAwareFallback == semantic } } diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy index c69d4b4ce6..5bf0afc15f 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy @@ -34,8 +34,11 @@ import graphql.schema.GraphQLType import graphql.schema.GraphQLUnionType import graphql.schema.GraphqlTypeComparatorRegistry import graphql.schema.GraphqlTypeComparators +import graphql.schema.SchemaElement +import graphql.schema.SchemaElementComparatorRegistry +import graphql.schema.SchemaElementComparators +import graphql.schema.SchemaNamedElement import graphql.schema.TypeResolver -import spock.lang.Specification import java.util.function.Predicate import java.util.function.UnaryOperator @@ -67,7 +70,7 @@ import static graphql.schema.idl.RuntimeWiring.newRuntimeWiring import static graphql.schema.idl.SchemaPrinter.ExcludeGraphQLSpecifiedDirectivesPredicate import static graphql.schema.idl.SchemaPrinter.Options.defaultOptions -class SchemaPrinterTest extends Specification { +class SchemaPrinterTest extends AbstractSchemaPrintingTest { def noDirectivesOption = defaultOptions().includeDirectives(false) @@ -209,7 +212,7 @@ class SchemaPrinterTest extends Specification { def "starWars default Test"() { GraphQLSchema schema = starWarsSchema() - def result = new SchemaPrinter().print(schema) + def result = printSchema(new SchemaPrinter(), schema) expect: result != null @@ -223,7 +226,7 @@ class SchemaPrinterTest extends Specification { .includeIntrospectionTypes(true) .includeScalarTypes(true) - def result = new SchemaPrinter(options).print(schema) + def result = printSchema(new SchemaPrinter(options), schema) expect: result != null @@ -247,7 +250,7 @@ class SchemaPrinterTest extends Specification { """) - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) expect: result == """type Mutation { @@ -283,7 +286,7 @@ type Subscription { .includeDirectives(false) .includeSchemaDefinition(true) - def result = new SchemaPrinter(options).print(schema) + def result = printSchema(new SchemaPrinter(options), schema) expect: result == """schema { @@ -330,7 +333,7 @@ type Subscription { """) - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) expect: result == """schema { @@ -360,7 +363,7 @@ type Subscription { def queryType = newObject().name("Query").description("About Query\nSecond Line").field(fieldDefinition).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''""" @@ -380,7 +383,7 @@ type Query { def queryType = newObject().name("Query").field(fieldDefinition).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''type Query { @@ -400,7 +403,7 @@ type Query { def queryType = newObject().name("Query").field(fieldDefinition).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == """type Query { @@ -421,7 +424,7 @@ type Query { def queryType = newObject().name("Query").field(fieldDefinition).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''type Query { @@ -460,7 +463,7 @@ enum Enum { .build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''"About union" @@ -502,7 +505,7 @@ type Query { .build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == """union Union = PossibleType1 | PossibleType2 @@ -536,7 +539,7 @@ type Query { def queryType = newObject().name("Query").field(fieldDefinition2).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''type Query { @@ -572,7 +575,7 @@ input Input { .build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''"about interface" @@ -611,7 +614,7 @@ type Query { def queryType = newObject().name("Query").field(fieldDefinition).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(defaultOptions().includeScalarTypes(true).includeDirectives(false)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeScalarTypes(true).includeDirectives(false)), schema) then: result == '''type Query { @@ -633,7 +636,7 @@ scalar Scalar def queryType = newObject().name("Query").field(fieldDefinition2).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''type Query { @@ -692,7 +695,7 @@ scalar Scalar def codeRegistry = GraphQLCodeRegistry.newCodeRegistry().typeResolver(interfaceType, { env -> null }).build() def schema = GraphQLSchema.newSchema().query(queryType).codeRegistry(codeRegistry).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''interface interfaceType { @@ -735,7 +738,7 @@ input inputObjectType { def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''"test" @@ -769,7 +772,7 @@ type Query { """) - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) expect: result == """type Query { @@ -877,7 +880,7 @@ scalar Asteroid ''') - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) expect: result == '''""" @@ -960,7 +963,7 @@ type Query { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def result = new SchemaPrinter(defaultOptions().includeScalarTypes(true)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeScalarTypes(true)), schema) then: // args and directives are sorted like the rest of the schema printer @@ -1072,10 +1075,9 @@ input SomeInput @inputTypeDirective { } ''' when: - def resultNoDirectives = new SchemaPrinter(defaultOptions() + def resultNoDirectives = printSchema(new SchemaPrinter(defaultOptions() .includeScalarTypes(true) - .includeDirectives(false)) - .print(schema) + .includeDirectives(false)), schema) then: // args and directives are sorted like the rest of the schema printer @@ -1149,7 +1151,7 @@ input SomeInput { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def result = new SchemaPrinter(defaultOptions().includeScalarTypes(true)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeScalarTypes(true)), schema) then: // args and directives are sorted like the rest of the schema printer @@ -1237,7 +1239,7 @@ input Input { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def resultWithNoDirectives = new SchemaPrinter(defaultOptions().includeDirectives(false)).print(schema) + def resultWithNoDirectives = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(false)), schema) then: resultWithNoDirectives == '''type Query { @@ -1246,7 +1248,7 @@ input Input { ''' when: - def resultWithSomeDirectives = new SchemaPrinter(defaultOptions().includeDirectives({ it == "example" })).print(schema) + def resultWithSomeDirectives = printSchema(new SchemaPrinter(defaultOptions().includeDirectives({ it == "example" })), schema) then: resultWithSomeDirectives == '''directive @example on FIELD_DEFINITION @@ -1257,7 +1259,7 @@ type Query { ''' when: - def resultWithDirectives = new SchemaPrinter(defaultOptions().includeDirectives(true)).print(schema) + def resultWithDirectives = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(true)), schema) then: resultWithDirectives == '''"This directive allows results to be deferred during execution" @@ -1327,7 +1329,7 @@ type Query { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def resultWithNoDirectiveDefinitions = new SchemaPrinter(defaultOptions().includeDirectiveDefinitions(false)).print(schema) + def resultWithNoDirectiveDefinitions = printSchema(new SchemaPrinter(defaultOptions().includeDirectiveDefinitions(false)), schema) then: resultWithNoDirectiveDefinitions == '''type Query { @@ -1336,7 +1338,7 @@ type Query { ''' when: - def resultWithDirectiveDefinitions = new SchemaPrinter(defaultOptions().includeDirectiveDefinitions(true)).print(schema) + def resultWithDirectiveDefinitions = printSchema(new SchemaPrinter(defaultOptions().includeDirectiveDefinitions(true)), schema) then: resultWithDirectiveDefinitions == '''"This directive allows results to be deferred during execution" @@ -1429,7 +1431,7 @@ type Query { def printOptions = defaultOptions() .useAstDefinitions(true) .includeSchemaDefinition(true) - def result = new SchemaPrinter(printOptions).print(schema) + def result = printSchema(new SchemaPrinter(printOptions), schema) then: result == '''""" @@ -1535,7 +1537,7 @@ type MySubscription { def printOptions = defaultOptions() .useAstDefinitions(false) .includeSchemaDefinition(true) - def result = new SchemaPrinter(printOptions).print(schema) + def result = printSchema(new SchemaPrinter(printOptions), schema) then: result == '''"My schema block description" @@ -1692,7 +1694,7 @@ type MyQuery { GraphQLSchema schema = new SchemaGenerator().makeExecutableSchema(options, types, runtimeWiring) def printOptions = defaultOptions().includeScalarTypes(true).useAstDefinitions(true) - def result = new SchemaPrinter(printOptions).print(schema) + def result = printSchema(new SchemaPrinter(printOptions), schema) then: result == '''"This directive allows results to be deferred during execution" @@ -1848,7 +1850,7 @@ extend type Query { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(false)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(false)), schema) then: result == '''type Field { @@ -1892,7 +1894,7 @@ input Input { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def result = new SchemaPrinter(defaultOptions().descriptionsAsHashComments(true).includeDirectives(false)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().descriptionsAsHashComments(true).includeDirectives(false)), schema) then: result == '''#This is the docstring of @@ -1920,7 +1922,7 @@ type Query { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) then: result == '''type Query { @@ -1945,7 +1947,7 @@ type Query { when: def printOptions = defaultOptions().includeDirectives({ dName -> (dName == "deprecated") }) - def result = new SchemaPrinter(printOptions).print(schema) + def result = printSchema(new SchemaPrinter(printOptions), schema) then: result == '''"Marks an element of a GraphQL schema as no longer supported." @@ -1970,7 +1972,7 @@ type Query { def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) expect: @@ -1997,7 +1999,7 @@ scalar RandomScalar def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) - def result = new SchemaPrinter(noDirectivesOption).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption), schema) expect: assert !result.contains("ID") && !result.contains("Float") && !result.contains("Boolean") @@ -2038,7 +2040,7 @@ scalar CustomScalar def schema = GraphQLSchema.newSchema().query(queryType).additionalType(myScalar).build() - def result = new SchemaPrinter(defaultOptions().includeDirectives(false)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(false)), schema) expect: ScalarInfo.GRAPHQL_SPECIFICATION_SCALARS.forEach({ @@ -2080,7 +2082,7 @@ scalar RandomScalar def schema = GraphQLSchema.newSchema().query(queryType).additionalType(myScalar).build() - def result = new SchemaPrinter(defaultOptions().includeScalarTypes(true).includeDirectives(false)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeScalarTypes(true).includeDirectives(false)), schema) expect: result == @@ -2107,7 +2109,7 @@ scalar Scalar def schema = new SchemaGenerator().makeExecutableSchema(options, registry, runtimeWiring) when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(false)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(false)), schema) then: result == """type Query { @@ -2147,7 +2149,7 @@ scalar Scalar """ def schema = TestUtil.schema(sdl) when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(false)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(false)), schema) then: result == """interface Node { @@ -2203,7 +2205,19 @@ type Query { } return false } - def result = new SchemaPrinter(defaultOptions().includeDirectives(true).includeSchemaElement(predicate)).print(schema) + Predicate schemaPredicate = { element -> + if (element instanceof SchemaNamedElement) { + return ((SchemaNamedElement) element) + .getName() + .contains("PrintMe") + } + return false + } + def options = defaultOptions() + .includeDirectives(true) + .includeSchemaElement(predicate) + .filterSchemaElements(schemaPredicate) + def result = printSchema(new SchemaPrinter(options), schema) then: result == """schema { @@ -2224,6 +2238,39 @@ type PrintMeType { } + def "schema element predicates are evaluated in compatibility order"() { + given: + def schema = TestUtil.schema(""" + type Query { + value: String + } + """) + def calls = [] + Predicate graphQLPredicate = { element -> + if (element instanceof GraphQLNamedSchemaElement + && element.name == "Query") { + calls.add("graphql") + } + return true + } + Predicate schemaPredicate = { element -> + if (element instanceof SchemaNamedElement + && element.name == "Query") { + calls.add("schema") + } + return true + } + def options = defaultOptions() + .includeSchemaElement(graphQLPredicate) + .filterSchemaElements(schemaPredicate) + + when: + new SchemaPrinter(options).print(schema) + + then: + calls == ["graphql", "schema"] + } + def "schema with directive prints directive"() { def sdl = """ directive @foo on SCHEMA @@ -2235,7 +2282,7 @@ type PrintMeType { def schema = TestUtil.schema(sdl) when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(true)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(true)), schema) then: result == """schema @foo{ @@ -2324,7 +2371,7 @@ type MyQuery { def queryType = newObject().name("Query").field(field).description(descriptionWithTripleQuote).build() def schema = GraphQLSchema.newSchema().query(queryType).build() when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(ExcludeGraphQLSpecifiedDirectivesPredicate)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(ExcludeGraphQLSpecifiedDirectivesPredicate)), schema) then: result == '''""" @@ -2347,7 +2394,7 @@ type Query { """ def schema = TestUtil.schema(sdl) when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(ExcludeGraphQLSpecifiedDirectivesPredicate)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeDirectives(ExcludeGraphQLSpecifiedDirectivesPredicate)), schema) then: @@ -2459,9 +2506,14 @@ type Query { // by name descending GraphqlTypeComparatorRegistry comparatorRegistry = { env -> return GraphqlTypeComparators.byNameAsc().reversed() } - def options = defaultOptions().includeDirectives(true).setComparators(comparatorRegistry) + SchemaElementComparatorRegistry schemaComparatorRegistry = + { env -> SchemaElementComparators.byNameAsc().reversed() } + def options = defaultOptions() + .includeDirectives(true) + .setComparators(comparatorRegistry) + .sortSchemaElements(schemaComparatorRegistry) when: - def result = new SchemaPrinter(options).print(schema) + def result = printSchema(new SchemaPrinter(options), schema) then: result == '''"Exposes a URL that specifies the behaviour of this scalar." @@ -2564,7 +2616,7 @@ type Query { def queryType = newObject().name("Query").field(fieldDefinition).build() def schema = GraphQLSchema.newSchema().description("About Schema").query(queryType).build() when: - def result = new SchemaPrinter(noDirectivesOption.includeSchemaDefinition(true)).print(schema) + def result = printSchema(new SchemaPrinter(noDirectivesOption.includeSchemaDefinition(true)), schema) println(result) then: @@ -2828,7 +2880,7 @@ input Gun { .query(queryType) .build() when: - def result = new SchemaPrinter(defaultOptions().includeSchemaDefinition(true).includeAstDefinitionComments(true)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeSchemaDefinition(true).includeAstDefinitionComments(true)), schema) println(result) then: @@ -2847,7 +2899,7 @@ input Gun { def options = SchemaGenerator.Options.defaultOptions().useCommentsAsDescriptions(false) def schema = new SchemaGenerator().makeExecutableSchema(options, registry, wiring) when: - def result = new SchemaPrinter(defaultOptions().includeSchemaDefinition(true).includeAstDefinitionComments(true)).print(schema) + def result = printSchema(new SchemaPrinter(defaultOptions().includeSchemaDefinition(true).includeAstDefinitionComments(true)), schema) println(result) // @TODO: Schema Parser seems to be ignoring directive and scalar comments and needs to be fixed. @@ -2989,7 +3041,7 @@ input Gun { when: def options = defaultOptions().includeDirectiveDefinitions(false) - def sdl = new SchemaPrinter(options).print(schema) + def sdl = printSchema(new SchemaPrinter(options), schema) then: sdl == '''type Query { f(arg: String = null): String @@ -3020,7 +3072,7 @@ input Gun { def printOptions = defaultOptions().includeDirectiveDefinitions(false).includeDirectives({ d -> true }) - def result = "\n" + new SchemaPrinter(printOptions).print(schema) + def result = "\n" + printSchema(new SchemaPrinter(printOptions), schema) println(result) then: @@ -3055,7 +3107,7 @@ input Input { def options = defaultOptions() .includeDirectiveDefinitions(true) .includeDirectiveDefinition({ it != "skip" }) - def result = new SchemaPrinter(options).print(schema) + def result = printSchema(new SchemaPrinter(options), schema) expect: "has no skip definition" @@ -3097,5 +3149,3 @@ type Query { """ } } - - diff --git a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy index d74248285b..44fa50a0d7 100644 --- a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy @@ -7,6 +7,7 @@ import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import graphql.schema.GraphqlTypeComparatorRegistry +import graphql.schema.SchemaElementComparatorRegistry import graphql.schema.idl.SchemaParser import graphql.schema.idl.SchemaPrinter import graphql.schema.idl.TypeDefinitionRegistry @@ -120,6 +121,7 @@ class SUExecutableSchemaPrinterTest extends Specification { new SchemaUniverse().importSchema(orderingName, registry) def options = SchemaPrinter.Options.defaultOptions() .setComparators(comparatorRegistry) + .sortSchemaElements(schemaComparatorRegistry) def printer = new SchemaPrinter(options) when: @@ -131,9 +133,9 @@ class SUExecutableSchemaPrinterTest extends Specification { graphQLSdl.contains(expectedFields) where: - orderingName | comparatorRegistry | expectedFields - "as_is" | GraphqlTypeComparatorRegistry.AS_IS_REGISTRY | " z(z: Int, a: Int): String\n a: String" - "by_name" | GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY | " a: String\n z(a: Int, z: Int): String" + orderingName | comparatorRegistry | schemaComparatorRegistry | expectedFields + "as_is" | GraphqlTypeComparatorRegistry.AS_IS_REGISTRY | SchemaElementComparatorRegistry.AS_IS_REGISTRY | " z(z: Int, a: Int): String\n a: String" + "by_name" | GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY | SchemaElementComparatorRegistry.BY_NAME_REGISTRY | " a: String\n z(a: Int, z: Int): String" } @Requires({ Boolean.getBoolean("graphql.largeSchemaSdlTest") }) diff --git a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy index e09b83ecc9..1875092eb4 100644 --- a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaValuesResolverTest.groovy @@ -13,13 +13,18 @@ import graphql.execution.ValuesResolver import graphql.execution.values.InputInterceptor import graphql.execution.values.legacycoercing.LegacyCoercingInputInterceptor import graphql.language.AstPrinter +import graphql.language.EnumValue import graphql.language.Field import graphql.language.ListType import graphql.language.NonNullType import graphql.language.OperationDefinition +import graphql.language.StringValue import graphql.language.TypeName import graphql.language.Value import graphql.normalized.NormalizedInputValue +import graphql.schema.CoercingParseLiteralException +import graphql.schema.CoercingParseValueException +import graphql.schema.CoercingSerializeException import graphql.schema.GraphQLArgument import graphql.schema.GraphQLEnumType import graphql.schema.GraphQLFieldDefinition @@ -311,12 +316,13 @@ class SUExecutableSchemaValuesResolverTest extends Specification { def modeVertex = universeSchema.schema.getEnumType("Mode") def fastVertex = universeSchema.schema.getEnumValue(modeVertex, "FAST") def slowVertex = universeSchema.schema.getEnumValue(modeVertex, "SLOW") + def context = GraphQLContext.default expect: - universeSchema.getEnumRuntimeValue( - originalEnum.getValue("FAST")) == 11 - universeSchema.getEnumRuntimeValue( - originalEnum.getValue("SLOW")) == 22 + originalEnum.parseValue( + "FAST", context, Locale.ENGLISH) == 11 + originalEnum.parseValue( + "SLOW", context, Locale.ENGLISH) == 22 when: def changed = universeSchema.transform { @@ -326,16 +332,69 @@ class SUExecutableSchemaValuesResolverTest extends Specification { SchemaEnum changedEnum = changed.getType("Mode") as SchemaEnum then: - universeSchema.getEnumRuntimeValue( - originalEnum.getValue("FAST")) == 11 - universeSchema.getEnumRuntimeValue( - originalEnum.getValue("SLOW")) == 22 - changed.getEnumRuntimeValue(changedEnum.getValue("FAST")) == 99 - changed.getEnumRuntimeValue(changedEnum.getValue("SLOW")) == "SLOW" + originalEnum.parseValue( + "FAST", context, Locale.ENGLISH) == 11 + originalEnum.parseValue( + "SLOW", context, Locale.ENGLISH) == 22 + changedEnum.parseValue( + "FAST", context, Locale.ENGLISH) == 99 + changedEnum.parseValue( + "SLOW", context, Locale.ENGLISH) == "SLOW" + changedEnum.parseLiteral( + EnumValue.newEnumValue("FAST").build(), + context, + Locale.ENGLISH) == 99 + changedEnum.serialize( + 99, context, Locale.ENGLISH) == "FAST" + changedEnum.valueToLiteral( + "FAST", context, Locale.ENGLISH).name == "FAST" changed.getEnumRuntimeValueById().get(fastVertex.id) == 99 changed.getEnumRuntimeValueById().get(slowVertex.id) == null } + def "enum coercion rejects invalid inputs"() { + given: + SchemaEnum enumType = universeSchema.getType("Mode") as SchemaEnum + def context = GraphQLContext.default + + when: + enumType.serialize(99, context, Locale.ENGLISH) + + then: + thrown(CoercingSerializeException) + + when: + enumType.parseValue("UNKNOWN", context, Locale.ENGLISH) + + then: + thrown(CoercingParseValueException) + + when: + enumType.parseLiteral( + StringValue.newStringValue("FAST").build(), + context, + Locale.ENGLISH) + + then: + thrown(CoercingParseLiteralException) + + when: + enumType.parseLiteral( + EnumValue.newEnumValue("UNKNOWN").build(), + context, + Locale.ENGLISH) + + then: + thrown(CoercingParseLiteralException) + + when: + enumType.valueToLiteral( + "UNKNOWN", context, Locale.ENGLISH) + + then: + thrown(AssertException) + } + def "enum runtime values reject null"() { given: def modeVertex = universeSchema.schema.getEnumType("Mode") From 850f71238fe977604fae6d88de7a2ce04eb2cbe6 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 7 Aug 2026 05:02:54 +0000 Subject: [PATCH 24/32] Use schema comparator in universe round trip tests --- .../graphql/schema/universe/SUSchemaRoundTripTest.groovy | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy index 2cc35aacba..f23bcfedc2 100644 --- a/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUSchemaRoundTripTest.groovy @@ -2,6 +2,7 @@ package graphql.schema.universe import graphql.schema.GraphQLSchema import graphql.schema.GraphqlTypeComparatorRegistry +import graphql.schema.SchemaElementComparatorRegistry import graphql.schema.idl.SchemaParser import graphql.schema.idl.SchemaPrinter import graphql.schema.idl.TypeDefinitionRegistry @@ -79,6 +80,8 @@ class SUSchemaRoundTripTest extends Specification { .includeSchemaDefinition(true) .includeScalarTypes(true) .setComparators(GraphqlTypeComparatorRegistry.BY_NAME_REGISTRY) + .sortSchemaElements( + SchemaElementComparatorRegistry.BY_NAME_REGISTRY) } private static String operationRootsAndCyclesSdl() { From 4a267e41264fd13bb2bc1c716ecb3ea124c96bf2 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Fri, 7 Aug 2026 20:40:04 +0000 Subject: [PATCH 25/32] Remove unreachable schema printer comparator mappings --- .../graphql/schema/idl/SchemaPrinter.java | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index 3da59432a1..e1dd14d227 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -2028,21 +2028,6 @@ private Class graphQLParentClass( if (element instanceof SchemaAppliedDirective) { return GraphQLAppliedDirective.class; } - if (element instanceof SchemaDirective) { - return GraphQLDirective.class; - } - if (element instanceof SchemaUnion) { - return GraphQLUnionType.class; - } - if (element instanceof SchemaEnum) { - return GraphQLEnumType.class; - } - if (element instanceof SchemaScalar) { - return GraphQLScalarType.class; - } - if (element instanceof SchemaInputObject) { - return GraphQLInputObjectType.class; - } return GraphQLSchemaElement.class; } @@ -2119,21 +2104,6 @@ private Class schemaParentClass( if (element instanceof SchemaAppliedDirective) { return SchemaAppliedDirective.class; } - if (element instanceof SchemaDirective) { - return SchemaDirective.class; - } - if (element instanceof SchemaUnion) { - return SchemaUnion.class; - } - if (element instanceof SchemaEnum) { - return SchemaEnum.class; - } - if (element instanceof SchemaScalar) { - return SchemaScalar.class; - } - if (element instanceof SchemaInputObject) { - return SchemaInputObject.class; - } return SchemaElement.class; } From b937c05e4367eca1b7777b279dcfb786513ddf76 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sat, 8 Aug 2026 21:38:12 +0000 Subject: [PATCH 26/32] Support direct printing of schema view elements --- .../graphql/schema/GraphQLTypeReference.java | 8 + .../schema/SchemaDirectiveContainer.java | 9 ++ .../graphql/schema/SchemaFieldsContainer.java | 9 ++ .../schema/SchemaInputFieldsContainer.java | 9 ++ .../java/graphql/schema/SchemaScalar.java | 7 + .../graphql/schema/idl/SchemaPrinter.java | 123 ++++++++++++---- .../graphql/schema/universe/SUImporter.java | 46 +++++- .../view/AbstractSUSchemaElement.java | 19 +++ .../universe/view/SUSchemaInputObject.java | 21 +++ .../universe/view/SUSchemaInterface.java | 14 ++ .../view/SUSchemaIntrospectionArgument.java | 8 + .../view/SUSchemaIntrospectionField.java | 6 + .../schema/universe/view/SUSchemaObject.java | 14 ++ .../schema/universe/view/SUSchemaScalar.java | 6 + .../schema/ExecutableSchemaTest.groovy | 22 ++- .../SchemaPrinterComparatorsTest.groovy | 40 +++-- .../schema/SchemaTransformerTest.groovy | 21 ++- .../idl/AbstractSchemaPrintingTest.groovy | 137 ++++++++++++++++++ ...irectivesOnDirectiveDefinitionsTest.groovy | 9 +- .../idl/SchemaPrinterAstBehaviorTest.groovy | 5 +- .../schema/idl/SchemaPrinterTest.groovy | 81 +++++++++-- .../SUExecutableSchemaPrinterTest.groovy | 2 + 22 files changed, 542 insertions(+), 74 deletions(-) diff --git a/src/main/java/graphql/schema/GraphQLTypeReference.java b/src/main/java/graphql/schema/GraphQLTypeReference.java index 08fb3df47a..b4011a117d 100644 --- a/src/main/java/graphql/schema/GraphQLTypeReference.java +++ b/src/main/java/graphql/schema/GraphQLTypeReference.java @@ -6,6 +6,9 @@ import graphql.util.TraversalControl; import graphql.util.TraverserContext; +import java.util.Collections; +import java.util.List; + import static graphql.Assert.assertValidName; /** @@ -45,6 +48,11 @@ public String getDescription() { return null; } + @Override + public List getAppliedDirectives() { + return Collections.emptyList(); + } + @Override public Node getDefinition() { return null; diff --git a/src/main/java/graphql/schema/SchemaDirectiveContainer.java b/src/main/java/graphql/schema/SchemaDirectiveContainer.java index 90f5f2c191..ab6e32fddb 100644 --- a/src/main/java/graphql/schema/SchemaDirectiveContainer.java +++ b/src/main/java/graphql/schema/SchemaDirectiveContainer.java @@ -3,10 +3,19 @@ import graphql.ExperimentalApi; import org.jspecify.annotations.NullMarked; +import java.util.List; + /** * A schema element on which directives can be applied. */ @ExperimentalApi @NullMarked public interface SchemaDirectiveContainer extends SchemaNamedElement { + + /** + * Returns the directives applied directly to this element. + * + * @return applied directives in declaration order + */ + List getAppliedDirectives(); } diff --git a/src/main/java/graphql/schema/SchemaFieldsContainer.java b/src/main/java/graphql/schema/SchemaFieldsContainer.java index df28697754..e8bb9e3712 100644 --- a/src/main/java/graphql/schema/SchemaFieldsContainer.java +++ b/src/main/java/graphql/schema/SchemaFieldsContainer.java @@ -3,10 +3,19 @@ import graphql.ExperimentalApi; import org.jspecify.annotations.NullMarked; +import java.util.List; + /** * A composite type that declares output fields. */ @ExperimentalApi @NullMarked public interface SchemaFieldsContainer extends SchemaComposite { + + /** + * Returns all fields declared by this type without applying schema-level visibility. + * + * @return the declared field definitions + */ + List getFieldDefinitions(); } diff --git a/src/main/java/graphql/schema/SchemaInputFieldsContainer.java b/src/main/java/graphql/schema/SchemaInputFieldsContainer.java index b161db92d1..5738e43e42 100644 --- a/src/main/java/graphql/schema/SchemaInputFieldsContainer.java +++ b/src/main/java/graphql/schema/SchemaInputFieldsContainer.java @@ -3,10 +3,19 @@ import graphql.ExperimentalApi; import org.jspecify.annotations.NullMarked; +import java.util.List; + /** * A named type that declares input fields. */ @ExperimentalApi @NullMarked public interface SchemaInputFieldsContainer extends SchemaNamedType { + + /** + * Returns all input fields declared by this type without applying schema-level visibility. + * + * @return the declared input field definitions + */ + List getFieldDefinitions(); } diff --git a/src/main/java/graphql/schema/SchemaScalar.java b/src/main/java/graphql/schema/SchemaScalar.java index ffd61a6da6..cb1f77cf5d 100644 --- a/src/main/java/graphql/schema/SchemaScalar.java +++ b/src/main/java/graphql/schema/SchemaScalar.java @@ -12,6 +12,13 @@ public interface SchemaScalar extends SchemaNamedType, SchemaInputType, SchemaOutputType { + /** + * Returns the coercing associated with this scalar. + * + * @return the scalar coercing + */ + Coercing getCoercing(); + /** * Returns the URL identifying the specification implemented by this scalar. * diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index e1dd14d227..b9269a6dce 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -33,6 +33,7 @@ import graphql.language.TypeDefinition; import graphql.language.UnionTypeDefinition; import graphql.language.Value; +import graphql.schema.Coercing; import graphql.schema.DefaultGraphqlTypeComparatorRegistry; import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLAppliedDirective; @@ -1486,9 +1487,29 @@ private String directiveDefinition( } public String print(GraphQLType type) { + return print((SchemaType) type); + } + + /** + * Prints a schema type without applying schema-level visibility. + * + * @param type the standalone schema type + * + * @return the logical type definition + */ + @ExperimentalApi + public String print(SchemaType type) { StringWriter sw = new StringWriter(); PrintWriter out = new PrintWriter(sw); + printType(out, type); + + return trimNewLineChars(sw.toString()); + } + + private void printType( + PrintWriter out, + SchemaType type) { if (type instanceof SchemaNamedElement) { printSchemaElement( out, @@ -1497,22 +1518,25 @@ public String print(GraphQLType type) { } else { out.print("Type not implemented : " + type + "\n"); } - - return trimNewLineChars(sw.toString()); } - public String print(List elements) { + /** + * Prints standalone schema types and directive definitions without applying schema-level + * visibility. + * + * @param elements the elements to print + * + * @return the logical schema definitions + */ + public String print(List elements) { StringWriter sw = new StringWriter(); PrintWriter out = new PrintWriter(sw); - for (GraphQLSchemaElement element : elements) { - if (element instanceof GraphQLDirective) { - out.print(print(((GraphQLDirective) element))); - } else if (element instanceof GraphQLType) { - printSchemaElement( - out, - (SchemaNamedElement) element, - null); + for (SchemaElement element : elements) { + if (element instanceof SchemaDirective) { + out.print(print((SchemaDirective) element)); + } else if (element instanceof SchemaType) { + printType(out, (SchemaType) element); } else { Assert.assertShouldNeverHappen("How did we miss a %s", element.getClass()); } @@ -1521,7 +1545,19 @@ public String print(List elements) { } public String print(GraphQLDirective graphQLDirective) { - return directiveDefinition(null, graphQLDirective); + return print((SchemaDirective) graphQLDirective); + } + + /** + * Prints a standalone directive definition. + * + * @param directive the directive definition + * + * @return the logical directive definition + */ + @ExperimentalApi + public String print(SchemaDirective directive) { + return directiveDefinition(null, directive); } private void printSchemaElement( @@ -1710,12 +1746,16 @@ private List getAppliedDirectives( if (schema != null) { return schema.getAppliedDirectives(container); } - GraphQLDirectiveContainer graphQLContainer = - (GraphQLDirectiveContainer) container; - if (isDeprecated(graphQLContainer)) { - return addOrUpdateDeprecatedDirectiveIfNeeded(graphQLContainer); + if (container instanceof GraphQLDirectiveContainer) { + GraphQLDirectiveContainer graphQLContainer = + (GraphQLDirectiveContainer) container; + if (isDeprecated(graphQLContainer)) { + return addOrUpdateDeprecatedDirectiveIfNeeded( + graphQLContainer); + } + return DirectivesUtil.toAppliedDirectives(graphQLContainer); } - return DirectivesUtil.toAppliedDirectives(graphQLContainer); + return container.getAppliedDirectives(); } private boolean isDeprecated( @@ -1744,10 +1784,7 @@ private List getFields( if (schema != null) { return schema.getFields(type); } - if (type instanceof GraphQLObjectType) { - return ((GraphQLObjectType) type).getFieldDefinitions(); - } - return ((GraphQLInterfaceType) type).getFieldDefinitions(); + return type.getFieldDefinitions(); } private List getInputFields( @@ -1756,7 +1793,7 @@ private List getInputFields( if (schema != null) { return schema.getInputFields(type); } - return ((GraphQLInputObjectType) type).getFieldDefinitions(); + return type.getFieldDefinitions(); } private String printValue( @@ -1776,14 +1813,14 @@ private String printValue( (Value) assertNotNull(value.getValue())); } return AstPrinter.printAst(valueToLiteral( - assertNotNull(schema), + schema, value.getValue(), type, value.isInternal())); } private Value valueToLiteral( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, @Nullable Object value, SchemaInputType type, boolean internal) { @@ -1806,7 +1843,10 @@ private Value valueToLiteral( internal); } if (unwrappedType instanceof SchemaEnum) { - return EnumValue.newEnumValue(String.valueOf(value)).build(); + return enumLiteral( + value, + (SchemaEnum) unwrappedType, + internal); } return scalarLiteral( schema, @@ -1824,7 +1864,7 @@ private SchemaInputType unwrapNonNull(SchemaInputType type) { } private Value listLiteral( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, Object value, SchemaList type, boolean internal) { @@ -1842,7 +1882,7 @@ private Value listLiteral( } private Value objectLiteral( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, Object value, SchemaInputObject type, boolean internal) { @@ -1855,7 +1895,7 @@ private Value objectLiteral( } Map map = (Map) value; List fields = new ArrayList<>(); - for (SchemaInputField field : schema.getInputFields(type)) { + for (SchemaInputField field : getInputFields(schema, type)) { if (!map.containsKey(field.getName())) { continue; } @@ -1871,18 +1911,39 @@ private Value objectLiteral( return ObjectValue.newObjectValue().objectFields(fields).build(); } + private Value enumLiteral( + Object value, + SchemaEnum type, + boolean internal) { + if (!internal) { + return type.valueToLiteral( + value, + GraphQLContext.getDefault(), + Locale.getDefault()); + } + Object serialized = type.serialize( + value, + GraphQLContext.getDefault(), + Locale.getDefault()); + return EnumValue.newEnumValue( + String.valueOf(serialized)).build(); + } + private Value scalarLiteral( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, Object value, SchemaScalar type, boolean internal) { + Coercing coercing = schema == null + ? type.getCoercing() + : schema.getScalarCoercing(type); Object externalValue = internal - ? schema.getScalarCoercing(type).serialize( + ? coercing.serialize( value, GraphQLContext.getDefault(), Locale.getDefault()) : value; - return schema.getScalarCoercing(type).valueToLiteral( + return coercing.valueToLiteral( assertNotNull(externalValue), GraphQLContext.getDefault(), Locale.getDefault()); diff --git a/src/main/java/graphql/schema/universe/SUImporter.java b/src/main/java/graphql/schema/universe/SUImporter.java index 8053a3d755..476e639b99 100644 --- a/src/main/java/graphql/schema/universe/SUImporter.java +++ b/src/main/java/graphql/schema/universe/SUImporter.java @@ -390,8 +390,25 @@ private void addAppliedDirectives( SUAppliedDirectiveContainer containerVertex) { List directives = DirectivesUtil.toAppliedDirectives(container); - addAppliedDirectives(directives, containerVertex); - addDeprecatedDirective(container, directives, containerVertex); + String reason = deprecationReason(container); + boolean hasDeprecatedDirective = false; + for (GraphQLAppliedDirective directive : directives) { + if (Directives.DeprecatedDirective.getName().equals( + directive.getName())) { + hasDeprecatedDirective = true; + if (replaceDeprecationReason( + directive, + reason, + containerVertex)) { + continue; + } + } + SUAppliedDirective directiveVertex = appliedDirective(directive); + builder().addAppliedDirective(containerVertex, directiveVertex); + } + if (reason != null && !hasDeprecatedDirective) { + addDeprecatedDirective(reason, containerVertex); + } } private void addAppliedDirectives( @@ -403,14 +420,27 @@ private void addAppliedDirectives( } } - private void addDeprecatedDirective( - GraphQLDirectiveContainer container, - List directives, + private boolean replaceDeprecationReason( + GraphQLAppliedDirective directive, + @Nullable String reason, SUAppliedDirectiveContainer containerVertex) { - String reason = deprecationReason(container); - if (reason == null || hasDirective(directives, Directives.DeprecatedDirective.getName())) { - return; + if (reason == null) { + return false; + } + GraphQLAppliedDirectiveArgument argument = + directive.getArgument("reason"); + if (argument != null + && argument.getArgumentValue() + == graphql.schema.InputValueWithState.NOT_SET) { + return false; } + addDeprecatedDirective(reason, containerVertex); + return true; + } + + private void addDeprecatedDirective( + String reason, + SUAppliedDirectiveContainer containerVertex) { addStringDirective( Directives.DeprecatedDirective.getName(), "reason", diff --git a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java index 7eb8d0ee56..962e8f01dd 100644 --- a/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java +++ b/src/main/java/graphql/schema/universe/view/AbstractSUSchemaElement.java @@ -2,10 +2,16 @@ import graphql.Internal; import graphql.language.Node; +import graphql.schema.SchemaAppliedDirective; import graphql.schema.SchemaElement; +import graphql.schema.universe.SUAppliedDirective; import graphql.schema.universe.SUVertex; import org.jspecify.annotations.Nullable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + import static graphql.Assert.assertNotNull; /** @@ -53,6 +59,19 @@ public final SUVertex getVertex() { return executableSchema.getSchema().getDefinition(vertex); } + public final List getAppliedDirectives() { + List directives = + executableSchema.getSchema().getAppliedDirectives(vertex); + List result = + new ArrayList<>(directives.size()); + for (SUAppliedDirective directive : directives) { + result.add(new SUSchemaAppliedDirective( + executableSchema, + directive)); + } + return Collections.unmodifiableList(result); + } + @Override public final boolean equals(Object other) { if (this == other) { diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java b/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java index c2ac15c4b8..b0be90ab04 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInputObject.java @@ -2,9 +2,15 @@ import graphql.Directives; import graphql.Internal; +import graphql.schema.SchemaInputField; import graphql.schema.SchemaInputObject; +import graphql.schema.universe.SUInputField; import graphql.schema.universe.SUInputObjectType; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + @Internal public final class SUSchemaInputObject extends AbstractSUSchemaNamedType implements SchemaInputObject { @@ -26,6 +32,21 @@ public boolean isOneOf() { .isEmpty(); } + @Override + public List getFieldDefinitions() { + List fields = getExecutableSchema() + .getSchema() + .getInputFields(getInputObjectTypeVertex()); + List result = + new ArrayList<>(fields.size()); + for (SUInputField field : fields) { + result.add(new SUSchemaInputField( + getExecutableSchema(), + field)); + } + return Collections.unmodifiableList(result); + } + @Internal public SUInputObjectType getInputObjectTypeVertex() { return (SUInputObjectType) getTypeVertex(); diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java index 7ee0a24ec8..1554e1a357 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaInterface.java @@ -1,7 +1,9 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.schema.SchemaField; import graphql.schema.SchemaInterface; +import graphql.schema.universe.SUField; import graphql.schema.universe.SUInterfaceType; import java.util.ArrayList; @@ -34,6 +36,18 @@ public List getInterfaces() { return Collections.unmodifiableList(result); } + @Override + public List getFieldDefinitions() { + List fields = getExecutableSchema() + .getSchema() + .getFields(getInterfaceTypeVertex()); + List result = new ArrayList<>(fields.size()); + for (SUField field : fields) { + result.add(new SUSchemaField(getExecutableSchema(), field)); + } + return Collections.unmodifiableList(result); + } + @Internal public SUInterfaceType getInterfaceTypeVertex() { return (SUInterfaceType) getTypeVertex(); diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java index b49a07ebda..a47021aecc 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionArgument.java @@ -2,12 +2,15 @@ import graphql.Internal; import graphql.language.InputValueDefinition; +import graphql.schema.GraphQLAppliedDirective; import graphql.schema.GraphQLArgument; import graphql.schema.InputValueWithState; import graphql.schema.SchemaArgument; import graphql.schema.SchemaInputType; import org.jspecify.annotations.Nullable; +import java.util.List; + import static graphql.Assert.assertNotNull; @Internal @@ -39,6 +42,11 @@ public String getName() { return argument.getDefinition(); } + @Override + public List getAppliedDirectives() { + return argument.getAppliedDirectives(); + } + @Override public SchemaInputType getType() { return executableSchema.adaptGraphQLInputType(argument.getType()); diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java index 79923aa72c..e573c56e60 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaIntrospectionField.java @@ -3,6 +3,7 @@ import graphql.Internal; import graphql.language.FieldDefinition; import graphql.schema.GraphQLArgument; +import graphql.schema.GraphQLAppliedDirective; import graphql.schema.GraphQLFieldDefinition; import graphql.schema.SchemaArgument; import graphql.schema.SchemaField; @@ -54,6 +55,11 @@ public String getName() { return field.getDefinition(); } + @Override + public List getAppliedDirectives() { + return field.getAppliedDirectives(); + } + @Override public SchemaOutputType getType() { if (type != null) { diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaObject.java b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java index 88bfb7f47a..b98e68d8fb 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaObject.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaObject.java @@ -1,7 +1,9 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.schema.SchemaField; import graphql.schema.SchemaObject; +import graphql.schema.universe.SUField; import graphql.schema.universe.SUInterfaceType; import graphql.schema.universe.SUObjectType; @@ -35,6 +37,18 @@ public List getInterfaces() { return Collections.unmodifiableList(result); } + @Override + public List getFieldDefinitions() { + List fields = getExecutableSchema() + .getSchema() + .getFields(getObjectTypeVertex()); + List result = new ArrayList<>(fields.size()); + for (SUField field : fields) { + result.add(new SUSchemaField(getExecutableSchema(), field)); + } + return Collections.unmodifiableList(result); + } + @Internal public SUObjectType getObjectTypeVertex() { return (SUObjectType) getTypeVertex(); diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java b/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java index b12d58e68a..fba0d7883b 100644 --- a/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java +++ b/src/main/java/graphql/schema/universe/view/SUSchemaScalar.java @@ -1,6 +1,7 @@ package graphql.schema.universe.view; import graphql.Internal; +import graphql.schema.Coercing; import graphql.schema.SchemaAppliedDirective; import graphql.schema.SchemaAppliedDirectiveArgument; import graphql.schema.SchemaScalar; @@ -18,6 +19,11 @@ public SUSchemaScalar( super(executableSchema, type); } + @Override + public Coercing getCoercing() { + return getExecutableSchema().getScalarCoercing(this); + } + @Override public @Nullable String getSpecifiedByUrl() { for (SchemaAppliedDirective directive : diff --git a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy index ff35ed0751..808c73bc93 100644 --- a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy @@ -46,6 +46,14 @@ class ExecutableSchemaTest extends Specification { and: def node = schema.getType("Node") as SchemaInterface + node.fieldDefinitions*.name == ["id"] + schema.queryType.fieldDefinitions*.name == [ + "node", + "search", + "colors", + "regular", + "today" + ] schema.getFields(node)*.name == ["id"] schema.getField(node, "id").type instanceof SchemaNonNull schema.getField( @@ -66,6 +74,7 @@ class ExecutableSchemaTest extends Specification { and: def choice = schema.getType("Choice") as SchemaInputObject choice.oneOf + choice.fieldDefinitions*.name == ["color", "date"] schema.getInputFields(choice)*.name == ["color", "date"] schema.getInputField(choice, "color").type.name == "Color" schema.getInputField(choice, "color") @@ -100,8 +109,8 @@ class ExecutableSchemaTest extends Specification { schema.getDirective("missing") == null and: - schema.getScalarCoercing( - schema.getType("Date") as SchemaScalar) != null + def date = schema.getType("Date") as SchemaScalar + date.coercing.is(schema.getScalarCoercing(date)) where: executableSchema << executableSchemas() @@ -128,6 +137,12 @@ class ExecutableSchemaTest extends Specification { .getArgument("reason").argumentValue.value) == "Use other" schema.getAppliedDirectives(directive)*.name == ["meta"] + and: + query.appliedDirectives*.name == ["meta"] + field.appliedDirectives*.name == ["meta"] + argument.appliedDirectives*.name == ["meta", "deprecated"] + directive.appliedDirectives*.name == ["meta"] + and: def appliedArgument = schema.getAppliedDirectives(field)[0] .getArgument("value") @@ -233,10 +248,13 @@ class ExecutableSchemaTest extends Specification { def filter = schema.getType("Filter") as SchemaInputObject expect: + schema.queryType.fieldDefinitions*.name == + ["visible", "hidden"] schema.getFields(schema.queryType)*.name == ["visible"] schema.getField(schema.queryType, "visible") != null schema.getField(schema.queryType, "hidden") == null schema.getInputFields(filter)*.name == ["visible"] + filter.fieldDefinitions*.name == ["visible", "hidden"] schema.getInputField(filter, "visible") != null schema.getInputField(filter, "hidden") == null diff --git a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy index c3be4af6aa..3afe499914 100644 --- a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy @@ -33,7 +33,7 @@ class SchemaPrinterComparatorsTest extends AbstractSchemaPrintingTest { when: def options = defaultOptions().includeScalarTypes(true) - def result = new SchemaPrinter(options).print(scalarType) + def result = printType(new SchemaPrinter(options), scalarType) then: result == '''"TestScalar" @@ -51,7 +51,7 @@ scalar TestScalar @a(a : 0, bb : 0) @bb(a : 0, bb : 0) when: def options = defaultOptions() - def result = new SchemaPrinter(options).print(enumType) + def result = printType(new SchemaPrinter(options), enumType) then: result == '''enum TestEnum @a(a : 0, bb : 0) @bb(a : 0, bb : 0) { @@ -65,13 +65,21 @@ scalar TestScalar @a(a : 0, bb : 0) @bb(a : 0, bb : 0) given: GraphQLUnionType unionType = newUnionType().name("TestUnion") .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) - .possibleType(newObject().name("a").build()) - .possibleType(newObject().name("bb").build()) + .possibleType(newObject().name("a") + .field(newFieldDefinition() + .name("value") + .type(GraphQLString)) + .build()) + .possibleType(newObject().name("bb") + .field(newFieldDefinition() + .name("value") + .type(GraphQLString)) + .build()) .build() when: def options = defaultOptions() - def result = new SchemaPrinter(options).print(unionType) + def result = printType(new SchemaPrinter(options), unionType) then: result == '''union TestUnion @a(a : 0, bb : 0) @bb(a : 0, bb : 0) = a | bb @@ -96,7 +104,7 @@ scalar TestScalar @a(a : 0, bb : 0) @bb(a : 0, bb : 0) when: def options = defaultOptions() - def result = new SchemaPrinter(options).print(interfaceType) + def result = printType(new SchemaPrinter(options), interfaceType) then: result == '''interface TypeA @a(a : 0, bb : 0) @bb(a : 0, bb : 0) { @@ -110,7 +118,17 @@ scalar TestScalar @a(a : 0, bb : 0) @bb(a : 0, bb : 0) given: // @formatter:off GraphQLObjectType objectType = newObject().name("TypeA") - .withInterfaces(newInterface().name("a").build(), newInterface().name("bb").build()) + .withInterfaces( + newInterface().name("a") + .field(newFieldDefinition() + .name("a") + .type(GraphQLString)) + .build(), + newInterface().name("bb") + .field(newFieldDefinition() + .name("bb") + .type(GraphQLString)) + .build()) .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) .field(newFieldDefinition().name("a") .arguments(mockArguments("a", "bb")) @@ -125,7 +143,7 @@ scalar TestScalar @a(a : 0, bb : 0) @bb(a : 0, bb : 0) when: def options = defaultOptions() - def result = new SchemaPrinter(options).print(objectType) + def result = printType(new SchemaPrinter(options), objectType) then: result == '''type TypeA implements a & bb @a(a : 0, bb : 0) @bb(a : 0, bb : 0) { @@ -151,7 +169,9 @@ scalar TestScalar @a(a : 0, bb : 0) @bb(a : 0, bb : 0) when: def options = defaultOptions() - def result = new SchemaPrinter(options).print(inputObjectType) + def result = printType( + new SchemaPrinter(options), + inputObjectType) then: result == '''input TypeA @a(a : 0, bb : 0) @bb(a : 0, bb : 0) { @@ -827,7 +847,7 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) when: def options = defaultOptions().includeScalarTypes(true) - def result = new SchemaPrinter(options).print(scalarType) + def result = printType(new SchemaPrinter(options), scalarType) then: result == '''"TestScalar" diff --git a/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy b/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy index bf0279755c..0b63f935fa 100644 --- a/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaTransformerTest.groovy @@ -5,11 +5,11 @@ import graphql.AssertException import graphql.GraphQL import graphql.Scalars import graphql.TestUtil +import graphql.schema.idl.AbstractSchemaPrintingTest import graphql.schema.idl.RuntimeWiring import graphql.schema.idl.SchemaPrinter import graphql.util.TraversalControl import graphql.util.TraverserContext -import spock.lang.Specification import static graphql.schema.FieldCoordinates.coordinates import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition @@ -18,7 +18,7 @@ import static graphql.schema.GraphQLSchema.newSchema import static graphql.schema.GraphQLTypeReference.typeRef import static graphql.schema.idl.TypeRuntimeWiring.newTypeWiring -class SchemaTransformerTest extends Specification { +class SchemaTransformerTest extends AbstractSchemaPrintingTest { def "can change field in schema"() { given: @@ -852,7 +852,7 @@ type Query { def printer = new SchemaPrinter(SchemaPrinter.Options.defaultOptions().includeDirectives(true)) def newQueryType = newSchema.getObjectType("Query") - printer.print(newQueryType) == '''type Query { + printType(printer, newSchema, newQueryType) == '''type Query { field: String @foo(changedArg1 : "fooArg") field2: String @bar(arg1 : "barArg") } @@ -1002,7 +1002,10 @@ type Query { then: def newQueryType = newSchema.getObjectType("QueryType") - def newQueryTypePrinted = new SchemaPrinter().print(newQueryType) + def newQueryTypePrinted = printType( + new SchemaPrinter(), + newSchema, + newQueryType) newQueryTypePrinted == """type QueryType { a: String @deprecated(reason : "NEW REASON") @@ -1010,14 +1013,20 @@ type Query { } """ def newInterfaceType = newSchema.getType("InterfaceType") - def newInterfaceTypePrinted = new SchemaPrinter().print(newInterfaceType) + def newInterfaceTypePrinted = printType( + new SchemaPrinter(), + newSchema, + newInterfaceType) newInterfaceTypePrinted == """interface InterfaceType { a: String @deprecated(reason : "NEW REASON") b: String @deprecated(reason : "NEW REASON") } """ def newInputType = newSchema.getType("InputType") - def newInputTypePrinted = new SchemaPrinter().print(newInputType) + def newInputTypePrinted = printType( + new SchemaPrinter(), + newSchema, + newInputType) newInputTypePrinted == """input InputType { a: String @deprecated(reason : "NEW REASON") b: String @deprecated(reason : "NEW REASON") diff --git a/src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy b/src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy index b60a266463..880828e9cf 100644 --- a/src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy +++ b/src/test/groovy/graphql/schema/idl/AbstractSchemaPrintingTest.groovy @@ -1,6 +1,21 @@ package graphql.schema.idl +import graphql.Scalars +import graphql.schema.GraphQLCodeRegistry +import graphql.schema.GraphQLDirective +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLImplementingType +import graphql.schema.GraphQLInterfaceType +import graphql.schema.GraphQLNamedType +import graphql.schema.GraphQLObjectType import graphql.schema.GraphQLSchema +import graphql.schema.GraphQLSchemaElement +import graphql.schema.GraphQLType +import graphql.schema.GraphQLUnionType +import graphql.schema.SchemaDirective +import graphql.schema.SchemaElement +import graphql.schema.SchemaType +import graphql.schema.TypeResolver import graphql.schema.universe.SchemaUniverse import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification @@ -16,4 +31,126 @@ abstract class AbstractSchemaPrintingTest extends Specification { assert printer.print(executable) == expected return expected } + + String printType( + SchemaPrinter printer, + GraphQLSchema schema, + GraphQLType type) { + assert type instanceof GraphQLNamedType + def expected = printer.print(type) + def executable = importSchema(schema) + SchemaType importedType = + executable.getType((type as GraphQLNamedType).name) + + assert importedType != null + assert printer.print(importedType) == expected + return expected + } + + String printType( + SchemaPrinter printer, + GraphQLType type) { + return printType(printer, schemaForTypes([type]), type) + } + + String printDirective( + SchemaPrinter printer, + GraphQLSchema schema, + GraphQLDirective directive) { + def expected = printer.print(directive) + def executable = importSchema(schema) + SchemaDirective importedDirective = + executable.getDirective(directive.name) + + assert importedDirective != null + assert printer.print(importedDirective) == expected + return expected + } + + String printTypes( + SchemaPrinter printer, + List types) { + assert types.every { it instanceof GraphQLNamedType } + def expected = printer.print(types) + def executable = importSchema(schemaForTypes(types)) + List importedTypes = types.collect { + executable.getType((it as GraphQLNamedType).name) + } + + assert importedTypes.every { it != null } + assert printer.print(importedTypes) == expected + return expected + } + + String printElements( + SchemaPrinter printer, + GraphQLSchema schema, + List elements) { + def expected = printer.print(elements) + def executable = importSchema(schema) + List importedElements = elements.collect { + if (it instanceof GraphQLDirective) { + return executable.getDirective(it.name) + } + assert it instanceof GraphQLNamedType + return executable.getType(it.name) + } + + assert importedElements.every { it != null } + assert printer.print(importedElements) == expected + return expected + } + + private static GraphQLSchema schemaForTypes( + List types) { + def query = GraphQLObjectType.newObject() + .name("SchemaPrinterTestQuery") + .field(GraphQLFieldDefinition.newFieldDefinition() + .name("value") + .type(Scalars.GraphQLString)) + .build() + def builder = GraphQLSchema.newSchema().query(query) + def codeRegistry = GraphQLCodeRegistry.newCodeRegistry() + Set registeredResolvers = [] + types.each { + GraphQLNamedType namedType = it as GraphQLNamedType + builder.additionalType(namedType) + registerTypeResolvers( + namedType, + codeRegistry, + registeredResolvers) + } + builder.codeRegistry(codeRegistry.build()) + return builder.build() + } + + private static void registerTypeResolvers( + GraphQLNamedType type, + GraphQLCodeRegistry.Builder codeRegistry, + Set registeredResolvers) { + if (type instanceof GraphQLInterfaceType + || type instanceof GraphQLUnionType) { + if (registeredResolvers.add(type.name)) { + codeRegistry.typeResolver( + type.name, + { null } as TypeResolver) + } + } + if (!(type instanceof GraphQLImplementingType)) { + return + } + type.interfaces.each { + registerTypeResolvers( + it as GraphQLNamedType, + codeRegistry, + registeredResolvers) + } + } + + private static SUExecutableSchema importSchema( + GraphQLSchema schema) { + def imported = new SchemaUniverse() + .importSchema("schema_printer_element_test", schema) + return SUExecutableSchema.fromGraphQLSchema(imported, schema) + } } diff --git a/src/test/groovy/graphql/schema/idl/DirectivesOnDirectiveDefinitionsTest.groovy b/src/test/groovy/graphql/schema/idl/DirectivesOnDirectiveDefinitionsTest.groovy index 1e1a1bd8c4..06018256ba 100644 --- a/src/test/groovy/graphql/schema/idl/DirectivesOnDirectiveDefinitionsTest.groovy +++ b/src/test/groovy/graphql/schema/idl/DirectivesOnDirectiveDefinitionsTest.groovy @@ -12,9 +12,9 @@ import graphql.schema.idl.errors.DirectiveIllegalLocationError import graphql.schema.idl.errors.DirectiveIllegalReferenceError import graphql.schema.idl.errors.DirectiveUndeclaredError import graphql.schema.idl.errors.SchemaProblem -import spock.lang.Specification -class DirectivesOnDirectiveDefinitionsTest extends Specification { +class DirectivesOnDirectiveDefinitionsTest + extends AbstractSchemaPrintingTest { def "registry stores merges exposes and removes directive extensions"() { given: @@ -82,7 +82,7 @@ class DirectivesOnDirectiveDefinitionsTest extends Specification { target.extensionDefinitions.size() == 2 and: - new SchemaPrinter().print(target) == + printDirective(new SchemaPrinter(), schema, target) == 'directive @target @meta(value : "definition") @meta(value : "first extension") @meta(value : "second extension") on FIELD_DEFINITION' } @@ -120,7 +120,8 @@ class DirectivesOnDirectiveDefinitionsTest extends Specification { directive.deprecationReason == "Use @replacement" directive.extensionDefinitions == [extension] directive.copy().appliedDirectives*.name == ["meta", "metaBuilder"] - new SchemaPrinter().print(directive).contains('@deprecated(reason : "Use @replacement")') + new SchemaPrinter().print(directive) + .contains('@deprecated(reason : "Use @replacement")') directive.transform { it.clearDirectives() }.appliedDirectives.empty } diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy index dbca52b2bd..b6d10a86bc 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterAstBehaviorTest.groovy @@ -111,7 +111,10 @@ class SchemaPrinterAstBehaviorTest extends AbstractSchemaPrintingTest { .descriptionsAsHashComments(true) when: - def result = new SchemaPrinter(printerOptions).print(elements) + def result = printElements( + new SchemaPrinter(printerOptions), + schema, + elements) then: [ diff --git a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy index 5bf0afc15f..750b3c7931 100644 --- a/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/idl/SchemaPrinterTest.groovy @@ -196,7 +196,8 @@ class SchemaPrinterTest extends AbstractSchemaPrintingTest { def "print type direct"() { GraphQLSchema schema = starWarsSchema() - def result = new SchemaPrinter().print(schema.getType("Character")) + def character = schema.getType("Character") + def result = printType(new SchemaPrinter(), schema, character) expect: result == @@ -1805,7 +1806,10 @@ extend input Input { when: // we can print by direct type using AST def queryType = schema.getType("Query") - result = new SchemaPrinter(printOptions).print(queryType) + result = printType( + new SchemaPrinter(printOptions), + schema, + queryType) then: result == '''type Query { @@ -2344,7 +2348,11 @@ type MyQuery { def directive = schema.getDirective("foo") when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(true)).print(directive) + def result = printDirective( + new SchemaPrinter( + defaultOptions().includeDirectives(true)), + schema, + directive) then: result == """directive @foo on FIELD_DEFINITION""" @@ -2359,7 +2367,11 @@ type MyQuery { def directive = schema.getDirective("foo") when: - def result = new SchemaPrinter(defaultOptions().includeDirectives(true)).print(directive) + def result = printDirective( + new SchemaPrinter( + defaultOptions().includeDirectives(true)), + schema, + directive) then: result == """directive @foo on OBJECT | FIELD_DEFINITION""" @@ -2412,31 +2424,42 @@ type Query { GraphQLInputObjectType compoundType = GraphQLInputObjectType.newInputObject().name("Compound") .field({ it.name("a").type(GraphQLString) }) .field({ it.name("b").type(GraphQLString) }) + .field({ it.name("items").type(list(GraphQLString)) }) .build() GraphQLObjectType objType = newObject().name("obj") .field({ it.name("f").type(GraphQLString) .argument({ - it.name("arg").type(compoundType).defaultValueProgrammatic(["a": "A", "b": "B"]) + it.name("arg").type(compoundType) + .defaultValueProgrammatic([ + "a" : "A", + "b" : "B", + "items": ["C", "D"] + ]) }) }).build() when: - def result = new SchemaPrinter().print(objType) + def result = printType(new SchemaPrinter(), objType) then: result == '''type obj { - f(arg: Compound = {a : "A", b : "B"}): String + f(arg: Compound = {a : "A", b : "B", items : ["C", "D"]}): String } ''' when: def newAppliedDirective = GraphQLAppliedDirective.newDirective().name("foo") .argument({ - it.name("arg").type(compoundType).valueProgrammatic(["a": "A", "b": "B"]) + it.name("arg").type(compoundType) + .valueProgrammatic([ + "a" : "A", + "b" : "B", + "items": ["C", "D"] + ]) }) .build() @@ -2444,12 +2467,44 @@ type Query { it.name("f").type(GraphQLString).withAppliedDirective(newAppliedDirective) }).build() - result = new SchemaPrinter().print(objType) + result = printType(new SchemaPrinter(), objType) then: result == '''type obj { - f: String @foo(arg : {a : "A", b : "B"}) + f: String @foo(arg : {a : "A", b : "B", items : ["C", "D"]}) +} +''' + } + + def "programmatic enum values are printed directly"() { + given: + def mode = newEnum() + .name("Mode") + .value("FAST", 11) + .value("SLOW", 22) + .build() + def type = newObject() + .name("Query") + .field(newFieldDefinition() + .name("value") + .type(GraphQLString) + .argument(newArgument() + .name("external") + .type(mode) + .defaultValueProgrammatic("SLOW")) + .argument(newArgument() + .name("internal") + .type(mode) + .defaultValue(11))) + .build() + + when: + def result = printType(new SchemaPrinter(), type) + + then: + result == '''type Query { + value(external: Mode = SLOW, internal: Mode = FAST): String } ''' } @@ -2466,7 +2521,7 @@ type Query { .build() when: - def result = new SchemaPrinter().print(type) + def result = printType(new SchemaPrinter(), type) then: @@ -2643,7 +2698,9 @@ type Query { .build() when: - def result = new SchemaPrinter().print([testObjectA, testObjectB]) + def result = printTypes( + new SchemaPrinter(), + [testObjectA, testObjectB]) println(result) then: diff --git a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy index 44fa50a0d7..bed8b04189 100644 --- a/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy +++ b/src/test/groovy/graphql/schema/universe/SUExecutableSchemaPrinterTest.groovy @@ -99,6 +99,8 @@ class SUExecutableSchemaPrinterTest extends Specification { expect: printer.print(executableSchema) == printer.print(graphQLSchema) + printer.print(executableSchema.queryType) == + printer.print(graphQLSchema.queryType) printer.print(executableSchema).contains( 'external: Date = "2026-08-05"') printer.print(executableSchema).contains( From d96f4bb5b0ecad743ccfb8465625fc6812ee001d Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sat, 8 Aug 2026 21:55:00 +0000 Subject: [PATCH 27/32] Remove dead SchemaPrinter helper --- src/main/java/graphql/schema/idl/SchemaPrinter.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index b9269a6dce..bde3fda95c 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -46,7 +46,6 @@ import graphql.schema.GraphQLFieldDefinition; import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLInputType; import graphql.schema.GraphQLInterfaceType; import graphql.schema.GraphQLNamedOutputType; import graphql.schema.GraphQLNamedType; @@ -1107,10 +1106,6 @@ private void printAsAst( } - private static String printAst(InputValueWithState value, GraphQLInputType type) { - return AstPrinter.printAst(ValuesResolver.valueToLiteral(value, type, GraphQLContext.getDefault(), Locale.getDefault())); - } - private void printSchema( PrintWriter out, ExecutableSchema schema) { From 5be73d812beb9f71438194050e079ad8d1fcaeaa Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sat, 8 Aug 2026 22:07:54 +0000 Subject: [PATCH 28/32] Cover schema-neutral comparator printing --- .../graphql/schema/idl/SchemaPrinter.java | 12 +- .../SchemaPrinterComparatorsTest.groovy | 256 +++++++++++++++--- 2 files changed, 218 insertions(+), 50 deletions(-) diff --git a/src/main/java/graphql/schema/idl/SchemaPrinter.java b/src/main/java/graphql/schema/idl/SchemaPrinter.java index bde3fda95c..800002d532 100644 --- a/src/main/java/graphql/schema/idl/SchemaPrinter.java +++ b/src/main/java/graphql/schema/idl/SchemaPrinter.java @@ -747,16 +747,8 @@ private SchemaElementPrinter scalarPrinter() { if (!options.isIncludeScalars()) { return; } - boolean printScalar; - if (ScalarInfo.isGraphqlSpecifiedScalar(type.getName())) { - printScalar = false; - //noinspection RedundantIfStatement - if (!ScalarInfo.isGraphqlSpecifiedScalar(type.getName())) { - printScalar = true; - } - } else { - printScalar = true; - } + boolean printScalar = + !ScalarInfo.isGraphqlSpecifiedScalar(type.getName()); if (printScalar) { if (shouldPrintAsAst( type.getDefinition(), diff --git a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy index 3afe499914..7f2aab11ea 100644 --- a/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy +++ b/src/test/groovy/graphql/schema/SchemaPrinterComparatorsTest.groovy @@ -217,9 +217,17 @@ scalar TestScalar @a(a : 0, bb : 0) @bb(a : 0, bb : 0) .addComparator({ it.parentType(GraphQLScalarType.class).elementType(GraphQLAppliedDirective.class) }, GraphQLAppliedDirective.class, TestUtil.byGreatestLength) .addComparator({ it.parentType(GraphQLAppliedDirective.class).elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByEnvironment( + schemaEnvironment(SchemaScalar, SchemaAppliedDirective), + schemaEnvironment( + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument)) - def options = defaultOptions().includeScalarTypes(true).setComparators(registry) - def result = new SchemaPrinter(options).print(scalarType) + def options = defaultOptions() + .includeScalarTypes(true) + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), scalarType) then: result == '''"TestScalar" @@ -238,9 +246,15 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.elementType(GraphQLAppliedDirective.class) }, GraphQLAppliedDirective.class, TestUtil.byGreatestLength) .addComparator({ it.elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByElement( + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument) - def options = defaultOptions().includeScalarTypes(true).setComparators(registry) - def result = new SchemaPrinter(options).print(scalarType) + def options = defaultOptions() + .includeScalarTypes(true) + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), scalarType) then: result == '''"TestScalar" @@ -263,9 +277,20 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.parentType(GraphQLEnumValueDefinition.class).elementType(GraphQLAppliedDirective.class) }, GraphQLAppliedDirective.class, TestUtil.byGreatestLength) .addComparator({ it.parentType(GraphQLAppliedDirective.class).elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByEnvironment( + schemaEnvironment(SchemaEnum, SchemaAppliedDirective), + schemaEnvironment(SchemaEnum, SchemaEnumValue), + schemaEnvironment( + SchemaEnumValue, + SchemaAppliedDirective), + schemaEnvironment( + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument)) - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(enumType) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), enumType) then: result == '''enum TestEnum @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -289,9 +314,15 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.elementType(GraphQLEnumValueDefinition.class) }, GraphQLEnumValueDefinition.class, TestUtil.byGreatestLength) .addComparator({ it.elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByElement( + SchemaAppliedDirective, + SchemaEnumValue, + SchemaAppliedDirectiveArgument) - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(enumType) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), enumType) then: result == '''enum TestEnum @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -305,8 +336,8 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) given: GraphQLUnionType unionType = newUnionType().name("TestUnion") .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) - .possibleType(newObject().name("a").build()) - .possibleType(newObject().name("bb").build()) + .possibleType(objectWithField("a")) + .possibleType(objectWithField("bb")) .build() when: @@ -315,9 +346,17 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.parentType(GraphQLUnionType.class).elementType(GraphQLOutputType.class) }, GraphQLOutputType.class, TestUtil.byGreatestLength) .addComparator({ it.parentType(GraphQLAppliedDirective.class).elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByEnvironment( + schemaEnvironment(SchemaUnion, SchemaAppliedDirective), + schemaEnvironment(SchemaUnion, SchemaOutputType), + schemaEnvironment( + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument)) - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(unionType) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), unionType) then: result == '''union TestUnion @bb(bb : 0, a : 0) @a(bb : 0, a : 0) = bb | a @@ -328,8 +367,8 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) given: GraphQLUnionType unionType = newUnionType().name("TestUnion") .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) - .possibleType(newObject().name("a").build()) - .possibleType(newObject().name("bb").build()) + .possibleType(objectWithField("a")) + .possibleType(objectWithField("bb")) .build() when: @@ -338,9 +377,15 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.elementType(GraphQLOutputType.class) }, GraphQLOutputType.class, TestUtil.byGreatestLength) .addComparator({ it.elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByElement( + SchemaAppliedDirective, + SchemaOutputType, + SchemaAppliedDirectiveArgument) - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(unionType) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), unionType) then: result == '''union TestUnion @bb(bb : 0, a : 0) @a(bb : 0, a : 0) = bb | a @@ -371,9 +416,19 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.parentType(GraphQLFieldDefinition.class).elementType(GraphQLAppliedDirective.class) }, GraphQLAppliedDirective.class, TestUtil.byGreatestLength) .addComparator({ it.parentType(GraphQLAppliedDirective.class).elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByEnvironment( + schemaEnvironment(SchemaInterface, SchemaAppliedDirective), + schemaEnvironment(SchemaInterface, SchemaField), + schemaEnvironment(SchemaField, SchemaArgument), + schemaEnvironment(SchemaField, SchemaAppliedDirective), + schemaEnvironment( + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument)) - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(interfaceType) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), interfaceType) then: result == '''interface TypeA @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -406,9 +461,16 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.elementType(GraphQLArgument.class) }, GraphQLArgument.class, TestUtil.byGreatestLength) .addComparator({ it.elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() + def schemaRegistry = schemaComparatorsByElement( + SchemaAppliedDirective, + SchemaField, + SchemaArgument, + SchemaAppliedDirectiveArgument) - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(interfaceType) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), interfaceType) then: result == '''interface TypeA @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -422,7 +484,9 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) given: // @formatter:off GraphQLObjectType objectType = newObject().name("TypeA") - .withInterfaces(newInterface().name("a") .build(), newInterface().name("bb").build()) + .withInterfaces( + interfaceWithField("a"), + interfaceWithField("bb")) .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) .field(newFieldDefinition().name("a") .arguments(mockArguments("a", "bb")) @@ -443,8 +507,19 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.parentType(GraphQLFieldDefinition.class).elementType(GraphQLAppliedDirective.class) }, GraphQLDirective.class, TestUtil.byGreatestLength) .addComparator({ it.parentType(GraphQLAppliedDirective.class).elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLArgument.class, TestUtil.byGreatestLength) .build() - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(objectType) + def schemaRegistry = schemaComparatorsByEnvironment( + schemaEnvironment(SchemaObject, SchemaAppliedDirective), + schemaEnvironment(SchemaObject, SchemaOutputType), + schemaEnvironment(SchemaObject, SchemaField), + schemaEnvironment(SchemaField, SchemaArgument), + schemaEnvironment(SchemaField, SchemaAppliedDirective), + schemaEnvironment( + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument)) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), objectType) then: result == '''type TypeA implements bb & a @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -458,7 +533,9 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) given: // @formatter:off GraphQLObjectType objectType = newObject().name("TypeA") - .withInterfaces(newInterface().name("a") .build(), newInterface().name("bb").build()) + .withInterfaces( + interfaceWithField("a"), + interfaceWithField("bb")) .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) .field(newFieldDefinition().name("a") .arguments(mockArguments("a", "bb")) @@ -478,8 +555,16 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.elementType(GraphQLArgument.class) }, GraphQLArgument.class, TestUtil.byGreatestLength) .addComparator({ it.elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(objectType) + def schemaRegistry = schemaComparatorsByElement( + SchemaAppliedDirective, + SchemaOutputType, + SchemaField, + SchemaArgument, + SchemaAppliedDirectiveArgument) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), objectType) then: result == '''type TypeA implements bb & a @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -510,8 +595,21 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.parentType(GraphQLInputObjectField.class).elementType(GraphQLAppliedDirective.class) }, GraphQLAppliedDirective.class, TestUtil.byGreatestLength) .addComparator({ it.parentType(GraphQLAppliedDirective.class).elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(inputObjectType) + def schemaRegistry = schemaComparatorsByEnvironment( + schemaEnvironment( + SchemaInputObject, + SchemaAppliedDirective), + schemaEnvironment(SchemaInputObject, SchemaInputField), + schemaEnvironment( + SchemaInputField, + SchemaAppliedDirective), + schemaEnvironment( + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument)) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), inputObjectType) then: result == '''input TypeA @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -541,8 +639,14 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.elementType(GraphQLInputObjectField.class) }, GraphQLInputObjectField.class, TestUtil.byGreatestLength) .addComparator({ it.elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .build() - def options = defaultOptions().setComparators(registry) - def result = new SchemaPrinter(options).print(inputObjectType) + def schemaRegistry = schemaComparatorsByElement( + SchemaAppliedDirective, + SchemaInputField, + SchemaAppliedDirectiveArgument) + def options = defaultOptions() + .setComparators(registry) + .sortSchemaElements(schemaRegistry) + def result = printType(new SchemaPrinter(options), inputObjectType) then: result == '''input TypeA @bb(bb : 0, a : 0) @a(bb : 0, a : 0) { @@ -716,8 +820,8 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) GraphQLUnionType unionType = newUnionType().name("TestUnion") .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) - .possibleType(newObject().name("a").build()) - .possibleType(newObject().name("bb").build()) + .possibleType(objectWithField("a")) + .possibleType(objectWithField("bb")) .build() GraphQLEnumType enumType = newEnum().name("TestEnum") @@ -727,7 +831,9 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .build() GraphQLObjectType objectType = newObject().name("TestObjectType") - .withInterfaces(newInterface().name("a") .build(), newInterface().name("bb").build()) + .withInterfaces( + interfaceWithField("a"), + interfaceWithField("bb")) .withAppliedDirectives(mockDirectivesWithArguments("a", "bb")) .field(newFieldDefinition().name("a") .arguments(mockArguments("a", "bb")) @@ -771,15 +877,26 @@ scalar TestScalar @bb(bb : 0, a : 0) @a(bb : 0, a : 0) .addComparator({ it.elementType(GraphQLAppliedDirectiveArgument.class) }, GraphQLAppliedDirectiveArgument.class, TestUtil.byGreatestLength) .addComparator({ it.elementType(GraphQLArgument.class) }, GraphQLArgument.class, TestUtil.byGreatestLength) .build() - def options = defaultOptions().includeScalarTypes(true).setComparators(registry) + def schemaRegistry = schemaComparatorsByElement( + SchemaField, + SchemaInputField, + SchemaEnumValue, + SchemaOutputType, + SchemaAppliedDirective, + SchemaAppliedDirectiveArgument, + SchemaArgument) + def options = defaultOptions() + .includeScalarTypes(true) + .setComparators(registry) + .sortSchemaElements(schemaRegistry) def printer = new SchemaPrinter(options) - def scalarResult = printer.print(scalarType) - def enumResult = printer.print(enumType) - def unionResult = printer.print(unionType) - def objectTypeResult = printer.print(objectType) - def interfaceTypeResult = printer.print(interfaceType) - def inputObjectTypeResult = printer.print(inputObjectType) + def scalarResult = printType(printer, scalarType) + def enumResult = printType(printer, enumType) + def unionResult = printType(printer, unionType) + def objectTypeResult = printType(printer, objectType) + def interfaceTypeResult = printType(printer, interfaceType) + def inputObjectTypeResult = printType(printer, inputObjectType) then: @@ -895,4 +1012,63 @@ scalar TestScalar @a @bb then: sortedList == [nonNullA, nonNullB] } + + private static SchemaElementComparatorEnvironment schemaEnvironment( + Class parentType, + Class elementType) { + return SchemaElementComparatorEnvironment.newEnvironment( + parentType, + elementType) + } + + private static SchemaElementComparatorRegistry + schemaComparatorsByEnvironment( + SchemaElementComparatorEnvironment... + matchingEnvironments) { + def matches = matchingEnvironments.toSet() + return { environment -> + if (matches.contains(environment)) { + return schemaByGreatestLength() + } + return SchemaElementComparatorRegistry.DEFAULT_REGISTRY + .getComparator(environment) + } + } + + private static SchemaElementComparatorRegistry schemaComparatorsByElement( + Class... matchingElementTypes) { + def matches = matchingElementTypes.toSet() + return { environment -> + if (matches.contains(environment.elementType)) { + return schemaByGreatestLength() + } + return SchemaElementComparatorRegistry.DEFAULT_REGISTRY + .getComparator(environment) + } + } + + private static Comparator schemaByGreatestLength() { + return { first, second -> + ((SchemaNamedElement) second).name.length() <=> + ((SchemaNamedElement) first).name.length() + } as Comparator + } + + private static GraphQLObjectType objectWithField(String name) { + return newObject() + .name(name) + .field(newFieldDefinition() + .name(name) + .type(GraphQLString)) + .build() + } + + private static GraphQLInterfaceType interfaceWithField(String name) { + return newInterface() + .name(name) + .field(newFieldDefinition() + .name(name) + .type(GraphQLString)) + .build() + } } From 73de5015160b1f128279317bf86be4e7df837426 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sat, 8 Aug 2026 22:22:40 +0000 Subject: [PATCH 29/32] Support schema views in conditional field collection --- .../ConditionalNodeDecisionEnvironment.java | 12 +- .../conditional/ConditionalNodes.java | 12 +- .../execution/ConditionalNodesTest.groovy | 137 ++++++++++++------ .../execution/FieldCollectorTest.groovy | 85 ++++++++--- 4 files changed, 172 insertions(+), 74 deletions(-) diff --git a/src/main/java/graphql/execution/conditional/ConditionalNodeDecisionEnvironment.java b/src/main/java/graphql/execution/conditional/ConditionalNodeDecisionEnvironment.java index 8851e2df4a..1116d3aa0a 100644 --- a/src/main/java/graphql/execution/conditional/ConditionalNodeDecisionEnvironment.java +++ b/src/main/java/graphql/execution/conditional/ConditionalNodeDecisionEnvironment.java @@ -1,17 +1,20 @@ package graphql.execution.conditional; +import graphql.ExperimentalApi; import graphql.GraphQLContext; import graphql.execution.CoercedVariables; import graphql.language.Directive; import graphql.language.DirectivesContainer; -import graphql.schema.GraphQLSchema; -import org.jspecify.annotations.Nullable; +import graphql.schema.ExecutableSchema; +import org.jspecify.annotations.NullMarked; import java.util.List; /** * The parameters given to a {@link ConditionalNodeDecision} */ +@ExperimentalApi +@NullMarked public interface ConditionalNodeDecisionEnvironment { /** @@ -37,10 +40,9 @@ default List getDirectives() { CoercedVariables getVariables(); /** - * @return the {@link GraphQLSchema} in question - this can be null for certain call paths + * @return the executable schema view in question */ - @Nullable - GraphQLSchema getGraphQlSchema(); + ExecutableSchema getSchema(); /** * @return a graphql context diff --git a/src/main/java/graphql/execution/conditional/ConditionalNodes.java b/src/main/java/graphql/execution/conditional/ConditionalNodes.java index 286a751bfa..1f1c14e055 100644 --- a/src/main/java/graphql/execution/conditional/ConditionalNodes.java +++ b/src/main/java/graphql/execution/conditional/ConditionalNodes.java @@ -11,7 +11,6 @@ import graphql.language.NodeUtil; import graphql.language.VariableReference; import graphql.schema.ExecutableSchema; -import graphql.schema.GraphQLSchema; import org.jspecify.annotations.Nullable; import java.util.List; @@ -46,13 +45,10 @@ public boolean shouldInclude(DirectivesContainer element, if (graphQLContext != null) { ConditionalNodeDecision conditionalDecision = graphQLContext.get(ConditionalNodeDecision.class); if (conditionalDecision != null) { - Assert.assertTrue( - schema instanceof GraphQLSchema, - "Custom conditional node decisions require a GraphQLSchema"); return customShouldInclude( variables, element, - (GraphQLSchema) schema, + schema, graphQLContext, conditionalDecision); } @@ -63,7 +59,7 @@ public boolean shouldInclude(DirectivesContainer element, private boolean customShouldInclude(Map variables, DirectivesContainer element, - GraphQLSchema graphQLSchema, + ExecutableSchema schema, GraphQLContext graphQLContext, ConditionalNodeDecision conditionalDecision ) { @@ -80,8 +76,8 @@ public CoercedVariables getVariables() { } @Override - public GraphQLSchema getGraphQlSchema() { - return graphQLSchema; + public ExecutableSchema getSchema() { + return schema; } @Override diff --git a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy index 64a31088c2..0b315d6e7b 100644 --- a/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy +++ b/src/test/groovy/graphql/execution/ConditionalNodesTest.groovy @@ -1,6 +1,5 @@ package graphql.execution -import graphql.AssertException import graphql.ExecutionInput import graphql.GraphQLContext import graphql.TestUtil @@ -14,70 +13,113 @@ import graphql.language.Field import graphql.language.NodeUtil import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment +import graphql.schema.ExecutableSchema +import graphql.schema.GraphQLSchema import graphql.schema.universe.SchemaUniverse import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification class ConditionalNodesTest extends Specification { - def "should include false for skip = true"() { + def "should include false for skip = true [#schemaKind]"() { given: def variables = new LinkedHashMap() ConditionalNodes conditionalNodes = new ConditionalNodes() + def schema = executableSchema(schemaKind) def directives = directive("skip", ifArg(true)) expect: - !conditionalNodes.shouldInclude(mkField(directives), variables, null, GraphQLContext.getDefault()) + !conditionalNodes.shouldInclude( + mkField(directives), + variables, + schema, + GraphQLContext.getDefault()) + + where: + schemaKind << schemaKinds() } - def "should include true for skip = false"() { + def "should include true for skip = false [#schemaKind]"() { given: def variables = new LinkedHashMap() ConditionalNodes conditionalNodes = new ConditionalNodes() + def schema = executableSchema(schemaKind) def directives = directive("skip", ifArg(false)) expect: - conditionalNodes.shouldInclude(mkField(directives), variables, null, GraphQLContext.getDefault()) + conditionalNodes.shouldInclude( + mkField(directives), + variables, + schema, + GraphQLContext.getDefault()) + + where: + schemaKind << schemaKinds() } - def "should include false for include = false"() { + def "should include false for include = false [#schemaKind]"() { given: def variables = new LinkedHashMap() ConditionalNodes conditionalNodes = new ConditionalNodes() + def schema = executableSchema(schemaKind) def directives = directive("include", ifArg(false)) expect: - !conditionalNodes.shouldInclude(mkField(directives), variables, null, GraphQLContext.getDefault()) + !conditionalNodes.shouldInclude( + mkField(directives), + variables, + schema, + GraphQLContext.getDefault()) + + where: + schemaKind << schemaKinds() } - def "should include true for include = true"() { + def "should include true for include = true [#schemaKind]"() { given: def variables = new LinkedHashMap() ConditionalNodes conditionalNodes = new ConditionalNodes() + def schema = executableSchema(schemaKind) def directives = directive("include", ifArg(true)) expect: - conditionalNodes.shouldInclude(mkField(directives), variables, null, GraphQLContext.getDefault()) + conditionalNodes.shouldInclude( + mkField(directives), + variables, + schema, + GraphQLContext.getDefault()) + + where: + schemaKind << schemaKinds() } - def "no directives means include"() { + def "no directives means include [#schemaKind]"() { given: def variables = new LinkedHashMap() ConditionalNodes conditionalNodes = new ConditionalNodes() + def schema = executableSchema(schemaKind) expect: - conditionalNodes.shouldInclude(mkField([]), variables, null, GraphQLContext.getDefault()) + conditionalNodes.shouldInclude( + mkField([]), + variables, + schema, + GraphQLContext.getDefault()) + + where: + schemaKind << schemaKinds() } - def "allows a custom implementation to check conditional nodes"() { + def "allows a custom implementation to check conditional nodes [#schemaKind]"() { given: def variables = ["x": "y"] def graphQLSchema = TestUtil.schema("type Query { f : String} ") + def schema = executableSchema(graphQLSchema, schemaKind) ConditionalNodes conditionalNodes = new ConditionalNodes() def graphQLContext = GraphQLContext.getDefault() @@ -92,7 +134,7 @@ class ConditionalNodesTest extends Specification { called = true assert env.variables.toMap() == variables assert env.directivesContainer == field - assert env.graphQlSchema == graphQLSchema + assert env.schema.is(schema) assert env.graphQLContext.get("assert") != null return false } @@ -101,34 +143,15 @@ class ConditionalNodesTest extends Specification { graphQLContext.put("assert", true) expect: - !conditionalNodes.shouldInclude(field, variables, graphQLSchema, graphQLContext) - called == true - } - - def "custom conditional decisions remain GraphQLSchema specific"() { - given: - def source = TestUtil.schema("type Query { f: String }") - def imported = new SchemaUniverse() - .importSchema("conditional_nodes", source) - def schema = SUExecutableSchema.fromGraphQLSchema( - imported, - source) - def context = GraphQLContext.getDefault() - context.put( - ConditionalNodeDecision.class, - { true } as ConditionalNodeDecision) - - when: - new ConditionalNodes().shouldInclude( - mkField([]), - [:], + !conditionalNodes.shouldInclude( + field, + variables, schema, - context) + graphQLContext) + called == true - then: - def exception = thrown(AssertException) - exception.message == - "Custom conditional node decisions require a GraphQLSchema" + where: + schemaKind << schemaKinds() } def "integration test showing conditional nodes can be custom included"() { @@ -159,11 +182,16 @@ class ConditionalNodesTest extends Specification { Directive foundDirective = NodeUtil.findNodeByName(env.getDirectives(), "featureFlag") if (foundDirective != null) { - def arguments = env.getGraphQlSchema().getDirective("featureFlag") + def arguments = env.schema + .getDirective("featureFlag") .getArguments() Map argumentValues = ValuesResolver.getArgumentValues( - arguments, foundDirective.getArguments(), - env.variables, env.graphQLContext, Locale.getDefault()) + env.schema, + arguments, + foundDirective.getArguments(), + env.variables, + env.graphQLContext, + Locale.getDefault()) Object flagName = argumentValues.get("flagName") return String.valueOf(flagName) == "ON" } @@ -252,4 +280,29 @@ class ConditionalNodesTest extends Specification { Field mkField(List directives) { return Field.newField("name").directives(directives).build() } + + private static List schemaKinds() { + return [ + GraphQLSchema.simpleName, + SUExecutableSchema.simpleName] + } + + private static ExecutableSchema executableSchema(String schemaKind) { + return executableSchema( + TestUtil.schema("type Query { f: String }"), + schemaKind) + } + + private static ExecutableSchema executableSchema( + GraphQLSchema graphQLSchema, + String schemaKind) { + if (schemaKind == GraphQLSchema.simpleName) { + return graphQLSchema + } + def imported = new SchemaUniverse() + .importSchema("conditional_nodes", graphQLSchema) + return SUExecutableSchema.fromGraphQLSchema( + imported, + graphQLSchema) + } } diff --git a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy index 6b25679a98..0fd778e287 100644 --- a/src/test/groovy/graphql/execution/FieldCollectorTest.groovy +++ b/src/test/groovy/graphql/execution/FieldCollectorTest.groovy @@ -6,7 +6,11 @@ import graphql.language.Field import graphql.language.InlineFragment import graphql.language.OperationDefinition import graphql.parser.Parser -import graphql.schema.GraphQLObjectType +import graphql.schema.ExecutableSchema +import graphql.schema.GraphQLSchema +import graphql.schema.SchemaObject +import graphql.schema.universe.SchemaUniverse +import graphql.schema.universe.view.SUExecutableSchema import spock.lang.Specification import static graphql.TestUtil.mergedField @@ -15,15 +19,16 @@ import static graphql.execution.FieldCollectorParameters.newParameters class FieldCollectorTest extends Specification { - def "collect fields"() { + def "collect fields [#schemaKind]"() { given: - def schema = TestUtil.schema(""" + def graphQLSchema = TestUtil.schema(""" type Query { bar1: String bar2: String } """) - def objectType = schema.getType("Query") as GraphQLObjectType + def schema = executableSchema(graphQLSchema, schemaKind) + def objectType = schema.getType("Query") as SchemaObject FieldCollector fieldCollector = new FieldCollector() FieldCollectorParameters fieldCollectorParameters = newParameters() .schema(schema) @@ -41,10 +46,13 @@ class FieldCollectorTest extends Specification { then: result.getSubField('bar1').getFields() == [bar1] result.getSubField('bar2').getFields() == [bar2] + + where: + schemaKind << schemaKinds() } - def "collect fields on inline fragments"() { - def schema = TestUtil.schema(""" + def "collect fields on inline fragments [#schemaKind]"() { + def graphQLSchema = TestUtil.schema(""" type Query{ bar1: String bar2: Test @@ -56,7 +64,8 @@ class FieldCollectorTest extends Specification { fieldOnInterface: String } """) - def object = schema.getType("TestImpl") as GraphQLObjectType + def schema = executableSchema(graphQLSchema, schemaKind) + def object = schema.getType("TestImpl") as SchemaObject FieldCollector fieldCollector = new FieldCollector() FieldCollectorParameters fieldCollectorParameters = newParameters() .schema(schema) @@ -73,16 +82,20 @@ class FieldCollectorTest extends Specification { then: result.getSubField('fieldOnInterface').getFields() == [interfaceField] + + where: + schemaKind << schemaKinds() } - def "ignores inline fragments with non composite type conditions"() { + def "ignores inline fragments with non composite type conditions [#schemaKind]"() { given: - def schema = TestUtil.schema(""" + def graphQLSchema = TestUtil.schema(""" type Query { value: String } """) - def object = schema.getType("Query") as GraphQLObjectType + def schema = executableSchema(graphQLSchema, schemaKind) + def object = schema.getType("Query") as SchemaObject def parameters = newParameters() .schema(schema) .objectType(object) @@ -99,14 +112,18 @@ class FieldCollectorTest extends Specification { then: result.empty + + where: + schemaKind << schemaKinds() } - def "collect fields that are merged together - one of the fields is on an inline fragment "() { - def schema = TestUtil.schema(""" + def "collect fields that are merged together - one of the fields is on an inline fragment [#schemaKind]"() { + def graphQLSchema = TestUtil.schema(""" type Query { echo: String } """) + def schema = executableSchema(graphQLSchema, schemaKind) Document document = new Parser().parseDocument(""" { @@ -118,7 +135,7 @@ class FieldCollectorTest extends Specification { """) - def object = schema.getType("TestImpl") as GraphQLObjectType + def object = schema.getType("TestImpl") as SchemaObject FieldCollector fieldCollector = new FieldCollector() FieldCollectorParameters fieldCollectorParameters = newParameters() .schema(schema) @@ -133,10 +150,13 @@ class FieldCollectorTest extends Specification { then: result.size() == 1 result.getSubField('echo').fields.size() == 1 + + where: + schemaKind << schemaKinds() } - def "collect fields that are merged together - fields have different selection sets "() { - def schema = TestUtil.schema(""" + def "collect fields that are merged together - fields have different selection sets [#schemaKind]"() { + def graphQLSchema = TestUtil.schema(""" type Query { me: Me } @@ -146,6 +166,7 @@ class FieldCollectorTest extends Specification { lastname: String } """) + def schema = executableSchema(graphQLSchema, schemaKind) Document document = new Parser().parseDocument(""" { @@ -159,7 +180,7 @@ class FieldCollectorTest extends Specification { """) - def object = schema.getType("TestImpl") as GraphQLObjectType + def object = schema.getType("TestImpl") as SchemaObject FieldCollector fieldCollector = new FieldCollector() FieldCollectorParameters fieldCollectorParameters = newParameters() .schema(schema) @@ -183,10 +204,13 @@ class FieldCollectorTest extends Specification { meField.fields[1].selectionSet.selections.size() == 1 meField.fields[1].selectionSet.selections[0].name == "lastname" + + where: + schemaKind << schemaKinds() } - def "collect fields that are merged together - fields have different directives"() { - def schema = TestUtil.schema(""" + def "collect fields that are merged together - fields have different directives [#schemaKind]"() { + def graphQLSchema = TestUtil.schema(""" directive @one on FIELD directive @two on FIELD @@ -194,6 +218,7 @@ class FieldCollectorTest extends Specification { echo: String } """) + def schema = executableSchema(graphQLSchema, schemaKind) Document document = new Parser().parseDocument(""" { @@ -203,7 +228,7 @@ class FieldCollectorTest extends Specification { """) - def object = schema.getType("TestImpl") as GraphQLObjectType + def object = schema.getType("TestImpl") as SchemaObject FieldCollector fieldCollector = new FieldCollector() FieldCollectorParameters fieldCollectorParameters = newParameters() .schema(schema) @@ -230,5 +255,27 @@ class FieldCollectorTest extends Specification { echoField.fields[1].name == "echo" echoField.fields[1].directives.size() == 1 echoField.fields[1].directives[0].name == "two" + + where: + schemaKind << schemaKinds() + } + + private static List schemaKinds() { + return [ + GraphQLSchema.simpleName, + SUExecutableSchema.simpleName] + } + + private static ExecutableSchema executableSchema( + GraphQLSchema graphQLSchema, + String schemaKind) { + if (schemaKind == GraphQLSchema.simpleName) { + return graphQLSchema + } + def imported = new SchemaUniverse() + .importSchema("field_collector", graphQLSchema) + return SUExecutableSchema.fromGraphQLSchema( + imported, + graphQLSchema) } } From 2a128f4fa9820350fe4c7d61d90f3c92fda6ab56 Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sat, 8 Aug 2026 23:08:23 +0000 Subject: [PATCH 30/32] Clean up schema type coverage paths --- .../NonNullableValueCoercedAsNullException.java | 15 ++------------- src/main/java/graphql/execution/TypeFromAST.java | 4 ++-- .../schema/GraphQLTypeReferenceTest.groovy | 1 + 3 files changed, 5 insertions(+), 15 deletions(-) diff --git a/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java index 14d7e0aad4..59b768d1b5 100644 --- a/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java +++ b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java @@ -29,23 +29,12 @@ public class NonNullableValueCoercedAsNullException extends GraphQLException imp private @Nullable List sourceLocations; private @Nullable List path; - public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinition, GraphQLType graphQLType) { + public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinition, SchemaType schemaType) { super(format("Variable '%s' has coerced Null value for NonNull type '%s'", - variableDefinition.getName(), GraphQLTypeUtil.simplePrint(graphQLType))); + variableDefinition.getName(), GraphQLTypeUtil.simplePrint(schemaType))); this.sourceLocations = Collections.singletonList(variableDefinition.getSourceLocation()); } - public NonNullableValueCoercedAsNullException( - VariableDefinition variableDefinition, - SchemaType schemaType) { - super(format( - "Variable '%s' has coerced Null value for NonNull type '%s'", - variableDefinition.getName(), - GraphQLTypeUtil.simplePrint(schemaType))); - this.sourceLocations = Collections.singletonList( - variableDefinition.getSourceLocation()); - } - public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinition, String fieldName, GraphQLType graphQLType) { super(format("Field '%s' of variable '%s' has coerced Null value for NonNull type '%s'", fieldName, variableDefinition.getName(), GraphQLTypeUtil.simplePrint(graphQLType))); diff --git a/src/main/java/graphql/execution/TypeFromAST.java b/src/main/java/graphql/execution/TypeFromAST.java index b2d155369a..f9a54a53ac 100644 --- a/src/main/java/graphql/execution/TypeFromAST.java +++ b/src/main/java/graphql/execution/TypeFromAST.java @@ -22,10 +22,10 @@ public static GraphQLType getTypeFromAST(GraphQLSchema schema, Type type) { GraphQLType innerType; if (type instanceof ListType) { innerType = getTypeFromAST(schema, ((ListType) type).getType()); - return innerType != null ? list(innerType) : null; + return list(innerType); } else if (type instanceof NonNullType) { innerType = getTypeFromAST(schema, ((NonNullType) type).getType()); - return innerType != null ? nonNull(innerType) : null; + return nonNull(innerType); } return schema.getType(((TypeName) type).getName()); diff --git a/src/test/groovy/graphql/schema/GraphQLTypeReferenceTest.groovy b/src/test/groovy/graphql/schema/GraphQLTypeReferenceTest.groovy index 61647aee6f..9f53da1724 100644 --- a/src/test/groovy/graphql/schema/GraphQLTypeReferenceTest.groovy +++ b/src/test/groovy/graphql/schema/GraphQLTypeReferenceTest.groovy @@ -45,5 +45,6 @@ class GraphQLTypeReferenceTest extends Specification { objInput.getField("value").getType() instanceof GraphQLScalarType objInput.getField("value2").getType() != ref objInput.getField("value2").getType() instanceof GraphQLScalarType + ref.getAppliedDirectives().isEmpty() } } From 5b48a4c1b131b52c3876f4a0dc35e114bec949ff Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sat, 8 Aug 2026 23:40:41 +0000 Subject: [PATCH 31/32] Simplify resolved schema type wrappers --- .../execution/ResolvedSchemaListType.java | 24 ---------------- .../execution/ResolvedSchemaNonNullType.java | 24 ---------------- .../java/graphql/execution/TypeFromAST.java | 15 +++++----- .../universe/view/SUExecutableSchema.java | 13 +++++---- .../view/SUSchemaSyntheticNonNull.java | 24 ---------------- .../schema/ExecutableSchemaTest.groovy | 28 +++++++++++++++++++ 6 files changed, 44 insertions(+), 84 deletions(-) delete mode 100644 src/main/java/graphql/execution/ResolvedSchemaListType.java delete mode 100644 src/main/java/graphql/execution/ResolvedSchemaNonNullType.java delete mode 100644 src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java diff --git a/src/main/java/graphql/execution/ResolvedSchemaListType.java b/src/main/java/graphql/execution/ResolvedSchemaListType.java deleted file mode 100644 index a2075e5ef8..0000000000 --- a/src/main/java/graphql/execution/ResolvedSchemaListType.java +++ /dev/null @@ -1,24 +0,0 @@ -package graphql.execution; - -import graphql.Internal; -import graphql.schema.SchemaList; -import graphql.schema.SchemaType; -import org.jspecify.annotations.NullMarked; - -import static graphql.Assert.assertNotNull; - -@Internal -@NullMarked -public final class ResolvedSchemaListType implements SchemaList { - - private final SchemaType wrappedType; - - public ResolvedSchemaListType(SchemaType wrappedType) { - this.wrappedType = assertNotNull(wrappedType); - } - - @Override - public SchemaType getWrappedType() { - return wrappedType; - } -} diff --git a/src/main/java/graphql/execution/ResolvedSchemaNonNullType.java b/src/main/java/graphql/execution/ResolvedSchemaNonNullType.java deleted file mode 100644 index 2f1965c098..0000000000 --- a/src/main/java/graphql/execution/ResolvedSchemaNonNullType.java +++ /dev/null @@ -1,24 +0,0 @@ -package graphql.execution; - -import graphql.Internal; -import graphql.schema.SchemaNonNull; -import graphql.schema.SchemaType; -import org.jspecify.annotations.NullMarked; - -import static graphql.Assert.assertNotNull; - -@Internal -@NullMarked -public final class ResolvedSchemaNonNullType implements SchemaNonNull { - - private final SchemaType wrappedType; - - public ResolvedSchemaNonNullType(SchemaType wrappedType) { - this.wrappedType = assertNotNull(wrappedType); - } - - @Override - public SchemaType getWrappedType() { - return wrappedType; - } -} diff --git a/src/main/java/graphql/execution/TypeFromAST.java b/src/main/java/graphql/execution/TypeFromAST.java index f9a54a53ac..a35a836141 100644 --- a/src/main/java/graphql/execution/TypeFromAST.java +++ b/src/main/java/graphql/execution/TypeFromAST.java @@ -9,6 +9,8 @@ import graphql.schema.ExecutableSchema; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; +import graphql.schema.SchemaList; +import graphql.schema.SchemaNonNull; import graphql.schema.SchemaType; import static graphql.schema.GraphQLList.list; @@ -34,22 +36,21 @@ public static GraphQLType getTypeFromAST(GraphQLSchema schema, Type type) { public static SchemaType getSchemaTypeFromAST( ExecutableSchema schema, Type type) { - SchemaType innerType; if (type instanceof ListType) { - innerType = getSchemaTypeFromAST( + SchemaType wrappedType = getSchemaTypeFromAST( schema, ((ListType) type).getType()); - return innerType == null + return wrappedType == null ? null - : new ResolvedSchemaListType(innerType); + : (SchemaList) () -> wrappedType; } if (type instanceof NonNullType) { - innerType = getSchemaTypeFromAST( + SchemaType wrappedType = getSchemaTypeFromAST( schema, ((NonNullType) type).getType()); - return innerType == null + return wrappedType == null ? null - : new ResolvedSchemaNonNullType(innerType); + : (SchemaNonNull) () -> wrappedType; } return schema.getType(((TypeName) type).getName()); } diff --git a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java index 91f7b5e24b..45dbe86637 100644 --- a/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java +++ b/src/main/java/graphql/schema/universe/view/SUExecutableSchema.java @@ -22,6 +22,7 @@ import graphql.schema.SchemaInputObject; import graphql.schema.SchemaInputType; import graphql.schema.SchemaNamedType; +import graphql.schema.SchemaNonNull; import graphql.schema.SchemaObject; import graphql.schema.SchemaOutputType; import graphql.schema.SchemaScalar; @@ -273,8 +274,7 @@ public SchemaObject getIntrospectionSchemaType() { return new SUSchemaIntrospectionField( this, Introspection.SchemaMetaFieldDef, - new SUSchemaSyntheticNonNull( - getIntrospectionSchemaType())); + syntheticNonNull(getIntrospectionSchemaType())); } if (name.equals(Introspection.TypeMetaFieldDef.getName())) { return new SUSchemaIntrospectionField( @@ -511,9 +511,8 @@ public SchemaOutputType adaptGraphQLOutputType(GraphQLType type) { private SchemaType adaptGraphQLType(GraphQLType type) { if (type instanceof GraphQLNonNull) { - return new SUSchemaSyntheticNonNull( - adaptGraphQLType( - ((GraphQLNonNull) type).getWrappedType())); + return syntheticNonNull(adaptGraphQLType( + ((GraphQLNonNull) type).getWrappedType())); } assertTrue( type instanceof GraphQLNamedType, @@ -525,6 +524,10 @@ private SchemaType adaptGraphQLType(GraphQLType type) { name); } + private static SchemaNonNull syntheticNonNull(SchemaType wrappedType) { + return () -> wrappedType; + } + private SUVertex elementVertex(Object element) { assertTrue( element instanceof AbstractSUSchemaElement, diff --git a/src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java b/src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java deleted file mode 100644 index e71eb8fec3..0000000000 --- a/src/main/java/graphql/schema/universe/view/SUSchemaSyntheticNonNull.java +++ /dev/null @@ -1,24 +0,0 @@ -package graphql.schema.universe.view; - -import graphql.Internal; -import graphql.schema.SchemaNonNull; -import graphql.schema.SchemaType; - -import static graphql.Assert.assertNotNull; - -@Internal -public final class SUSchemaSyntheticNonNull implements SchemaNonNull { - - private final SchemaType wrappedType; - - @Internal - public SUSchemaSyntheticNonNull(SchemaType wrappedType) { - this.wrappedType = assertNotNull(wrappedType); - } - - @Override - public SchemaType getWrappedType() { - return wrappedType; - } - -} diff --git a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy index 808c73bc93..77d7b0030c 100644 --- a/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy +++ b/src/test/groovy/graphql/schema/ExecutableSchemaTest.groovy @@ -3,8 +3,12 @@ package graphql.schema import graphql.AssertException import graphql.Scalars import graphql.TestUtil +import graphql.execution.TypeFromAST import graphql.introspection.Introspection import graphql.introspection.IntrospectionWithDirectivesSupport +import graphql.language.ListType +import graphql.language.NonNullType +import graphql.language.TypeName import graphql.schema.universe.SchemaUniverse import graphql.schema.universe.view.SUExecutableSchema import graphql.schema.visibility.BlockedFields @@ -180,6 +184,30 @@ class ExecutableSchemaTest extends Specification { executableSchema << executableSchemas() } + def "resolved operation type modifiers are request specific"() { + given: + ExecutableSchema schema = executableSchema + def type = new NonNullType( + new ListType( + new NonNullType( + new TypeName("Color")))) + + when: + SchemaType resolved = TypeFromAST.getSchemaTypeFromAST(schema, type) + def nonNull = resolved as SchemaNonNull + def list = nonNull.wrappedType as SchemaList + def elementNonNull = list.wrappedType as SchemaNonNull + + then: + GraphQLTypeUtil.simplePrint(resolved) == "[Color!]!" + elementNonNull.wrappedType == schema.getType("Color") + !(nonNull instanceof GraphQLNonNull) + !(list instanceof GraphQLList) + + where: + executableSchema << executableSchemas() + } + def "SUSchema element relationships follow their schema snapshot"() { given: def universe = new SchemaUniverse() From e4e3059f0339a09979de05bf03f40b2d3b12ac4f Mon Sep 17 00:00:00 2001 From: Andreas Marek Date: Sun, 9 Aug 2026 00:32:12 +0000 Subject: [PATCH 32/32] Generalize ValuesResolver for executable schemas --- ...InputMapDefinesTooManyFieldsException.java | 2 +- ...onNullableValueCoercedAsNullException.java | 14 +- .../graphql/execution/ValuesResolver.java | 140 +-- .../execution/ValuesResolverConversion.java | 960 +++++------------- .../ValuesResolverOneOfValidation.java | 105 +- .../values/InputInterceptorTest.groovy | 34 + 6 files changed, 332 insertions(+), 923 deletions(-) diff --git a/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java b/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java index f24c2bf9ea..ecb6e9fd2c 100644 --- a/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java +++ b/src/main/java/graphql/execution/InputMapDefinesTooManyFieldsException.java @@ -23,7 +23,7 @@ public class InputMapDefinesTooManyFieldsException extends GraphQLException implements GraphQLError { public InputMapDefinesTooManyFieldsException(GraphQLType graphQLType, String fieldName) { - super(String.format("The variables input contains a field name '%s' that is not defined for input object type '%s' ", fieldName, GraphQLTypeUtil.simplePrint(graphQLType))); + this((SchemaType) graphQLType, fieldName); } public InputMapDefinesTooManyFieldsException( diff --git a/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java index 59b768d1b5..ea30a2015e 100644 --- a/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java +++ b/src/main/java/graphql/execution/NonNullableValueCoercedAsNullException.java @@ -29,6 +29,12 @@ public class NonNullableValueCoercedAsNullException extends GraphQLException imp private @Nullable List sourceLocations; private @Nullable List path; + public NonNullableValueCoercedAsNullException( + VariableDefinition variableDefinition, + GraphQLType graphQLType) { + this(variableDefinition, (SchemaType) graphQLType); + } + public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinition, SchemaType schemaType) { super(format("Variable '%s' has coerced Null value for NonNull type '%s'", variableDefinition.getName(), GraphQLTypeUtil.simplePrint(schemaType))); @@ -49,7 +55,7 @@ public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinit } public NonNullableValueCoercedAsNullException(GraphQLType graphQLType) { - super(format("Coerced Null value for NonNull type '%s'", GraphQLTypeUtil.simplePrint(graphQLType))); + this((SchemaType) graphQLType); } public NonNullableValueCoercedAsNullException(SchemaType schemaType) { @@ -64,9 +70,7 @@ public NonNullableValueCoercedAsNullException(VariableDefinition variableDefinit } public NonNullableValueCoercedAsNullException(String fieldName, List path, GraphQLType graphQLType) { - super(format("Field '%s' has coerced Null value for NonNull type '%s'", - fieldName, GraphQLTypeUtil.simplePrint(graphQLType))); - this.path = path; + this(fieldName, path, (SchemaType) graphQLType); } public NonNullableValueCoercedAsNullException( @@ -92,7 +96,7 @@ public NonNullableValueCoercedAsNullException(GraphQLInputObjectField inputTypeF } public NonNullableValueCoercedAsNullException(GraphQLArgument graphQLArgument) { - super(format("Argument '%s' has coerced Null value for NonNull type '%s'", graphQLArgument.getName(), graphQLArgument.getType())); + this((SchemaArgument) graphQLArgument); } public NonNullableValueCoercedAsNullException( diff --git a/src/main/java/graphql/execution/ValuesResolver.java b/src/main/java/graphql/execution/ValuesResolver.java index 258d727f01..b224af248a 100644 --- a/src/main/java/graphql/execution/ValuesResolver.java +++ b/src/main/java/graphql/execution/ValuesResolver.java @@ -83,11 +83,8 @@ public static CoercedVariables coerceVariableValues(GraphQLSchema schema, RawVariables rawVariables, GraphQLContext graphqlContext, Locale locale) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { - - InputInterceptor inputInterceptor = graphqlContext.get(InputInterceptor.class); - return ValuesResolverConversion.externalValueToInternalValueForVariables( - inputInterceptor, - schema, + return coerceVariableValues( + (ExecutableSchema) schema, variableDefinitions, rawVariables, graphqlContext, @@ -131,30 +128,12 @@ public static NormalizedVariables getNormalizedVariableValues( GraphQLContext graphqlContext, Locale locale ) { - GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); - Map result = Maps.newLinkedHashMapWithExpectedSize(variableDefinitions.size()); - for (VariableDefinition variableDefinition : variableDefinitions) { - String variableName = variableDefinition.getName(); - GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); - assertTrue(variableType instanceof GraphQLInputType); - // can be NullValue - Value defaultValue = variableDefinition.getDefaultValue(); - boolean hasValue = rawVariables.containsKey(variableName); - Object value = rawVariables.get(variableName); - if (!hasValue && defaultValue != null) { - result.put(variableName, new NormalizedInputValue(simplePrint(variableType), defaultValue)); - } else if (isNonNull(variableType) && (!hasValue || value == null)) { - return assertShouldNeverHappen("variable values are expected to be valid"); - } else if (hasValue) { - if (value == null) { - result.put(variableName, new NormalizedInputValue(simplePrint(variableType), null)); - } else { - Object literal = ValuesResolverConversion.externalValueToLiteral(fieldVisibility, value, (GraphQLInputType) variableType, NORMALIZED, graphqlContext, locale); - result.put(variableName, new NormalizedInputValue(simplePrint(variableType), literal)); - } - } - } - return NormalizedVariables.of(result); + return getNormalizedVariableValues( + (ExecutableSchema) schema, + variableDefinitions, + rawVariables, + graphqlContext, + locale); } public static NormalizedVariables getNormalizedVariableValues( @@ -217,7 +196,15 @@ public static Map getArgumentValues( Locale locale ) { InputInterceptor inputInterceptor = graphqlContext.get(InputInterceptor.class); - return getArgumentValuesImpl(inputInterceptor, DEFAULT_FIELD_VISIBILITY, argumentTypes, arguments, coercedVariables, graphqlContext, locale); + return getArgumentValuesImpl( + inputInterceptor, + null, + DEFAULT_FIELD_VISIBILITY, + argumentTypes, + arguments, + coercedVariables, + graphqlContext, + locale); } public static Map getArgumentValues( @@ -233,6 +220,7 @@ public static Map getArgumentValues( return getArgumentValuesImpl( inputInterceptor, schema, + DEFAULT_FIELD_VISIBILITY, argumentTypes, arguments, coercedVariables, @@ -289,7 +277,15 @@ public static Map getArgumentValues( Locale locale ) { InputInterceptor inputInterceptor = graphqlContext.get(InputInterceptor.class); - return getArgumentValuesImpl(inputInterceptor, codeRegistry.getFieldVisibility(), argumentTypes, arguments, coercedVariables, graphqlContext, locale); + return getArgumentValuesImpl( + inputInterceptor, + null, + codeRegistry.getFieldVisibility(), + argumentTypes, + arguments, + coercedVariables, + graphqlContext, + locale); } /** @@ -402,74 +398,8 @@ public static T getInputValueImpl( @NonNull private static Map getArgumentValuesImpl( InputInterceptor inputInterceptor, + @Nullable ExecutableSchema schema, GraphqlFieldVisibility fieldVisibility, - List argumentTypes, - List arguments, - CoercedVariables coercedVariables, - GraphQLContext graphqlContext, - Locale locale - ) { - if (argumentTypes.isEmpty()) { - return ImmutableKit.emptyMap(); - } - - Map coercedValues = Maps.newLinkedHashMapWithExpectedSize(arguments.size()); - Map argumentMap = argumentMap(arguments); - for (GraphQLArgument argumentDefinition : argumentTypes) { - GraphQLInputType argumentType = argumentDefinition.getType(); - String argumentName = argumentDefinition.getName(); - Argument argument = argumentMap.get(argumentName); - InputValueWithState defaultValue = argumentDefinition.getArgumentDefaultValue(); - boolean hasValue = argument != null; - Object value; - Value argumentValue = argument != null ? argument.getValue() : null; - if (argumentValue instanceof VariableReference) { - String variableName = ((VariableReference) argumentValue).getName(); - hasValue = coercedVariables.containsKey(variableName); - value = coercedVariables.get(variableName); - } else { - value = argumentValue; - } - if (!hasValue && argumentDefinition.hasSetDefaultValue()) { - Object coercedDefaultValue = ValuesResolverConversion.defaultValueToInternalValue( - inputInterceptor, - fieldVisibility, - defaultValue, - argumentType, - graphqlContext, - locale); - coercedValues.put(argumentName, coercedDefaultValue); - } else if (isNonNull(argumentType) && (!hasValue || ValuesResolverConversion.isNullValue(value))) { - throw new NonNullableValueCoercedAsNullException(argumentDefinition); - } else if (hasValue) { - if (ValuesResolverConversion.isNullValue(value)) { - coercedValues.put(argumentName, value); - } else if (argumentValue instanceof VariableReference) { - coercedValues.put(argumentName, value); - } else { - value = ValuesResolverConversion.literalToInternalValue(inputInterceptor, - fieldVisibility, - argumentType, - argument.getValue(), - coercedVariables, - graphqlContext, - locale); - coercedValues.put(argumentName, value); - } - - ValuesResolverOneOfValidation.validateOneOfInputTypes(argumentType, value, argumentValue, argumentName, locale); - - } - } - - - return coercedValues; - } - - @NonNull - private static Map getArgumentValuesImpl( - InputInterceptor inputInterceptor, - ExecutableSchema schema, List argumentTypes, List arguments, CoercedVariables coercedVariables, @@ -501,13 +431,15 @@ private static Map getArgumentValuesImpl( Object coercedDefaultValue = ValuesResolverConversion.defaultValueToInternalValue( inputInterceptor, schema, + fieldVisibility, defaultValue, argumentType, graphqlContext, locale); coercedValues.put(argumentName, coercedDefaultValue); } else if (isNonNull(argumentType) && (!hasValue || ValuesResolverConversion.isNullValue(value))) { - throw new NonNullableValueCoercedAsNullException(argumentDefinition); + throw newNonNullableValueCoercedAsNullException( + argumentDefinition); } else if (hasValue) { if (ValuesResolverConversion.isNullValue(value)) { coercedValues.put(argumentName, value); @@ -516,6 +448,7 @@ private static Map getArgumentValuesImpl( } else { value = ValuesResolverConversion.literalToInternalValue(inputInterceptor, schema, + fieldVisibility, argumentType, argument.getValue(), coercedVariables, @@ -539,6 +472,15 @@ private static Map getArgumentValuesImpl( return coercedValues; } + private static NonNullableValueCoercedAsNullException newNonNullableValueCoercedAsNullException( + SchemaArgument argument) { + if (argument instanceof GraphQLArgument) { + return new NonNullableValueCoercedAsNullException( + (GraphQLArgument) argument); + } + return new NonNullableValueCoercedAsNullException(argument); + } + private static Map argumentMap(List arguments) { Map result = Maps.newLinkedHashMapWithExpectedSize(arguments.size()); for (Argument argument : arguments) { diff --git a/src/main/java/graphql/execution/ValuesResolverConversion.java b/src/main/java/graphql/execution/ValuesResolverConversion.java index f4cd080b5a..320d5ad7d4 100644 --- a/src/main/java/graphql/execution/ValuesResolverConversion.java +++ b/src/main/java/graphql/execution/ValuesResolverConversion.java @@ -13,18 +13,14 @@ import graphql.language.VariableDefinition; import graphql.language.VariableReference; import graphql.normalized.NormalizedInputValue; +import graphql.schema.Coercing; import graphql.schema.CoercingParseLiteralException; import graphql.schema.CoercingParseValueException; import graphql.schema.ExecutableSchema; -import graphql.schema.GraphQLEnumType; -import graphql.schema.GraphQLInputObjectField; import graphql.schema.GraphQLInputObjectType; import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLScalarType; import graphql.schema.GraphQLSchema; import graphql.schema.GraphQLType; -import graphql.schema.GraphQLTypeUtil; import graphql.schema.InputValueWithState; import graphql.schema.SchemaEnum; import graphql.schema.SchemaInputField; @@ -34,7 +30,6 @@ import graphql.schema.SchemaNonNull; import graphql.schema.SchemaScalar; import graphql.schema.SchemaType; -import graphql.schema.visibility.DefaultGraphqlFieldVisibility; import graphql.schema.visibility.GraphqlFieldVisibility; import graphql.util.FpKit; import org.jspecify.annotations.NonNull; @@ -54,13 +49,9 @@ import static graphql.execution.ValuesResolver.ValueMode.NORMALIZED; import static graphql.language.NullValue.newNullValue; import static graphql.language.ObjectField.newObjectField; -import static graphql.scalar.CoercingUtil.i18nMsg; -import static graphql.scalar.CoercingUtil.typeName; import static graphql.schema.GraphQLTypeUtil.isList; import static graphql.schema.GraphQLTypeUtil.isNonNull; import static graphql.schema.GraphQLTypeUtil.simplePrint; -import static graphql.schema.GraphQLTypeUtil.unwrapNonNull; -import static graphql.schema.GraphQLTypeUtil.unwrapOneAs; import static graphql.schema.visibility.DefaultGraphqlFieldVisibility.DEFAULT_FIELD_VISIBILITY; /** @@ -77,39 +68,19 @@ static Object valueToLiteralImpl(GraphqlFieldVisibility fieldVisibility, ValuesResolver.ValueMode valueMode, GraphQLContext graphqlContext, Locale locale) { - if (inputValueWithState.isInternal()) { - if (valueMode == NORMALIZED) { - return assertShouldNeverHappen("can't infer normalized structure"); - } - Value value = ValuesResolverLegacy.valueToLiteralLegacy( - inputValueWithState.getValue(), - type, - graphqlContext, - locale); - // - // the valueToLiteralLegacy() nominally cant know if null means never set or is set to a null value - // but this code can know - its is SET to a value so, it MUST be a Null Literal - // this method would assert at the end of it if inputValueWithState.isNotSet() were true - // - return value == null ? NullValue.of() : value; - } - if (inputValueWithState.isLiteral()) { - return inputValueWithState.getValue(); - } - if (inputValueWithState.isExternal()) { - return externalValueToLiteral( - fieldVisibility, - inputValueWithState.getValue(), - (GraphQLInputType) type, - valueMode, - graphqlContext, - locale); - } - return assertShouldNeverHappen("unexpected value state " + inputValueWithState); + return valueToLiteralImpl( + null, + fieldVisibility, + inputValueWithState, + type, + valueMode, + graphqlContext, + locale); } - static Object valueToLiteralImpl( - ExecutableSchema schema, + private static Object valueToLiteralImpl( + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, InputValueWithState inputValueWithState, SchemaType type, ValuesResolver.ValueMode valueMode, @@ -129,6 +100,7 @@ static Object valueToLiteralImpl( } return internalValueToLiteral( schema, + fieldVisibility, inputValueWithState.getValue(), type, graphqlContext, @@ -140,6 +112,7 @@ static Object valueToLiteralImpl( if (inputValueWithState.isExternal()) { return externalValueToLiteral( schema, + fieldVisibility, inputValueWithState.getValue(), (SchemaInputType) type, valueMode, @@ -183,7 +156,7 @@ static Object valueToInternalValueImpl( GraphQLContext graphqlContext, Locale locale ) { - DefaultGraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY; + GraphqlFieldVisibility fieldVisibility = DEFAULT_FIELD_VISIBILITY; if (inputValueWithState.isInternal()) { return inputValueWithState.getValue(); @@ -210,64 +183,27 @@ static Object valueToInternalValueImpl( return assertShouldNeverHappen("unexpected value state " + inputValueWithState); } - /** - * No validation: the external value is assumed to be valid. - */ static Object externalValueToLiteral( - GraphqlFieldVisibility fieldVisibility, + ExecutableSchema schema, @Nullable Object value, - GraphQLInputType type, + SchemaInputType type, ValuesResolver.ValueMode valueMode, GraphQLContext graphqlContext, Locale locale ) { - if (value == null) { - return newNullValue().build(); - } - if (GraphQLTypeUtil.isNonNull(type)) { - return externalValueToLiteral( - fieldVisibility, - value, - (GraphQLInputType) unwrapNonNull(type), - valueMode, - graphqlContext, - locale); - } - if (type instanceof GraphQLScalarType) { - return externalValueToLiteralForScalar( - (GraphQLScalarType) type, - value, - graphqlContext, - locale); - } else if (type instanceof GraphQLEnumType) { - return externalValueToLiteralForEnum( - (GraphQLEnumType) type, - value, - graphqlContext, - locale); - } else if (type instanceof GraphQLList) { - return externalValueToLiteralForList( - fieldVisibility, - (GraphQLList) type, - value, - valueMode, - graphqlContext, - locale); - } else if (type instanceof GraphQLInputObjectType) { - return externalValueToLiteralForObject( - fieldVisibility, - (GraphQLInputObjectType) type, - value, - valueMode, - graphqlContext, - locale); - } else { - return assertShouldNeverHappen("unexpected type %s", type); - } + return externalValueToLiteral( + schema, + DEFAULT_FIELD_VISIBILITY, + value, + type, + valueMode, + graphqlContext, + locale); } - static Object externalValueToLiteral( - ExecutableSchema schema, + private static Object externalValueToLiteral( + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, @Nullable Object value, SchemaInputType type, ValuesResolver.ValueMode valueMode, @@ -277,9 +213,10 @@ static Object externalValueToLiteral( if (value == null) { return newNullValue().build(); } - if (GraphQLTypeUtil.isNonNull(type)) { + if (isNonNull(type)) { return externalValueToLiteral( schema, + fieldVisibility, value, (SchemaInputType) ((SchemaNonNull) type) .getWrappedType(), @@ -303,6 +240,7 @@ static Object externalValueToLiteral( } else if (type instanceof SchemaList) { return externalValueToLiteralForList( schema, + fieldVisibility, (SchemaList) type, value, valueMode, @@ -311,6 +249,7 @@ static Object externalValueToLiteral( } else if (type instanceof SchemaInputObject) { return externalValueToLiteralForObject( schema, + fieldVisibility, (SchemaInputObject) type, value, valueMode, @@ -325,138 +264,13 @@ static Object externalValueToLiteral( * No validation */ private static Value externalValueToLiteralForScalar( - GraphQLScalarType scalarType, - Object value, - GraphQLContext graphqlContext, - @NonNull Locale locale - ) { - return scalarType.getCoercing().valueToLiteral(value, graphqlContext, locale); - - } - - /** - * No validation - */ - private static Value externalValueToLiteralForEnum( - GraphQLEnumType enumType, - Object value, - GraphQLContext graphqlContext, - Locale locale) { - return enumType.valueToLiteral( - value, - graphqlContext, - locale); - } - - /** - * No validation - */ - @SuppressWarnings("unchecked") - private static Object externalValueToLiteralForList( - GraphqlFieldVisibility fieldVisibility, - GraphQLList listType, - Object value, - ValuesResolver.ValueMode valueMode, - GraphQLContext graphqlContext, - Locale locale - ) { - GraphQLInputType wrappedType = (GraphQLInputType) listType.getWrappedType(); - List valueList = FpKit.toListOrSingletonList(value); - ImmutableList.Builder resultBuilder = ImmutableList.builderWithExpectedSize(valueList.size()); - for (Object item : valueList) { - resultBuilder.add(externalValueToLiteral( - fieldVisibility, - item, - wrappedType, - valueMode, - graphqlContext, - locale)); - } - ImmutableList result = resultBuilder.build(); - - if (valueMode == NORMALIZED) { - return result; - } else { - return ArrayValue.newArrayValue().values((ImmutableList) result).build(); - } - } - - /** - * No validation - */ - @SuppressWarnings("unchecked") - private static Object externalValueToLiteralForObject( - GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType inputObjectType, - Object inputValue, - ValuesResolver.ValueMode valueMode, - GraphQLContext graphqlContext, - Locale locale - ) { - assertTrue(inputValue instanceof Map, "Expect Map as input"); - Map inputMap = (Map) inputValue; - List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); - - Map normalizedResult = new LinkedHashMap<>(); - ImmutableList.Builder objectFields = ImmutableList.builder(); - for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); - String fieldName = inputFieldDefinition.getName(); - boolean hasValue = inputMap.containsKey(fieldName); - Object fieldValue = inputMap.getOrDefault(fieldName, null); - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { - //TODO: consider valueMode - Object defaultValueLiteral = valueToLiteralImpl( - fieldVisibility, - inputFieldDefinition.getInputFieldDefaultValue(), - fieldType, - ValuesResolver.ValueMode.LITERAL, - graphqlContext, - locale); - if (valueMode == ValuesResolver.ValueMode.LITERAL) { - normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), defaultValueLiteral)); - } else { - objectFields.add(newObjectField().name(fieldName).value((Value) defaultValueLiteral).build()); - } - } else if (hasValue) { - if (fieldValue == null) { - if (valueMode == NORMALIZED) { - normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), null)); - } else { - objectFields.add(newObjectField().name(fieldName).value(newNullValue().build()).build()); - } - } else { - Object literal = externalValueToLiteral( - fieldVisibility, - fieldValue, - fieldType, - valueMode, - graphqlContext, locale); - if (valueMode == NORMALIZED) { - normalizedResult.put(fieldName, new NormalizedInputValue(simplePrint(fieldType), literal)); - } else { - objectFields.add(newObjectField().name(fieldName).value((Value) literal).build()); - } - } - } - } - if (valueMode == NORMALIZED) { - return normalizedResult; - } - return ObjectValue.newObjectValue().objectFields(objectFields.build()).build(); - } - - /** - * No validation - */ - private static Value externalValueToLiteralForScalar( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, SchemaScalar scalarType, Object value, GraphQLContext graphqlContext, @NonNull Locale locale ) { - return schema.getScalarCoercing(scalarType) + return getScalarCoercing(schema, scalarType) .valueToLiteral(value, graphqlContext, locale); } @@ -477,7 +291,8 @@ private static Value externalValueToLiteralForEnum( */ @SuppressWarnings("unchecked") private static Object externalValueToLiteralForList( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaList listType, Object value, ValuesResolver.ValueMode valueMode, @@ -491,6 +306,7 @@ private static Object externalValueToLiteralForList( for (Object item : valueList) { resultBuilder.add(externalValueToLiteral( schema, + fieldVisibility, item, wrappedType, valueMode, @@ -511,7 +327,8 @@ private static Object externalValueToLiteralForList( */ @SuppressWarnings("unchecked") private static Object externalValueToLiteralForObject( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaInputObject inputObjectType, Object inputValue, ValuesResolver.ValueMode valueMode, @@ -521,7 +338,7 @@ private static Object externalValueToLiteralForObject( assertTrue(inputValue instanceof Map, "Expect Map as input"); Map inputMap = (Map) inputValue; List fieldDefinitions = - schema.getInputFields(inputObjectType); + getInputFields(schema, fieldVisibility, inputObjectType); Map normalizedResult = new LinkedHashMap<>(); ImmutableList.Builder objectFields = ImmutableList.builder(); @@ -536,6 +353,7 @@ private static Object externalValueToLiteralForObject( .isSet()) { Object defaultValueLiteral = valueToLiteralImpl( schema, + fieldVisibility, inputFieldDefinition.getInputFieldDefaultValue(), fieldType, ValuesResolver.ValueMode.LITERAL, @@ -560,6 +378,7 @@ private static Object externalValueToLiteralForObject( } else { Object literal = externalValueToLiteral( schema, + fieldVisibility, fieldValue, fieldType, valueMode, @@ -579,7 +398,8 @@ private static Object externalValueToLiteralForObject( } private static Value internalValueToLiteral( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, @Nullable Object value, SchemaType type, GraphQLContext graphqlContext, @@ -590,13 +410,14 @@ private static Value internalValueToLiteral( if (type instanceof SchemaNonNull) { return internalValueToLiteral( schema, + fieldVisibility, value, ((SchemaNonNull) type).getWrappedType(), graphqlContext, locale); } if (type instanceof SchemaScalar) { - return schema.getScalarCoercing((SchemaScalar) type) + return getScalarCoercing(schema, (SchemaScalar) type) .valueToLiteral(value, graphqlContext, locale); } if (type instanceof SchemaEnum) { @@ -609,6 +430,7 @@ private static Value internalValueToLiteral( if (type instanceof SchemaList) { return internalListValueToLiteral( schema, + fieldVisibility, value, (SchemaList) type, graphqlContext, @@ -617,6 +439,7 @@ private static Value internalValueToLiteral( if (type instanceof SchemaInputObject) { return internalObjectValueToLiteral( schema, + fieldVisibility, value, (SchemaInputObject) type, graphqlContext, @@ -639,7 +462,8 @@ private static Value internalEnumValueToLiteral( @SuppressWarnings("unchecked") private static Value internalListValueToLiteral( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, Object value, SchemaList listType, GraphQLContext graphqlContext, @@ -650,6 +474,7 @@ private static Value internalListValueToLiteral( for (Object item : values) { result.add(internalValueToLiteral( schema, + fieldVisibility, item, listType.getWrappedType(), graphqlContext, @@ -660,7 +485,8 @@ private static Value internalListValueToLiteral( @SuppressWarnings("unchecked") private static Value internalObjectValueToLiteral( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, Object value, SchemaInputObject inputObjectType, GraphQLContext graphqlContext, @@ -669,12 +495,13 @@ private static Value internalObjectValueToLiteral( Map values = (Map) value; ImmutableList.Builder fields = ImmutableList.builder(); for (SchemaInputField field : - schema.getInputFields(inputObjectType)) { + getInputFields(schema, fieldVisibility, inputObjectType)) { if (!values.containsKey(field.getName())) { continue; } Value fieldValue = internalValueToLiteral( schema, + fieldVisibility, values.get(field.getName()), field.getType(), graphqlContext, @@ -694,19 +521,21 @@ private static Value internalObjectValueToLiteral( */ static CoercedVariables externalValueToInternalValueForVariables( InputInterceptor inputInterceptor, - GraphQLSchema schema, + ExecutableSchema schema, List variableDefinitions, RawVariables rawVariables, GraphQLContext graphqlContext, Locale locale ) { - GraphqlFieldVisibility fieldVisibility = schema.getCodeRegistry().getFieldVisibility(); Map coercedValues = new LinkedHashMap<>(); for (VariableDefinition variableDefinition : variableDefinitions) { try { String variableName = variableDefinition.getName(); - GraphQLType variableType = TypeFromAST.getTypeFromAST(schema, variableDefinition.getType()); - assertTrue(variableType instanceof GraphQLInputType); - GraphQLInputType variableInputType = (GraphQLInputType) variableType; + SchemaType variableType = getVariableType( + schema, + variableDefinition); + assertTrue(variableType instanceof SchemaInputType); + SchemaInputType variableInputType = + (SchemaInputType) variableType; // can be NullValue Value defaultValue = variableDefinition.getDefaultValue(); boolean hasValue = rawVariables.containsKey(variableName); @@ -714,7 +543,7 @@ static CoercedVariables externalValueToInternalValueForVariables( if (!hasValue && defaultValue != null) { Object coercedDefaultValue = literalToInternalValue( inputInterceptor, - fieldVisibility, + schema, variableInputType, defaultValue, CoercedVariables.emptyVariables(), @@ -722,7 +551,9 @@ static CoercedVariables externalValueToInternalValueForVariables( locale); coercedValues.put(variableName, coercedDefaultValue); } else if (isNonNull(variableType) && (!hasValue || value == null)) { - throw new NonNullableValueCoercedAsNullException(variableDefinition, variableType); + throw newNonNullableValueCoercedAsNullException( + variableDefinition, + variableType); } else if (hasValue) { if (value == null) { coercedValues.put(variableName, null); @@ -730,7 +561,7 @@ static CoercedVariables externalValueToInternalValueForVariables( Object coercedValue = externalValueToInternalValueImpl( variableName, inputInterceptor, - fieldVisibility, + schema, variableInputType, value, graphqlContext, @@ -753,177 +584,63 @@ static CoercedVariables externalValueToInternalValueForVariables( return CoercedVariables.of(coercedValues); } - /** - * performs validation too - */ - static CoercedVariables externalValueToInternalValueForVariables( + private static SchemaType getVariableType( + ExecutableSchema schema, + VariableDefinition variableDefinition) { + if (schema instanceof GraphQLSchema) { + return TypeFromAST.getTypeFromAST( + (GraphQLSchema) schema, + variableDefinition.getType()); + } + return TypeFromAST.getSchemaTypeFromAST( + schema, + variableDefinition.getType()); + } + + static Object externalValueToInternalValueImpl( + InputInterceptor inputInterceptor, + GraphqlFieldVisibility fieldVisibility, + GraphQLInputType graphQLType, + Object originalValue, + GraphQLContext graphqlContext, + Locale locale + ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { + return externalValueToInternalValueImpl( + "externalValue", + inputInterceptor, + null, + fieldVisibility, + graphQLType, + originalValue, + graphqlContext, + locale); + } + + static Object externalValueToInternalValueImpl( + String variableName, InputInterceptor inputInterceptor, ExecutableSchema schema, - List variableDefinitions, - RawVariables rawVariables, - GraphQLContext graphqlContext, Locale locale - ) { - Map coercedValues = new LinkedHashMap<>(); - for (VariableDefinition variableDefinition : variableDefinitions) { - try { - String variableName = variableDefinition.getName(); - SchemaType variableType = - TypeFromAST.getSchemaTypeFromAST( - schema, - variableDefinition.getType()); - assertTrue(variableType instanceof SchemaInputType); - SchemaInputType variableInputType = - (SchemaInputType) variableType; - // can be NullValue - Value defaultValue = variableDefinition.getDefaultValue(); - boolean hasValue = rawVariables.containsKey(variableName); - Object value = rawVariables.get(variableName); - if (!hasValue && defaultValue != null) { - Object coercedDefaultValue = literalToInternalValue( - inputInterceptor, - schema, - variableInputType, - defaultValue, - CoercedVariables.emptyVariables(), - graphqlContext, - locale); - coercedValues.put(variableName, coercedDefaultValue); - } else if (isNonNull(variableType) && (!hasValue || value == null)) { - throw new NonNullableValueCoercedAsNullException(variableDefinition, variableType); - } else if (hasValue) { - if (value == null) { - coercedValues.put(variableName, null); - } else { - Object coercedValue = externalValueToInternalValueImpl( - variableName, - inputInterceptor, - schema, - variableInputType, - value, - graphqlContext, - locale); - coercedValues.put(variableName, coercedValue); - } - } - } catch (CoercingParseValueException e) { - throw CoercingParseValueException.newCoercingParseValueException() - .message(String.format("Variable '%s' has an invalid value: %s", variableDefinition.getName(), e.getMessage())) - .extensions(e.getExtensions()) - .cause(e.getCause()) - .sourceLocation(variableDefinition.getSourceLocation()) - .build(); - } catch (NonNullableValueCoercedAsNullException e) { - throw new NonNullableValueCoercedAsNullException(variableDefinition, e.getMessage()); - } - } - - return CoercedVariables.of(coercedValues); - } - - static Object externalValueToInternalValueImpl( - InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLInputType graphQLType, + SchemaInputType type, Object originalValue, GraphQLContext graphqlContext, Locale locale - ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - return externalValueToInternalValueImpl("externalValue", + ) throws NonNullableValueCoercedAsNullException, + CoercingParseValueException { + return externalValueToInternalValueImpl( + variableName, inputInterceptor, - fieldVisibility, - graphQLType, + schema, + DEFAULT_FIELD_VISIBILITY, + type, originalValue, graphqlContext, locale); } - /** - * Performs validation too - */ - static Object externalValueToInternalValueImpl( - String variableName, + private static Object externalValueToInternalValueImpl( InputInterceptor inputInterceptor, + @Nullable ExecutableSchema schema, GraphqlFieldVisibility fieldVisibility, - GraphQLInputType graphQLType, - Object originalValue, - GraphQLContext graphqlContext, - Locale locale - ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - if (isNonNull(graphQLType)) { - Object returnValue = externalValueToInternalValueImpl( - variableName, - inputInterceptor, - fieldVisibility, - unwrapOneAs(graphQLType), - originalValue, - graphqlContext, - locale); - if (returnValue == null) { - throw new NonNullableValueCoercedAsNullException(graphQLType); - } - return returnValue; - } - // - // we have a @Internal hook that allows input values to be changed before they are - // presented to scalars and enums - if it's not present then the cost is an extra `if` - // statement. We expect this to be NOT present most of the time - // - Object value = originalValue; - if (inputInterceptor != null) { - value = inputInterceptor.intercept(originalValue, graphQLType, graphqlContext, locale); - } - if (value == null) { - return null; - } - - if (graphQLType instanceof GraphQLScalarType) { - return externalValueToInternalValueForScalar( - (GraphQLScalarType) graphQLType, - value, - graphqlContext, - locale); - } else if (graphQLType instanceof GraphQLEnumType) { - return externalValueToInternalValueForEnum( - (GraphQLEnumType) graphQLType, - value, - graphqlContext, - locale); - } else if (graphQLType instanceof GraphQLList) { - return externalValueToInternalValueForList( - inputInterceptor, - fieldVisibility, - (GraphQLList) graphQLType, - value, - graphqlContext, - locale); - } else if (graphQLType instanceof GraphQLInputObjectType) { - if (value instanceof Map) { - GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) graphQLType; - //noinspection unchecked - Map coercedMap = externalValueToInternalValueForObject( - inputInterceptor, - fieldVisibility, - inputObjectType, - (Map) value, - graphqlContext, - locale); - - ValuesResolverOneOfValidation.validateOneOfInputTypes(inputObjectType, coercedMap, null, variableName, locale); - return coercedMap; - } else { - throw CoercingParseValueException.newCoercingParseValueException() - .message("Expected type 'Map' but was '" + value.getClass().getSimpleName() + - "'. Variables for input objects must be an instance of type 'Map'.") - .build(); - } - } else { - return assertShouldNeverHappen("unhandled type %s", graphQLType); - } - } - - static Object externalValueToInternalValueImpl( - InputInterceptor inputInterceptor, - ExecutableSchema schema, SchemaInputType type, Object originalValue, GraphQLContext graphqlContext, @@ -934,16 +651,18 @@ static Object externalValueToInternalValueImpl( "externalValue", inputInterceptor, schema, + fieldVisibility, type, originalValue, graphqlContext, locale); } - static Object externalValueToInternalValueImpl( + private static Object externalValueToInternalValueImpl( String variableName, InputInterceptor inputInterceptor, - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaInputType type, Object originalValue, GraphQLContext graphqlContext, @@ -955,13 +674,14 @@ static Object externalValueToInternalValueImpl( variableName, inputInterceptor, schema, + fieldVisibility, (SchemaInputType) ((SchemaNonNull) type) .getWrappedType(), originalValue, graphqlContext, locale); if (returnValue == null) { - throw new NonNullableValueCoercedAsNullException(type); + throw newNonNullableValueCoercedAsNullException(type); } return returnValue; } @@ -999,6 +719,7 @@ static Object externalValueToInternalValueImpl( return externalValueToInternalValueForList( inputInterceptor, schema, + fieldVisibility, (SchemaList) type, value, graphqlContext, @@ -1011,6 +732,7 @@ static Object externalValueToInternalValueImpl( Map coercedMap = externalValueToInternalValueForObject( inputInterceptor, schema, + fieldVisibility, inputObjectType, (Map) value, graphqlContext, @@ -1040,133 +762,23 @@ static Object externalValueToInternalValueImpl( */ private static Map externalValueToInternalValueForObject( InputInterceptor inputInterceptor, + @Nullable ExecutableSchema schema, GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType inputObjectType, - Map inputMap, - GraphQLContext graphqlContext, - Locale locale - ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { - List fieldDefinitions = fieldVisibility.getFieldDefinitions(inputObjectType); - List fieldNames = map(fieldDefinitions, GraphQLInputObjectField::getName); - for (String providedFieldName : inputMap.keySet()) { - if (!fieldNames.contains(providedFieldName)) { - throw new InputMapDefinesTooManyFieldsException(inputObjectType, providedFieldName); - } - } - - Map coercedValues = new LinkedHashMap<>(); - - for (GraphQLInputObjectField inputFieldDefinition : fieldDefinitions) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); - String fieldName = inputFieldDefinition.getName(); - InputValueWithState defaultValue = inputFieldDefinition.getInputFieldDefaultValue(); - boolean hasValue = inputMap.containsKey(fieldName); - Object value = inputMap.getOrDefault(fieldName, null); - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { - Object coercedDefaultValue = defaultValueToInternalValue( - inputInterceptor, - fieldVisibility, - defaultValue, - fieldType, - graphqlContext, - locale); - coercedValues.put(fieldName, coercedDefaultValue); - } else if (isNonNull(fieldType) && (!hasValue || value == null)) { - throw new NonNullableValueCoercedAsNullException(fieldName, emptyList(), fieldType); - } else if (hasValue) { - if (value == null) { - coercedValues.put(fieldName, null); - } else { - value = externalValueToInternalValueImpl( - inputInterceptor, - fieldVisibility, - fieldType, - value, - graphqlContext, - locale); - coercedValues.put(fieldName, value); - } - } - } - return coercedValues; - } - - /** - * including validation - */ - private static Object externalValueToInternalValueForScalar( - GraphQLScalarType graphQLScalarType, - Object value, - GraphQLContext graphqlContext, - Locale locale - ) throws CoercingParseValueException { - return graphQLScalarType.getCoercing().parseValue( - value, - graphqlContext, - locale); - } - - /** - * including validation - */ - private static Object externalValueToInternalValueForEnum( - GraphQLEnumType graphQLEnumType, - Object value, - GraphQLContext graphqlContext, - Locale locale - ) throws CoercingParseValueException { - return graphQLEnumType.parseValue( - value, - graphqlContext, - locale); - } - - /** - * including validation - */ - private static List externalValueToInternalValueForList( - InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLList graphQLList, - Object value, - GraphQLContext graphqlContext, - Locale locale - ) throws CoercingParseValueException, NonNullableValueCoercedAsNullException { - - GraphQLInputType wrappedType = (GraphQLInputType) graphQLList.getWrappedType(); - List listOrSingletonList = FpKit.toListOrSingletonList(value); - List list = FpKit.arrayListSizedTo(listOrSingletonList); - for (Object val : listOrSingletonList) { - list.add(externalValueToInternalValueImpl( - inputInterceptor, - fieldVisibility, - wrappedType, - val, - graphqlContext, - locale)); - } - return list; - } - - /** - * performs validation - */ - private static Map externalValueToInternalValueForObject( - InputInterceptor inputInterceptor, - ExecutableSchema schema, SchemaInputObject inputObjectType, Map inputMap, GraphQLContext graphqlContext, Locale locale ) throws NonNullableValueCoercedAsNullException, CoercingParseValueException { List fieldDefinitions = - schema.getInputFields(inputObjectType); + getInputFields(schema, fieldVisibility, inputObjectType); List fieldNames = map( fieldDefinitions, SchemaInputField::getName); for (String providedFieldName : inputMap.keySet()) { if (!fieldNames.contains(providedFieldName)) { - throw new InputMapDefinesTooManyFieldsException(inputObjectType, providedFieldName); + throw newInputMapDefinesTooManyFieldsException( + inputObjectType, + providedFieldName); } } @@ -1182,13 +794,16 @@ private static Map externalValueToInternalValueForObject( Object coercedDefaultValue = defaultValueToInternalValue( inputInterceptor, schema, + fieldVisibility, defaultValue, fieldType, graphqlContext, locale); coercedValues.put(fieldName, coercedDefaultValue); } else if (isNonNull(fieldType) && (!hasValue || value == null)) { - throw new NonNullableValueCoercedAsNullException(fieldName, emptyList(), fieldType); + throw newNonNullableValueCoercedAsNullException( + fieldName, + fieldType); } else if (hasValue) { if (value == null) { coercedValues.put(fieldName, null); @@ -1196,6 +811,7 @@ private static Map externalValueToInternalValueForObject( value = externalValueToInternalValueImpl( inputInterceptor, schema, + fieldVisibility, fieldType, value, graphqlContext, @@ -1211,13 +827,13 @@ private static Map externalValueToInternalValueForObject( * including validation */ private static Object externalValueToInternalValueForScalar( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, SchemaScalar scalarType, Object value, GraphQLContext graphqlContext, Locale locale ) throws CoercingParseValueException { - return schema.getScalarCoercing(scalarType).parseValue( + return getScalarCoercing(schema, scalarType).parseValue( value, graphqlContext, locale); @@ -1240,7 +856,8 @@ private static Object externalValueToInternalValueForEnum( */ private static List externalValueToInternalValueForList( InputInterceptor inputInterceptor, - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaList listType, Object value, GraphQLContext graphqlContext, @@ -1255,6 +872,7 @@ private static List externalValueToInternalValueForList( list.add(externalValueToInternalValueImpl( inputInterceptor, schema, + fieldVisibility, wrappedType, val, graphqlContext, @@ -1286,6 +904,7 @@ static Object literalToInternalValue( ) { return literalToInternalValueImpl( inputInterceptor, + null, fieldVisibility, type, inputValue, @@ -1294,193 +913,30 @@ static Object literalToInternalValue( locale); } - @Nullable - private static Object literalToInternalValueImpl( + static Object literalToInternalValue( InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLType type, + ExecutableSchema schema, + SchemaInputType type, Value inputValue, CoercedVariables coercedVariables, GraphQLContext graphqlContext, Locale locale ) { - if (inputValue instanceof VariableReference) { - String variableName = ((VariableReference) inputValue).getName(); - return coercedVariables.get(variableName); - } - if (inputValue instanceof NullValue) { - return null; - } - if (type instanceof GraphQLScalarType) { - return literalToInternalValueForScalar( - inputValue, - (GraphQLScalarType) type, - coercedVariables, - graphqlContext, - locale); - } - if (isNonNull(type)) { - return literalToInternalValue( - inputInterceptor, - fieldVisibility, - unwrapOneAs(type), - inputValue, - coercedVariables, - graphqlContext, - locale); - } - if (type instanceof GraphQLInputObjectType) { - return literalToInternalValueForInputObject( - inputInterceptor, - fieldVisibility, - (GraphQLInputObjectType) type, - (ObjectValue) inputValue, - coercedVariables, - graphqlContext, - locale); - } - if (type instanceof GraphQLEnumType) { - return ((GraphQLEnumType) type).parseLiteral(inputValue, graphqlContext, locale); - } - if (isList(type)) { - return literalToInternalValueForList( - inputInterceptor, - fieldVisibility, - (GraphQLList) type, - inputValue, - coercedVariables, - graphqlContext, - locale); - } - return null; - } - - /** - * no validation - */ - private static Object literalToInternalValueForScalar( - Value inputValue, - GraphQLScalarType scalarType, - CoercedVariables coercedVariables, - GraphQLContext graphqlContext, - @NonNull Locale locale - ) { - // the CoercingParseLiteralException exception that could happen here has been validated earlier via ValidationUtil - return scalarType.getCoercing().parseLiteral( + return literalToInternalValueImpl( + inputInterceptor, + schema, + DEFAULT_FIELD_VISIBILITY, + type, inputValue, coercedVariables, graphqlContext, locale); } - /** - * no validation - */ - private static Object literalToInternalValueForList( - InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLList graphQLList, - Value value, - CoercedVariables coercedVariables, - GraphQLContext graphqlContext, - Locale locale - ) { - - GraphQLInputType inputType = (GraphQLInputType) graphQLList.getWrappedType(); - if (value instanceof ArrayValue) { - ArrayValue arrayValue = (ArrayValue) value; - List result = new ArrayList<>(); - for (Value singleValue : arrayValue.getValues()) { - result.add(literalToInternalValue( - inputInterceptor, - fieldVisibility, - inputType, - singleValue, - coercedVariables, - graphqlContext, - locale)); - } - return result; - } else { - return Collections.singletonList( - literalToInternalValue( - inputInterceptor, - fieldVisibility, - inputType, - value, - coercedVariables, - graphqlContext, - locale)); - } - } - - /** - * no validation - */ - private static Object literalToInternalValueForInputObject( - InputInterceptor inputInterceptor, - GraphqlFieldVisibility fieldVisibility, - GraphQLInputObjectType type, - ObjectValue inputValue, - CoercedVariables coercedVariables, - GraphQLContext graphqlContext, - Locale locale - ) { - Map coercedValues = new LinkedHashMap<>(); - - Map inputFieldsByName = mapObjectValueFieldsByName(inputValue); - - - List inputFieldTypes = fieldVisibility.getFieldDefinitions(type); - for (GraphQLInputObjectField inputFieldDefinition : inputFieldTypes) { - GraphQLInputType fieldType = inputFieldDefinition.getType(); - String fieldName = inputFieldDefinition.getName(); - ObjectField field = inputFieldsByName.get(fieldName); - boolean hasValue = field != null; - Object value; - Value fieldValue = field != null ? field.getValue() : null; - if (fieldValue instanceof VariableReference) { - String variableName = ((VariableReference) fieldValue).getName(); - hasValue = coercedVariables.containsKey(variableName); - value = coercedVariables.get(variableName); - } else { - value = fieldValue; - } - if (!hasValue && inputFieldDefinition.hasSetDefaultValue()) { - Object coercedDefaultValue = defaultValueToInternalValue( - inputInterceptor, - fieldVisibility, - inputFieldDefinition.getInputFieldDefaultValue(), - fieldType, - graphqlContext, - locale); - coercedValues.put(fieldName, coercedDefaultValue); - } else if (isNonNull(fieldType) && (!hasValue || isNullValue(value))) { - return assertShouldNeverHappen("Should have been validated before"); - } else if (hasValue) { - if (isNullValue(value)) { - coercedValues.put(fieldName, value); - } else if (fieldValue instanceof VariableReference) { - coercedValues.put(fieldName, value); - } else { - value = literalToInternalValue( - inputInterceptor, - fieldVisibility, - fieldType, - fieldValue, - coercedVariables, - graphqlContext, - locale); - coercedValues.put(fieldName, value); - } - } - } - return coercedValues; - } - static Object literalToInternalValue( InputInterceptor inputInterceptor, - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaInputType type, Value inputValue, CoercedVariables coercedVariables, @@ -1490,6 +946,7 @@ static Object literalToInternalValue( return literalToInternalValueImpl( inputInterceptor, schema, + fieldVisibility, type, inputValue, coercedVariables, @@ -1500,7 +957,8 @@ static Object literalToInternalValue( @Nullable private static Object literalToInternalValueImpl( InputInterceptor inputInterceptor, - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaType type, Value inputValue, CoercedVariables coercedVariables, @@ -1527,6 +985,7 @@ private static Object literalToInternalValueImpl( return literalToInternalValue( inputInterceptor, schema, + fieldVisibility, (SchemaInputType) ((SchemaNonNull) type) .getWrappedType(), inputValue, @@ -1538,6 +997,7 @@ private static Object literalToInternalValueImpl( return literalToInternalValueForInputObject( inputInterceptor, schema, + fieldVisibility, (SchemaInputObject) type, (ObjectValue) inputValue, coercedVariables, @@ -1555,6 +1015,7 @@ private static Object literalToInternalValueImpl( return literalToInternalValueForList( inputInterceptor, schema, + fieldVisibility, (SchemaList) type, inputValue, coercedVariables, @@ -1568,7 +1029,7 @@ private static Object literalToInternalValueImpl( * no validation */ private static Object literalToInternalValueForScalar( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, Value inputValue, SchemaScalar scalarType, CoercedVariables coercedVariables, @@ -1576,7 +1037,7 @@ private static Object literalToInternalValueForScalar( @NonNull Locale locale ) { // the CoercingParseLiteralException exception that could happen here has been validated earlier via ValidationUtil - return schema.getScalarCoercing(scalarType).parseLiteral( + return getScalarCoercing(schema, scalarType).parseLiteral( inputValue, coercedVariables, graphqlContext, @@ -1588,7 +1049,8 @@ private static Object literalToInternalValueForScalar( */ private static Object literalToInternalValueForList( InputInterceptor inputInterceptor, - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaList listType, Value value, CoercedVariables coercedVariables, @@ -1605,6 +1067,7 @@ private static Object literalToInternalValueForList( result.add(literalToInternalValue( inputInterceptor, schema, + fieldVisibility, inputType, singleValue, coercedVariables, @@ -1617,6 +1080,7 @@ private static Object literalToInternalValueForList( literalToInternalValue( inputInterceptor, schema, + fieldVisibility, inputType, value, coercedVariables, @@ -1630,7 +1094,8 @@ private static Object literalToInternalValueForList( */ private static Object literalToInternalValueForInputObject( InputInterceptor inputInterceptor, - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, SchemaInputObject type, ObjectValue inputValue, CoercedVariables coercedVariables, @@ -1643,7 +1108,7 @@ private static Object literalToInternalValueForInputObject( List inputFieldTypes = - schema.getInputFields(type); + getInputFields(schema, fieldVisibility, type); for (SchemaInputField inputFieldDefinition : inputFieldTypes) { SchemaInputType fieldType = inputFieldDefinition.getType(); String fieldName = inputFieldDefinition.getName(); @@ -1665,6 +1130,7 @@ private static Object literalToInternalValueForInputObject( Object coercedDefaultValue = defaultValueToInternalValue( inputInterceptor, schema, + fieldVisibility, inputFieldDefinition.getInputFieldDefaultValue(), fieldType, graphqlContext, @@ -1681,6 +1147,7 @@ private static Object literalToInternalValueForInputObject( value = literalToInternalValue( inputInterceptor, schema, + fieldVisibility, fieldType, fieldValue, coercedVariables, @@ -1722,44 +1189,33 @@ private static Map mapObjectValueFieldsByName(ObjectValue i return inputValueFieldsByName; } - static Object defaultValueToInternalValue( - InputInterceptor inputInterceptor, + private static List getInputFields( + @Nullable ExecutableSchema schema, GraphqlFieldVisibility fieldVisibility, - InputValueWithState defaultValue, - GraphQLInputType type, - GraphQLContext graphqlContext, - Locale locale - ) { - if (defaultValue.isInternal()) { - return defaultValue.getValue(); - } - if (defaultValue.isLiteral()) { - // default value literals can't reference variables, this is why the variables are empty - return literalToInternalValue( - inputInterceptor, - fieldVisibility, - type, - (Value) defaultValue.getValue(), - CoercedVariables.emptyVariables(), - graphqlContext, - locale); - } - if (defaultValue.isExternal()) { - // performs validation too - return externalValueToInternalValueImpl( - inputInterceptor, - fieldVisibility, - type, - defaultValue.getValue(), - graphqlContext, - locale); + SchemaInputObject inputObjectType) { + if (schema != null) { + return schema.getInputFields(inputObjectType); + } + assertTrue( + inputObjectType instanceof GraphQLInputObjectType, + "A schema-less input object must be a GraphQLInputObjectType"); + return fieldVisibility.getFieldDefinitions( + (GraphQLInputObjectType) inputObjectType); + } + + private static Coercing getScalarCoercing( + @Nullable ExecutableSchema schema, + SchemaScalar scalarType) { + if (schema != null) { + return schema.getScalarCoercing(scalarType); } - return assertShouldNeverHappen(); + return scalarType.getCoercing(); } static Object defaultValueToInternalValue( InputInterceptor inputInterceptor, - ExecutableSchema schema, + @Nullable ExecutableSchema schema, + GraphqlFieldVisibility fieldVisibility, InputValueWithState defaultValue, SchemaInputType type, GraphQLContext graphqlContext, @@ -1773,6 +1229,7 @@ static Object defaultValueToInternalValue( return literalToInternalValue( inputInterceptor, schema, + fieldVisibility, type, (Value) defaultValue.getValue(), CoercedVariables.emptyVariables(), @@ -1784,6 +1241,7 @@ static Object defaultValueToInternalValue( return externalValueToInternalValueImpl( inputInterceptor, schema, + fieldVisibility, type, defaultValue.getValue(), graphqlContext, @@ -1791,4 +1249,52 @@ static Object defaultValueToInternalValue( } return assertShouldNeverHappen(); } + + private static NonNullableValueCoercedAsNullException newNonNullableValueCoercedAsNullException( + VariableDefinition variableDefinition, + SchemaType type) { + if (type instanceof GraphQLType) { + return new NonNullableValueCoercedAsNullException( + variableDefinition, + (GraphQLType) type); + } + return new NonNullableValueCoercedAsNullException( + variableDefinition, + type); + } + + private static NonNullableValueCoercedAsNullException newNonNullableValueCoercedAsNullException( + SchemaType type) { + if (type instanceof GraphQLType) { + return new NonNullableValueCoercedAsNullException( + (GraphQLType) type); + } + return new NonNullableValueCoercedAsNullException(type); + } + + private static NonNullableValueCoercedAsNullException newNonNullableValueCoercedAsNullException( + String fieldName, + SchemaType type) { + if (type instanceof GraphQLType) { + return new NonNullableValueCoercedAsNullException( + fieldName, + emptyList(), + (GraphQLType) type); + } + return new NonNullableValueCoercedAsNullException( + fieldName, + emptyList(), + type); + } + + private static InputMapDefinesTooManyFieldsException newInputMapDefinesTooManyFieldsException( + SchemaType type, + String fieldName) { + if (type instanceof GraphQLType) { + return new InputMapDefinesTooManyFieldsException( + (GraphQLType) type, + fieldName); + } + return new InputMapDefinesTooManyFieldsException(type, fieldName); + } } diff --git a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java index 9ce9a18c25..107f5461b7 100644 --- a/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java +++ b/src/main/java/graphql/execution/ValuesResolverOneOfValidation.java @@ -8,17 +8,13 @@ import graphql.language.ObjectValue; import graphql.language.Value; import graphql.schema.ExecutableSchema; -import graphql.schema.GraphQLInputObjectField; -import graphql.schema.GraphQLInputObjectType; -import graphql.schema.GraphQLInputType; -import graphql.schema.GraphQLList; -import graphql.schema.GraphQLType; import graphql.schema.GraphQLTypeUtil; import graphql.schema.SchemaInputField; import graphql.schema.SchemaInputObject; import graphql.schema.SchemaList; import graphql.schema.SchemaNonNull; import graphql.schema.SchemaType; +import org.jspecify.annotations.Nullable; import java.util.List; import java.util.Locale; @@ -30,93 +26,9 @@ @Internal final class ValuesResolverOneOfValidation { - @SuppressWarnings("unchecked") - static void validateOneOfInputTypes(GraphQLType type, Object inputValue, Value argumentValue, String argumentName, Locale locale) { - GraphQLType unwrappedNonNullType = GraphQLTypeUtil.unwrapNonNull(type); - - if (isList(unwrappedNonNullType) - && !ValuesResolverConversion.isNullValue(inputValue) - && inputValue instanceof List - && argumentValue instanceof ArrayValue) { - GraphQLType elementType = ((GraphQLList) unwrappedNonNullType).getWrappedType(); - List inputList = (List) inputValue; - List argumentList = ((ArrayValue) argumentValue).getValues(); - - for (int i = 0; i < argumentList.size(); i++) { - validateOneOfInputTypes(elementType, inputList.get(i), argumentList.get(i), argumentName, locale); - } - } - - if (unwrappedNonNullType instanceof GraphQLInputObjectType && !ValuesResolverConversion.isNullValue(inputValue)) { - Assert.assertTrue(inputValue instanceof Map, "The coerced argument %s GraphQLInputObjectType is unexpectedly not a map", argumentName); - Map objectMap = (Map) inputValue; - - GraphQLInputObjectType inputObjectType = (GraphQLInputObjectType) unwrappedNonNullType; - - if (inputObjectType.isOneOf()) { - validateOneOfInputTypesInternal(inputObjectType, argumentValue, objectMap, locale); - } - - for (GraphQLInputObjectField fieldDefinition : inputObjectType.getFields()) { - GraphQLInputType childFieldType = fieldDefinition.getType(); - String childFieldName = fieldDefinition.getName(); - Object childFieldInputValue = objectMap.get(childFieldName); - - if (argumentValue instanceof ObjectValue) { - List values = ((ObjectValue) argumentValue).getObjectFields().stream() - .filter(of -> of.getName().equals(childFieldName)) - .map(ObjectField::getValue) - .collect(Collectors.toList()); - - if (values.size() > 1) { - Assert.assertShouldNeverHappen("argument %s has %s object fields with the same name: '%s'. A maximum of 1 is expected", argumentName, values.size(), childFieldName); - } else if (!values.isEmpty()) { - validateOneOfInputTypes(childFieldType, childFieldInputValue, values.get(0), argumentName, locale); - } - } else { - validateOneOfInputTypes(childFieldType, childFieldInputValue, argumentValue, argumentName, locale); - } - } - } - } - - private static void validateOneOfInputTypesInternal(GraphQLInputObjectType oneOfInputType, Value argumentValue, Map objectMap, Locale locale) { - final String fieldName; - if (argumentValue instanceof ObjectValue) { - List objectFields = ((ObjectValue) argumentValue).getObjectFields(); - if (objectFields.size() != 1) { - throwNotOneFieldError(oneOfInputType, locale); - } - - fieldName = objectFields.iterator().next().getName(); - } else { - if (objectMap.size() != 1) { - throwNotOneFieldError(oneOfInputType, locale); - } - - fieldName = objectMap.keySet().iterator().next(); - } - - if (objectMap.get(fieldName) == null) { - throwValueIsNullError(oneOfInputType, locale, fieldName); - } - } - - private static void throwValueIsNullError(GraphQLInputObjectType oneOfInputType, Locale locale, String fieldName) { - String msg = I18n.i18n(I18n.BundleType.Execution, locale) - .msg("Execution.handleOneOfValueIsNullError", oneOfInputType.getName() + "." + fieldName); - throw new OneOfNullValueException(msg); - } - - private static void throwNotOneFieldError(GraphQLInputObjectType oneOfInputType, Locale locale) { - String msg = I18n.i18n(I18n.BundleType.Execution, locale) - .msg("Execution.handleOneOfNotOneFieldError", oneOfInputType.getName()); - throw new OneOfTooManyKeysException(msg); - } - @SuppressWarnings("unchecked") static void validateOneOfInputTypes( - ExecutableSchema schema, + @Nullable ExecutableSchema schema, SchemaType type, Object inputValue, Value argumentValue, @@ -162,7 +74,9 @@ static void validateOneOfInputTypes( } for (SchemaInputField fieldDefinition : - schema.getInputFields(inputObjectType)) { + getInputFields( + schema, + inputObjectType)) { SchemaType childFieldType = fieldDefinition.getType(); String childFieldName = fieldDefinition.getName(); Object childFieldInputValue = objectMap.get(childFieldName); @@ -223,6 +137,15 @@ private static void validateOneOfInputTypesInternal( } } + private static List getInputFields( + @Nullable ExecutableSchema schema, + SchemaInputObject inputObjectType) { + if (schema != null) { + return schema.getInputFields(inputObjectType); + } + return inputObjectType.getFieldDefinitions(); + } + private static void throwValueIsNullError( SchemaInputObject oneOfInputType, Locale locale, diff --git a/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy b/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy index f259714b82..3c9cd7dc61 100644 --- a/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy +++ b/src/test/groovy/graphql/execution/values/InputInterceptorTest.groovy @@ -10,8 +10,10 @@ import graphql.execution.ValuesResolver import graphql.schema.DataFetcher import graphql.schema.DataFetchingEnvironment import graphql.schema.GraphQLInputType +import graphql.schema.GraphQLList import spock.lang.Specification +import static graphql.language.ListType.newListType import static graphql.language.TypeName.newTypeName import static graphql.language.VariableDefinition.newVariableDefinition @@ -81,6 +83,38 @@ class InputInterceptorTest extends Specification { ] } + def "GraphQLSchema variables retain native wrapper types for interceptors"() { + given: + def seenTypes = [] + InputInterceptor trackingInterceptor = { + Object value, + GraphQLInputType type, + GraphQLContext context, + Locale locale -> + seenTypes.add(type) + value + } as InputInterceptor + def variableDefinition = newVariableDefinition( + "values", + newListType(newTypeName("String").build()).build()) + .build() + def graphQLContext = GraphQLContext.newContext() + .put(InputInterceptor.class, trackingInterceptor) + .build() + + when: + ValuesResolver.coerceVariableValues( + schema, + [variableDefinition], + RawVariables.of([values: ["a"]]), + graphQLContext, + Locale.CANADA) + + then: + seenTypes.any { it instanceof GraphQLList } + seenTypes.contains(Scalars.GraphQLString) + } + def "integration test of interceptor being called"() { DataFetcher df = { DataFetchingEnvironment env -> return env.getArguments().entrySet()