8000 Merge duplicate selections across fragments and reject overlapping fragment types by rpiaggio · Pull Request #860 · gemini-hlsw/clue · GitHub
[go: up one dir, main page]

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ A bare `__typename` is consumed by the decoder and is not generated as a field.
`kind: __typename`), it is both the decoder key and a regular `String` field on the trait and every
case class — the way to keep the raw type name around, e.g. for the types folded into `Other`.

Fragments whose type conditions overlap on a concrete type the field can return (e.g. an interface
`Pilot` implemented only by `Human`, alongside a fragment on `Human` itself) are rejected: a
response of that concrete type could only ever decode to one of them, silently losing the other's
fields. Merge the overlapping fragments into one (e.g. on the concrete type they share).

A field selected both at the base level and inside a fragment (which GraphQL merges into a single
response value) is likewise generated once, not duplicated in the fragment's case class, as long as
both selections are the same: same type (a top-level `@include`/`@skip` doesn't count as different;
the base level's type wins) and, for object fields, the same sub-selection (then the nested class
is generated once, in the enclosing companion). Selecting the same field with *different*
sub-selections at the two levels is not merged and fails to compile.

Fragments whose type condition is the field's own type or one of its supertypes are plain grouping
(e.g. for `@include`), not subtypes, and are flattened rather than turned into cases.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// Non-null `id` is selected unconditionally at the base level and again with `@include` inside
// the variant fragment. GraphQL merges them; since the base occurrence is unconditional, the field
// is always present, so the merged param keeps the base level's non-optional `String` (which is
// also what the trait requires of the `override val`).
@GraphQL
trait StarWarsDuplicateSelectionConditional extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!, $$withId: Boolean!) {
hero(episode: $$ep) {
__typename
id
... on Human {
id @include(if: $$withId)
homePlanet
}
}
}
"""
}
// format: on
8000
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// The base-level field is duplicated with different optionality across a same-type fragment: `id`
// is selected with `@include` and again unconditionally via `... on Character` (a fragment on the
// field's own type, so plain grouping, not a variant). The base merge sees no `override` params and
// keeps the non-optional occurrence for the trait; the variant merge must pick the same occurrence
// among the base-level ones, so trait, variant and `Other` all agree on the non-optional type.
@GraphQL
trait StarWarsDuplicateSelectionConditionalBase extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!, $$withId: Boolean!) {
hero(episode: $$ep) {
__typename
id @include(if: $$withId)
... on Character { id }
... on Human { homePlanet }
}
}
"""
}
// format: on
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// `friends { name }` is selected at the base level AND again inside the variant fragment. Both
// must merge into one `friends` param on `Human`, typed with the base-level `Data.Hero.Friends`
// (emitted once, in `Hero`'s companion): a second `Friends` inside `Human` would shadow it and give
// the `override val` a type incompatible with the trait's.
@GraphQL
trait StarWarsDuplicateSelectionNested extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!) {
hero(episode: $$ep) {
__typename
friends {
name
}
... on Human {
friends {
name
}
homePlanet
}
}
}
"""
}
// format: on
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// The nested `id` is selected unconditionally at the base level and again with `@include` inside
// the variant fragment. GraphQL merges them, so `friends` must merge into one param typed with the
// single base-level `Data.Hero.Friends`, non-optional (the base occurrence).
@GraphQL
trait StarWarsDuplicateSelectionNestedConditional extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!, $$withId: Boolean!) {
hero(episode: $$ep) {
__typename
friends {
id
}
... on Human {
friends {
id @include(if: $$withId)
}
homePlanet
}
}
}
"""
}
// format: on
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// The nested polymorphic `friends` is duplicated with its fragments in the opposite order; the
// response shape is identical, so it must merge into one `friends` param typed with the single
// base-level sealed trait `Data.Hero.Friends` (cases `Human`, `Droid`, no `Other` since Character
// has only those two implementors).
@GraphQL // assert: GraphQLGen
trait StarWarsDuplicateSelectionNestedSumReordered extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!) {
hero(episode: $$ep) {
__typename
friends {
__typename
name
... on Human { homePlanet }
... on Droid { primaryFunction }
}
... on Human {
friends {
__typename
name
... on Droid { primaryFunction }
... on Human { homePlanet }
}
homePlanet
}
}
}
"""
}
// format: on
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// The same nested selection with its fields in a different order is still one selection (GraphQL
// field order is insignificant), so it must merge exactly like `StarWarsDuplicateSelectionNested`,
// keeping the base level's field order.
@GraphQL
trait StarWarsDuplicateSelectionReordered extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!) {
hero(episode: $$ep) {
__typename
friends {
id
name
}
... on Human {
friends {
name
id
}
homePlanet
}
}
}
"""
}
// format: on
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// `name` is selected at the base level AND again inside the single variant fragment (same
// response name): GraphQL merges them, so the generator must too, or the generated case class
// would get a duplicate `name` param.
@GraphQL
trait StarWarsDuplicateSelectionSingle extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!) {
hero(episode: $$ep) {
__typename
name
... on Human {
__typename
name
homePlanet
}
}
}
"""
}
// format: on
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// The same subquery is spliced on `friends` at the base level AND again inside the variant
// fragment. A splice contributes an ordinary param typed with the subquery's `Data` (and no nested
// class of its own), so the two must merge into a single `friends` param whose type the variant's
// `override val` shares with the trait's.
@GraphQL
trait StarWarsDuplicateSelectionSpliced extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!) {
hero(episode: $$ep) {
__typename
friends $StarWarsSubquery
... on Human {
friends $StarWarsSubquery
homePlanet
}
}
}
"""
}
// format: on
36 changes: 36 additions & 0 deletions gen/input/src/main/scala/test/StarWarsDuplicateSelectionSum.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// `name` is selected at the base level AND again inside both variant fragments (same response
// name): GraphQL merges them, so each variant's generated case class must carry `name` once (as
// `override val`), not twice. `Human` and `Droid` are Character's only implementors, so no `Other`.
@GraphQL // assert: GraphQLGen
trait StarWarsDuplicateSelectionSum extends GraphQLOperation[StarWars] {
override val document = gql"""
query ($$ep: Episode!) {
hero(episode: $$ep) {
__typename
name
... on Human {
name
homePlanet
}
... on Droid {
name
primaryFunction
}
}
}
"""
}
// format: on
21 changes: 21 additions & 0 deletions gen/input/src/main/scala/test/StarWarsOverlappingFragments.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) 2016-2026 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

// format: off
/*
rules = [GraphQLGen]
Clue.schemaDirs = ["gen/input/src/main/resources/graphql/schemas"]
*/
package test

import clue.GraphQLOperation
import clue.annotation.GraphQL

// Human and Pilot both cover the concrete type Human, so one case would be unreachable and its
// fields lost.
@GraphQL
trait StarWarsOverlappingFragments extends GraphQLOperation[StarWars] {
override val document =
gql"query ($$ep: Episode!) { hero(episode: $$ep) { __typename name ... on Human { homePlanet } ... on Pilot { vehicle } } }" // assert: GraphQLGen
}
// format: on
Loading
Loading
0