From 942ce90ad19c466518b3626def5793f790d9f37e Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 28 Sep 2023 14:43:19 -0700 Subject: [PATCH 01/41] Adds @defer SelectionSetTemplate tests --- .../SelectionSetTemplateTests.swift | 4586 +++++++++++------ 1 file changed, 3062 insertions(+), 1524 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index 7f76756d3..8bc1aca8a 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -331,7 +331,7 @@ class SelectionSetTemplateTests: XCTestCase { let expected = """ public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Nested } - + """ // when @@ -345,7 +345,7 @@ class SelectionSetTemplateTests: XCTestCase { // then expect(actual).to(equalLineByLine(expected, atLine: 6, ignoringExtraLines: true)) } - + func test__render_selections__givenCustomRootTypes_doesNotGenerateTypenameField() throws { // given schemaSDL = """ @@ -353,11 +353,11 @@ class SelectionSetTemplateTests: XCTestCase { query: RootQueryType mutation: RootMutationType } - + type RootQueryType { allAnimals: [Animal!] } - + type RootMutationType { feedAnimal: Animal! } @@ -404,7 +404,7 @@ class SelectionSetTemplateTests: XCTestCase { type Animal { string: String! - string_optional: String + string_optional: String int: Int! int_optional: Int float: Float! @@ -504,7 +504,7 @@ class SelectionSetTemplateTests: XCTestCase { // then expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - + func test__render_selections__givenAllUppercase_generatesCorrectCasing() throws { // given schemaSDL = """ @@ -1488,695 +1488,726 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - // MARK: Selections - Include/Skip + // MARK: Selections - Deferred Inline Fragment - func test__render_selections__givenFieldWithIncludeCondition_rendersFieldSelections() throws { + func test__render_selections__givenDeferredInlineFragment_rendersDeferredFragmentSelection() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - fieldName @include(if: $a) + __typename + id + ... on Dog @defer(label: "root") { + species + } } } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .include(if: "a", .field("fieldName", String.self)), + .deferred(Root.self, label: "root") ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) } - func test__render_selections__givenFieldWithSkipCondition_rendersFieldSelections() throws { + func test__render_selections__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothDeferredFragmentSelections() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($b: Boolean!) { + query TestOperation { allAnimals { - fieldName @skip(if: $b) + __typename + id + ... on Dog @defer(label: "one") { + species + } + ... on Dog @defer(label: "two") { + genus + } } } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .include(if: !"b", .field("fieldName", String.self)), + .deferred(One.self, label: "one"), + .deferred(Two.self, label: "two") ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) } - func test__render_selections__givenFieldWithMultipleConditions_rendersFieldSelections() throws { + func test__render_selections__givenDeferredInlineFragmentWithCondition_rendersDeferredFragmentSelectionWithCondition() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($b: Boolean!) { + query TestOperation { allAnimals { - fieldName @skip(if: $b) @include(if: $a) + __typename + id + ... on Dog @defer(if: "a", label: "root") { + species + } } } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .include(if: !"b" && "a", .field("fieldName", String.self)), + .deferred(if: "a", Root.self, label: "root") ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) } - func test__render_selections__givenMergedFieldsWithMultipleConditions_rendersFieldSelections() throws { + func test__render_selections__givenDeferredInlineFragmentWithTrueCondition_rendersDeferredFragmentSelectionWithoutCondition() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($b: Boolean!) { + query TestOperation { allAnimals { - fieldName @skip(if: $b) @include(if: $a) - fieldName @skip(if: $c) - fieldName @include(if: $d) @skip(if: $e) - fieldName @include(if: $f) + __typename + id + ... on Dog @defer(if: true, label: "root") { + species + } } } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .include(if: (!"b" && "a") || !"c" || ("d" && !"e") || "f", .field("fieldName", String.self)), + .deferred(Root.self, label: "root") ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 10, ignoringExtraLines: true)) } - func test__render_selections__givenMultipleSelectionsWithSameIncludeConditions_rendersFieldSelections() throws { + func test__render_selections__givenDeferredInlineFragmentWithFalseCondition_doesNotRendersDeferredFragmentSelection() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldA: String! - fieldB: String! + interface Animal { + id: String + species: String + genus: String } - interface Pet { - fieldA: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - fieldA @include(if: $a) - fieldB @include(if: $a) - ... on Pet @include(if: $a) { - fieldA + __typename + id + ... on Dog @defer(if: false, label: "root") { + species } - ...FragmentA @include(if: $a) } } - - fragment FragmentA on Animal { - fieldA - } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .include(if: "a", [ - .field("fieldA", String.self), - .field("fieldB", String.self), - .inlineFragment(AsPetIfA.self), - .inlineFragment(IfA.self), - ]), + .field("species", String?.self), ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - func test__render_selections__givenFragmentWithNonMatchingTypeAndInclusionCondition_rendersTypeCaseSelectionWithInclusionCondition() throws { + func test__render_selections__givenNestedDeferredInlineFragments_rendersNestedDeferredFragmentSelections() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String } - type Pet { - string: String! - int: Int! + type Dog implements Animal { + id: String + species: String + genus: String + name: String + friend: Animal + } + + type Cat implements Animal { + id: String + species: String + genus: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ...FragmentA @include(if: $a) + __typename + id + ... on Dog @defer(label: "one") { + friend { + ... on Cat @defer(label: "two") { + species + } + } + } } } - - fragment FragmentA on Pet { - int - } """ - let expected = """ + let expectedOne = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .include(if: "a", .inlineFragment(AsPetIfA.self)), + .deferred(One.self, label: "one"), + ] } + """ + let expectedTwo = """ + public static var __selections: [ApolloAPI.Selection] { [ + .deferred(Two.self, label: "two"), ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + let allAnimals_asDog_asCat = try XCTUnwrap( + allAnimals_asDog[as: "One"]?[field: "friend"]?[as: "Cat"] ) - let actual = subject.render(field: allAnimals) + let actualOne = subject.render(inlineFragment: allAnimals_asDog) + let actualTwo = subject.render(inlineFragment: allAnimals_asDog_asCat) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actualOne).to(equalLineByLine(expectedOne, atLine: 11, ignoringExtraLines: true)) + expect(actualTwo).to(equalLineByLine(expectedTwo, atLine: 11, ignoringExtraLines: true)) } - func test__render_selections__givenInlineFragmentOnSameTypeWithConditions_rendersInlineFragmentSelectionSetAccessorWithCorrectName() throws { + // MARK: Selections - Deferred Named Fragment + + func test__render_selections__givenDeferredNamedFragment_rendersDeferredFragmentSelection() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldA: String! + interface Animal { + id: String + species: String + genus: String } - interface Pet { - fieldA: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ... on Animal @include(if: $a) { - fieldA - } + __typename + id + ...DogFragment @defer(label: "root") } } + + fragment DogFragment on Dog { + species + } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .include(if: "a", .inlineFragment(IfA.self)), + .deferred(DogFragment.self) ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - func test__render_selections__givenFragmentWithInclusionConditionThatMatchesScope_rendersFragmentSelectionWithoutInclusionCondition() throws { + func test__render_selections__givenDeferredNamedFragmentWithLabel_rendersDeferredFragmentSelectionUsingNamedFragmentType() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String } - type Pet { - string: String! - int: Int! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ...FragmentA @include(if: $a) + __typename + id + ...DogFragment @defer(label: "root") } } - fragment FragmentA on Pet { - int + fragment DogFragment on Dog { + species } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .fragment(FragmentA.self), + .deferred(DogFragment.self, label: "root") ] } """ // when try buildSubjectAndOperation() - let allAnimals_asPet = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Pet", if: "a"] + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(inlineFragment: allAnimals_asPet) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - // MARK: Selections - __typename Selection - - func test__render_selections__givenEntityRootSelectionSet_rendersTypenameSelection() throws { + func test__render_selections__givenDeferredNamedFragmentWithCondition_rendersDeferredFragmentSelectionWithCondition() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ query TestOperation { allAnimals { - fieldName + __typename + id + ...DogFragment @defer(if: "a", label: "root") } } + + fragment DogFragment on Dog { + species + } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("fieldName", String.self), + .deferred(if: "a", DogFragment.self), ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - func test__render_selections__givenInlineFragment_doesNotRenderTypenameSelection() throws { + func test__render_selections__givenDeferredNamedFragmentWithTrueCondition_rendersDeferredFragmentSelection() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + id: String + species: String + genus: String } - interface Pet { - fieldName: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ query TestOperation { allAnimals { - ... on Pet { - fieldName - } + __typename + id + ...DogFragment @defer(if: true, label: "root") } } + + fragment DogFragment on Dog { + species + } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("fieldName", String.self), + .deferred(DogFragment.self), ] } """ // when try buildSubjectAndOperation() - let allAnimals_asPet = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Pet"] + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(inlineFragment: allAnimals_asPet) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - func test__render_selections__givenOperationRootSelectionSet_doesNotRenderTypenameSelection() throws { + func test__render_selections__givenDeferredNamedFragmentWithFalseCondition_doesNotRenderDeferredFragmentSelection() throws { // given schemaSDL = """ type Query { - allAnimals: [Animal!]! + allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ query TestOperation { allAnimals { - fieldName + __typename + id + ...DogFragment @defer(if: false, label: "root") } } + + fragment DogFragment on Dog { + species + } """ let expected = """ public static var __selections: [ApolloAPI.Selection] { [ - .field("allAnimals", [AllAnimal].self), + .fragment(DogFragment.self), ] } """ // when try buildSubjectAndOperation() - let queryRoot = try XCTUnwrap( - operation[field: "query"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: queryRoot) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - // MARK: Merged Sources + // MARK: Selections - Include/Skip - func test__render_mergedSources__givenMergedTypeCasesFromSingleMergedTypeCaseSource_rendersMergedSources() throws { + func test__render_selections__givenFieldWithIncludeCondition_rendersFieldSelections() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - } - - interface Pet implements Animal { - species: String! - predator: Animal! - name: String! - } - - type Dog implements Animal & Pet { - species: String! - predator: Animal! - name: String! + type Animal { + fieldName: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - species - predator { - ... on Pet { - name - } - } - ... on Dog { - name - predator { - species - } - } + fieldName @include(if: $a) } } """ let expected = """ - public static var __mergedSources: [any ApolloAPI.SelectionSet.Type] { [ - TestOperationQuery.Data.AllAnimal.Predator.AsPet.self, - TestOperationQuery.Data.AllAnimal.AsDog.Predator.self + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .include(if: "a", .field("fieldName", String.self)), ] } """ // when try buildSubjectAndOperation() - let allAnimals_asDog_predator_asPet = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"]?[as: "Pet"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: allAnimals_asDog_predator_asPet) + let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - func test__render_mergedSources__givenTypeCaseMergedFromFragmentWithOtherMergedFields_rendersMergedSources() throws { + func test__render_selections__givenFieldWithSkipCondition_rendersFieldSelections() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - } - - interface Pet { - favoriteToy: Item - } - - type Item { - name: String! + type Animal { + fieldName: String! } """ document = """ - query TestOperation { + query TestOperation($b: Boolean!) { allAnimals { - predator { - ...PredatorDetails - species - } - } - } - - fragment PredatorDetails on Animal { - ... on Pet { - favoriteToy { - ...PetToy - } + fieldName @skip(if: $b) } } - - fragment PetToy on Item { - name - } """ let expected = """ - public static var __mergedSources: [any ApolloAPI.SelectionSet.Type] { [ - TestOperationQuery.Data.AllAnimal.Predator.self, - PredatorDetails.AsPet.self + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .include(if: !"b", .field("fieldName", String.self)), ] } """ // when try buildSubjectAndOperation() - let predator_asPet = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "predator"]?[as: "Pet"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: predator_asPet) + let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - /// Test for edge case in [#2949](https://github.com/apollographql/apollo-ios/issues/2949) - /// - /// When the `MergedSource` would have duplicate naming, due to child fields with the same name - /// (or alias), the fully qualified name must be used. In this example, a `MergedSource` of - /// `Predator.Predator` the first usage of the name `Predator` would be referencing the nearest - /// enclosing type (ie. `TestOperationQuery.Predator.Predator`), so it is looking for another - /// `Predator` type in that scope, which does not exist - /// (ie. `TestOperationQuery.Predator.Predator.Predator`). - /// - /// To correct this we must always use the fully qualified name including the operation name and - /// `Data` objects to ensure we are referring to the correct type. - func test__render_mergedSources__givenMergedTypeCaseWithConflictingNames_rendersMergedSourceWithFullyQualifiedName() throws { + func test__render_selections__givenFieldWithMultipleConditions_rendersFieldSelections() throws { // given schemaSDL = """ type Query { - predators: [Animal!]! - } - - interface Animal { - species: String! - predator: Animal! - } - - interface Pet implements Animal { - species: String! - predator: Animal! - name: String! + allAnimals: [Animal!] } - type Dog implements Animal & Pet { - species: String! - predator: Animal! - name: String! + type Animal { + fieldName: String! } """ document = """ - query TestOperation { - predators { - species - predator { - ... on Pet { - name - } - } - ... on Dog { - name - predator { - species - } - } + query TestOperation($b: Boolean!) { + allAnimals { + fieldName @skip(if: $b) @include(if: $a) } } """ let expected = """ - public static var __mergedSources: [any ApolloAPI.SelectionSet.Type] { [ - TestOperationQuery.Data.Predator.Predator.AsPet.self, - TestOperationQuery.Data.Predator.AsDog.Predator.self + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .include(if: !"b" && "a", .field("fieldName", String.self)), ] } """ // when try buildSubjectAndOperation() - let allAnimals_asDog_predator_asPet = try XCTUnwrap( - operation[field: "query"]?[field: "predators"]?[as: "Dog"]?[field: "predator"]?[as: "Pet"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: allAnimals_asDog_predator_asPet) + let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - // MARK: - Field Accessors - Scalar - - func test__render_fieldAccessors__givenScalarFields_rendersAllFieldAccessors() throws { + func test__render_selections__givenMergedFieldsWithMultipleConditions_rendersFieldSelections() throws { // given schemaSDL = """ type Query { @@ -2184,91 +2215,26 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - string: String! - string_optional: String - int: Int! - int_optional: Int - float: Float! - float_optional: Float - boolean: Boolean! - boolean_optional: Boolean - custom: Custom! - custom_optional: Custom - custom_required_list: [Custom!]! - custom_optional_list: [Custom!] - list_required_required: [String!]! - list_optional_required: [String!] - list_required_optional: [String]! - list_optional_optional: [String] - nestedList_required_required_required: [[String!]!]! - nestedList_required_required_optional: [[String]!]! - nestedList_required_optional_optional: [[String]]! - nestedList_required_optional_required: [[String!]]! - nestedList_optional_required_required: [[String!]!] - nestedList_optional_required_optional: [[String]!] - nestedList_optional_optional_required: [[String!]] - nestedList_optional_optional_optional: [[String]] + fieldName: String! } - - scalar Custom """ document = """ - query TestOperation { + query TestOperation($b: Boolean!) { allAnimals { - string - string_optional - int - int_optional - float - float_optional - boolean - boolean_optional - custom - custom_optional - custom_required_list - custom_optional_list - list_required_required - list_optional_required - list_required_optional - list_optional_optional - nestedList_required_required_required - nestedList_required_required_optional - nestedList_required_optional_optional - nestedList_required_optional_required - nestedList_optional_required_required - nestedList_optional_required_optional - nestedList_optional_optional_required - nestedList_optional_optional_optional + fieldName @skip(if: $b) @include(if: $a) + fieldName @skip(if: $c) + fieldName @include(if: $d) @skip(if: $e) + fieldName @include(if: $f) } } """ let expected = """ - public var string: String { __data["string"] } - public var string_optional: String? { __data["string_optional"] } - public var int: Int { __data["int"] } - public var int_optional: Int? { __data["int_optional"] } - public var float: Double { __data["float"] } - public var float_optional: Double? { __data["float_optional"] } - public var boolean: Bool { __data["boolean"] } - public var boolean_optional: Bool? { __data["boolean_optional"] } - public var custom: TestSchema.Custom { __data["custom"] } - public var custom_optional: TestSchema.Custom? { __data["custom_optional"] } - public var custom_required_list: [TestSchema.Custom] { __data["custom_required_list"] } - public var custom_optional_list: [TestSchema.Custom]? { __data["custom_optional_list"] } - public var list_required_required: [String] { __data["list_required_required"] } - public var list_optional_required: [String]? { __data["list_optional_required"] } - public var list_required_optional: [String?] { __data["list_required_optional"] } - public var list_optional_optional: [String?]? { __data["list_optional_optional"] } - public var nestedList_required_required_required: [[String]] { __data["nestedList_required_required_required"] } - public var nestedList_required_required_optional: [[String?]] { __data["nestedList_required_required_optional"] } - public var nestedList_required_optional_optional: [[String?]?] { __data["nestedList_required_optional_optional"] } - public var nestedList_required_optional_required: [[String]?] { __data["nestedList_required_optional_required"] } - public var nestedList_optional_required_required: [[String]]? { __data["nestedList_optional_required_required"] } - public var nestedList_optional_required_optional: [[String?]]? { __data["nestedList_optional_required_optional"] } - public var nestedList_optional_optional_required: [[String]?]? { __data["nestedList_optional_optional_required"] } - public var nestedList_optional_optional_optional: [[String?]?]? { __data["nestedList_optional_optional_optional"] } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .include(if: (!"b" && "a") || !"c" || ("d" && !"e") || "f", .field("fieldName", String.self)), + ] } """ // when @@ -2280,10 +2246,10 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 35, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenCustomScalarFields_rendersFieldAccessorsWithNamespaceWhenRequiredInAllConfigurations() throws { + func test__render_selections__givenMultipleSelectionsWithSameIncludeConditions_rendersFieldSelections() throws { // given schemaSDL = """ type Query { @@ -2291,64 +2257,57 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - custom: Custom! - custom_optional: Custom - custom_required_list: [Custom!]! - custom_optional_list: [Custom!] - lowercaseScalar: lowercaseScalar! + fieldA: String! + fieldB: String! } - scalar Custom - scalar lowercaseScalar + interface Pet { + fieldA: String! + } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - custom - custom_optional - custom_required_list - custom_optional_list - lowercaseScalar + fieldA @include(if: $a) + fieldB @include(if: $a) + ... on Pet @include(if: $a) { + fieldA + } + ...FragmentA @include(if: $a) } } + + fragment FragmentA on Animal { + fieldA + } """ let expected = """ - public var custom: TestSchema.Custom { __data["custom"] } - public var custom_optional: TestSchema.Custom? { __data["custom_optional"] } - public var custom_required_list: [TestSchema.Custom] { __data["custom_required_list"] } - public var custom_optional_list: [TestSchema.Custom]? { __data["custom_optional_list"] } - public var lowercaseScalar: TestSchema.LowercaseScalar { __data["lowercaseScalar"] } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .include(if: "a", [ + .field("fieldA", String.self), + .field("fieldB", String.self), + .inlineFragment(AsPetIfA.self), + .inlineFragment(IfA.self), + ]), + ] } """ - let tests: [ApolloCodegenConfiguration.FileOutput] = [ - .mock(moduleType: .swiftPackageManager, operations: .relative(subpath: nil, accessModifier: .public)), - .mock(moduleType: .swiftPackageManager, operations: .absolute(path: "custom", accessModifier: .public)), - .mock(moduleType: .swiftPackageManager, operations: .inSchemaModule), - .mock(moduleType: .other, operations: .relative(subpath: nil, accessModifier: .public)), - .mock(moduleType: .other, operations: .absolute(path: "custom", accessModifier: .public)), - .mock(moduleType: .other, operations: .inSchemaModule), - .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .relative(subpath: nil, accessModifier: .public)), - .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .absolute(path: "custom", accessModifier: .public)), - .mock(moduleType: .embeddedInTarget(name: "CustomTarget", accessModifier: .public), operations: .inSchemaModule) - ] - - for test in tests { - // when - try buildSubjectAndOperation(configOutput: test) - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(field: allAnimals) - // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) - } + // then + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEnumField_rendersFieldAccessorsWithNamespacedInAllConfigurations() throws { + func test__render_selections__givenFragmentWithNonMatchingTypeAndInclusionCondition_rendersTypeCaseSelectionWithInclusionCondition() throws { // given schemaSDL = """ type Query { @@ -2356,141 +2315,143 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - testEnum: TestEnum! - testEnumOptional: TestEnumOptional - lowercaseEnum: lowercaseEnum! - } - - enum TestEnum { - CASE_ONE - } - - enum TestEnumOptional { - CASE_ONE + string: String! + int: Int! } - enum lowercaseEnum { - CASE_ONE + type Pet { + string: String! + int: Int! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - testEnum - testEnumOptional - lowercaseEnum + ...FragmentA @include(if: $a) } } + + fragment FragmentA on Pet { + int + } """ let expected = """ - public var testEnum: GraphQLEnum { __data["testEnum"] } - public var testEnumOptional: GraphQLEnum? { __data["testEnumOptional"] } - public var lowercaseEnum: GraphQLEnum { __data["lowercaseEnum"] } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .include(if: "a", .inlineFragment(AsPetIfA.self)), + ] } """ - let tests: [ApolloCodegenConfiguration.FileOutput] = [ - .mock(moduleType: .swiftPackageManager, operations: .relative(subpath: nil, accessModifier: .public)), - .mock(moduleType: .swiftPackageManager, operations: .absolute(path: "custom", accessModifier: .public)), - .mock(moduleType: .swiftPackageManager, operations: .inSchemaModule), - .mock(moduleType: .other, operations: .relative(subpath: nil, accessModifier: .public)), - .mock(moduleType: .other, operations: .absolute(path: "custom", accessModifier: .public)), - .mock(moduleType: .other, operations: .inSchemaModule), - .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .relative(subpath: nil, accessModifier: .public)), - .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .absolute(path: "custom", accessModifier: .public)), - .mock(moduleType: .embeddedInTarget(name: "CustomTarget", accessModifier: .public), operations: .inSchemaModule) - ] - - for test in tests { - // when - try buildSubjectAndOperation(configOutput: test) - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(field: allAnimals) - // then - expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) - } + // then + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenFieldWithUpperCaseName_rendersFieldAccessorWithLowercaseName() throws { + func test__render_selections__givenInlineFragmentOnSameTypeWithConditions_rendersInlineFragmentSelectionSetAccessorWithCorrectName() throws { // given schemaSDL = """ type Query { - AllAnimals: [Animal!] + allAnimals: [Animal!] } type Animal { - FieldName: String! + fieldA: String! } - scalar Custom + interface Pet { + fieldA: String! + } """ document = """ - query TestOperation { - AllAnimals { - FieldName + query TestOperation($a: Boolean!) { + allAnimals { + ... on Animal @include(if: $a) { + fieldA + } } } """ let expected = """ - public var fieldName: String { __data["FieldName"] } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .include(if: "a", .inlineFragment(IfA.self)), + ] } """ // when try buildSubjectAndOperation() let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - - func test__render_fieldAccessors__givenFieldWithAllUpperCaseName_rendersFieldAccessorWithLowercaseName() throws { + + func test__render_selections__givenFragmentWithInclusionConditionThatMatchesScope_rendersFragmentSelectionWithoutInclusionCondition() throws { // given schemaSDL = """ type Query { - AllAnimals: [Animal!] + allAnimals: [Animal!] } type Animal { - FIELDNAME: String! + string: String! + int: Int! + } + + type Pet { + string: String! + int: Int! } """ document = """ - query TestOperation { - AllAnimals { - FIELDNAME + query TestOperation($a: Boolean!) { + allAnimals { + ...FragmentA @include(if: $a) } } + + fragment FragmentA on Pet { + int + } """ let expected = """ - public var fieldname: String { __data["FIELDNAME"] } + public static var __selections: [ApolloAPI.Selection] { [ + .fragment(FragmentA.self), + ] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + let allAnimals_asPet = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Pet", if: "a"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asPet) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenFieldWithAlias_rendersAllFieldAccessors() throws { + // MARK: Selections - __typename Selection + + func test__render_selections__givenEntityRootSelectionSet_rendersTypenameSelection() throws { // given schemaSDL = """ type Query { @@ -2498,22 +2459,23 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - string: String! + fieldName: String! } - - scalar Custom """ document = """ query TestOperation { allAnimals { - aliasedFieldName: string + fieldName } } """ let expected = """ - public var aliasedFieldName: String { __data["aliasedFieldName"] } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("fieldName", String.self), + ] } """ // when @@ -2525,255 +2487,893 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenMergedScalarField_rendersFieldAccessor() throws { + func test__render_selections__givenInlineFragment_doesNotRenderTypenameSelection() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - a: String! + type Animal { + fieldName: String! } - type Dog { - b: String! + interface Pet { + fieldName: String! } """ document = """ query TestOperation { allAnimals { - a - ... on Dog { - b + ... on Pet { + fieldName } } } """ let expected = """ - public var b: String { __data["b"] } - public var a: String { __data["a"] } + public static var __selections: [ApolloAPI.Selection] { [ + .field("fieldName", String.self), + ] } """ // when try buildSubjectAndOperation() - let dog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + let allAnimals_asPet = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Pet"] ) - let actual = subject.render(inlineFragment: dog) + let actual = subject.render(inlineFragment: allAnimals_asPet) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - - func test__render_fieldAccessors__givenFieldWithSnakeCaseName_rendersFieldAccessorAsCamelCase() throws { + + func test__render_selections__givenOperationRootSelectionSet_doesNotRenderTypenameSelection() throws { // given schemaSDL = """ type Query { - AllAnimals: [Animal!] + allAnimals: [Animal!]! } type Animal { - field_name: String! + fieldName: String! } """ document = """ query TestOperation { - AllAnimals { - field_name + allAnimals { + fieldName } } """ let expected = """ - public var fieldName: String { __data["field_name"] } + public static var __selections: [ApolloAPI.Selection] { [ + .field("allAnimals", [AllAnimal].self), + ] } """ // when - try buildSubjectAndOperation(conversionStrategies: .init(fieldAccessors: .camelCase)) - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + try buildSubjectAndOperation() + let queryRoot = try XCTUnwrap( + operation[field: "query"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(field: queryRoot) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - - func test__render_fieldAccessors__givenFieldWithSnakeCaseUppercaseName_rendersFieldAccessorAsCamelCase() throws { + + // MARK: Merged Sources + + func test__render_mergedSources__givenMergedTypeCasesFromSingleMergedTypeCaseSource_rendersMergedSources() throws { // given schemaSDL = """ type Query { - AllAnimals: [Animal!] + allAnimals: [Animal!] } - type Animal { - FIELD_NAME: String! + interface Animal { + species: String! + predator: Animal! + } + + interface Pet implements Animal { + species: String! + predator: Animal! + name: String! + } + + type Dog implements Animal & Pet { + species: String! + predator: Animal! + name: String! } """ document = """ query TestOperation { - AllAnimals { - FIELD_NAME + allAnimals { + species + predator { + ... on Pet { + name + } + } + ... on Dog { + name + predator { + species + } + } } } """ let expected = """ - public var fieldName: String { __data["FIELD_NAME"] } + public static var __mergedSources: [any ApolloAPI.SelectionSet.Type] { [ + TestOperationQuery.Data.AllAnimal.Predator.AsPet.self, + TestOperationQuery.Data.AllAnimal.AsDog.Predator.self + ] } """ // when - try buildSubjectAndOperation(conversionStrategies: .init(fieldAccessors: .camelCase)) - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + try buildSubjectAndOperation() + let allAnimals_asDog_predator_asPet = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"]?[as: "Pet"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog_predator_asPet) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - // MARK: Field Accessors - Reserved Keywords + Special Names - - func test__render_fieldAccessors__givenFieldsWithSwiftReservedKeywordNames_rendersFieldsBacktickEscaped() throws { + func test__render_mergedSources__givenTypeCaseMergedFromFragmentWithOtherMergedFields_rendersMergedSources() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - associatedtype: String! - class: String! - deinit: String! - enum: String! - extension: String! - fileprivate: String! - func: String! - import: String! - init: String! - inout: String! - internal: String! - let: String! - operator: String! - private: String! - precedencegroup: String! - protocol: String! - Protocol: String! - public: String! - rethrows: String! - static: String! - struct: String! - subscript: String! - typealias: String! - var: String! - break: String! - case: String! - catch: String! - continue: String! - default: String! - defer: String! - do: String! - else: String! - fallthrough: String! - for: String! - guard: String! - if: String! - in: String! - repeat: String! - return: String! - throw: String! - switch: String! - where: String! - while: String! - as: String! - false: String! - is: String! - nil: String! - self: String! - Self: String! - super: String! - throws: String! - true: String! - try: String! - _: String! + interface Animal { + species: String! + predator: Animal! + } + + interface Pet { + favoriteToy: Item + } + + type Item { + name: String! } """ document = """ query TestOperation { allAnimals { - associatedtype - class - deinit - enum - extension - fileprivate - func - import - init - inout - internal - let - operator - private - precedencegroup - protocol - Protocol - public - rethrows - static - struct - subscript - typealias - var - break - case - catch - continue - default - defer - do - else - fallthrough - for - guard - if - in - repeat - return - throw - switch - where - while - as - false - is - nil - self - Self - super - throws - true - try + predator { + ...PredatorDetails + species + } + } + } + + fragment PredatorDetails on Animal { + ... on Pet { + favoriteToy { + ...PetToy + } } } + + fragment PetToy on Item { + name + } """ let expected = """ - public var `associatedtype`: String { __data["associatedtype"] } - public var `class`: String { __data["class"] } + public static var __mergedSources: [any ApolloAPI.SelectionSet.Type] { [ + TestOperationQuery.Data.AllAnimal.Predator.self, + PredatorDetails.AsPet.self + ] } + """ + + // when + try buildSubjectAndOperation() + let predator_asPet = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"]?[as: "Pet"] + ) + + let actual = subject.render(inlineFragment: predator_asPet) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) + } + + /// Test for edge case in [#2949](https://github.com/apollographql/apollo-ios/issues/2949) + /// + /// When the `MergedSource` would have duplicate naming, due to child fields with the same name + /// (or alias), the fully qualified name must be used. In this example, a `MergedSource` of + /// `Predator.Predator` the first usage of the name `Predator` would be referencing the nearest + /// enclosing type (ie. `TestOperationQuery.Predator.Predator`), so it is looking for another + /// `Predator` type in that scope, which does not exist + /// (ie. `TestOperationQuery.Predator.Predator.Predator`). + /// + /// To correct this we must always use the fully qualified name including the operation name and + /// `Data` objects to ensure we are referring to the correct type. + func test__render_mergedSources__givenMergedTypeCaseWithConflictingNames_rendersMergedSourceWithFullyQualifiedName() throws { + // given + schemaSDL = """ + type Query { + predators: [Animal!]! + } + + interface Animal { + species: String! + predator: Animal! + } + + interface Pet implements Animal { + species: String! + predator: Animal! + name: String! + } + + type Dog implements Animal & Pet { + species: String! + predator: Animal! + name: String! + } + """ + + document = """ + query TestOperation { + predators { + species + predator { + ... on Pet { + name + } + } + ... on Dog { + name + predator { + species + } + } + } + } + """ + + let expected = """ + public static var __mergedSources: [any ApolloAPI.SelectionSet.Type] { [ + TestOperationQuery.Data.Predator.Predator.AsPet.self, + TestOperationQuery.Data.Predator.AsDog.Predator.self + ] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog_predator_asPet = try XCTUnwrap( + operation[field: "query"]?[field: "predators"]?[as: "Dog"]?[field: "predator"]?[as: "Pet"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog_predator_asPet) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) + } + + // MARK: - Field Accessors - Scalar + + func test__render_fieldAccessors__givenScalarFields_rendersAllFieldAccessors() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + string: String! + string_optional: String + int: Int! + int_optional: Int + float: Float! + float_optional: Float + boolean: Boolean! + boolean_optional: Boolean + custom: Custom! + custom_optional: Custom + custom_required_list: [Custom!]! + custom_optional_list: [Custom!] + list_required_required: [String!]! + list_optional_required: [String!] + list_required_optional: [String]! + list_optional_optional: [String] + nestedList_required_required_required: [[String!]!]! + nestedList_required_required_optional: [[String]!]! + nestedList_required_optional_optional: [[String]]! + nestedList_required_optional_required: [[String!]]! + nestedList_optional_required_required: [[String!]!] + nestedList_optional_required_optional: [[String]!] + nestedList_optional_optional_required: [[String!]] + nestedList_optional_optional_optional: [[String]] + } + + scalar Custom + """ + + document = """ + query TestOperation { + allAnimals { + string + string_optional + int + int_optional + float + float_optional + boolean + boolean_optional + custom + custom_optional + custom_required_list + custom_optional_list + list_required_required + list_optional_required + list_required_optional + list_optional_optional + nestedList_required_required_required + nestedList_required_required_optional + nestedList_required_optional_optional + nestedList_required_optional_required + nestedList_optional_required_required + nestedList_optional_required_optional + nestedList_optional_optional_required + nestedList_optional_optional_optional + } + } + """ + + let expected = """ + public var string: String { __data["string"] } + public var string_optional: String? { __data["string_optional"] } + public var int: Int { __data["int"] } + public var int_optional: Int? { __data["int_optional"] } + public var float: Double { __data["float"] } + public var float_optional: Double? { __data["float_optional"] } + public var boolean: Bool { __data["boolean"] } + public var boolean_optional: Bool? { __data["boolean_optional"] } + public var custom: TestSchema.Custom { __data["custom"] } + public var custom_optional: TestSchema.Custom? { __data["custom_optional"] } + public var custom_required_list: [TestSchema.Custom] { __data["custom_required_list"] } + public var custom_optional_list: [TestSchema.Custom]? { __data["custom_optional_list"] } + public var list_required_required: [String] { __data["list_required_required"] } + public var list_optional_required: [String]? { __data["list_optional_required"] } + public var list_required_optional: [String?] { __data["list_required_optional"] } + public var list_optional_optional: [String?]? { __data["list_optional_optional"] } + public var nestedList_required_required_required: [[String]] { __data["nestedList_required_required_required"] } + public var nestedList_required_required_optional: [[String?]] { __data["nestedList_required_required_optional"] } + public var nestedList_required_optional_optional: [[String?]?] { __data["nestedList_required_optional_optional"] } + public var nestedList_required_optional_required: [[String]?] { __data["nestedList_required_optional_required"] } + public var nestedList_optional_required_required: [[String]]? { __data["nestedList_optional_required_required"] } + public var nestedList_optional_required_optional: [[String?]]? { __data["nestedList_optional_required_optional"] } + public var nestedList_optional_optional_required: [[String]?]? { __data["nestedList_optional_optional_required"] } + public var nestedList_optional_optional_optional: [[String?]?]? { __data["nestedList_optional_optional_optional"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 35, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenCustomScalarFields_rendersFieldAccessorsWithNamespaceWhenRequiredInAllConfigurations() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + custom: Custom! + custom_optional: Custom + custom_required_list: [Custom!]! + custom_optional_list: [Custom!] + lowercaseScalar: lowercaseScalar! + } + + scalar Custom + scalar lowercaseScalar + """ + + document = """ + query TestOperation { + allAnimals { + custom + custom_optional + custom_required_list + custom_optional_list + lowercaseScalar + } + } + """ + + let expected = """ + public var custom: TestSchema.Custom { __data["custom"] } + public var custom_optional: TestSchema.Custom? { __data["custom_optional"] } + public var custom_required_list: [TestSchema.Custom] { __data["custom_required_list"] } + public var custom_optional_list: [TestSchema.Custom]? { __data["custom_optional_list"] } + public var lowercaseScalar: TestSchema.LowercaseScalar { __data["lowercaseScalar"] } + """ + + let tests: [ApolloCodegenConfiguration.FileOutput] = [ + .mock(moduleType: .swiftPackageManager, operations: .relative(subpath: nil, accessModifier: .public)), + .mock(moduleType: .swiftPackageManager, operations: .absolute(path: "custom", accessModifier: .public)), + .mock(moduleType: .swiftPackageManager, operations: .inSchemaModule), + .mock(moduleType: .other, operations: .relative(subpath: nil, accessModifier: .public)), + .mock(moduleType: .other, operations: .absolute(path: "custom", accessModifier: .public)), + .mock(moduleType: .other, operations: .inSchemaModule), + .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .relative(subpath: nil, accessModifier: .public)), + .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .absolute(path: "custom", accessModifier: .public)), + .mock(moduleType: .embeddedInTarget(name: "CustomTarget", accessModifier: .public), operations: .inSchemaModule) + ] + + for test in tests { + // when + try buildSubjectAndOperation(configOutput: test) + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + } + } + + func test__render_fieldAccessors__givenEnumField_rendersFieldAccessorsWithNamespacedInAllConfigurations() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + testEnum: TestEnum! + testEnumOptional: TestEnumOptional + lowercaseEnum: lowercaseEnum! + } + + enum TestEnum { + CASE_ONE + } + + enum TestEnumOptional { + CASE_ONE + } + + enum lowercaseEnum { + CASE_ONE + } + """ + + document = """ + query TestOperation { + allAnimals { + testEnum + testEnumOptional + lowercaseEnum + } + } + """ + + let expected = """ + public var testEnum: GraphQLEnum { __data["testEnum"] } + public var testEnumOptional: GraphQLEnum? { __data["testEnumOptional"] } + public var lowercaseEnum: GraphQLEnum { __data["lowercaseEnum"] } + """ + + let tests: [ApolloCodegenConfiguration.FileOutput] = [ + .mock(moduleType: .swiftPackageManager, operations: .relative(subpath: nil, accessModifier: .public)), + .mock(moduleType: .swiftPackageManager, operations: .absolute(path: "custom", accessModifier: .public)), + .mock(moduleType: .swiftPackageManager, operations: .inSchemaModule), + .mock(moduleType: .other, operations: .relative(subpath: nil, accessModifier: .public)), + .mock(moduleType: .other, operations: .absolute(path: "custom", accessModifier: .public)), + .mock(moduleType: .other, operations: .inSchemaModule), + .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .relative(subpath: nil, accessModifier: .public)), + .mock(moduleType: .embeddedInTarget(name: "CustomTarget"), operations: .absolute(path: "custom", accessModifier: .public)), + .mock(moduleType: .embeddedInTarget(name: "CustomTarget", accessModifier: .public), operations: .inSchemaModule) + ] + + for test in tests { + // when + try buildSubjectAndOperation(configOutput: test) + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + } + } + + func test__render_fieldAccessors__givenFieldWithUpperCaseName_rendersFieldAccessorWithLowercaseName() throws { + // given + schemaSDL = """ + type Query { + AllAnimals: [Animal!] + } + + type Animal { + FieldName: String! + } + + scalar Custom + """ + + document = """ + query TestOperation { + AllAnimals { + FieldName + } + } + """ + + let expected = """ + public var fieldName: String { __data["FieldName"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenFieldWithAllUpperCaseName_rendersFieldAccessorWithLowercaseName() throws { + // given + schemaSDL = """ + type Query { + AllAnimals: [Animal!] + } + + type Animal { + FIELDNAME: String! + } + """ + + document = """ + query TestOperation { + AllAnimals { + FIELDNAME + } + } + """ + + let expected = """ + public var fieldname: String { __data["FIELDNAME"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenFieldWithAlias_rendersAllFieldAccessors() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + string: String! + } + + scalar Custom + """ + + document = """ + query TestOperation { + allAnimals { + aliasedFieldName: string + } + } + """ + + let expected = """ + public var aliasedFieldName: String { __data["aliasedFieldName"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenMergedScalarField_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + a: String! + } + + type Dog { + b: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + a + ... on Dog { + b + } + } + } + """ + + let expected = """ + public var b: String { __data["b"] } + public var a: String { __data["a"] } + """ + + // when + try buildSubjectAndOperation() + let dog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: dog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenFieldWithSnakeCaseName_rendersFieldAccessorAsCamelCase() throws { + // given + schemaSDL = """ + type Query { + AllAnimals: [Animal!] + } + + type Animal { + field_name: String! + } + """ + + document = """ + query TestOperation { + AllAnimals { + field_name + } + } + """ + + let expected = """ + public var fieldName: String { __data["field_name"] } + """ + + // when + try buildSubjectAndOperation(conversionStrategies: .init(fieldAccessors: .camelCase)) + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenFieldWithSnakeCaseUppercaseName_rendersFieldAccessorAsCamelCase() throws { + // given + schemaSDL = """ + type Query { + AllAnimals: [Animal!] + } + + type Animal { + FIELD_NAME: String! + } + """ + + document = """ + query TestOperation { + AllAnimals { + FIELD_NAME + } + } + """ + + let expected = """ + public var fieldName: String { __data["FIELD_NAME"] } + """ + + // when + try buildSubjectAndOperation(conversionStrategies: .init(fieldAccessors: .camelCase)) + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "AllAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + // MARK: Field Accessors - Reserved Keywords + Special Names + + func test__render_fieldAccessors__givenFieldsWithSwiftReservedKeywordNames_rendersFieldsBacktickEscaped() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + associatedtype: String! + class: String! + deinit: String! + enum: String! + extension: String! + fileprivate: String! + func: String! + import: String! + init: String! + inout: String! + internal: String! + let: String! + operator: String! + private: String! + precedencegroup: String! + protocol: String! + Protocol: String! + public: String! + rethrows: String! + static: String! + struct: String! + subscript: String! + typealias: String! + var: String! + break: String! + case: String! + catch: String! + continue: String! + default: String! + defer: String! + do: String! + else: String! + fallthrough: String! + for: String! + guard: String! + if: String! + in: String! + repeat: String! + return: String! + throw: String! + switch: String! + where: String! + while: String! + as: String! + false: String! + is: String! + nil: String! + self: String! + Self: String! + super: String! + throws: String! + true: String! + try: String! + _: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + associatedtype + class + deinit + enum + extension + fileprivate + func + import + init + inout + internal + let + operator + private + precedencegroup + protocol + Protocol + public + rethrows + static + struct + subscript + typealias + var + break + case + catch + continue + default + defer + do + else + fallthrough + for + guard + if + in + repeat + return + throw + switch + where + while + as + false + is + nil + self + Self + super + throws + true + try + } + } + """ + + let expected = """ + public var `associatedtype`: String { __data["associatedtype"] } + public var `class`: String { __data["class"] } public var `deinit`: String { __data["deinit"] } public var `enum`: String { __data["enum"] } public var `extension`: String { __data["extension"] } @@ -2829,232 +3429,964 @@ class SelectionSetTemplateTests: XCTestCase { // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine( + expected, + atLine: 11 + allAnimals.selectionSet.selections.direct!.fields.count, + ignoringExtraLines: true) + ) + } + + func test__render_fieldAccessors__givenEntityFieldWithUnderscorePrefixedName_rendersFieldWithTypeFirstUppercased() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + _oneUnderscore: Animal! + __twoUnderscore: Animal! + species: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + _oneUnderscore { + species + } + __twoUnderscore { + species + } + } + } + """ + + let expected = """ + public var _oneUnderscore: _OneUnderscore { __data["_oneUnderscore"] } + public var __twoUnderscore: __TwoUnderscore { __data["__twoUnderscore"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine( + expected, + atLine: 11 + allAnimals.selectionSet.selections.direct!.fields.count, + ignoringExtraLines: true) + ) + } + + func test__render_fieldAccessors__givenEntityFieldWithSwiftKeywordAndApolloReservedTypeNames_rendersFieldAccessorWithTypeNameSuffixed() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + type Animal { + self: Animal! + parentType: Animal! + dataDict: Animal! + selection: Animal! + schema: Animal! + fragmentContainer: Animal! + string: Animal! + bool: Animal! + int: Animal! + float: Animal! + double: Animal! + iD: Animal! + any: Animal! + protocol: Animal! + type: Animal! + species: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + self { + species + } + parentType { + species + } + dataDict { + species + } + selection { + species + } + schema { + species + } + fragmentContainer { + species + } + string { + species + } + bool { + species + } + int { + species + } + float { + species + } + double { + species + } + iD { + species + } + any { + species + } + protocol { + species + } + type { + species + } + } + } + """ + + let expected = """ + public var `self`: Self_SelectionSet { __data["self"] } + public var parentType: ParentType_SelectionSet { __data["parentType"] } + public var dataDict: DataDict_SelectionSet { __data["dataDict"] } + public var selection: Selection_SelectionSet { __data["selection"] } + public var schema: Schema_SelectionSet { __data["schema"] } + public var fragmentContainer: FragmentContainer_SelectionSet { __data["fragmentContainer"] } + public var string: String_SelectionSet { __data["string"] } + public var bool: Bool_SelectionSet { __data["bool"] } + public var int: Int_SelectionSet { __data["int"] } + public var float: Float_SelectionSet { __data["float"] } + public var double: Double_SelectionSet { __data["double"] } + public var iD: ID_SelectionSet { __data["iD"] } + public var any: Any_SelectionSet { __data["any"] } + public var `protocol`: Protocol_SelectionSet { __data["protocol"] } + public var type: Type_SelectionSet { __data["type"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine( + expected, + atLine: 11 + allAnimals.selectionSet.selections.direct!.fields.count, + ignoringExtraLines: true) + ) + } + + // MARK: Field Accessors - Entity + + func test__render_fieldAccessors__givenDirectEntityField_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + lowercaseType: lowercaseType! + } + + type lowercaseType { + a: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + lowercaseType { + a + } + } + } + """ + + let expected = """ + public var predator: Predator { __data["predator"] } + public var lowercaseType: LowercaseType { __data["lowercaseType"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenDirectEntityFieldWithAlias_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + } + """ + + document = """ + query TestOperation { + allAnimals { + aliasedPredator: predator { + species + } + } + } + """ + + let expected = """ + public var aliasedPredator: AliasedPredator { __data["aliasedPredator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenDirectEntityFieldAsOptional_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + } + } + """ + + let expected = """ + public var predator: Predator? { __data["predator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenDirectEntityFieldAsList_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predators: [Animal!] + } + """ + + document = """ + query TestOperation { + allAnimals { + predators { + species + } + } + } + """ + + let expected = """ + public var predators: [Predator]? { __data["predators"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldWithDirectSelectionsAndMergedFromFragment_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + name: String! + predator: Animal! + } + """ + + document = """ + query TestOperation { + allAnimals { + ...PredatorDetails + predator { + name + } + } + } + + fragment PredatorDetails on Animal { + predator { + species + } + } + """ + + let expected = """ + public var predator: Predator { __data["predator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + } + + // MARK: Field Accessors - Merged Fragment + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragment_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + } + """ + + document = """ + query TestOperation { + allAnimals { + ...PredatorDetails + } + } + + fragment PredatorDetails on Animal { + predator { + species + } + } + """ + + let expected = """ + public var predator: PredatorDetails.Predator { __data["predator"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentEntityNestedInEntity_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + ...PredatorDetails + } + } + + fragment PredatorDetails on Animal { + predator { + height { + feet + } + } + } + """ + + let expected = """ + public var species: String { __data["species"] } + public var height: PredatorDetails.Predator.Height { __data["height"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals_predator) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentInTypeCaseWithEntityNestedInEntity_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + interface Pet { + predator: Animal! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + ...PredatorDetails + } + } + + fragment PredatorDetails on Pet { + predator { + height { + feet + } + } + } + """ + + let expected = """ + public var height: PredatorDetails.Predator.Height { __data["height"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asPet_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Pet"]?[field: "predator"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals_asPet_predator) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 9, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromTypeCaseInFragment_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + interface Pet { + height: Height! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + ...PredatorDetails + } + } + } + + fragment PredatorDetails on Animal { + ... on Pet { + height { + feet + } + } + } + """ + + let predator_expected = """ + public var species: String { __data["species"] } + + """ + + let predator_asPet_expected = """ + public var species: String { __data["species"] } + public var height: PredatorDetails.AsPet.Height { __data["height"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + ) + + let allAnimals_predator_asPet = try XCTUnwrap(allAnimals_predator[as: "Pet"]) + + let allAnimals_predator_actual = subject.render(field: allAnimals_predator) + let allAnimals_predator_asPet_actual = subject.render(inlineFragment: allAnimals_predator_asPet) + + // then + expect(allAnimals_predator_actual).to(equalLineByLine(predator_expected, atLine: 13, ignoringExtraLines: true)) + expect(allAnimals_predator_asPet_actual).to(equalLineByLine(predator_asPet_expected, atLine: 13, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentWithEntityNestedInEntityTypeCase_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + height: Height! + } + + interface Pet { + height: Height! + } + + type Height { + feet: Int! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + species + } + ...PredatorDetails + } + } + + fragment PredatorDetails on Animal { + predator { + ... on Pet { + height { + feet + } + } + } + } + """ + + let predator_expected = """ + public var species: String { __data["species"] } + + """ + + let predator_asPet_expected = """ + public var species: String { __data["species"] } + public var height: PredatorDetails.Predator.AsPet.Height { __data["height"] } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + ) + + let allAnimals_predator_asPet = try XCTUnwrap(allAnimals_predator[as: "Pet"]) + + let allAnimals_predator_actual = subject.render(field: allAnimals_predator) + let allAnimals_predator_asPet_actual = subject.render(inlineFragment: allAnimals_predator_asPet) + + // then + expect(allAnimals_predator_actual).to(equalLineByLine(predator_expected, atLine: 12, ignoringExtraLines: true)) + expect(allAnimals_predator_asPet_actual).to(equalLineByLine(predator_asPet_expected, atLine: 13, ignoringExtraLines: true)) + } + + func test__render_fieldAccessors__givenTypeCaseMergedFromFragmentWithOtherMergedFields_rendersFieldAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + } + + interface Pet { + favoriteToy: Item + } + + type Item { + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { + ...PredatorDetails + species + } + } + } + + fragment PredatorDetails on Animal { + ... on Pet { + favoriteToy { + ...PetToy + } + } + } + + fragment PetToy on Item { + name + } + """ + + let predator_expected = """ + public var asPet: AsPet? { _asInlineFragment() } + """ + + // when + try buildSubjectAndOperation() + let predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals) + let predator_actual = subject.render(field: predator) // then - expect(actual).to(equalLineByLine( - expected, - atLine: 11 + allAnimals.selectionSet.selections.direct!.fields.count, - ignoringExtraLines: true) - ) + expect(predator_actual) + .to(equalLineByLine(predator_expected, atLine: 15, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldWithUnderscorePrefixedName_rendersFieldWithTypeFirstUppercased() throws { + func test__render_fieldAccessors__givenTypeCaseMergedFromFragmentWithNoOtherMergedFields_rendersFieldAccessor() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - _oneUnderscore: Animal! - __twoUnderscore: Animal! + interface Animal { species: String! + predator: Animal! + } + + interface Pet { + favoriteToy: Item + } + + type Item { + name: String! } """ document = """ query TestOperation { allAnimals { - _oneUnderscore { - species + predator { + ...PredatorDetails } - __twoUnderscore { - species + } + } + + fragment PredatorDetails on Animal { + ... on Pet { + favoriteToy { + ...PetToy } } } + + fragment PetToy on Item { + name + } """ - let expected = """ - public var _oneUnderscore: _OneUnderscore { __data["_oneUnderscore"] } - public var __twoUnderscore: __TwoUnderscore { __data["__twoUnderscore"] } + let predator_expected = """ + public var asPet: AsPet? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals) + let predator_actual = subject.render(field: predator) // then - expect(actual).to(equalLineByLine( - expected, - atLine: 11 + allAnimals.selectionSet.selections.direct!.fields.count, - ignoringExtraLines: true) - ) + expect(predator_actual) + .to(equalLineByLine(predator_expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldWithSwiftKeywordAndApolloReservedTypeNames_rendersFieldAccessorWithTypeNameSuffixed() throws { + func test__render_fieldAccessors__givenEntityFieldMergedAsRootOfNestedFragment_rendersFieldAccessor() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - self: Animal! - parentType: Animal! - dataDict: Animal! - selection: Animal! - schema: Animal! - fragmentContainer: Animal! - string: Animal! - bool: Animal! - int: Animal! - float: Animal! - double: Animal! - iD: Animal! - any: Animal! - protocol: Animal! - type: Animal! + interface Animal { species: String! + predator: Animal! + } + + interface Pet { + favoriteToy: Item + } + + type Item { + name: String! } """ document = """ query TestOperation { allAnimals { - self { - species - } - parentType { - species - } - dataDict { - species - } - selection { - species - } - schema { - species - } - fragmentContainer { - species - } - string { - species - } - bool { - species - } - int { - species - } - float { - species - } - double { - species - } - iD { - species + predator { + ...PredatorDetails } - any { - species + } + } + + fragment PredatorDetails on Animal { + ... on Pet { + favoriteToy { + ...PetToy } - protocol { + } + } + + fragment PetToy on Item { + name + } + """ + + let predator_asPet_expected = """ + public var favoriteToy: FavoriteToy? { __data["favoriteToy"] } + """ + + // when + try buildSubjectAndOperation() + let predator_asPet = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "predator"]?[as: "Pet"] + ) + + let predator_asPet_actual = subject.render(inlineFragment: predator_asPet) + + // then + expect(predator_asPet_actual) + .to(equalLineByLine(predator_asPet_expected, atLine: 13, ignoringExtraLines: true)) + } + + // MARK: Field Accessors - Merged From Parent + + func test__render_fieldAccessors__givenEntityFieldMergedFromParent_notOperationRoot_rendersFieldAccessorWithNameNotIncludingParent() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String! + predator: Animal! + } + + type Dog implements Animal { + species: String! + predator: Animal! + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + predator { species } - type { - species + ... on Dog { + name } } } """ let expected = """ - public var `self`: Self_SelectionSet { __data["self"] } - public var parentType: ParentType_SelectionSet { __data["parentType"] } - public var dataDict: DataDict_SelectionSet { __data["dataDict"] } - public var selection: Selection_SelectionSet { __data["selection"] } - public var schema: Schema_SelectionSet { __data["schema"] } - public var fragmentContainer: FragmentContainer_SelectionSet { __data["fragmentContainer"] } - public var string: String_SelectionSet { __data["string"] } - public var bool: Bool_SelectionSet { __data["bool"] } - public var int: Int_SelectionSet { __data["int"] } - public var float: Float_SelectionSet { __data["float"] } - public var double: Double_SelectionSet { __data["double"] } - public var iD: ID_SelectionSet { __data["iD"] } - public var any: Any_SelectionSet { __data["any"] } - public var `protocol`: Protocol_SelectionSet { __data["protocol"] } - public var type: Type_SelectionSet { __data["type"] } + public var name: String { __data["name"] } + public var predator: Predator { __data["predator"] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine( - expected, - atLine: 11 + allAnimals.selectionSet.selections.direct!.fields.count, - ignoringExtraLines: true) - ) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - // MARK: Field Accessors - Entity - - func test__render_fieldAccessors__givenDirectEntityField_rendersFieldAccessor() throws { + func test__render_fieldAccessors__givenEntityFieldMergedFromParent_atOperationRoot_rendersFieldAccessorWithFullyQualifiedName() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - lowercaseType: lowercaseType! + type AdminQuery { + name: String! } - type lowercaseType { - a: String! + interface Animal { + species: String! } """ document = """ query TestOperation { allAnimals { - predator { - species - } - lowercaseType { - a - } + species + } + ... on AdminQuery { + name } } """ let expected = """ - public var predator: Predator { __data["predator"] } - public var lowercaseType: LowercaseType { __data["lowercaseType"] } + public var name: String { __data["name"] } + public var allAnimals: [TestOperationQuery.Data.AllAnimal]? { __data["allAnimals"] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let query_asAdminQuery = try XCTUnwrap( + operation[field: "query"]?[as: "AdminQuery"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: query_asAdminQuery) // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenDirectEntityFieldWithAlias_rendersFieldAccessor() throws { + func test__render_fieldAccessors__givenEntityFieldMergedFromSiblingTypeCase_notOperationRoot_rendersFieldAccessorWithNameNotIncludingSharedParent() throws { // given schemaSDL = """ type Query { @@ -3065,74 +4397,103 @@ class SelectionSetTemplateTests: XCTestCase { species: String! predator: Animal! } + + interface Pet implements Animal { + species: String! + predator: Animal! + } + + type Dog implements Animal & Pet { + species: String! + predator: Animal! + name: String! + } """ document = """ query TestOperation { allAnimals { - aliasedPredator: predator { - species + ... on Pet { + predator { + species + } + } + ... on Dog { + name } } } """ let expected = """ - public var aliasedPredator: AliasedPredator { __data["aliasedPredator"] } + public var name: String { __data["name"] } + public var predator: AsPet.Predator { __data["predator"] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenDirectEntityFieldAsOptional_rendersFieldAccessor() throws { + func test__render_fieldAccessors__givenEntityFieldMergedFromSiblingTypeCase_atOperationRoot_rendersFieldAccessorWithFullyQualifiedName() throws { // given schemaSDL = """ type Query { + role: String! + } + + type AdminQuery implements ModeratorQuery { + name: String! + allAnimals: [Animal!] + } + + interface ModeratorQuery { allAnimals: [Animal!] } interface Animal { species: String! - predator: Animal } """ document = """ query TestOperation { - allAnimals { - predator { + ... on ModeratorQuery { + allAnimals { species } } + ... on AdminQuery { + name + } } """ let expected = """ - public var predator: Predator? { __data["predator"] } + public var name: String { __data["name"] } + public var allAnimals: [TestOperationQuery.Data.AsModeratorQuery.AllAnimal]? { __data["allAnimals"] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let query_asAdminQuery = try XCTUnwrap( + operation[field: "query"]?[as: "AdminQuery"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: query_asAdminQuery) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenDirectEntityFieldAsList_rendersFieldAccessor() throws { + func test__render_fieldAccessors__givenEntityFieldNestedInEntityFieldMergedFromParent_rendersFieldAccessorWithCorrectName() throws { // given schemaSDL = """ type Query { @@ -3141,37 +4502,57 @@ class SelectionSetTemplateTests: XCTestCase { interface Animal { species: String! - predators: [Animal!] + predator: Animal! + height: Height! + } + + type Dog implements Animal { + name: String! + species: String! + predator: Animal! + height: Height! + } + + type Height { + feet: Int! } """ document = """ query TestOperation { allAnimals { - predators { - species + predator { + height { + feet + } + } + ... on Dog { + predator { + species + } } } } """ let expected = """ - public var predators: [Predator]? { __data["predators"] } + public var species: String { __data["species"] } + public var height: AllAnimal.Predator.Height { __data["height"] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(field: allAnimals_asDog_predator) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldWithDirectSelectionsAndMergedFromFragment_rendersFieldAccessor() throws { + func test__render_fieldAccessors__givenEntityFieldNestedInEntityFieldInMatchingTypeCaseMergedFromParent_rendersFieldAccessorWithCorrectName() throws { // given schemaSDL = """ type Query { @@ -3180,75 +4561,88 @@ class SelectionSetTemplateTests: XCTestCase { interface Animal { species: String! + predator: Animal! + height: Height! + } + + interface Pet implements Animal { + species: String! + predator: Animal! + height: Height! + } + + type Dog implements Animal & Pet { name: String! + species: String! predator: Animal! + height: Height! + } + + type Height { + feet: Int! } """ document = """ query TestOperation { allAnimals { - ...PredatorDetails - predator { - name + ... on Pet { + predator { + height { + feet + } + } + } + ... on Dog { + predator { + species + } } - } - } - - fragment PredatorDetails on Animal { - predator { - species } } """ let expected = """ - public var predator: Predator { __data["predator"] } + public var species: String { __data["species"] } + public var height: AllAnimal.AsPet.Predator.Height { __data["height"] } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(field: allAnimals_asDog_predator) // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - // MARK: Field Accessors - Merged Fragment + // MARK: Field Accessors - Include/Skip - func test__render_fieldAccessors__givenEntityFieldMergedFromFragment_rendersFieldAccessor() throws { + func test__render_fieldAccessor__givenNonNullFieldWithIncludeCondition_rendersAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! + type Animal { + fieldName: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - ...PredatorDetails - } - } - - fragment PredatorDetails on Animal { - predator { - species + fieldName @include(if: $a) } } """ let expected = """ - public var predator: PredatorDetails.Predator { __data["predator"] } + public var fieldName: String? { __data["fieldName"] } """ // when @@ -3263,61 +4657,43 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentEntityNestedInEntity_rendersFieldAccessor() throws { + func test__render_fieldAccessor__givenNonNullFieldWithSkipCondition_rendersAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - height: Height! - } - - type Height { - feet: Int! + type Animal { + fieldName: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - predator { - species - } - ...PredatorDetails - } - } - - fragment PredatorDetails on Animal { - predator { - height { - feet - } + fieldName @skip(if: $a) } } """ let expected = """ - public var species: String { __data["species"] } - public var height: PredatorDetails.Predator.Height { __data["height"] } + public var fieldName: String? { __data["fieldName"] } """ // when try buildSubjectAndOperation() - let allAnimals_predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals_predator) + let actual = subject.render(field: allAnimals) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentInTypeCaseWithEntityNestedInEntity_rendersFieldAccessor() throws { + func test__render_fieldAccessors__givenEntityFieldMergedFromParentWithInclusionCondition_rendersFieldAccessorAsOptional() throws { // given schemaSDL = """ type Query { @@ -3327,311 +4703,228 @@ class SelectionSetTemplateTests: XCTestCase { interface Animal { species: String! predator: Animal! - height: Height! } - interface Pet { + type Dog implements Animal { + species: String! predator: Animal! - } - - type Height { - feet: Int! + name: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - predator { + predator @include(if: $a) { species } - ...PredatorDetails - } - } - - fragment PredatorDetails on Pet { - predator { - height { - feet + ... on Dog { + name } } } """ let expected = """ - public var height: PredatorDetails.Predator.Height { __data["height"] } + public var name: String { __data["name"] } + public var predator: Predator? { __data["predator"] } """ // when try buildSubjectAndOperation() - let allAnimals_asPet_predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Pet"]?[field: "predator"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals_asPet_predator) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 9, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromTypeCaseInFragment_rendersFieldAccessor() throws { + func test__render_fieldAccessor__givenNonNullFieldMergedFromParentWithIncludeConditionThatMatchesScope_rendersAsNotOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - height: Height! - } - - interface Pet { - height: Height! - } - - type Height { - feet: Int! + type Animal { + fieldName: String! + a: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - predator { - species - ...PredatorDetails - } - } - } - - fragment PredatorDetails on Animal { - ... on Pet { - height { - feet + fieldName @include(if: $a) + ... @include(if: $a) { + a } } } """ - let predator_expected = """ - public var species: String { __data["species"] } - - """ - - let predator_asPet_expected = """ - public var species: String { __data["species"] } - public var height: PredatorDetails.AsPet.Height { __data["height"] } + let expected = """ + public var a: String { __data["a"] } + public var fieldName: String { __data["fieldName"] } """ // when try buildSubjectAndOperation() - let allAnimals_predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + let allAnimals_ifA = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[if: "a"] ) - let allAnimals_predator_asPet = try XCTUnwrap(allAnimals_predator[as: "Pet"]) - - let allAnimals_predator_actual = subject.render(field: allAnimals_predator) - let allAnimals_predator_asPet_actual = subject.render(inlineFragment: allAnimals_predator_asPet) + let actual = subject.render(inlineFragment: allAnimals_ifA) // then - expect(allAnimals_predator_actual).to(equalLineByLine(predator_expected, atLine: 13, ignoringExtraLines: true)) - expect(allAnimals_predator_asPet_actual).to(equalLineByLine(predator_asPet_expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromFragmentWithEntityNestedInEntityTypeCase_rendersFieldAccessor() throws { + func test__render_fieldAccessor__givenNonNullFieldWithIncludeConditionThatMatchesScope_rendersAsNotOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - height: Height! - } - - interface Pet { - height: Height! - } - - type Height { - feet: Int! + type Animal { + fieldName: String! } """ document = """ - query TestOperation { - allAnimals { - predator { - species - } - ...PredatorDetails - } - } - - fragment PredatorDetails on Animal { - predator { - ... on Pet { - height { - feet - } - } + query TestOperation($a: Boolean!) { + allAnimals @include(if: $a) { + fieldName @include(if: $a) } } """ - let predator_expected = """ - public var species: String { __data["species"] } - - """ - - let predator_asPet_expected = """ - public var species: String { __data["species"] } - public var height: PredatorDetails.Predator.AsPet.Height { __data["height"] } + let expected = """ + public var fieldName: String { __data["fieldName"] } """ // when try buildSubjectAndOperation() - let allAnimals_predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let allAnimals_predator_asPet = try XCTUnwrap(allAnimals_predator[as: "Pet"]) - - let allAnimals_predator_actual = subject.render(field: allAnimals_predator) - let allAnimals_predator_asPet_actual = subject.render(inlineFragment: allAnimals_predator_asPet) + let actual = subject.render(field: allAnimals) // then - expect(allAnimals_predator_actual).to(equalLineByLine(predator_expected, atLine: 12, ignoringExtraLines: true)) - expect(allAnimals_predator_asPet_actual).to(equalLineByLine(predator_asPet_expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenTypeCaseMergedFromFragmentWithOtherMergedFields_rendersFieldAccessor() throws { + func test__render_fieldAccessor__givenNonNullFieldMergedFromNestedEntityInNamedFragmentWithIncludeCondition_doesNotRenderField() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - } - - interface Pet { - favoriteToy: Item + type Animal { + child: Child! } - type Item { - name: String! + type Child { + a: String! + b: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - predator { - ...PredatorDetails - species + ...ChildFragment @include(if: $a) + child { + a } } } - fragment PredatorDetails on Animal { - ... on Pet { - favoriteToy { - ...PetToy - } + fragment ChildFragment on Animal { + child { + b } } - - fragment PetToy on Item { - name - } """ - let predator_expected = """ - public var asPet: AsPet? { _asInlineFragment() } + let expected = """ + public var a: String { __data["a"] } """ // when try buildSubjectAndOperation() - let predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + let allAnimals_child = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[field: "child"] as? IR.EntityField ) - let predator_actual = subject.render(field: predator) + let actual = subject.render(field: allAnimals_child) // then - expect(predator_actual) - .to(equalLineByLine(predator_expected, atLine: 15, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenTypeCaseMergedFromFragmentWithNoOtherMergedFields_rendersFieldAccessor() throws { + func test__render_fieldAccessor__givenNonNullFieldMergedFromNestedEntityInNamedFragmentWithIncludeCondition_inConditionalFragment_rendersFieldAsNonOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - } - - interface Pet { - favoriteToy: Item + type Animal { + child: Child! } - type Item { - name: String! + type Child { + a: String! + b: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - predator { - ...PredatorDetails + ...ChildFragment @include(if: $a) + child { + a } } } - fragment PredatorDetails on Animal { - ... on Pet { - favoriteToy { - ...PetToy - } + fragment ChildFragment on Animal { + child { + b } } - - fragment PetToy on Item { - name - } """ - let predator_expected = """ - public var asPet: AsPet? { _asInlineFragment() } + let expected = """ + public var a: String { __data["a"] } + public var b: String { __data["b"] } """ // when try buildSubjectAndOperation() - let predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "predator"] as? IR.EntityField + let allAnimals_child = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[if: "a"]?[field: "child"] as? IR.EntityField ) - let predator_actual = subject.render(field: predator) + let actual = subject.render(field: allAnimals_child) // then - expect(predator_actual) - .to(equalLineByLine(predator_expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedAsRootOfNestedFragment_rendersFieldAccessor() throws { + + + // MARK: - Inline Fragment Accessors + + func test__render_inlineFragmentAccessors__givenDirectTypeCases_rendersTypeCaseAccessorWithCorrectName() throws { // given schemaSDL = """ type Query { @@ -3643,11 +4936,15 @@ class SelectionSetTemplateTests: XCTestCase { predator: Animal! } - interface Pet { - favoriteToy: Item + interface Pet implements Animal { + species: String! + predator: Animal! + name: String! } - type Item { + type Dog implements Animal & Pet { + species: String! + predator: Animal! name: String! } """ @@ -3655,45 +4952,35 @@ class SelectionSetTemplateTests: XCTestCase { document = """ query TestOperation { allAnimals { - predator { - ...PredatorDetails + species + ... on Pet { + name } - } - } - - fragment PredatorDetails on Animal { - ... on Pet { - favoriteToy { - ...PetToy + ... on Dog { + name } } } - - fragment PetToy on Item { - name - } """ - let predator_asPet_expected = """ - public var favoriteToy: FavoriteToy? { __data["favoriteToy"] } + let expected = """ + public var asPet: AsPet? { _asInlineFragment() } + public var asDog: AsDog? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let predator_asPet = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "predator"]?[as: "Pet"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let predator_asPet_actual = subject.render(inlineFragment: predator_asPet) + let actual = subject.render(field: allAnimals) // then - expect(predator_asPet_actual) - .to(equalLineByLine(predator_asPet_expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) } - // MARK: Field Accessors - Merged From Parent - - func test__render_fieldAccessors__givenEntityFieldMergedFromParent_notOperationRoot_rendersFieldAccessorWithNameNotIncludingParent() throws { + func test__render_inlineFragmentAccessors__givenMergedTypeCasesFromSingleMergedTypeCaseSource_rendersTypeCaseAccessorWithCorrectName() throws { // given schemaSDL = """ type Query { @@ -3705,7 +4992,13 @@ class SelectionSetTemplateTests: XCTestCase { predator: Animal! } - type Dog implements Animal { + interface Pet implements Animal { + species: String! + predator: Animal! + name: String! + } + + type Dog implements Animal & Pet { species: String! predator: Animal! name: String! @@ -3715,334 +5008,285 @@ class SelectionSetTemplateTests: XCTestCase { document = """ query TestOperation { allAnimals { + species predator { - species + ... on Pet { + name + } } ... on Dog { name + predator { + species + } } } } """ let expected = """ - public var name: String { __data["name"] } - public var predator: Predator { __data["predator"] } + public var asPet: AsPet? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + let allAnimals_asDog_predator = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: allAnimals_asDog) + let actual = subject.render(field: allAnimals_asDog_predator) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromParent_atOperationRoot_rendersFieldAccessorWithFullyQualifiedName() throws { + // MARK: Inline Fragment Accessors - Include/Skip + + func test__render_inlineFragmentAccessors__givenInlineFragmentOnDifferentTypeWithCondition_renders() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type AdminQuery { - name: String! + type Animal { + fieldA: String! } - interface Animal { - species: String! + interface Pet { + fieldA: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - species - } - ... on AdminQuery { - name + ... on Pet @include(if: $a) { + fieldA + } } } """ let expected = """ - public var name: String { __data["name"] } - public var allAnimals: [TestOperationQuery.Data.AllAnimal]? { __data["allAnimals"] } + public var asPetIfA: AsPetIfA? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let query_asAdminQuery = try XCTUnwrap( - operation[field: "query"]?[as: "AdminQuery"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: query_asAdminQuery) + let actual = subject.render(field: allAnimals) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromSiblingTypeCase_notOperationRoot_rendersFieldAccessorWithNameNotIncludingSharedParent() throws { + func test__render_inlineFragmentAccessors__givenInlineFragmentOnDifferentTypeWithSkipCondition_renders() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - } - - interface Pet implements Animal { - species: String! - predator: Animal! + type Animal { + fieldA: String! } - type Dog implements Animal & Pet { - species: String! - predator: Animal! - name: String! + interface Pet { + fieldA: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - ... on Pet { - predator { - species - } - } - ... on Dog { - name + ... on Pet @skip(if: $a) { + fieldA } } } """ let expected = """ - public var name: String { __data["name"] } - public var predator: AsPet.Predator { __data["predator"] } + public var asPetIfNotA: AsPetIfNotA? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: allAnimals_asDog) + let actual = subject.render(field: allAnimals) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromSiblingTypeCase_atOperationRoot_rendersFieldAccessorWithFullyQualifiedName() throws { + func test__render_inlineFragmentAccessors__givenInlineFragmentOnDifferentTypeWithMultipleConditions_renders() throws { // given schemaSDL = """ type Query { - role: String! - } - - type AdminQuery implements ModeratorQuery { - name: String! allAnimals: [Animal!] } - interface ModeratorQuery { - allAnimals: [Animal!] + type Animal { + fieldA: String! } - interface Animal { - species: String! + interface Pet { + fieldA: String! } """ document = """ - query TestOperation { - ... on ModeratorQuery { - allAnimals { - species + query TestOperation($a: Boolean!) { + allAnimals { + ... on Pet @include(if: $a) @skip(if: $b) { + fieldA } } - ... on AdminQuery { - name - } } """ let expected = """ - public var name: String { __data["name"] } - public var allAnimals: [TestOperationQuery.Data.AsModeratorQuery.AllAnimal]? { __data["allAnimals"] } + public var asPetIfAAndNotB: AsPetIfAAndNotB? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let query_asAdminQuery = try XCTUnwrap( - operation[field: "query"]?[as: "AdminQuery"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: query_asAdminQuery) + let actual = subject.render(field: allAnimals) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldNestedInEntityFieldMergedFromParent_rendersFieldAccessorWithCorrectName() throws { + func test__render_inlineFragmentAccessors__givenInlineFragmentOnSameTypeWithMultipleConditions_renders() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - height: Height! - } - - type Dog implements Animal { - name: String! - species: String! - predator: Animal! - height: Height! + type Animal { + fieldA: String! } - type Height { - feet: Int! + interface Pet { + fieldA: String! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - predator { - height { - feet - } - } - ... on Dog { - predator { - species - } + ... on Animal @include(if: $a) @skip(if: $b) { + fieldA } } } """ let expected = """ - public var species: String { __data["species"] } - public var height: AllAnimal.Predator.Height { __data["height"] } + public var ifAAndNotB: IfAAndNotB? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let allAnimals_asDog_predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] as? IR.EntityField + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals_asDog_predator) + let actual = subject.render(field: allAnimals) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldNestedInEntityFieldInMatchingTypeCaseMergedFromParent_rendersFieldAccessorWithCorrectName() throws { + func test__render_inlineFragmentAccessor__givenNamedFragmentMatchingParentTypeWithInclusionCondition_renders() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - interface Animal { - species: String! - predator: Animal! - height: Height! - } - - interface Pet implements Animal { - species: String! - predator: Animal! - height: Height! - } - - type Dog implements Animal & Pet { - name: String! - species: String! - predator: Animal! - height: Height! - } - - type Height { - feet: Int! + type Animal { + string: String! + int: Int! } """ document = """ - query TestOperation { + query TestOperation($a: Boolean!) { allAnimals { - ... on Pet { - predator { - height { - feet - } - } - } - ... on Dog { - predator { - species - } - } + ...FragmentA @include(if: $a) } } + + fragment FragmentA on Animal { + int + } """ let expected = """ - public var species: String { __data["species"] } - public var height: AllAnimal.AsPet.Predator.Height { __data["height"] } + public var ifA: IfA? { _asInlineFragment() } """ // when try buildSubjectAndOperation() - let allAnimals_asDog_predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] as? IR.EntityField + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals_asDog_predator) + let actual = subject.render(field: allAnimals) // then expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - // MARK: Field Accessors - Include/Skip - - func test__render_fieldAccessor__givenNonNullFieldWithIncludeCondition_rendersAsOptional() throws { + func test__render_inlineFragmentAccessor__givenInlineFragmentAndNamedFragmentOnSameTypeWithInclusionCondition_rendersBothInlineFragments() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldName: String! + interface Animal { + string: String! + int: Int! + } + + type Bird implements Animal { + string: String! + int: Int! } """ document = """ query TestOperation($a: Boolean!) { allAnimals { - fieldName @include(if: $a) + ... on Bird { + string + } + ...FragmentA @include(if: $a) } } + + fragment FragmentA on Bird { + int + } """ let expected = """ - public var fieldName: String? { __data["fieldName"] } + public var asBird: AsBird? { _asInlineFragment() } + public var asBirdIfA: AsBirdIfA? { _asInlineFragment() } """ // when @@ -4054,10 +5298,12 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) } - func test__render_fieldAccessor__givenNonNullFieldWithSkipCondition_rendersAsOptional() throws { + // MARK: - Fragment Accessors + + func test__render_fragmentAccessor__givenFragments_rendersFragmentAccessor() throws { // given schemaSDL = """ type Query { @@ -4065,20 +5311,36 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - fieldName: String! + string: String! + int: Int! } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - fieldName @skip(if: $a) + ...FragmentA + ...lowercaseFragment } } + + fragment FragmentA on Animal { + int + } + + fragment lowercaseFragment on Animal { + string + } """ let expected = """ - public var fieldName: String? { __data["fieldName"] } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fragmentA: FragmentA { _toFragment() } + public var lowercaseFragment: LowercaseFragment { _toFragment() } + } """ // when @@ -4090,10 +5352,10 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) } - func test__render_fieldAccessors__givenEntityFieldMergedFromParentWithInclusionCondition_rendersFieldAccessorAsOptional() throws { + func test__render_fragmentAccessor__givenInheritedFragmentFromParent_rendersFragmentAccessor() throws { // given schemaSDL = """ type Query { @@ -4101,48 +5363,59 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - species: String! - predator: Animal! + string: String! + int: Int! } - type Dog implements Animal { - species: String! - predator: Animal! - name: String! + type Cat implements Animal { + string: String! + int: Int! } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - predator @include(if: $a) { - species - } - ... on Dog { - name + ...FragmentA + ... on Cat { + string } } } + + fragment FragmentA on Animal { + int + } + + fragment lowercaseFragment on Animal { + string + } """ let expected = """ - public var name: String { __data["name"] } - public var predator: Predator? { __data["predator"] } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fragmentA: FragmentA { _toFragment() } + } """ // when try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + let allAnimals_asCat = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Cat"] ) - let actual = subject.render(inlineFragment: allAnimals_asDog) + let actual = subject.render(inlineFragment: allAnimals_asCat) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) } - func test__render_fieldAccessor__givenNonNullFieldMergedFromParentWithIncludeConditionThatMatchesScope_rendersAsNotOptional() throws { + // MARK: - Fragment Accessors - Include Skip + + func test__render_fragmentAccessor__givenFragmentOnSameTypeWithInclusionCondition_rendersFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { @@ -4150,40 +5423,51 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - fieldName: String! - a: String! + string: String! + int: Int! } """ document = """ query TestOperation($a: Boolean!) { allAnimals { - fieldName @include(if: $a) - ... @include(if: $a) { - a - } + ...FragmentA @include(if: $a) + ...lowercaseFragment } } + + fragment FragmentA on Animal { + int + } + + fragment lowercaseFragment on Animal { + string + } """ let expected = """ - public var a: String { __data["a"] } - public var fieldName: String { __data["fieldName"] } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var lowercaseFragment: LowercaseFragment { _toFragment() } + public var fragmentA: FragmentA? { _toFragment() } + } """ // when try buildSubjectAndOperation() - let allAnimals_ifA = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[if: "a"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: allAnimals_ifA) + let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } - func test__render_fieldAccessor__givenNonNullFieldWithIncludeConditionThatMatchesScope_rendersAsNotOptional() throws { + func test__render_fragmentAccessor__givenFragmentOnSameTypeWithInclusionConditionThatMatchesScope_rendersFragmentAccessorAsNotOptional() throws { // given schemaSDL = """ type Query { @@ -4191,20 +5475,30 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - fieldName: String! + string: String! + int: Int! } """ document = """ query TestOperation($a: Boolean!) { allAnimals @include(if: $a) { - fieldName @include(if: $a) + ...FragmentA @include(if: $a) } } + + fragment FragmentA on Animal { + int + } """ let expected = """ - public var fieldName: String { __data["fieldName"] } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fragmentA: FragmentA { _toFragment() } + } """ // when @@ -4219,7 +5513,7 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fieldAccessor__givenNonNullFieldMergedFromNestedEntityInNamedFragmentWithIncludeCondition_doesNotRenderField() throws { + func test__render_fragmentAccessor__givenFragmentOnSameTypeWithInclusionConditionThatPartiallyMatchesScope_rendersFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { @@ -4227,49 +5521,45 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - child: Child! - } - - type Child { - a: String! - b: String! + string: String! + int: Int! } """ document = """ - query TestOperation($a: Boolean!) { - allAnimals { - ...ChildFragment @include(if: $a) - child { - a - } + query TestOperation($a: Boolean!, $b: Boolean!) { + allAnimals @include(if: $a) { + ...FragmentA @include(if: $a) @include(if: $b) } } - fragment ChildFragment on Animal { - child { - b - } + fragment FragmentA on Animal { + int } """ let expected = """ - public var a: String { __data["a"] } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fragmentA: FragmentA? { _toFragment() } + } """ // when try buildSubjectAndOperation() - let allAnimals_child = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[field: "child"] as? IR.EntityField + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals_child) + let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - func test__render_fieldAccessor__givenNonNullFieldMergedFromNestedEntityInNamedFragmentWithIncludeCondition_inConditionalFragment_rendersFieldAsNonOptional() throws { + func test__render_fragmentAccessor__givenFragmentMergedFromParent_withInclusionConditionThatMatchesScope_rendersFragmentAccessorAsNotOptional() throws { // given schemaSDL = """ type Query { @@ -4277,54 +5567,47 @@ class SelectionSetTemplateTests: XCTestCase { } type Animal { - child: Child! - } - - type Child { - a: String! - b: String! + string: String! + int: Int! } """ document = """ query TestOperation($a: Boolean!) { allAnimals { - ...ChildFragment @include(if: $a) - child { - a - } + ...FragmentA @include(if: $a) } } - fragment ChildFragment on Animal { - child { - b - } + fragment FragmentA on Animal { + int } """ let expected = """ - public var a: String { __data["a"] } - public var b: String { __data["b"] } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var fragmentA: FragmentA { _toFragment() } + } """ // when try buildSubjectAndOperation() - let allAnimals_child = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[if: "a"]?[field: "child"] as? IR.EntityField + let allAnimals_ifA = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[if: "a"] ) - let actual = subject.render(field: allAnimals_child) + let actual = subject.render(inlineFragment: allAnimals_ifA) // then - expect(actual).to(equalLineByLine(expected, atLine: 8, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - - - // MARK: - Inline Fragment Accessors + // MARK: - Fragment Accessors - Deferred Inline Fragment - func test__render_inlineFragmentAccessors__givenDirectTypeCases_rendersTypeCaseAccessorWithCorrectName() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragment_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { @@ -4332,40 +5615,41 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - species: String! - predator: Animal! - } - - interface Pet implements Animal { - species: String! - predator: Animal! - name: String! + id: String + species: String + genus: String } - type Dog implements Animal & Pet { - species: String! - predator: Animal! - name: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ query TestOperation { allAnimals { - species - ... on Pet { - name - } - ... on Dog { - name + __typename + id + ... on Dog @defer(label: "root") { + species } } } """ let expected = """ - public var asPet: AsPet? { _asInlineFragment() } - public var asDog: AsDog? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: AsDog.Root? + } """ // when @@ -4377,10 +5661,10 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } - func test__render_inlineFragmentAccessors__givenMergedTypeCasesFromSingleMergedTypeCaseSource_rendersTypeCaseAccessorWithCorrectName() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragment_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { @@ -4388,88 +5672,103 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - species: String! - predator: Animal! - } - - interface Pet implements Animal { - species: String! - predator: Animal! - name: String! + id: String + species: String + genus: String } - type Dog implements Animal & Pet { - species: String! - predator: Animal! - name: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ query TestOperation { allAnimals { - species - predator { - ... on Pet { - name - } - } - ... on Dog { - name - predator { - species - } + __typename + id + ... on Dog @defer(label: "root") { + species } } } """ let expected = """ - public var asPet: AsPet? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: Root? + } """ // when try buildSubjectAndOperation() - let allAnimals_asDog_predator = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"]?[field: "predator"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals_asDog_predator) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - // MARK: Inline Fragment Accessors - Include/Skip - - func test__render_inlineFragmentAccessors__givenInlineFragmentOnDifferentTypeWithCondition_renders() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothConvenienceDeferredFragmentAccessorsAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldA: String! + interface Animal { + id: String + species: String + genus: String } - interface Pet { - fieldA: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ... on Pet @include(if: $a) { - fieldA + __typename + id + ... on Dog @defer(label: "one") { + species + } + ... on Dog @defer(label: "two") { + genus } } } """ let expected = """ - public var asPetIfA: AsPetIfA? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _one = Deferred(_dataDict: _dataDict) + _two = Deferred(_dataDict: _dataDict) + } + + @Deferred public var one: AsDog.One? + @Deferred public var two: AsDog.Two? + } """ // when @@ -4481,79 +5780,114 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } - func test__render_inlineFragmentAccessors__givenInlineFragmentOnDifferentTypeWithSkipCondition_renders() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothTypeCaseDeferredFragmentAccessorsAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldA: String! + interface Animal { + id: String + species: String + genus: String } - interface Pet { - fieldA: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ... on Pet @skip(if: $a) { - fieldA + __typename + id + ... on Dog @defer(label: "one") { + species + } + ... on Dog @defer(label: "two") { + genus } } } """ let expected = """ - public var asPetIfNotA: AsPetIfNotA? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _one = Deferred(_dataDict: _dataDict) + _two = Deferred(_dataDict: _dataDict) + } + + @Deferred public var one: One? + @Deferred public var two: Two? + } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) } - func test__render_inlineFragmentAccessors__givenInlineFragmentOnDifferentTypeWithMultipleConditions_renders() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithCondition_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldA: String! + interface Animal { + id: String + species: String + genus: String } - interface Pet { - fieldA: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ... on Pet @include(if: $a) @skip(if: $b) { - fieldA + __typename + id + ... on Dog @defer(if: "a", label: "root") { + species } } } """ let expected = """ - public var asPetIfAAndNotB: AsPetIfAAndNotB? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: AsDog.Root? + } """ // when @@ -4565,78 +5899,109 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } - func test__render_inlineFragmentAccessors__givenInlineFragmentOnSameTypeWithMultipleConditions_renders() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithCondition_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - fieldA: String! + interface Animal { + id: String + species: String + genus: String } - interface Pet { - fieldA: String! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ... on Animal @include(if: $a) @skip(if: $b) { - fieldA + __typename + id + ... on Dog @defer(if: "a", label: "root") { + species } } } """ let expected = """ - public var ifAAndNotB: IfAAndNotB? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: Root? + } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - func test__render_inlineFragmentAccessor__givenNamedFragmentMatchingParentTypeWithInclusionCondition_renders() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithTrueCondition_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ...FragmentA @include(if: $a) + __typename + id + ... on Dog @defer(if: true, label: "root") { + species + } } } - - fragment FragmentA on Animal { - int - } """ let expected = """ - public var ifA: IfA? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: AsDog.Root? + } """ // when @@ -4648,10 +6013,10 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) } - func test__render_inlineFragmentAccessor__givenInlineFragmentAndNamedFragmentOnSameTypeWithInclusionCondition_rendersBothInlineFragments() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithTrueCondition_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { @@ -4659,88 +6024,93 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - string: String! - int: Int! + id: String + species: String + genus: String } - type Bird implements Animal { - string: String! - int: Int! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ... on Bird { - string + __typename + id + ... on Dog @defer(if: true, label: "root") { + species } - ...FragmentA @include(if: $a) } } - - fragment FragmentA on Bird { - int - } """ let expected = """ - public var asBird: AsBird? { _asInlineFragment() } - public var asBirdIfA: AsBirdIfA? { _asInlineFragment() } + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: Root? + } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 13, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - // MARK: - Fragment Accessors - - func test__render_fragmentAccessor__givenFragments_rendersFragmentAccessor() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithFalseCondition_doesNotRenderConvenienceDeferredFragmentAccessor() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ query TestOperation { allAnimals { - ...FragmentA - ...lowercaseFragment + __typename + id + ... on Dog @defer(if: false, label: "root") { + species + } } } - - fragment FragmentA on Animal { - int - } - - fragment lowercaseFragment on Animal { - string - } """ let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } + public var asDog: AsDog? { _asInlineFragment() } - public var fragmentA: FragmentA { _toFragment() } - public var lowercaseFragment: LowercaseFragment { _toFragment() } - } + /// Parent Type: `Dog` + public struct AsDog: TestSchema.InlineFragment { """ // when @@ -4752,10 +6122,10 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 16, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenInheritedFragmentFromParent_rendersFragmentAccessor() throws { + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithFalseCondition_doesNotRenderTypeCaseDeferredFragmentAccessor() throws { // given schemaSDL = """ type Query { @@ -4763,224 +6133,260 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - string: String! - int: Int! + id: String + species: String + genus: String } - type Cat implements Animal { - string: String! - int: Int! + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ query TestOperation { allAnimals { - ...FragmentA - ... on Cat { - string + __typename + id + ... on Dog @defer(if: false, label: "root") { + species } } } - - fragment FragmentA on Animal { - int - } - - fragment lowercaseFragment on Animal { - string - } """ let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public var fragmentA: FragmentA { _toFragment() } + public var species: String? { __data["species"] } } + } """ // when try buildSubjectAndOperation() - let allAnimals_asCat = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Cat"] + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(inlineFragment: allAnimals_asCat) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - // MARK: - Fragment Accessors - Include Skip + // MARK: - Fragment Accessors - Deferred Named Fragment - func test__render_fragmentAccessor__givenFragmentOnSameTypeWithInclusionCondition_rendersFragmentAccessorAsOptional() throws { + func test__render_fragmentAccessor__givenDeferredNamedFragment_rendersDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ...FragmentA @include(if: $a) - ...lowercaseFragment + __typename + id + ...DogFragment @defer(label: "root") } } - fragment FragmentA on Animal { - int - } - - fragment lowercaseFragment on Animal { - string + fragment DogFragment on Dog { + species } """ let expected = """ public struct Fragments: FragmentContainer { public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } + public init(_dataDict: DataDict) { + __data = _dataDict + _dogFragment = Deferred(_dataDict: _dataDict) + } - public var lowercaseFragment: LowercaseFragment { _toFragment() } - public var fragmentA: FragmentA? { _toFragment() } + @Deferred public var dogFragment: DogFragment? } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenFragmentOnSameTypeWithInclusionConditionThatMatchesScope_rendersFragmentAccessorAsNotOptional() throws { + func test__render_fragmentAccessor__givenDeferredNamedFragmentWithCondition_rendersDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { - allAnimals @include(if: $a) { - ...FragmentA @include(if: $a) + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: "a", label: "root") } } - fragment FragmentA on Animal { - int + fragment DogFragment on Dog { + species } """ let expected = """ public struct Fragments: FragmentContainer { public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } + public init(_dataDict: DataDict) { + __data = _dataDict + _dogFragment = Deferred(_dataDict: _dataDict) + } - public var fragmentA: FragmentA { _toFragment() } + @Deferred public var dogFragment: DogFragment? } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenFragmentOnSameTypeWithInclusionConditionThatPartiallyMatchesScope_rendersFragmentAccessorAsOptional() throws { + func test__render_fragmentAccessor__givenDeferredNamedFragmentWithTrueCondition_rendersDeferredFragmentAccessorAsOptional() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!, $b: Boolean!) { - allAnimals @include(if: $a) { - ...FragmentA @include(if: $a) @include(if: $b) + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: true, label: "root") } } - fragment FragmentA on Animal { - int + fragment DogFragment on Dog { + species } """ let expected = """ public struct Fragments: FragmentContainer { public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } + public init(_dataDict: DataDict) { + __data = _dataDict + _dogFragment = Deferred(_dataDict: _dataDict) + } - public var fragmentA: FragmentA? { _toFragment() } + @Deferred public var dogFragment: DogFragment? } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenFragmentMergedFromParent_withInclusionConditionThatMatchesScope_rendersFragmentAccessorAsNotOptional() throws { + func test__render_fragmentAccessor__givenDeferredNamedFragmentWithFalseCondition_doesNotRenderDeferredFragmentAccessor() throws { // given schemaSDL = """ type Query { allAnimals: [Animal!] } - type Animal { - string: String! - int: Int! + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String } """ document = """ - query TestOperation($a: Boolean!) { + query TestOperation { allAnimals { - ...FragmentA @include(if: $a) + __typename + id + ...DogFragment @defer(if: false, label: "root") } } - fragment FragmentA on Animal { - int + fragment DogFragment on Dog { + species } """ @@ -4989,20 +6395,20 @@ class SelectionSetTemplateTests: XCTestCase { public let __data: DataDict public init(_dataDict: DataDict) { __data = _dataDict } - public var fragmentA: FragmentA { _toFragment() } + public var dogFragment: DogFragment { _toFragment() } } """ // when try buildSubjectAndOperation() - let allAnimals_ifA = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[if: "a"] + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] ) - let actual = subject.render(inlineFragment: allAnimals_ifA) + let actual = subject.render(inlineFragment: allAnimals_asDog) // then - expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) } // MARK: - Nested Selection Sets @@ -5989,6 +7395,138 @@ class SelectionSetTemplateTests: XCTestCase { } } + // MARK: Nested Selection Sets - Deferred Inline Fragments + + func test__render_nestedSelectionSet__givenDeferredInlineFragment_rendersNestedSelectionSet() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "root") { + species + } + } + } + """ + + let expected = """ + public struct Root: TestSchema.InlineFragment, ApolloAPI.Deferrable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public typealias RootEntityType = TestOperation.Data.AllAnimal + public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Dog } + + public var species: String { __data["species"] } + public var id: String { __data["id"] } + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_AsDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_AsDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 27, ignoringExtraLines: true)) + } + + func test__render_nestedSelectionSet__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothNestedSelectionSets() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "one") { + species + } + ... on Dog @defer(label: "two") { + genus + } + } + } + """ + + let expected = """ + public struct One: TestSchema.InlineFragment, ApolloAPI.Deferrable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public typealias RootEntityType = TestOperation.Data.AllAnimal + public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Dog } + + public var species: String { __data["species"] } + public var id: String? { __data["id"] } + } + + public struct Two: TestSchema.InlineFragment, ApolloAPI.Deferrable { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public typealias RootEntityType = TestOperation.Data.AllAnimal + public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Dog } + + public var genus: String { __data["genus"] } + public var id: String? { __data["id"] } + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 30, ignoringExtraLines: true)) + } + // MARK: - InlineFragment RootEntityType Tests func test__render_nestedTypeCase__rendersRootEntityType() throws { @@ -6233,7 +7771,7 @@ class SelectionSetTemplateTests: XCTestCase { public typealias RootEntityType = TestOperationQuery.Data """ - + // when try buildSubjectAndOperation() let query_ifA = try XCTUnwrap( @@ -6795,9 +8333,9 @@ class SelectionSetTemplateTests: XCTestCase { // then expect(actual).to(equalLineByLine(expected, atLine: 7, ignoringExtraLines: true)) } - + // MARK: - Reserved Keyword Type Tests - + func test__render_enumType__usingReservedKeyword_rendersAsSuffixedType() throws { // given schemaSDL = """ @@ -6828,7 +8366,7 @@ class SelectionSetTemplateTests: XCTestCase { let expectedOne = """ .field("type", GraphQLEnum.self), """ - + let expectedTwo = """ public var type: GraphQLEnum { __data["type"] } """ @@ -6845,7 +8383,7 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expectedOne, atLine: 9, ignoringExtraLines: true)) expect(actual).to(equalLineByLine(expectedTwo, atLine: 12, ignoringExtraLines: true)) } - + func test__render_NamedFragmentType__usingReservedKeyword_rendersAsSuffixedType() throws { // given schemaSDL = """ @@ -6881,7 +8419,7 @@ class SelectionSetTemplateTests: XCTestCase { let expectedOne = """ .fragment(Type_Fragment.self), """ - + let expectedTwo = """ public var type: Type_Fragment { _toFragment() } """ @@ -6898,7 +8436,7 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expectedOne, atLine: 9, ignoringExtraLines: true)) expect(actual).to(equalLineByLine(expectedTwo, atLine: 19, ignoringExtraLines: true)) } - + func test__render_CustomScalarType__usingReservedKeyword_rendersAsSuffixedType() throws { // given schemaSDL = """ @@ -6926,7 +8464,7 @@ class SelectionSetTemplateTests: XCTestCase { let expectedOne = """ .field("type", TestSchema.Type_Scalar.self), """ - + let expectedTwo = """ public var type: TestSchema.Type_Scalar { __data["type"] } """ @@ -6943,7 +8481,7 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expectedOne, atLine: 9, ignoringExtraLines: true)) expect(actual).to(equalLineByLine(expectedTwo, atLine: 12, ignoringExtraLines: true)) } - + func test__render_InterfaceType__usingReservedKeyword_rendersAsSuffixedType() throws { // given schemaSDL = """ @@ -6983,7 +8521,7 @@ class SelectionSetTemplateTests: XCTestCase { // then expect(actual).to(equalLineByLine(expected, atLine: 6, ignoringExtraLines: true)) } - + func test__render_UnionType__usingReservedKeyword_rendersAsSuffixedType() throws { // given schemaSDL = """ @@ -7033,7 +8571,7 @@ class SelectionSetTemplateTests: XCTestCase { // then expect(actual).to(equalLineByLine(expected, atLine: 6, ignoringExtraLines: true)) } - + func test__render_ObjectType__usingReservedKeyword_rendersAsSuffixedType() throws { // given schemaSDL = """ @@ -7070,5 +8608,5 @@ class SelectionSetTemplateTests: XCTestCase { // then expect(actual).to(equalLineByLine(expected, atLine: 6, ignoringExtraLines: true)) } - -} \ No newline at end of file + +} From 228fe3286fca78c181d6daa6b2c130553ad641cf Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 28 Sep 2023 14:45:44 -0700 Subject: [PATCH 02/41] Fix property name casing --- apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift | 2 +- apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 68583078a..fd69acb8f 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -137,7 +137,7 @@ class RootFieldBuilder { addSelections(from: selectionSet, to: target, atTypePath: typeInfo) self.hasDeferredFragments = { - switch typeInfo.scopePath.last.value.IsDeferred { + switch typeInfo.scopePath.last.value.isDeferred { case .value(false): return false case .value(true), .if(_): diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index 5036abb0f..b33125e45 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -70,7 +70,7 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { let allTypesInSchema: Schema.ReferencedTypes - let IsDeferred: IsDeferred + let isDeferred: IsDeferred private init( typePath: LinkedList, @@ -85,7 +85,7 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { self.matchingTypes = matchingTypes self.matchingConditions = matchingConditions self.allTypesInSchema = allTypesInSchema - self.IsDeferred = isDeferred + self.isDeferred = isDeferred } /// Creates a `ScopeDescriptor` for a root `SelectionSet`. From 55b91a2e20d6724b0338d9a2f9f4f048d9c00987 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 3 Oct 2023 15:55:57 -0700 Subject: [PATCH 03/41] Refactor IsDeferred --- .../GraphQLCompiler/CompilationResult.swift | 51 ++++++++++++------- .../Sources/IR/IR+IsDeferred.swift | 35 +++---------- .../Sources/IR/IR+RootFieldBuilder.swift | 4 +- .../Sources/IR/IR+ScopeDescriptor.swift | 13 +++-- 4 files changed, 50 insertions(+), 53 deletions(-) diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift index 4687f3a49..1b272b630 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift @@ -12,6 +12,7 @@ public class CompilationResult: JavaScriptObject { enum ArgumentLabels { static let `If` = "if" + static let Label = "label" } } @@ -164,7 +165,7 @@ public class CompilationResult: JavaScriptObject { lazy var directives: [Directive]? = self["directives"] - public lazy var isDeferred: IsDeferred = getIsDeferred() + public lazy var deferCondition: DeferCondition? = getDeferCondition() public override var debugDescription: String { selectionSet.debugDescription @@ -191,7 +192,7 @@ public class CompilationResult: JavaScriptObject { public lazy var directives: [Directive]? = self["directives"] - public lazy var isDeferred: IsDeferred = getIsDeferred() + public lazy var deferCondition: DeferCondition? = getDeferCondition() @inlinable public var parentType: GraphQLCompositeType { fragment.type } @@ -408,16 +409,13 @@ public class CompilationResult: JavaScriptObject { } } - public enum IsDeferred: ExpressibleByBooleanLiteral { - case value(Bool) - case variable(String) + public struct DeferCondition { + public let label: String + public let variable: String? - public init(booleanLiteral value: BooleanLiteralType) { - self = .value(value) - } - - static func `if`(_ variable: String) -> Self { - .variable(variable) + init(label: String, variable: String? = nil) { + self.label = label + self.variable = variable } } @@ -428,28 +426,43 @@ fileprivate protocol Deferrable { } fileprivate extension Deferrable where Self: JavaScriptObject { - func getIsDeferred() -> CompilationResult.IsDeferred { + func getDeferCondition() -> CompilationResult.DeferCondition? { guard let directive = directives?.first( where: { $0.name == CompilationResult.Constants.DirectiveNames.Defer } ) else { - return false + return nil } - guard let argument = directive.arguments?.first( + guard + let labelArgument = directive.arguments?.first( + where: { $0.name == CompilationResult.Constants.ArgumentLabels.Label }), + case let .string(labelValue) = labelArgument.value + else { + preconditionFailure("Incorrect `label` argument. Either missing or value is not a String.") + } + + guard let variableArgument = directive.arguments?.first( where: { $0.name == CompilationResult.Constants.ArgumentLabels.If } ) else { - return true + return .init(label: labelValue) } - switch (argument.value) { + switch (variableArgument.value) { case let .boolean(value): - return .value(value) + if value { + return .init(label: labelValue) + } else { + return nil + } case let .string(value), let .variable(value): - return .variable(value) + return .init(label: labelValue, variable: value) default: - preconditionFailure("Incompatible argument value. Expected Boolean or Variable, got \(argument.value).") + preconditionFailure(""" + Incompatible variable value. Expected Boolean, String or Variable, + got \(variableArgument.value). + """) } } } diff --git a/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift b/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift index 6ec2f7413..a9f9e0d8d 100644 --- a/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift +++ b/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift @@ -1,35 +1,16 @@ import GraphQLCompiler // TODO: Documentation for this to be completed in issue #3141 -public enum IsDeferred: Hashable, ExpressibleByBooleanLiteral { - case value(Bool) - case `if`(_ variable: String) +public struct DeferCondition: Equatable { + public let label: String + public let variable: String? - public init(booleanLiteral value: BooleanLiteralType) { - switch value { - case true: - self = .value(true) - case false: - self = .value(false) - } + init(label: String, variable: String? = nil) { + self.label = label + self.variable = variable } - init(_ compilationResult: CompilationResult.IsDeferred) { - switch compilationResult { - case let .value(value): - self = .value(value) - - case let .variable(variable): - self = .if(variable) - } - } - - var definitionDirectiveDescription: String { - switch self { - case .value(false): return "" - case .value(true): return " @defer" - case let .if(variable): - return " @defer(if: \(variable))" - } + init(_ compilationResult: CompilationResult.DeferCondition) { + self.init(label: compilationResult.label, variable: compilationResult.variable) } } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index fd69acb8f..20626fd56 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -331,10 +331,10 @@ class RootFieldBuilder { from selectionSet: CompilationResult.SelectionSet?, with scopeCondition: ScopeCondition, inParentTypePath enclosingTypeInfo: SelectionSet.TypeInfo, - isDeferred: IsDeferred = false + deferCondition: DeferCondition? = nil ) -> InlineFragmentSpread { let typePath = enclosingTypeInfo.scopePath.mutatingLast { - $0.appending(scopeCondition, isDeferred: isDeferred) + $0.appending(scopeCondition, deferCondition: deferCondition) } let irSelectionSet = SelectionSet( diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index b33125e45..45ed01187 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -70,7 +70,7 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { let allTypesInSchema: Schema.ReferencedTypes - let isDeferred: IsDeferred + let deferCondition: DeferCondition? private init( typePath: LinkedList, @@ -78,14 +78,14 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { matchingTypes: TypeScope, matchingConditions: InclusionConditions?, allTypesInSchema: Schema.ReferencedTypes, - isDeferred: IsDeferred = false + deferCondition: DeferCondition? = nil ) { self.scopePath = typePath self.type = type self.matchingTypes = matchingTypes self.matchingConditions = matchingConditions self.allTypesInSchema = allTypesInSchema - self.isDeferred = isDeferred + self.deferCondition = deferCondition } /// Creates a `ScopeDescriptor` for a root `SelectionSet`. @@ -144,7 +144,10 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { /// /// This should be used to create a `ScopeDescriptor` for a conditional `SelectionSet` inside /// of an entity, by appending the conditions to the parent `SelectionSet`'s `ScopeDescriptor`. - func appending(_ scopeCondition: ScopeCondition, isDeferred: IsDeferred = false) -> ScopeDescriptor { + func appending( + _ scopeCondition: ScopeCondition, + deferCondition: DeferCondition? = nil + ) -> ScopeDescriptor { let matchingTypes: TypeScope if let newType = scopeCondition.type { matchingTypes = Self.typeScope( @@ -167,7 +170,7 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { matchingTypes: matchingTypes, matchingConditions: matchingConditions, allTypesInSchema: self.allTypesInSchema, - isDeferred: isDeferred + deferCondition: deferCondition ) } From 7aca041be2eebc36cc8a16b796ddd1a3184e06e7 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 3 Oct 2023 19:25:37 -0700 Subject: [PATCH 04/41] Refactor building deferred inline fragment --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 139 +++++++++++++++++- .../Sources/IR/IR+RootFieldBuilder.swift | 77 ++++++++-- 2 files changed, 198 insertions(+), 18 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 984ba3f2a..adfe1b455 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4327,7 +4327,7 @@ class IRRootFieldBuilderTests: XCTestCase { expect(self.computedReferencedFragments).to(equal(expected)) } - // MARK: - Deferred Fragments + // MARK: - Deferred Fragments - hasDeferredFragments property func test__deferredFragments__givenNoDeferredFragment_hasDeferredFragmentsFalse() throws { // given @@ -4515,7 +4515,7 @@ class IRRootFieldBuilderTests: XCTestCase { allAnimals { __typename id - ...DogFragment @defer + ...DogFragment @defer(label: "root") } } @@ -4575,4 +4575,139 @@ class IRRootFieldBuilderTests: XCTestCase { expect(self.result.hasDeferredFragments).to(beTrue()) } + #warning("tests to match IR struct changes") + + // MARK: Deferred Fragments - type case + + func test__deferredFragments__givenDeferredInlineFragment_buildsDeferredInlineFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + genus: String! + } + + type Dog implements Animal { + id: String! + species: String! + genus: String! + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"]?.selectionSet + expect(allAnimals?.selections.direct?.fields.values).to(shallowlyMatch([ + .mock("id", type: .nonNull(.scalar(Scalar_String))) + ])) + expect(allAnimals?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ + .mock(parentType: Object_Dog) + ])) + expect(allAnimals?.scope.deferCondition).to(beNil()) + + let allAnimals_AsDog = allAnimals?[as: "Dog"] + expect(allAnimals_AsDog?.selections.direct?.fields).to(beEmpty()) + expect(allAnimals_AsDog?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ + .mock(parentType: Object_Dog) // ? should this match on the type Root.self instead + ])) + expect(allAnimals_AsDog?.scope.deferCondition).to(beNil()) + + let allAnimals_AsDog_Deferred = allAnimals_AsDog?[as: "Dog"] + expect(allAnimals_AsDog_Deferred?.selections.direct?.fields.values).to(shallowlyMatch([ + .mock("species", type: .nonNull(.scalar(Scalar_String))) + ])) + expect(allAnimals_AsDog_Deferred?.selections.direct?.inlineFragments).to(beEmpty()) + expect(allAnimals_AsDog_Deferred?.scope.deferCondition.unsafelyUnwrapped).to(equal( + DeferCondition(label: "root") + )) + } + + func test__deferredFragments__givenDeferredInlineFragmentWithCondition_buildsDeferredInlineFragmentWithCondition() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + genus: String! + } + + type Dog implements Animal { + id: String! + species: String! + genus: String! + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: "a", label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"]?.selectionSet + expect(allAnimals?.selections.direct?.fields.values).to(shallowlyMatch([ + .mock("id", type: .nonNull(.scalar(Scalar_String))) + ])) + expect(allAnimals?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ + .mock(parentType: Object_Dog) + ])) + expect(allAnimals?.scope.deferCondition).to(beNil()) + + let allAnimals_AsDog = allAnimals?[as: "Dog"] + expect(allAnimals_AsDog?.selections.direct?.fields).to(beEmpty()) + expect(allAnimals_AsDog?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ + .mock(parentType: Object_Dog) // ? should this match on the type Root.self instead + ])) + expect(allAnimals_AsDog?.scope.deferCondition).to(beNil()) + + let allAnimals_AsDog_Deferred = allAnimals_AsDog?[as: "Dog"] + expect(allAnimals_AsDog_Deferred?.selections.direct?.fields.values).to(shallowlyMatch([ + .mock("species", type: .nonNull(.scalar(Scalar_String))) + ])) + expect(allAnimals_AsDog_Deferred?.selections.direct?.inlineFragments).to(beEmpty()) + expect(allAnimals_AsDog_Deferred?.scope.deferCondition.unsafelyUnwrapped).to(equal( + DeferCondition(label: "root", variable: "a") + )) + } } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 20626fd56..1141dd948 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -137,12 +137,11 @@ class RootFieldBuilder { addSelections(from: selectionSet, to: target, atTypePath: typeInfo) self.hasDeferredFragments = { - switch typeInfo.scopePath.last.value.isDeferred { - case .value(false): + guard let _ = typeInfo.scope.deferCondition else { return false - case .value(true), .if(_): - return true } + + return true }() typeInfo.entity.selectionTree.mergeIn( @@ -178,24 +177,42 @@ class RootFieldBuilder { } case let .inlineFragment(inlineFragment): - let inlineSelectionSet = inlineFragment.selectionSet - guard let scope = scopeCondition(for: inlineFragment, in: typeInfo) else { + guard let scope = scopeCondition( + for: inlineFragment, + in: typeInfo, + deferCondition: inlineFragment.deferCondition + ) else { return } - if typeInfo.scope.matches(scope) { - addSelections( + let inlineSelectionSet = inlineFragment.selectionSet + + switch (typeInfo.scope.matches(scope), inlineFragment.deferCondition) { + case let (true, .some(deferCondition)): + let irTypeCase = buildInlineFragmentSpread( from: inlineSelectionSet, - to: target, - atTypePath: typeInfo + with: scope, + inParentTypePath: typeInfo, + deferCondition: .init(deferCondition) + ) + target.mergeIn(irTypeCase) + + case (true, .none): + addSelections(from: inlineSelectionSet, to: target, atTypePath: typeInfo) + + case (false, .some): + let deferredTypeCase = buildInlineFragmentSpread( + toWrap: selection, + with: scope, + inParentTypePath: typeInfo ) + target.mergeIn(deferredTypeCase) - } else { + case (false, .none): let irTypeCase = buildInlineFragmentSpread( from: inlineSelectionSet, with: scope, - inParentTypePath: typeInfo, - isDeferred: .init(inlineFragment.isDeferred) + inParentTypePath: typeInfo ) target.mergeIn(irTypeCase) } @@ -245,15 +262,20 @@ class RootFieldBuilder { private func scopeCondition( for conditionalSelectionSet: ConditionallyIncludable, - in parentTypePath: SelectionSet.TypeInfo + in parentTypePath: SelectionSet.TypeInfo, + deferCondition: CompilationResult.DeferCondition? = nil ) -> ScopeCondition? { let inclusionResult = inclusionResult(for: conditionalSelectionSet.inclusionConditions) guard inclusionResult != .skipped else { return nil } - let type = parentTypePath.parentType == conditionalSelectionSet.parentType ? - nil : conditionalSelectionSet.parentType + let type = ( + parentTypePath.parentType == conditionalSelectionSet.parentType + && deferCondition == nil + ) + ? nil + : conditionalSelectionSet.parentType return ScopeCondition(type: type, conditions: inclusionResult.conditions) } @@ -353,6 +375,29 @@ class RootFieldBuilder { return InlineFragmentSpread(selectionSet: irSelectionSet) } + private func buildInlineFragmentSpread( + toWrap selection: CompilationResult.Selection, + with scopeCondition: ScopeCondition, + inParentTypePath enclosingTypeInfo: SelectionSet.TypeInfo + ) -> InlineFragmentSpread { + let typePath = enclosingTypeInfo.scopePath.mutatingLast { + $0.appending(scopeCondition) + } + + let irSelectionSet = SelectionSet( + entity: enclosingTypeInfo.entity, + scopePath: typePath + ) + + add( + selection, + to: irSelectionSet.selections.direct.unsafelyUnwrapped, + atTypePath: irSelectionSet.typeInfo + ) + + return InlineFragmentSpread(selectionSet: irSelectionSet) + } + private func buildNamedFragmentSpread( fromFragment fragmentSpread: CompilationResult.FragmentSpread, with scopeCondition: ScopeCondition, From be2bb8c82bb1947f5d2d759ce092c2e2e8f91010 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 6 Oct 2023 16:32:48 -0700 Subject: [PATCH 05/41] Refactor inlineFragment selection logic --- .../Sources/IR/IR+RootFieldBuilder.swift | 87 ++++++++++--------- .../Sources/IR/IR+ScopeDescriptor.swift | 9 +- 2 files changed, 54 insertions(+), 42 deletions(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 1141dd948..b1d776b40 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -177,45 +177,7 @@ class RootFieldBuilder { } case let .inlineFragment(inlineFragment): - guard let scope = scopeCondition( - for: inlineFragment, - in: typeInfo, - deferCondition: inlineFragment.deferCondition - ) else { - return - } - - let inlineSelectionSet = inlineFragment.selectionSet - - switch (typeInfo.scope.matches(scope), inlineFragment.deferCondition) { - case let (true, .some(deferCondition)): - let irTypeCase = buildInlineFragmentSpread( - from: inlineSelectionSet, - with: scope, - inParentTypePath: typeInfo, - deferCondition: .init(deferCondition) - ) - target.mergeIn(irTypeCase) - - case (true, .none): - addSelections(from: inlineSelectionSet, to: target, atTypePath: typeInfo) - - case (false, .some): - let deferredTypeCase = buildInlineFragmentSpread( - toWrap: selection, - with: scope, - inParentTypePath: typeInfo - ) - target.mergeIn(deferredTypeCase) - - case (false, .none): - let irTypeCase = buildInlineFragmentSpread( - from: inlineSelectionSet, - with: scope, - inParentTypePath: typeInfo - ) - target.mergeIn(irTypeCase) - } + add(inlineFragment, from: selection, to: target, atTypePath: typeInfo) case let .fragmentSpread(fragmentSpread): guard let scope = scopeCondition(for: fragmentSpread, in: typeInfo) else { @@ -260,6 +222,53 @@ class RootFieldBuilder { } } + private func add( + _ inlineFragment: CompilationResult.InlineFragment, + from selection: CompilationResult.Selection, + to target: DirectSelections, + atTypePath typeInfo: SelectionSet.TypeInfo + ) { + guard let scope = scopeCondition( + for: inlineFragment, + in: typeInfo, + deferCondition: inlineFragment.deferCondition + ) else { + return + } + + let inlineSelectionSet = inlineFragment.selectionSet + + switch (typeInfo.scope.matches(scope), inlineFragment.deferCondition) { + case let (true, .some(deferCondition)): + let irTypeCase = buildInlineFragmentSpread( + from: inlineSelectionSet, + with: scope, + inParentTypePath: typeInfo, + deferCondition: .init(deferCondition) + ) + target.mergeIn(irTypeCase) + + case (true, .none): + addSelections(from: inlineSelectionSet, to: target, atTypePath: typeInfo) + + case (false, .some): + let irTypeCase = buildInlineFragmentSpread( + toWrap: selection, + with: scope, + inParentTypePath: typeInfo + ) + target.mergeIn(irTypeCase) + + case (false, .none): + let irTypeCase = buildInlineFragmentSpread( + from: inlineSelectionSet, + with: scope, + inParentTypePath: typeInfo + ) + target.mergeIn(irTypeCase) + } + } + private func scopeCondition( for conditionalSelectionSet: ConditionallyIncludable, in parentTypePath: SelectionSet.TypeInfo, diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index 45ed01187..43a9cc9b9 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -9,23 +9,26 @@ import Utilities public struct ScopeCondition: Hashable, CustomDebugStringConvertible { public let type: GraphQLCompositeType? public let conditions: InclusionConditions? + public let deferDirective: DeferCondition? init( type: GraphQLCompositeType? = nil, - conditions: InclusionConditions? = nil + conditions: InclusionConditions? = nil, + deferDirective: DeferCondition? = nil ) { self.type = type self.conditions = conditions + self.deferDirective = deferDirective } public var debugDescription: String { - [type?.debugDescription, conditions?.debugDescription] + [type?.debugDescription, conditions?.debugDescription, deferDirective?.debugDescription] .compactMap { $0 } .joined(separator: " ") } var isEmpty: Bool { - type == nil && (conditions?.isEmpty ?? true) + type == nil && (conditions?.isEmpty ?? true) && deferDirective == nil } } From bbb98d461b2e7db9fba32436eab50692da8d7309 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 6 Oct 2023 16:36:14 -0700 Subject: [PATCH 06/41] Build new ScopeCondition for inline fragments --- apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift | 11 ++++++++++- .../Sources/IR/IR+RootFieldBuilder.swift | 10 ++++++++-- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift b/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift index a9f9e0d8d..3f8d1bc2c 100644 --- a/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift +++ b/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift @@ -1,7 +1,7 @@ import GraphQLCompiler // TODO: Documentation for this to be completed in issue #3141 -public struct DeferCondition: Equatable { +public struct DeferCondition: Hashable, CustomDebugStringConvertible { public let label: String public let variable: String? @@ -13,4 +13,13 @@ public struct DeferCondition: Equatable { init(_ compilationResult: CompilationResult.DeferCondition) { self.init(label: compilationResult.label, variable: compilationResult.variable) } + + public var debugDescription: String { + var string = "Defer \"\(label)\"" + if let variable { + string += " - if \"\(variable)\"" + } + + return string + } } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index b1d776b40..dbf1da5aa 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -207,8 +207,9 @@ class RootFieldBuilder { ), with: scope, inParentTypePath: typeInfo, - isDeferred: .init(fragmentSpread.isDeferred) + deferCondition: (fragmentSpread.deferCondition != nil ? .init(fragmentSpread.deferCondition!) : nil) ) + #warning("remove force unwrap above") target.mergeIn(irTypeCaseEnclosingFragment) @@ -364,8 +365,13 @@ class RootFieldBuilder { inParentTypePath enclosingTypeInfo: SelectionSet.TypeInfo, deferCondition: DeferCondition? = nil ) -> InlineFragmentSpread { + let scope = ScopeCondition( + type: scopeCondition.type, + conditions: scopeCondition.conditions, + deferDirective: deferCondition + ) let typePath = enclosingTypeInfo.scopePath.mutatingLast { - $0.appending(scopeCondition, deferCondition: deferCondition) + $0.appending(scope, deferCondition: deferCondition) } let irSelectionSet = SelectionSet( From 678886907147cee36ff8b20f1762047ccd7bae78 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sat, 7 Oct 2023 10:46:25 -0700 Subject: [PATCH 07/41] Add working tests for simple type case --- .../MockIRSubscripts.swift | 17 +- .../CodeGenIR/IRRootFieldBuilderTests.swift | 452 ++++++++++++++++-- 2 files changed, 436 insertions(+), 33 deletions(-) diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift index 927c2ddb9..a92090e5c 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift @@ -48,7 +48,8 @@ extension ScopeConditionalSubscriptAccessing { private func scopeCondition( type typeCase: String?, - conditions conditionsResult: IR.InclusionConditions.Result? + conditions conditionsResult: IR.InclusionConditions.Result?, + deferCondition: DeferCondition? = nil ) -> IR.ScopeCondition? { let type: GraphQLCompositeType? if let typeCase = typeCase { @@ -70,7 +71,7 @@ extension ScopeConditionalSubscriptAccessing { conditions = nil } - return IR.ScopeCondition(type: type, conditions: conditions) + return IR.ScopeCondition(type: type, conditions: conditions, deferDirective: deferCondition) } } @@ -145,6 +146,18 @@ extension IR.SelectionSet: ScopeConditionalSubscriptAccessing { public subscript(fragment fragment: String) -> IR.NamedFragmentSpread? { selections[fragment: fragment] } + + public subscript( + deferredAs label: String, + withVariable variable: String? = nil + ) -> IR.SelectionSet? { + let scope = ScopeCondition( + type: self.parentType, + conditions: self.inclusionConditions, + deferDirective: DeferCondition(label: label, variable: variable) + ) + return selections[scope] + } } extension IR.SelectionSet.Selections: ScopeConditionalSubscriptAccessing { diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index adfe1b455..f6b7b9e20 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4577,7 +4577,7 @@ class IRRootFieldBuilderTests: XCTestCase { #warning("tests to match IR struct changes") - // MARK: Deferred Fragments - type case + // MARK: Deferred Fragments - Inline Fragments func test__deferredFragments__givenDeferredInlineFragment_buildsDeferredInlineFragment() throws { // given @@ -4617,35 +4617,57 @@ class IRRootFieldBuilderTests: XCTestCase { // then let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) - let allAnimals = self.subject[field: "allAnimals"]?.selectionSet - expect(allAnimals?.selections.direct?.fields.values).to(shallowlyMatch([ - .mock("id", type: .nonNull(.scalar(Scalar_String))) - ])) - expect(allAnimals?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ - .mock(parentType: Object_Dog) - ])) - expect(allAnimals?.scope.deferCondition).to(beNil()) - + let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - expect(allAnimals_AsDog?.selections.direct?.fields).to(beEmpty()) - expect(allAnimals_AsDog?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ - .mock(parentType: Object_Dog) // ? should this match on the type Root.self instead - ])) - expect(allAnimals_AsDog?.scope.deferCondition).to(beNil()) + let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) - let allAnimals_AsDog_Deferred = allAnimals_AsDog?[as: "Dog"] - expect(allAnimals_AsDog_Deferred?.selections.direct?.fields.values).to(shallowlyMatch([ - .mock("species", type: .nonNull(.scalar(Scalar_String))) - ])) - expect(allAnimals_AsDog_Deferred?.selections.direct?.inlineFragments).to(beEmpty()) - expect(allAnimals_AsDog_Deferred?.scope.deferCondition.unsafelyUnwrapped).to(equal( - DeferCondition(label: "root") + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .inlineFragment(parentType: Object_Dog) + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_Deferred), // wrong - will change when merging of deferred fragments is disabled + ] + ) + )) + + expect(allAnimals_AsDog_Deferred).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) )) } - func test__deferredFragments__givenDeferredInlineFragmentWithCondition_buildsDeferredInlineFragmentWithCondition() throws { + func test__deferredFragments__givenDeferredInlineFragmentWithVariableCondition_buildsDeferredInlineFragmentWithVariable() throws { // given schemaSDL = """ type Query { @@ -4681,6 +4703,373 @@ class IRRootFieldBuilderTests: XCTestCase { // when try buildSubjectRootField() + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root", withVariable: "a"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .inlineFragment(parentType: Object_Dog) + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_Deferred), // wrong - will change when merging of deferred fragments is disabled + ] + ) + )) + + expect(allAnimals_AsDog_Deferred).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } + + func test__deferredFragments__givenDeferredInlineFragmentWithTrueCondition_buildsDeferredInlineFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + genus: String! + } + + type Dog implements Animal { + id: String! + species: String! + genus: String! + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: true, label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .inlineFragment(parentType: Object_Dog) + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_Deferred), // wrong - will change when merging of deferred fragments is disabled + ] + ) + )) + + expect(allAnimals_AsDog_Deferred).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } + + func test__deferredFragments__givenDeferredInlineFragmentWithFalseCondition_doesNotBuildDeferredInlineFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + genus: String! + } + + type Dog implements Animal { + id: String! + species: String! + genus: String! + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: false, label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_AsDog_Deferred).to(beNil()) + } + + func test__deferredFragments__givenMultipleDeferredInlineFragments_buildsMultipleDeferredInlineFragments() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + genus: String! + } + + type Dog implements Animal { + id: String! + species: String! + genus: String! + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "one") { + species + } + ... on Dog @defer(label: "two") { + genus + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_Deferred_AsOne = allAnimals_AsDog?[deferredAs: "one"] + let allAnimals_AsDog_Deferred_AsTwo = allAnimals_AsDog?[deferredAs: "two"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .inlineFragment(parentType: Object_Dog), + .inlineFragment(parentType: Object_Dog), + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), +// .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled + .field("genus", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled + ], + mergedSources: [ + try .mock(allAnimals), +// try .mock(allAnimals_AsDog_Deferred_AsOne), // wrong - will change when merging of deferred fragments is disabled + try .mock(allAnimals_AsDog_Deferred_AsTwo), // wrong - will change when merging of deferred fragments is disabled + ] + ) + )) + + expect(allAnimals_AsDog_Deferred_AsOne).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .field("genus", type: .nonNull(.scalar(Scalar_String))), // bug + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_Deferred_AsTwo), // bug + ] + ) + )) + + expect(allAnimals_AsDog_Deferred_AsTwo).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("genus", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } + + // MARK: Deferred Fragments - Named Fragments + + func test__deferredFragments__givenDeferredNamedFragment_buildsDeferredInlineFragment() throws { +// throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + genus: String! + } + + type Dog implements Animal { + id: String! + species: String! + genus: String! + name: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + // when + try buildSubjectRootField() + // then let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) @@ -4701,13 +5090,14 @@ class IRRootFieldBuilderTests: XCTestCase { ])) expect(allAnimals_AsDog?.scope.deferCondition).to(beNil()) - let allAnimals_AsDog_Deferred = allAnimals_AsDog?[as: "Dog"] - expect(allAnimals_AsDog_Deferred?.selections.direct?.fields.values).to(shallowlyMatch([ - .mock("species", type: .nonNull(.scalar(Scalar_String))) - ])) - expect(allAnimals_AsDog_Deferred?.selections.direct?.inlineFragments).to(beEmpty()) - expect(allAnimals_AsDog_Deferred?.scope.deferCondition.unsafelyUnwrapped).to(equal( - DeferCondition(label: "root", variable: "a") - )) +// let allAnimals_AsDog_Deferred = allAnimals_AsDog?[as: "Dog"] +// expect(allAnimals_AsDog_Deferred?.selections.direct?.fields.values).to(shallowlyMatch([ +// .mock("species", type: .nonNull(.scalar(Scalar_String))) +// ])) +// expect(allAnimals_AsDog_Deferred?.selections.direct?.inlineFragments).to(beEmpty()) +// expect(allAnimals_AsDog_Deferred?.scope.deferCondition.unsafelyUnwrapped).to(equal( +// DeferCondition(label: "root") +// )) } + } From 500da11cbd4c17a9b30e71ce702bf9ecc227740f Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 9 Oct 2023 13:53:14 -0700 Subject: [PATCH 08/41] Remove defer from ScopeDescriptor --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 55 ++++++++++++++++++- .../Sources/IR/IR+RootFieldBuilder.swift | 10 +--- .../Sources/IR/IR+ScopeDescriptor.swift | 12 +--- .../Sources/IR/IR+SelectionSet.swift | 14 +++++ 4 files changed, 72 insertions(+), 19 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index f6b7b9e20..ebab64014 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4575,6 +4575,57 @@ class IRRootFieldBuilderTests: XCTestCase { expect(self.result.hasDeferredFragments).to(beTrue()) } + func test__deferredFragments__givenDeferredNamedFragment_withSelectionOnDifferentTypeCase_hasDeferredFragmentsTrue() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + interface Pet implements Animal { + id: String + species: String + friends: [Pet] + name: String + } + + type Dog implements Pet { + id: String + species: String + friends: [Pet] + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + ...FriendsFragment @defer(label: "root") + } + } + + fragment FriendsFragment on Dog { + id + ... on Pet { + friends { + name + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + expect(self.result.hasDeferredFragments).to(beTrue()) + } + #warning("tests to match IR struct changes") // MARK: Deferred Fragments - Inline Fragments @@ -5081,14 +5132,14 @@ class IRRootFieldBuilderTests: XCTestCase { expect(allAnimals?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ .mock(parentType: Object_Dog) ])) - expect(allAnimals?.scope.deferCondition).to(beNil()) +// expect(allAnimals?.scope.deferCondition).to(beNil()) let allAnimals_AsDog = allAnimals?[as: "Dog"] expect(allAnimals_AsDog?.selections.direct?.fields).to(beEmpty()) expect(allAnimals_AsDog?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ .mock(parentType: Object_Dog) // ? should this match on the type Root.self instead ])) - expect(allAnimals_AsDog?.scope.deferCondition).to(beNil()) +// expect(allAnimals_AsDog?.scope.deferCondition).to(beNil()) // let allAnimals_AsDog_Deferred = allAnimals_AsDog?[as: "Dog"] // expect(allAnimals_AsDog_Deferred?.selections.direct?.fields.values).to(shallowlyMatch([ diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index dbf1da5aa..dd27b1e55 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -136,13 +136,7 @@ class RootFieldBuilder { ) { addSelections(from: selectionSet, to: target, atTypePath: typeInfo) - self.hasDeferredFragments = { - guard let _ = typeInfo.scope.deferCondition else { - return false - } - - return true - }() + self.hasDeferredFragments = typeInfo.scope.scopePath.hasDeferredDirective typeInfo.entity.selectionTree.mergeIn( selections: target.readOnlyView, @@ -371,7 +365,7 @@ class RootFieldBuilder { deferDirective: deferCondition ) let typePath = enclosingTypeInfo.scopePath.mutatingLast { - $0.appending(scope, deferCondition: deferCondition) + $0.appending(scope) } let irSelectionSet = SelectionSet( diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index 43a9cc9b9..973d45628 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -73,22 +73,18 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { let allTypesInSchema: Schema.ReferencedTypes - let deferCondition: DeferCondition? - private init( typePath: LinkedList, type: GraphQLCompositeType, matchingTypes: TypeScope, matchingConditions: InclusionConditions?, - allTypesInSchema: Schema.ReferencedTypes, - deferCondition: DeferCondition? = nil + allTypesInSchema: Schema.ReferencedTypes ) { self.scopePath = typePath self.type = type self.matchingTypes = matchingTypes self.matchingConditions = matchingConditions self.allTypesInSchema = allTypesInSchema - self.deferCondition = deferCondition } /// Creates a `ScopeDescriptor` for a root `SelectionSet`. @@ -148,8 +144,7 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { /// This should be used to create a `ScopeDescriptor` for a conditional `SelectionSet` inside /// of an entity, by appending the conditions to the parent `SelectionSet`'s `ScopeDescriptor`. func appending( - _ scopeCondition: ScopeCondition, - deferCondition: DeferCondition? = nil + _ scopeCondition: ScopeCondition ) -> ScopeDescriptor { let matchingTypes: TypeScope if let newType = scopeCondition.type { @@ -172,8 +167,7 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { type: scopeCondition.type ?? self.type, matchingTypes: matchingTypes, matchingConditions: matchingConditions, - allTypesInSchema: self.allTypesInSchema, - deferCondition: deferCondition + allTypesInSchema: self.allTypesInSchema ) } diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift index 34a747256..ca9fe3d4b 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift @@ -160,3 +160,17 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { } } + +extension LinkedList where T == ScopeCondition { + var hasDeferredDirective: Bool { + var node: Node? = last + var deferDirective = node?.value.deferDirective + + while node?.previous != nil && deferDirective == nil { + node = node?.previous + deferDirective = node?.value.deferDirective + } + + return deferDirective != nil + } +} From c8e083b78f3409791be18d943eca4990cd9bf4be Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 14:43:19 -0700 Subject: [PATCH 09/41] Disable field merging for deferred fragments --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 14 +------------- .../Sources/IR/IR+RootFieldBuilder.swift | 15 +++++++-------- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index ebab64014..0989a3202 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4693,11 +4693,9 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), - .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_Deferred), // wrong - will change when merging of deferred fragments is disabled ] ) )) @@ -4781,11 +4779,9 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), - .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_Deferred), // wrong - will change when merging of deferred fragments is disabled ] ) )) @@ -4869,11 +4865,9 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), - .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_Deferred), // wrong - will change when merging of deferred fragments is disabled ] ) )) @@ -5035,13 +5029,9 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), -// .field("species", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled - .field("genus", type: .nonNull(.scalar(Scalar_String))), // wrong - will change when merging of deferred fragments is disabled ], mergedSources: [ try .mock(allAnimals), -// try .mock(allAnimals_AsDog_Deferred_AsOne), // wrong - will change when merging of deferred fragments is disabled - try .mock(allAnimals_AsDog_Deferred_AsTwo), // wrong - will change when merging of deferred fragments is disabled ] ) )) @@ -5054,11 +5044,9 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), - .field("genus", type: .nonNull(.scalar(Scalar_String))), // bug ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_Deferred_AsTwo), // bug ] ) )) @@ -5082,7 +5070,7 @@ class IRRootFieldBuilderTests: XCTestCase { // MARK: Deferred Fragments - Named Fragments func test__deferredFragments__givenDeferredNamedFragment_buildsDeferredInlineFragment() throws { -// throw XCTSkip() + throw XCTSkip() // given schemaSDL = """ diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index dd27b1e55..7c197635d 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -138,10 +138,12 @@ class RootFieldBuilder { self.hasDeferredFragments = typeInfo.scope.scopePath.hasDeferredDirective - typeInfo.entity.selectionTree.mergeIn( - selections: target.readOnlyView, - with: typeInfo - ) + if typeInfo.scope.scopePath.last.value.deferDirective == nil { + typeInfo.entity.selectionTree.mergeIn( + selections: target.readOnlyView, + with: typeInfo + ) + } } private func addSelections( @@ -163,10 +165,7 @@ class RootFieldBuilder { ) { switch selection { case let .field(field): - if let irField = buildField( - from: field, - atTypePath: typeInfo - ) { + if let irField = buildField(from: field, atTypePath: typeInfo) { target.mergeIn(irField) } From 408ea80771a2818290b4422525d874361e74cd77 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 17:17:43 -0700 Subject: [PATCH 10/41] Rename property --- .../IR+Mocking.swift | 4 ++-- .../CodeGenIR/IRRootFieldBuilderTests.swift | 14 +++++++------- .../Templates/OperationDefinitionTemplate.swift | 2 +- .../Sources/IR/IR+NamedFragment.swift | 6 +++--- apollo-ios-codegen/Sources/IR/IR+Operation.swift | 6 +++--- .../Sources/IR/IR+RootFieldBuilder.swift | 10 +++++----- .../Sources/IR/IR+SelectionSet.swift | 2 +- apollo-ios-codegen/Sources/IR/IRBuilder.swift | 4 ++-- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift b/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift index 7bebf6fef..0f321cc0a 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift @@ -96,7 +96,7 @@ extension IR.Operation { public static func mock( definition: CompilationResult.OperationDefinition? = nil, referencedFragments: OrderedSet = [], - hasDeferredFragments: Bool = false + containsDeferredFragments: Bool = false ) -> IR.Operation { let definition = definition ?? .mock() return IR.Operation.init( @@ -116,7 +116,7 @@ extension IR.Operation { ]) ), referencedFragments: referencedFragments, - hasDeferredFragments: hasDeferredFragments + containsDeferredFragments: containsDeferredFragments ) } diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 0989a3202..cf0bcb596 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4366,7 +4366,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.hasDeferredFragments).to(beFalse()) + expect(self.result.containsDeferredFragments).to(beFalse()) } func test__deferredFragments__givenDeferredInlineFragment_hasDeferredFragmentsTrue() throws { @@ -4406,7 +4406,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.hasDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragments).to(beTrue()) } func test__deferredFragments__givenDeferredInlineFragmentWithCondition_hasDeferredFragmentsTrue() throws { @@ -4446,7 +4446,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.hasDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragments).to(beTrue()) } func test__deferredFragments__givenDeferredInlineFragmentWithConditionFalse_hasDeferredFragmentsFalse() throws { @@ -4486,7 +4486,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.hasDeferredFragments).to(beFalse()) + expect(self.result.containsDeferredFragments).to(beFalse()) } func test__deferredFragments__givenDeferredNamedFragment_onDifferentTypeCase_hasDeferredFragmentsTrue() throws { @@ -4528,7 +4528,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.hasDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragments).to(beTrue()) } func test__deferredFragments__givenDeferredInlineFragment_withinNamedFragment_hasDeferredFragmentsTrue() throws { @@ -4572,7 +4572,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.hasDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragments).to(beTrue()) } func test__deferredFragments__givenDeferredNamedFragment_withSelectionOnDifferentTypeCase_hasDeferredFragmentsTrue() throws { @@ -4623,7 +4623,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.hasDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragments).to(beTrue()) } #warning("tests to match IR struct changes") diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift index 54f9623f9..934b484b6 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift @@ -27,7 +27,7 @@ struct OperationDefinitionTemplate: OperationTemplateRenderer { accessControlRenderer: { accessControlModifier(for: .member) }() )) - \(section: DeferredProperties(operation.hasDeferredFragments)) + \(section: DeferredProperties(operation.containsDeferredFragments)) \(section: VariableProperties(operation.definition.variables)) diff --git a/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift b/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift index 493a96165..53172a883 100644 --- a/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift +++ b/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift @@ -10,7 +10,7 @@ public class NamedFragment: Hashable, CustomDebugStringConvertible { /// `True` if any selection set, or nested selection set, within the fragment contains any /// fragment marked with the `@defer` directive. - public let hasDeferredFragments: Bool + public let containsDeferredFragments: Bool /// All of the Entities that exist in the fragment's selection set, /// keyed by their relative location (ie. path) within the fragment. @@ -26,13 +26,13 @@ public class NamedFragment: Hashable, CustomDebugStringConvertible { definition: CompilationResult.FragmentDefinition, rootField: EntityField, referencedFragments: OrderedSet, - hasDeferredFragments: Bool = false, + containsDeferredFragments: Bool = false, entities: [Entity.Location: Entity] ) { self.definition = definition self.rootField = rootField self.referencedFragments = referencedFragments - self.hasDeferredFragments = hasDeferredFragments + self.containsDeferredFragments = containsDeferredFragments self.entities = entities } diff --git a/apollo-ios-codegen/Sources/IR/IR+Operation.swift b/apollo-ios-codegen/Sources/IR/IR+Operation.swift index d572869f5..783397585 100644 --- a/apollo-ios-codegen/Sources/IR/IR+Operation.swift +++ b/apollo-ios-codegen/Sources/IR/IR+Operation.swift @@ -13,17 +13,17 @@ public class Operation { /// `True` if any selection set, or nested selection set, within the operation contains any /// fragment marked with the `@defer` directive. - public let hasDeferredFragments: Bool + public let containsDeferredFragments: Bool init( definition: CompilationResult.OperationDefinition, rootField: EntityField, referencedFragments: OrderedSet, - hasDeferredFragments: Bool + containsDeferredFragments: Bool ) { self.definition = definition self.rootField = rootField self.referencedFragments = referencedFragments - self.hasDeferredFragments = hasDeferredFragments + self.containsDeferredFragments = containsDeferredFragments } } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 7c197635d..fc72f1dda 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -69,7 +69,7 @@ class RootFieldBuilder { let rootField: EntityField let referencedFragments: ReferencedFragments let entities: [Entity.Location: Entity] - let hasDeferredFragments: Bool + let containsDeferredFragments: Bool } typealias ReferencedFragments = OrderedSet @@ -87,7 +87,7 @@ class RootFieldBuilder { private let rootEntity: Entity private let entityStorage: RootFieldEntityStorage private var referencedFragments: ReferencedFragments = [] - @IsEverTrue private var hasDeferredFragments: Bool + @IsEverTrue private var containsDeferredFragments: Bool private var schema: Schema { ir.schema } @@ -125,7 +125,7 @@ class RootFieldBuilder { rootField: EntityField(rootField, selectionSet: rootIrSelectionSet), referencedFragments: referencedFragments, entities: entityStorage.entitiesForFields, - hasDeferredFragments: hasDeferredFragments + containsDeferredFragments: containsDeferredFragments ) } @@ -136,7 +136,7 @@ class RootFieldBuilder { ) { addSelections(from: selectionSet, to: target, atTypePath: typeInfo) - self.hasDeferredFragments = typeInfo.scope.scopePath.hasDeferredDirective + self.containsDeferredFragments = typeInfo.scope.scopePath.containsDeferredFragments if typeInfo.scope.scopePath.last.value.deferDirective == nil { typeInfo.entity.selectionTree.mergeIn( @@ -415,7 +415,7 @@ class RootFieldBuilder { referencedFragments.append(fragment) referencedFragments.append(contentsOf: fragment.referencedFragments) - self.hasDeferredFragments = fragment.hasDeferredFragments + self.containsDeferredFragments = fragment.containsDeferredFragments let scopePath = scopeCondition.isEmpty ? parentTypeInfo.scopePath : diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift index ca9fe3d4b..b266cc092 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift @@ -162,7 +162,7 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { } extension LinkedList where T == ScopeCondition { - var hasDeferredDirective: Bool { + var containsDeferredFragments: Bool { var node: Node? = last var deferDirective = node?.value.deferDirective diff --git a/apollo-ios-codegen/Sources/IR/IRBuilder.swift b/apollo-ios-codegen/Sources/IR/IRBuilder.swift index 1e15759c5..8255cec4a 100644 --- a/apollo-ios-codegen/Sources/IR/IRBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IRBuilder.swift @@ -54,7 +54,7 @@ public class IRBuilder { definition: operationDefinition, rootField: result.rootField, referencedFragments: result.referencedFragments, - hasDeferredFragments: result.hasDeferredFragments + containsDeferredFragments: result.containsDeferredFragments ) } @@ -83,7 +83,7 @@ public class IRBuilder { definition: fragmentDefinition, rootField: result.rootField, referencedFragments: result.referencedFragments, - hasDeferredFragments: result.hasDeferredFragments, + containsDeferredFragments: result.containsDeferredFragments, entities: result.entities ) From eb32997ab3f3952c58c506c5f479fa63e871d58d Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 17:18:22 -0700 Subject: [PATCH 11/41] Add tests for deferred inline fragments --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index cf0bcb596..f3d165b36 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4961,7 +4961,7 @@ class IRRootFieldBuilderTests: XCTestCase { expect(allAnimals_AsDog_Deferred).to(beNil()) } - func test__deferredFragments__givenMultipleDeferredInlineFragments_buildsMultipleDeferredInlineFragments() throws { + func test__deferredFragments__givenSiblingSelectionSetIsSameObjectType_doesNotMergesDeferredFragments() throws { // given schemaSDL = """ type Query { @@ -5067,6 +5067,101 @@ class IRRootFieldBuilderTests: XCTestCase { )) } + func test__deferredFragments__givenSiblingSelectionSetIsDifferentObjectType_doesNotMergesDeferredFragments() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + species: String + } + + type Bird implements Animal { + species: String + wingspan: Int + } + + type Cat implements Animal { + species: String + } + """ + + document = """ + query Test { + allAnimals { + ... on Bird @defer(label: "bird") { + wingspan + } + ... on Cat @defer(label: "cat") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Bird = try XCTUnwrap(schema[object: "Bird"]) + let Object_Cat = try XCTUnwrap(schema[object: "Cat"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsBird = allAnimals?[as: "Bird"] + let allAnimals_AsCat = allAnimals?[as: "Cat"] + let allAnimals_AsBird_Deferred_AsBird = allAnimals_AsBird?[deferredAs: "bird"] + let allAnimals_AsCat_Deferred_AsCat = allAnimals_AsCat?[deferredAs: "cat"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .inlineFragment(parentType: Object_Bird), + .inlineFragment(parentType: Object_Cat), + ] + ) + )) + + expect(allAnimals_AsBird).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Bird, + directSelections: [ + .inlineFragment(parentType: Object_Bird), + ] + ) + )) + + expect(allAnimals_AsCat).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Cat, + directSelections: [ + .inlineFragment(parentType: Object_Cat), + ] + ) + )) + + expect(allAnimals_AsBird_Deferred_AsBird).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Bird, + directSelections: [ + .field("wingspan", type: .integer()), + ] + ) + )) + + expect(allAnimals_AsCat_Deferred_AsCat).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Cat, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + } + // MARK: Deferred Fragments - Named Fragments func test__deferredFragments__givenDeferredNamedFragment_buildsDeferredInlineFragment() throws { From 53f936ef780616f515f0dc4a709b62a51be5c67e Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 17:23:08 -0700 Subject: [PATCH 12/41] Remove selection set template deferred tests --- .../SelectionSetTemplateTests.swift | 1060 +++-------------- 1 file changed, 138 insertions(+), 922 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index 8bc1aca8a..7d0ce0f12 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -1491,6 +1491,8 @@ class SelectionSetTemplateTests: XCTestCase { // MARK: Selections - Deferred Inline Fragment func test__render_selections__givenDeferredInlineFragment_rendersDeferredFragmentSelection() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1542,6 +1544,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_selections__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothDeferredFragmentSelections() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1597,6 +1601,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_selections__givenDeferredInlineFragmentWithCondition_rendersDeferredFragmentSelectionWithCondition() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1648,6 +1654,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_selections__givenDeferredInlineFragmentWithTrueCondition_rendersDeferredFragmentSelectionWithoutCondition() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1750,6 +1758,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_selections__givenNestedDeferredInlineFragments_rendersNestedDeferredFragmentSelections() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1824,6 +1834,8 @@ class SelectionSetTemplateTests: XCTestCase { // MARK: Selections - Deferred Named Fragment func test__render_selections__givenDeferredNamedFragment_rendersDeferredFragmentSelection() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1877,6 +1889,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_selections__givenDeferredNamedFragmentWithLabel_rendersDeferredFragmentSelectionUsingNamedFragmentType() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1930,6 +1944,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_selections__givenDeferredNamedFragmentWithCondition_rendersDeferredFragmentSelectionWithCondition() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -1983,6 +1999,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_selections__givenDeferredNamedFragmentWithTrueCondition_rendersDeferredFragmentSelection() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -5605,9 +5623,9 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } - // MARK: - Fragment Accessors - Deferred Inline Fragment + // MARK: - Nested Selection Sets - func test__render_fragmentAccessor__givenDeferredInlineFragment_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { + func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_rendersNestedSelectionSet() throws { // given schemaSDL = """ type Query { @@ -5615,25 +5633,15 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String + species: String! + predators: [Animal!] } """ document = """ query TestOperation { allAnimals { - __typename - id - ... on Dog @defer(label: "root") { + predators { species } } @@ -5641,15 +5649,10 @@ class SelectionSetTemplateTests: XCTestCase { """ let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _root = Deferred(_dataDict: _dataDict) - } + public var predators: [Predator]? { __data["predators"] } - @Deferred public var root: AsDog.Root? - } + /// AllAnimal.Predator + public struct Predator: TestSchema.SelectionSet { """ // when @@ -5661,10 +5664,10 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenDeferredInlineFragment_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { + func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_withIrregularPluralizationRule_rendersNestedSelectionSetWithCorrectSingularName() throws { // given schemaSDL = """ type Query { @@ -5672,25 +5675,15 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String + species: String! + people: [Animal!] } """ document = """ query TestOperation { allAnimals { - __typename - id - ... on Dog @defer(label: "root") { + people { species } } @@ -5698,30 +5691,25 @@ class SelectionSetTemplateTests: XCTestCase { """ let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _root = Deferred(_dataDict: _dataDict) - } + public var people: [Person]? { __data["people"] } - @Deferred public var root: Root? - } + /// AllAnimal.Person + public struct Person: TestSchema.SelectionSet { """ // when try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: allAnimals_asDog) + let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothConvenienceDeferredFragmentAccessorsAsOptional() throws { + func test__render_nestedSelectionSets__givenDirectEntityFieldAsNonNullList_withIrregularPluralizationRule_rendersNestedSelectionSetWithCorrectSingularName() throws { // given schemaSDL = """ type Query { @@ -5729,46 +5717,26 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String + species: String! + people: [Animal!]! } """ document = """ query TestOperation { allAnimals { - __typename - id - ... on Dog @defer(label: "one") { + people { species } - ... on Dog @defer(label: "two") { - genus - } } } """ let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _one = Deferred(_dataDict: _dataDict) - _two = Deferred(_dataDict: _dataDict) - } + public var people: [Person] { __data["people"] } - @Deferred public var one: AsDog.One? - @Deferred public var two: AsDog.Two? - } + /// AllAnimal.Person + public struct Person: TestSchema.SelectionSet { """ // when @@ -5780,10 +5748,10 @@ class SelectionSetTemplateTests: XCTestCase { let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothTypeCaseDeferredFragmentAccessorsAsOptional() throws { + func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_withCustomIrregularPluralizationRule_rendersNestedSelectionSetWithCorrectSingularName() throws { // given schemaSDL = """ type Query { @@ -5791,921 +5759,165 @@ class SelectionSetTemplateTests: XCTestCase { } interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String + species: String! + people: [Animal!] } """ document = """ query TestOperation { allAnimals { - __typename - id - ... on Dog @defer(label: "one") { + people { species } - ... on Dog @defer(label: "two") { - genus - } } } """ let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _one = Deferred(_dataDict: _dataDict) - _two = Deferred(_dataDict: _dataDict) - } + public var people: [Peep]? { __data["people"] } - @Deferred public var one: One? - @Deferred public var two: Two? - } + /// AllAnimal.Peep + public struct Peep: TestSchema.SelectionSet { """ // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + try buildSubjectAndOperation(inflectionRules: [ + ApolloCodegenLib.InflectionRule.irregular(singular: "Peep", plural: "people") + ]) + + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField ) - let actual = subject.render(inlineFragment: allAnimals_asDog) + let actual = subject.render(field: allAnimals) // then - expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenDeferredInlineFragmentWithCondition_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { + /// Explicit test for edge case surfaced in issue + /// [#1825](https://github.com/apollographql/apollo-ios/issues/1825) + func test__render_nestedSelectionSets__givenDirectEntityField_withTwoObjects_oneWithPluralizedNameAsObject_oneWithSingularNameAsList_rendersNestedSelectionSetsWithCorrectNames() throws { // given schemaSDL = """ type Query { - allAnimals: [Animal!] + badge: [Badge] + badges: ProductBadge } - interface Animal { - id: String - species: String - genus: String + type Badge { + a: String } - type Dog implements Animal { - id: String - species: String - genus: String - name: String + type ProductBadge { + b: String } """ document = """ query TestOperation { - allAnimals { - __typename - id - ... on Dog @defer(if: "a", label: "root") { - species - } + badge { + a + } + badges { + b } } """ let expected = """ - public struct Fragments: FragmentContainer { + public var badge: [Badge?]? { __data["badge"] } + public var badges: Badges? { __data["badges"] } + + /// Badge + public struct Badge: TestSchema.SelectionSet { public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _root = Deferred(_dataDict: _dataDict) - } + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Badge } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("a", String?.self), + ] } + + public var a: String? { __data["a"] } + } + + /// Badges + public struct Badges: TestSchema.SelectionSet { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.ProductBadge } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("b", String?.self), + ] } - @Deferred public var root: AsDog.Root? + public var b: String? { __data["b"] } } """ // when try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + + let query = try XCTUnwrap( + operation[field: "query"] as? IR.EntityField ) - let actual = subject.render(field: allAnimals) + let actual = subject.render(field: query) // then - expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) } - func test__render_fragmentAccessor__givenDeferredInlineFragmentWithCondition_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { + /// Explicit test for edge case surfaced in issue + /// [#1825](https://github.com/apollographql/apollo-ios/issues/1825) + func test__render_nestedSelectionSets__givenDirectEntityField_withTwoObjectsNonNullFields_oneWithPluralizedNameAsObject_oneWithSingularNameAsList_rendersNestedSelectionSetsWithCorrectNames() throws { // given schemaSDL = """ type Query { - allAnimals: [Animal!] + badge: [Badge!]! + badges: ProductBadge! } - interface Animal { - id: String - species: String - genus: String + type Badge { + a: String } - type Dog implements Animal { - id: String - species: String - genus: String - name: String + type ProductBadge { + b: String } """ document = """ query TestOperation { - allAnimals { - __typename - id - ... on Dog @defer(if: "a", label: "root") { - species - } + badge { + a + } + badges { + b } } """ let expected = """ - public struct Fragments: FragmentContainer { + public var badge: [Badge] { __data["badge"] } + public var badges: Badges { __data["badges"] } + + /// Badge + public struct Badge: TestSchema.SelectionSet { public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _root = Deferred(_dataDict: _dataDict) - } + public init(_dataDict: DataDict) { __data = _dataDict } + + public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Badge } + public static var __selections: [ApolloAPI.Selection] { [ + .field("__typename", String.self), + .field("a", String?.self), + ] } - @Deferred public var root: Root? - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] - ) - - let actual = subject.render(inlineFragment: allAnimals_asDog) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) - } - - func test__render_fragmentAccessor__givenDeferredInlineFragmentWithTrueCondition_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ... on Dog @defer(if: true, label: "root") { - species - } - } - } - """ - - let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _root = Deferred(_dataDict: _dataDict) - } - - @Deferred public var root: AsDog.Root? - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) - - let actual = subject.render(field: allAnimals) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) - } - - func test__render_fragmentAccessor__givenDeferredInlineFragmentWithTrueCondition_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ... on Dog @defer(if: true, label: "root") { - species - } - } - } - """ - - let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _root = Deferred(_dataDict: _dataDict) - } - - @Deferred public var root: Root? - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] - ) - - let actual = subject.render(inlineFragment: allAnimals_asDog) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) - } - - func test__render_fragmentAccessor__givenDeferredInlineFragmentWithFalseCondition_doesNotRenderConvenienceDeferredFragmentAccessor() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ... on Dog @defer(if: false, label: "root") { - species - } - } - } - """ - - let expected = """ - public var asDog: AsDog? { _asInlineFragment() } - - /// Parent Type: `Dog` - public struct AsDog: TestSchema.InlineFragment { - """ - - // when - try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) - - let actual = subject.render(field: allAnimals) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) - } - - func test__render_fragmentAccessor__givenDeferredInlineFragmentWithFalseCondition_doesNotRenderTypeCaseDeferredFragmentAccessor() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ... on Dog @defer(if: false, label: "root") { - species - } - } - } - """ - - let expected = """ - public var species: String? { __data["species"] } - } - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] - ) - - let actual = subject.render(inlineFragment: allAnimals_asDog) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) - } - - // MARK: - Fragment Accessors - Deferred Named Fragment - - func test__render_fragmentAccessor__givenDeferredNamedFragment_rendersDeferredFragmentAccessorAsOptional() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ...DogFragment @defer(label: "root") - } - } - - fragment DogFragment on Dog { - species - } - """ - - let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _dogFragment = Deferred(_dataDict: _dataDict) - } - - @Deferred public var dogFragment: DogFragment? - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] - ) - - let actual = subject.render(inlineFragment: allAnimals_asDog) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) - } - - func test__render_fragmentAccessor__givenDeferredNamedFragmentWithCondition_rendersDeferredFragmentAccessorAsOptional() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ...DogFragment @defer(if: "a", label: "root") - } - } - - fragment DogFragment on Dog { - species - } - """ - - let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _dogFragment = Deferred(_dataDict: _dataDict) - } - - @Deferred public var dogFragment: DogFragment? - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] - ) - - let actual = subject.render(inlineFragment: allAnimals_asDog) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) - } - - func test__render_fragmentAccessor__givenDeferredNamedFragmentWithTrueCondition_rendersDeferredFragmentAccessorAsOptional() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ...DogFragment @defer(if: true, label: "root") - } - } - - fragment DogFragment on Dog { - species - } - """ - - let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { - __data = _dataDict - _dogFragment = Deferred(_dataDict: _dataDict) - } - - @Deferred public var dogFragment: DogFragment? - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] - ) - - let actual = subject.render(inlineFragment: allAnimals_asDog) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) - } - - func test__render_fragmentAccessor__givenDeferredNamedFragmentWithFalseCondition_doesNotRenderDeferredFragmentAccessor() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String - species: String - genus: String - } - - type Dog implements Animal { - id: String - species: String - genus: String - name: String - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ...DogFragment @defer(if: false, label: "root") - } - } - - fragment DogFragment on Dog { - species - } - """ - - let expected = """ - public struct Fragments: FragmentContainer { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public var dogFragment: DogFragment { _toFragment() } - } - """ - - // when - try buildSubjectAndOperation() - let allAnimals_asDog = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] - ) - - let actual = subject.render(inlineFragment: allAnimals_asDog) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) - } - - // MARK: - Nested Selection Sets - - func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_rendersNestedSelectionSet() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - species: String! - predators: [Animal!] - } - """ - - document = """ - query TestOperation { - allAnimals { - predators { - species - } - } - } - """ - - let expected = """ - public var predators: [Predator]? { __data["predators"] } - - /// AllAnimal.Predator - public struct Predator: TestSchema.SelectionSet { - """ - - // when - try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) - - let actual = subject.render(field: allAnimals) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) - } - - func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_withIrregularPluralizationRule_rendersNestedSelectionSetWithCorrectSingularName() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - species: String! - people: [Animal!] - } - """ - - document = """ - query TestOperation { - allAnimals { - people { - species - } - } - } - """ - - let expected = """ - public var people: [Person]? { __data["people"] } - - /// AllAnimal.Person - public struct Person: TestSchema.SelectionSet { - """ - - // when - try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) - - let actual = subject.render(field: allAnimals) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) - } - - func test__render_nestedSelectionSets__givenDirectEntityFieldAsNonNullList_withIrregularPluralizationRule_rendersNestedSelectionSetWithCorrectSingularName() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - species: String! - people: [Animal!]! - } - """ - - document = """ - query TestOperation { - allAnimals { - people { - species - } - } - } - """ - - let expected = """ - public var people: [Person] { __data["people"] } - - /// AllAnimal.Person - public struct Person: TestSchema.SelectionSet { - """ - - // when - try buildSubjectAndOperation() - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) - - let actual = subject.render(field: allAnimals) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) - } - - func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_withCustomIrregularPluralizationRule_rendersNestedSelectionSetWithCorrectSingularName() throws { - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - species: String! - people: [Animal!] - } - """ - - document = """ - query TestOperation { - allAnimals { - people { - species - } - } - } - """ - - let expected = """ - public var people: [Peep]? { __data["people"] } - - /// AllAnimal.Peep - public struct Peep: TestSchema.SelectionSet { - """ - - // when - try buildSubjectAndOperation(inflectionRules: [ - ApolloCodegenLib.InflectionRule.irregular(singular: "Peep", plural: "people") - ]) - - let allAnimals = try XCTUnwrap( - operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField - ) - - let actual = subject.render(field: allAnimals) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) - } - - /// Explicit test for edge case surfaced in issue - /// [#1825](https://github.com/apollographql/apollo-ios/issues/1825) - func test__render_nestedSelectionSets__givenDirectEntityField_withTwoObjects_oneWithPluralizedNameAsObject_oneWithSingularNameAsList_rendersNestedSelectionSetsWithCorrectNames() throws { - // given - schemaSDL = """ - type Query { - badge: [Badge] - badges: ProductBadge - } - - type Badge { - a: String - } - - type ProductBadge { - b: String - } - """ - - document = """ - query TestOperation { - badge { - a - } - badges { - b - } - } - """ - - let expected = """ - public var badge: [Badge?]? { __data["badge"] } - public var badges: Badges? { __data["badges"] } - - /// Badge - public struct Badge: TestSchema.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Badge } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("a", String?.self), - ] } - - public var a: String? { __data["a"] } - } - - /// Badges - public struct Badges: TestSchema.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.ProductBadge } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("b", String?.self), - ] } - - public var b: String? { __data["b"] } - } - """ - - // when - try buildSubjectAndOperation() - - let query = try XCTUnwrap( - operation[field: "query"] as? IR.EntityField - ) - - let actual = subject.render(field: query) - - // then - expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) - } - - /// Explicit test for edge case surfaced in issue - /// [#1825](https://github.com/apollographql/apollo-ios/issues/1825) - func test__render_nestedSelectionSets__givenDirectEntityField_withTwoObjectsNonNullFields_oneWithPluralizedNameAsObject_oneWithSingularNameAsList_rendersNestedSelectionSetsWithCorrectNames() throws { - // given - schemaSDL = """ - type Query { - badge: [Badge!]! - badges: ProductBadge! - } - - type Badge { - a: String - } - - type ProductBadge { - b: String - } - """ - - document = """ - query TestOperation { - badge { - a - } - badges { - b - } - } - """ - - let expected = """ - public var badge: [Badge] { __data["badge"] } - public var badges: Badges { __data["badges"] } - - /// Badge - public struct Badge: TestSchema.SelectionSet { - public let __data: DataDict - public init(_dataDict: DataDict) { __data = _dataDict } - - public static var __parentType: ApolloAPI.ParentType { TestSchema.Objects.Badge } - public static var __selections: [ApolloAPI.Selection] { [ - .field("__typename", String.self), - .field("a", String?.self), - ] } - - public var a: String? { __data["a"] } + public var a: String? { __data["a"] } } /// Badges @@ -7398,6 +6610,8 @@ class SelectionSetTemplateTests: XCTestCase { // MARK: Nested Selection Sets - Deferred Inline Fragments func test__render_nestedSelectionSet__givenDeferredInlineFragment_rendersNestedSelectionSet() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { @@ -7456,6 +6670,8 @@ class SelectionSetTemplateTests: XCTestCase { } func test__render_nestedSelectionSet__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothNestedSelectionSets() throws { + throw XCTSkip() + // given schemaSDL = """ type Query { From d6de41af0dd573d2380f1f563bdd2999a166b286 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 17:23:57 -0700 Subject: [PATCH 13/41] Remove nested fragment defer root field builder tests --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 72 ------------------- 1 file changed, 72 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index f3d165b36..684bd26ac 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -5162,76 +5162,4 @@ class IRRootFieldBuilderTests: XCTestCase { )) } - // MARK: Deferred Fragments - Named Fragments - - func test__deferredFragments__givenDeferredNamedFragment_buildsDeferredInlineFragment() throws { - throw XCTSkip() - - // given - schemaSDL = """ - type Query { - allAnimals: [Animal!] - } - - interface Animal { - id: String! - species: String! - genus: String! - } - - type Dog implements Animal { - id: String! - species: String! - genus: String! - name: String! - } - """ - - document = """ - query TestOperation { - allAnimals { - __typename - id - ...DogFragment @defer(label: "root") - } - } - - fragment DogFragment on Dog { - species - } - """ - - // when - try buildSubjectRootField() - - // then - let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) - let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) - - let allAnimals = self.subject[field: "allAnimals"]?.selectionSet - expect(allAnimals?.selections.direct?.fields.values).to(shallowlyMatch([ - .mock("id", type: .nonNull(.scalar(Scalar_String))) - ])) - expect(allAnimals?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ - .mock(parentType: Object_Dog) - ])) -// expect(allAnimals?.scope.deferCondition).to(beNil()) - - let allAnimals_AsDog = allAnimals?[as: "Dog"] - expect(allAnimals_AsDog?.selections.direct?.fields).to(beEmpty()) - expect(allAnimals_AsDog?.selections.direct?.inlineFragments.values.map(\.selectionSet)).to(shallowlyMatch([ - .mock(parentType: Object_Dog) // ? should this match on the type Root.self instead - ])) -// expect(allAnimals_AsDog?.scope.deferCondition).to(beNil()) - -// let allAnimals_AsDog_Deferred = allAnimals_AsDog?[as: "Dog"] -// expect(allAnimals_AsDog_Deferred?.selections.direct?.fields.values).to(shallowlyMatch([ -// .mock("species", type: .nonNull(.scalar(Scalar_String))) -// ])) -// expect(allAnimals_AsDog_Deferred?.selections.direct?.inlineFragments).to(beEmpty()) -// expect(allAnimals_AsDog_Deferred?.scope.deferCondition.unsafelyUnwrapped).to(equal( -// DeferCondition(label: "root") -// )) - } - } From abd296449b6aeeb707b05ce7f5fd6bf17228105d Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 21:29:22 -0700 Subject: [PATCH 14/41] Match naming --- .../MockIRSubscripts.swift | 4 ++-- .../Sources/IR/IR+RootFieldBuilder.swift | 4 ++-- apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift | 10 +++++----- apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift index a92090e5c..c380d65ec 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift @@ -71,7 +71,7 @@ extension ScopeConditionalSubscriptAccessing { conditions = nil } - return IR.ScopeCondition(type: type, conditions: conditions, deferDirective: deferCondition) + return IR.ScopeCondition(type: type, conditions: conditions, deferCondition: deferCondition) } } @@ -154,7 +154,7 @@ extension IR.SelectionSet: ScopeConditionalSubscriptAccessing { let scope = ScopeCondition( type: self.parentType, conditions: self.inclusionConditions, - deferDirective: DeferCondition(label: label, variable: variable) + deferCondition: DeferCondition(label: label, variable: variable) ) return selections[scope] } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index fc72f1dda..a59c594fb 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -138,7 +138,7 @@ class RootFieldBuilder { self.containsDeferredFragments = typeInfo.scope.scopePath.containsDeferredFragments - if typeInfo.scope.scopePath.last.value.deferDirective == nil { + if typeInfo.scope.scopePath.last.value.deferCondition == nil { typeInfo.entity.selectionTree.mergeIn( selections: target.readOnlyView, with: typeInfo @@ -361,7 +361,7 @@ class RootFieldBuilder { let scope = ScopeCondition( type: scopeCondition.type, conditions: scopeCondition.conditions, - deferDirective: deferCondition + deferCondition: deferCondition ) let typePath = enclosingTypeInfo.scopePath.mutatingLast { $0.appending(scope) diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index 973d45628..00d1139e9 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -9,26 +9,26 @@ import Utilities public struct ScopeCondition: Hashable, CustomDebugStringConvertible { public let type: GraphQLCompositeType? public let conditions: InclusionConditions? - public let deferDirective: DeferCondition? + public let deferCondition: DeferCondition? init( type: GraphQLCompositeType? = nil, conditions: InclusionConditions? = nil, - deferDirective: DeferCondition? = nil + deferCondition: DeferCondition? = nil ) { self.type = type self.conditions = conditions - self.deferDirective = deferDirective + self.deferCondition = deferCondition } public var debugDescription: String { - [type?.debugDescription, conditions?.debugDescription, deferDirective?.debugDescription] + [type?.debugDescription, conditions?.debugDescription, deferCondition?.debugDescription] .compactMap { $0 } .joined(separator: " ") } var isEmpty: Bool { - type == nil && (conditions?.isEmpty ?? true) && deferDirective == nil + type == nil && (conditions?.isEmpty ?? true) && deferCondition == nil } } diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift index b266cc092..e47ef18a9 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift @@ -164,13 +164,13 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { extension LinkedList where T == ScopeCondition { var containsDeferredFragments: Bool { var node: Node? = last - var deferDirective = node?.value.deferDirective + var deferCondition = node?.value.deferCondition - while node?.previous != nil && deferDirective == nil { + while node?.previous != nil && deferCondition == nil { node = node?.previous - deferDirective = node?.value.deferDirective + deferCondition = node?.value.deferCondition } - return deferDirective != nil + return deferCondition != nil } } From a2e0129a6bfe0d927be54ea22b787aa2ac71768f Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 21:41:59 -0700 Subject: [PATCH 15/41] Rename to singular from plural --- .../IR+Mocking.swift | 4 ++-- .../CodeGenIR/IRRootFieldBuilderTests.swift | 14 +++++++------- .../Templates/OperationDefinitionTemplate.swift | 2 +- .../Sources/IR/IR+NamedFragment.swift | 6 +++--- apollo-ios-codegen/Sources/IR/IR+Operation.swift | 6 +++--- .../Sources/IR/IR+RootFieldBuilder.swift | 10 +++++----- .../Sources/IR/IR+SelectionSet.swift | 2 +- apollo-ios-codegen/Sources/IR/IRBuilder.swift | 4 ++-- 8 files changed, 24 insertions(+), 24 deletions(-) diff --git a/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift b/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift index 0f321cc0a..5388da8f4 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/IR+Mocking.swift @@ -96,7 +96,7 @@ extension IR.Operation { public static func mock( definition: CompilationResult.OperationDefinition? = nil, referencedFragments: OrderedSet = [], - containsDeferredFragments: Bool = false + containsDeferredFragment: Bool = false ) -> IR.Operation { let definition = definition ?? .mock() return IR.Operation.init( @@ -116,7 +116,7 @@ extension IR.Operation { ]) ), referencedFragments: referencedFragments, - containsDeferredFragments: containsDeferredFragments + containsDeferredFragment: containsDeferredFragment ) } diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 684bd26ac..5e4deae3d 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4366,7 +4366,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.containsDeferredFragments).to(beFalse()) + expect(self.result.containsDeferredFragment).to(beFalse()) } func test__deferredFragments__givenDeferredInlineFragment_hasDeferredFragmentsTrue() throws { @@ -4406,7 +4406,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.containsDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragment).to(beTrue()) } func test__deferredFragments__givenDeferredInlineFragmentWithCondition_hasDeferredFragmentsTrue() throws { @@ -4446,7 +4446,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.containsDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragment).to(beTrue()) } func test__deferredFragments__givenDeferredInlineFragmentWithConditionFalse_hasDeferredFragmentsFalse() throws { @@ -4486,7 +4486,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.containsDeferredFragments).to(beFalse()) + expect(self.result.containsDeferredFragment).to(beFalse()) } func test__deferredFragments__givenDeferredNamedFragment_onDifferentTypeCase_hasDeferredFragmentsTrue() throws { @@ -4528,7 +4528,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.containsDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragment).to(beTrue()) } func test__deferredFragments__givenDeferredInlineFragment_withinNamedFragment_hasDeferredFragmentsTrue() throws { @@ -4572,7 +4572,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.containsDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragment).to(beTrue()) } func test__deferredFragments__givenDeferredNamedFragment_withSelectionOnDifferentTypeCase_hasDeferredFragmentsTrue() throws { @@ -4623,7 +4623,7 @@ class IRRootFieldBuilderTests: XCTestCase { try buildSubjectRootField() // then - expect(self.result.containsDeferredFragments).to(beTrue()) + expect(self.result.containsDeferredFragment).to(beTrue()) } #warning("tests to match IR struct changes") diff --git a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift index 934b484b6..0cbb7c287 100644 --- a/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift +++ b/apollo-ios-codegen/Sources/ApolloCodegenLib/Templates/OperationDefinitionTemplate.swift @@ -27,7 +27,7 @@ struct OperationDefinitionTemplate: OperationTemplateRenderer { accessControlRenderer: { accessControlModifier(for: .member) }() )) - \(section: DeferredProperties(operation.containsDeferredFragments)) + \(section: DeferredProperties(operation.containsDeferredFragment)) \(section: VariableProperties(operation.definition.variables)) diff --git a/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift b/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift index 53172a883..dde952199 100644 --- a/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift +++ b/apollo-ios-codegen/Sources/IR/IR+NamedFragment.swift @@ -10,7 +10,7 @@ public class NamedFragment: Hashable, CustomDebugStringConvertible { /// `True` if any selection set, or nested selection set, within the fragment contains any /// fragment marked with the `@defer` directive. - public let containsDeferredFragments: Bool + public let containsDeferredFragment: Bool /// All of the Entities that exist in the fragment's selection set, /// keyed by their relative location (ie. path) within the fragment. @@ -26,13 +26,13 @@ public class NamedFragment: Hashable, CustomDebugStringConvertible { definition: CompilationResult.FragmentDefinition, rootField: EntityField, referencedFragments: OrderedSet, - containsDeferredFragments: Bool = false, + containsDeferredFragment: Bool = false, entities: [Entity.Location: Entity] ) { self.definition = definition self.rootField = rootField self.referencedFragments = referencedFragments - self.containsDeferredFragments = containsDeferredFragments + self.containsDeferredFragment = containsDeferredFragment self.entities = entities } diff --git a/apollo-ios-codegen/Sources/IR/IR+Operation.swift b/apollo-ios-codegen/Sources/IR/IR+Operation.swift index 783397585..1880b5d9c 100644 --- a/apollo-ios-codegen/Sources/IR/IR+Operation.swift +++ b/apollo-ios-codegen/Sources/IR/IR+Operation.swift @@ -13,17 +13,17 @@ public class Operation { /// `True` if any selection set, or nested selection set, within the operation contains any /// fragment marked with the `@defer` directive. - public let containsDeferredFragments: Bool + public let containsDeferredFragment: Bool init( definition: CompilationResult.OperationDefinition, rootField: EntityField, referencedFragments: OrderedSet, - containsDeferredFragments: Bool + containsDeferredFragment: Bool ) { self.definition = definition self.rootField = rootField self.referencedFragments = referencedFragments - self.containsDeferredFragments = containsDeferredFragments + self.containsDeferredFragment = containsDeferredFragment } } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index a59c594fb..28c87fba7 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -69,7 +69,7 @@ class RootFieldBuilder { let rootField: EntityField let referencedFragments: ReferencedFragments let entities: [Entity.Location: Entity] - let containsDeferredFragments: Bool + let containsDeferredFragment: Bool } typealias ReferencedFragments = OrderedSet @@ -87,7 +87,7 @@ class RootFieldBuilder { private let rootEntity: Entity private let entityStorage: RootFieldEntityStorage private var referencedFragments: ReferencedFragments = [] - @IsEverTrue private var containsDeferredFragments: Bool + @IsEverTrue private var containsDeferredFragment: Bool private var schema: Schema { ir.schema } @@ -125,7 +125,7 @@ class RootFieldBuilder { rootField: EntityField(rootField, selectionSet: rootIrSelectionSet), referencedFragments: referencedFragments, entities: entityStorage.entitiesForFields, - containsDeferredFragments: containsDeferredFragments + containsDeferredFragment: containsDeferredFragment ) } @@ -136,7 +136,7 @@ class RootFieldBuilder { ) { addSelections(from: selectionSet, to: target, atTypePath: typeInfo) - self.containsDeferredFragments = typeInfo.scope.scopePath.containsDeferredFragments + self.containsDeferredFragment = typeInfo.scope.scopePath.containsDeferredFragment if typeInfo.scope.scopePath.last.value.deferCondition == nil { typeInfo.entity.selectionTree.mergeIn( @@ -415,7 +415,7 @@ class RootFieldBuilder { referencedFragments.append(fragment) referencedFragments.append(contentsOf: fragment.referencedFragments) - self.containsDeferredFragments = fragment.containsDeferredFragments + self.containsDeferredFragment = fragment.containsDeferredFragment let scopePath = scopeCondition.isEmpty ? parentTypeInfo.scopePath : diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift index e47ef18a9..b0eab6c30 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift @@ -162,7 +162,7 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { } extension LinkedList where T == ScopeCondition { - var containsDeferredFragments: Bool { + var containsDeferredFragment: Bool { var node: Node? = last var deferCondition = node?.value.deferCondition diff --git a/apollo-ios-codegen/Sources/IR/IRBuilder.swift b/apollo-ios-codegen/Sources/IR/IRBuilder.swift index 8255cec4a..a8b8bb784 100644 --- a/apollo-ios-codegen/Sources/IR/IRBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IRBuilder.swift @@ -54,7 +54,7 @@ public class IRBuilder { definition: operationDefinition, rootField: result.rootField, referencedFragments: result.referencedFragments, - containsDeferredFragments: result.containsDeferredFragments + containsDeferredFragment: result.containsDeferredFragment ) } @@ -83,7 +83,7 @@ public class IRBuilder { definition: fragmentDefinition, rootField: result.rootField, referencedFragments: result.referencedFragments, - containsDeferredFragments: result.containsDeferredFragments, + containsDeferredFragment: result.containsDeferredFragment, entities: result.entities ) From f15cd2fcd2c41a8871e3e2cb1f2754877b2129c6 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 21:45:40 -0700 Subject: [PATCH 16/41] Rename file to match type --- .../Sources/IR/{IR+IsDeferred.swift => IR+DeferCondition.swift} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename apollo-ios-codegen/Sources/IR/{IR+IsDeferred.swift => IR+DeferCondition.swift} (100%) diff --git a/apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift b/apollo-ios-codegen/Sources/IR/IR+DeferCondition.swift similarity index 100% rename from apollo-ios-codegen/Sources/IR/IR+IsDeferred.swift rename to apollo-ios-codegen/Sources/IR/IR+DeferCondition.swift From a15f21a3394ec6ae8be47761bc1cd847438b0e60 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 10 Oct 2023 22:07:48 -0700 Subject: [PATCH 17/41] Skip defer selection set template tests --- .../SelectionSetTemplateTests.swift | 832 ++++++++++++++++++ 1 file changed, 832 insertions(+) diff --git a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift index 7d0ce0f12..9e9d21146 100644 --- a/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift +++ b/Tests/ApolloCodegenTests/CodeGeneration/Templates/SelectionSet/SelectionSetTemplateTests.swift @@ -5623,6 +5623,838 @@ class SelectionSetTemplateTests: XCTestCase { expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) } + // MARK: - Fragment Accessors - Deferred Inline Fragment + + func test__render_fragmentAccessor__givenDeferredInlineFragment_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "root") { + species + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: AsDog.Root? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragment_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "root") { + species + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: Root? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothConvenienceDeferredFragmentAccessorsAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "one") { + species + } + ... on Dog @defer(label: "two") { + genus + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _one = Deferred(_dataDict: _dataDict) + _two = Deferred(_dataDict: _dataDict) + } + + @Deferred public var one: AsDog.One? + @Deferred public var two: AsDog.Two? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentsWithDifferentLabels_rendersBothTypeCaseDeferredFragmentAccessorsAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "one") { + species + } + ... on Dog @defer(label: "two") { + genus + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _one = Deferred(_dataDict: _dataDict) + _two = Deferred(_dataDict: _dataDict) + } + + @Deferred public var one: One? + @Deferred public var two: Two? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithCondition_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: "a", label: "root") { + species + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: AsDog.Root? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithCondition_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: "a", label: "root") { + species + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: Root? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithTrueCondition_rendersConvenienceDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: true, label: "root") { + species + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: AsDog.Root? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 17, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithTrueCondition_rendersTypeCaseDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: true, label: "root") { + species + } + } + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _root = Deferred(_dataDict: _dataDict) + } + + @Deferred public var root: Root? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithFalseCondition_doesNotRenderConvenienceDeferredFragmentAccessor() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: false, label: "root") { + species + } + } + } + """ + + let expected = """ + public var asDog: AsDog? { _asInlineFragment() } + + /// Parent Type: `Dog` + public struct AsDog: TestSchema.InlineFragment { + """ + + // when + try buildSubjectAndOperation() + let allAnimals = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"] as? IR.EntityField + ) + + let actual = subject.render(field: allAnimals) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredInlineFragmentWithFalseCondition_doesNotRenderTypeCaseDeferredFragmentAccessor() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(if: false, label: "root") { + species + } + } + } + """ + + let expected = """ + public var species: String? { __data["species"] } + } + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 12, ignoringExtraLines: true)) + } + + // MARK: - Fragment Accessors - Deferred Named Fragment + + func test__render_fragmentAccessor__givenDeferredNamedFragment_rendersDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _dogFragment = Deferred(_dataDict: _dataDict) + } + + @Deferred public var dogFragment: DogFragment? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 14, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredNamedFragmentWithCondition_rendersDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: "a", label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _dogFragment = Deferred(_dataDict: _dataDict) + } + + @Deferred public var dogFragment: DogFragment? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredNamedFragmentWithTrueCondition_rendersDeferredFragmentAccessorAsOptional() throws { + throw XCTSkip() + + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: true, label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _dogFragment = Deferred(_dataDict: _dataDict) + } + + @Deferred public var dogFragment: DogFragment? + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) + } + + func test__render_fragmentAccessor__givenDeferredNamedFragmentWithFalseCondition_doesNotRenderDeferredFragmentAccessor() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: false, label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + let expected = """ + public struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { __data = _dataDict } + + public var dogFragment: DogFragment { _toFragment() } + } + """ + + // when + try buildSubjectAndOperation() + let allAnimals_asDog = try XCTUnwrap( + operation[field: "query"]?[field: "allAnimals"]?[as: "Dog"] + ) + + let actual = subject.render(inlineFragment: allAnimals_asDog) + + // then + expect(actual).to(equalLineByLine(expected, atLine: 15, ignoringExtraLines: true)) + } + // MARK: - Nested Selection Sets func test__render_nestedSelectionSets__givenDirectEntityFieldAsList_rendersNestedSelectionSet() throws { From b294e08f61fa85eeef89629df5296101689a125a Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 12 Oct 2023 11:57:36 -0700 Subject: [PATCH 18/41] Add convenience defer condition property --- apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift | 2 +- apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 28c87fba7..0f29b4377 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -138,7 +138,7 @@ class RootFieldBuilder { self.containsDeferredFragment = typeInfo.scope.scopePath.containsDeferredFragment - if typeInfo.scope.scopePath.last.value.deferCondition == nil { + if typeInfo.deferCondition == nil { typeInfo.entity.selectionTree.mergeIn( selections: target.readOnlyView, with: typeInfo diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift index b0eab6c30..1533ec931 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift @@ -23,6 +23,8 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { public var inclusionConditions: InclusionConditions? { scope.scopePath.last.value.conditions } + public var deferCondition: DeferCondition? { scope.scopePath.last.value.deferCondition } + /// Indicates if the `SelectionSet` represents a root selection set. /// If `true`, the `SelectionSet` belongs to a field directly. /// If `false`, the `SelectionSet` belongs to a conditional selection set enclosed From 979eb78b511ae1ada614a0fd8019b423c5b3cf63 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 12 Oct 2023 13:25:06 -0700 Subject: [PATCH 19/41] Add direct selection matcher for deferred inline fragments --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 16 +++---- .../TestHelpers/IRMatchers.swift | 45 +++++++++++++++---- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 5e4deae3d..b315d2329 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4626,8 +4626,6 @@ class IRRootFieldBuilderTests: XCTestCase { expect(self.result.containsDeferredFragment).to(beTrue()) } - #warning("tests to match IR struct changes") - // MARK: Deferred Fragments - Inline Fragments func test__deferredFragments__givenDeferredInlineFragment_buildsDeferredInlineFragment() throws { @@ -4689,7 +4687,7 @@ class IRRootFieldBuilderTests: XCTestCase { SelectionSetMatcher( parentType: Object_Dog, directSelections: [ - .inlineFragment(parentType: Object_Dog) + .deferred(Object_Dog, label: "root"), ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), @@ -4775,7 +4773,7 @@ class IRRootFieldBuilderTests: XCTestCase { SelectionSetMatcher( parentType: Object_Dog, directSelections: [ - .inlineFragment(parentType: Object_Dog) + .deferred(Object_Dog, label: "root", variable: "a"), ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), @@ -4861,7 +4859,7 @@ class IRRootFieldBuilderTests: XCTestCase { SelectionSetMatcher( parentType: Object_Dog, directSelections: [ - .inlineFragment(parentType: Object_Dog) + .deferred(Object_Dog, label: "root"), ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), @@ -5024,8 +5022,8 @@ class IRRootFieldBuilderTests: XCTestCase { SelectionSetMatcher( parentType: Object_Dog, directSelections: [ - .inlineFragment(parentType: Object_Dog), - .inlineFragment(parentType: Object_Dog), + .deferred(Object_Dog, label: "one"), + .deferred(Object_Dog, label: "two"), ], mergedSelections: [ .field("id", type: .nonNull(.scalar(Scalar_String))), @@ -5129,7 +5127,7 @@ class IRRootFieldBuilderTests: XCTestCase { SelectionSetMatcher( parentType: Object_Bird, directSelections: [ - .inlineFragment(parentType: Object_Bird), + .deferred(Object_Bird, label: "bird"), ] ) )) @@ -5138,7 +5136,7 @@ class IRRootFieldBuilderTests: XCTestCase { SelectionSetMatcher( parentType: Object_Cat, directSelections: [ - .inlineFragment(parentType: Object_Cat), + .deferred(Object_Cat, label: "cat"), ] ) )) diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index d1a44350e..92956020d 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift @@ -212,10 +212,13 @@ public enum ShallowSelectionMatcher { public static func inlineFragment( parentType: GraphQLCompositeType, - inclusionConditions: [IR.InclusionCondition]? = nil + inclusionConditions: [IR.InclusionCondition]? = nil, + deferCondition: IR.DeferCondition? = nil ) -> ShallowSelectionMatcher { .shallowInlineFragment(ShallowInlineFragmentMatcher( - parentType: parentType, inclusionConditions: inclusionConditions + parentType: parentType, + inclusionConditions: inclusionConditions, + deferCondition: deferCondition )) } @@ -252,6 +255,19 @@ public enum ShallowSelectionMatcher { inclusionConditions: inclusionConditions )) } + + public static func deferred( + _ parentType: GraphQLCompositeType, + label: String, + variable: String? = nil, + inclusionConditions: [IR.InclusionCondition]? = nil + ) -> ShallowSelectionMatcher { + .shallowInlineFragment(ShallowInlineFragmentMatcher( + parentType: parentType, + inclusionConditions: inclusionConditions, + deferCondition: IR.DeferCondition(label: label, variable: variable) + )) + } } // MARK: - Shallow Field Matcher @@ -349,12 +365,16 @@ fileprivate func shallowlyMatch(expected: ShallowFieldMatcher, actual: IR.Field) public struct ShallowInlineFragmentMatcher: Equatable, CustomDebugStringConvertible { let parentType: GraphQLCompositeType let inclusionConditions: IR.InclusionConditions? + let deferCondition: IR.DeferCondition? init( parentType: GraphQLCompositeType, - inclusionConditions: [IR.InclusionCondition]? + inclusionConditions: [IR.InclusionCondition]?, + deferCondition: IR.DeferCondition? ) { self.parentType = parentType + self.deferCondition = deferCondition + if let inclusionConditions = inclusionConditions { self.inclusionConditions = IR.InclusionConditions.allOf(inclusionConditions).conditions } else { @@ -364,9 +384,14 @@ public struct ShallowInlineFragmentMatcher: Equatable, CustomDebugStringConverti public static func mock( parentType: GraphQLCompositeType, - inclusionConditions: [IR.InclusionCondition]? = nil + inclusionConditions: [IR.InclusionCondition]? = nil, + deferCondition: IR.DeferCondition? = nil ) -> ShallowInlineFragmentMatcher { - self.init(parentType: parentType, inclusionConditions: inclusionConditions) + self.init( + parentType: parentType, + inclusionConditions: inclusionConditions, + deferCondition: deferCondition + ) } public var debugDescription: String { @@ -408,9 +433,13 @@ fileprivate func shallowlyMatch( return PredicateResult(status: .matches, message: message) } -fileprivate func shallowlyMatch(expected: ShallowInlineFragmentMatcher, actual: IR.SelectionSet) -> Bool { - return expected.parentType == actual.typeInfo.parentType && - expected.inclusionConditions == actual.inclusionConditions +fileprivate func shallowlyMatch( + expected: ShallowInlineFragmentMatcher, + actual: IR.SelectionSet +) -> Bool { + return expected.parentType == actual.typeInfo.parentType + && expected.inclusionConditions == actual.inclusionConditions + && expected.deferCondition == actual.deferCondition } // MARK: - Shallow Fragment Spread Matcher From ad5e8cc956155b26b4822799d5b225ff732458a9 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 13 Oct 2023 10:59:38 -0700 Subject: [PATCH 20/41] Remove unneeded matcher --- Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index 92956020d..d322f45e2 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift @@ -212,13 +212,12 @@ public enum ShallowSelectionMatcher { public static func inlineFragment( parentType: GraphQLCompositeType, - inclusionConditions: [IR.InclusionCondition]? = nil, - deferCondition: IR.DeferCondition? = nil + inclusionConditions: [IR.InclusionCondition]? = nil ) -> ShallowSelectionMatcher { .shallowInlineFragment(ShallowInlineFragmentMatcher( parentType: parentType, inclusionConditions: inclusionConditions, - deferCondition: deferCondition + deferCondition: nil )) } From 64a88e410ae6c68ded658c040ca043d0cbc69f01 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sun, 15 Oct 2023 19:24:25 -0700 Subject: [PATCH 21/41] [2/x] Refactor root field builder for deferred named fragments (#89) --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 296 +++++++++++++++++- .../TestHelpers/IRMatchers.swift | 75 +++-- .../Sources/IR/IR+RootFieldBuilder.swift | 139 +++++--- 3 files changed, 438 insertions(+), 72 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index b315d2329..06e10fb42 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4499,14 +4499,11 @@ class IRRootFieldBuilderTests: XCTestCase { interface Animal { id: String species: String - genus: String } type Dog implements Animal { id: String species: String - genus: String - name: String } """ @@ -5160,4 +5157,297 @@ class IRRootFieldBuilderTests: XCTestCase { )) } + // MARK: Deferred Fragments - Named Fragments + + func test__deferredFragments__givenDeferredNamedFragment_buildsDeferredNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + type Dog implements Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Fragment_DogFragment, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + .field("species", type: .string()), // wrong + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_DogFragment), // wrong + ] + ) + )) + #warning("fix these 'wrong' merges") + } + + func test__deferredFragments__givenDeferredNamedFragmentWithVariableCondition_buildsDeferredNamedFragmentWithVariable() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + type Dog implements Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: "a", label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Fragment_DogFragment, label: "root", variable: "a"), + ], + mergedSelections: [ + .field("id", type: .string()), + .field("species", type: .string()), // wrong + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_DogFragment), // wrong + ] + ) + )) + #warning("fix these 'wrong' merges") + } + + func test__deferredFragments__givenDeferredNamedFragmentWithTrueCondition_buildsDeferredNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + type Dog implements Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: true, label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Fragment_DogFragment, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + .field("species", type: .string()), // wrong + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_DogFragment), // wrong + ] + ) + )) + #warning("fix these 'wrong' merges") + } + + func test__deferredFragments__givenDeferredNamedFragmentWithFalseCondition_doesNotBuildDeferredNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + type Dog implements Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment @defer(if: false, label: "root") + } + } + + fragment DogFragment on Dog { + species + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .fragmentSpread("DogFragment", type: Object_Dog), + ], + mergedSelections: [ + .field("id", type: .string()), + .field("species", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog_DogFragment), + ] + ) + )) + } + } diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index d322f45e2..cd207f4a6 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift @@ -229,7 +229,8 @@ public enum ShallowSelectionMatcher { .shallowFragmentSpread(ShallowFragmentSpreadMatcher( name: name, type: type, - inclusionConditions: inclusionConditions + inclusionConditions: inclusionConditions, + deferCondition: nil )) } @@ -240,7 +241,8 @@ public enum ShallowSelectionMatcher { .shallowFragmentSpread(ShallowFragmentSpreadMatcher( name: fragment.name, type: fragment.type, - inclusionConditions: inclusionConditions + inclusionConditions: inclusionConditions, + deferCondition: nil )) } @@ -251,22 +253,35 @@ public enum ShallowSelectionMatcher { .shallowFragmentSpread(ShallowFragmentSpreadMatcher( name: fragment.name, type: fragment.type, - inclusionConditions: inclusionConditions + inclusionConditions: inclusionConditions, + deferCondition: nil )) } public static func deferred( _ parentType: GraphQLCompositeType, label: String, - variable: String? = nil, - inclusionConditions: [IR.InclusionCondition]? = nil + variable: String? = nil ) -> ShallowSelectionMatcher { .shallowInlineFragment(ShallowInlineFragmentMatcher( parentType: parentType, - inclusionConditions: inclusionConditions, + inclusionConditions: nil, deferCondition: IR.DeferCondition(label: label, variable: variable) )) } + + public static func deferred( + _ fragment: CompilationResult.FragmentDefinition, + label: String, + variable: String? = nil + ) -> ShallowSelectionMatcher { + .shallowFragmentSpread(ShallowFragmentSpreadMatcher( + name: fragment.name, + type: fragment.type, + inclusionConditions: nil, + deferCondition: DeferCondition(label: label, variable: variable) + )) + } } // MARK: - Shallow Field Matcher @@ -395,7 +410,9 @@ public struct ShallowInlineFragmentMatcher: Equatable, CustomDebugStringConverti public var debugDescription: String { TemplateString(""" - ... on \(parentType.debugDescription)\(ifLet: inclusionConditions, { " \($0.debugDescription)"}) + ... on \(parentType.debugDescription)\ + \(ifLet: inclusionConditions, { " \($0.debugDescription)"})\ + \(ifLet: deferCondition, { " \($0.debugDescription)"}) """).description } } @@ -447,14 +464,18 @@ public struct ShallowFragmentSpreadMatcher: Equatable, CustomDebugStringConverti let name: String let type: GraphQLCompositeType let inclusionConditions: AnyOf? + let deferCondition: IR.DeferCondition? init( name: String, type: GraphQLCompositeType, - inclusionConditions: [IR.InclusionCondition]? + inclusionConditions: [IR.InclusionCondition]?, + deferCondition: IR.DeferCondition? ) { self.name = name self.type = type + self.deferCondition = deferCondition + if let inclusionConditions = inclusionConditions, let evaluatedConditions = IR.InclusionConditions.allOf(inclusionConditions).conditions { self.inclusionConditions = AnyOf(evaluatedConditions) @@ -463,36 +484,51 @@ public struct ShallowFragmentSpreadMatcher: Equatable, CustomDebugStringConverti } } + @_disfavoredOverload init( name: String, type: GraphQLCompositeType, - inclusionConditions: AnyOf? + inclusionConditions: AnyOf?, + deferCondition: IR.DeferCondition? ) { self.name = name self.type = type self.inclusionConditions = inclusionConditions + self.deferCondition = deferCondition } public static func mock( _ name: String, type: GraphQLCompositeType, - inclusionConditions: AnyOf? = nil + inclusionConditions: AnyOf? = nil, + deferCondition: IR.DeferCondition? = nil ) -> ShallowFragmentSpreadMatcher { - self.init(name: name, type: type, inclusionConditions: inclusionConditions) + self.init( + name: name, + type: type, + inclusionConditions: inclusionConditions, + deferCondition: deferCondition + ) } public static func mock( _ fragment: CompilationResult.FragmentDefinition, - inclusionConditions: AnyOf? = nil + inclusionConditions: AnyOf? = nil, + deferCondition: IR.DeferCondition? = nil ) -> ShallowFragmentSpreadMatcher { - self.init(name: fragment.name, type: fragment.type, inclusionConditions: inclusionConditions) + self.init( + name: fragment.name, + type: fragment.type, + inclusionConditions: inclusionConditions, + deferCondition: deferCondition + ) } public var debugDescription: String { TemplateString(""" - fragment \(name) on \(type.debugDescription)\(ifLet: inclusionConditions, { - " \($0.debugDescription)" - }) + fragment \(name) on \(type.debugDescription)\ + \(ifLet: inclusionConditions, { " \($0.debugDescription)" }) + \(ifLet: deferCondition, { " \($0.debugDescription)" }) """).description } } @@ -538,9 +574,10 @@ fileprivate func shallowlyMatch( } fileprivate func shallowlyMatch(expected: ShallowFragmentSpreadMatcher, actual: IR.NamedFragmentSpread) -> Bool { - return expected.name == actual.fragment.name && - expected.type == actual.fragment.type && - expected.inclusionConditions == actual.inclusionConditions + return expected.name == actual.fragment.name + && expected.type == actual.fragment.type + && expected.inclusionConditions == actual.inclusionConditions + && expected.deferCondition == actual.typeInfo.deferCondition } // MARK: - Predicate Mapping diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 0f29b4377..3ed416fba 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -136,8 +136,6 @@ class RootFieldBuilder { ) { addSelections(from: selectionSet, to: target, atTypePath: typeInfo) - self.containsDeferredFragment = typeInfo.scope.scopePath.containsDeferredFragment - if typeInfo.deferCondition == nil { typeInfo.entity.selectionTree.mergeIn( selections: target.readOnlyView, @@ -173,46 +171,7 @@ class RootFieldBuilder { add(inlineFragment, from: selection, to: target, atTypePath: typeInfo) case let .fragmentSpread(fragmentSpread): - guard let scope = scopeCondition(for: fragmentSpread, in: typeInfo) else { - return - } - let selectionSetScope = typeInfo.scope - - var matchesType: Bool { - guard let typeCondition = scope.type else { return true } - return selectionSetScope.matches(typeCondition) - } - let matchesScope = selectionSetScope.matches(scope) - - if matchesScope { - let irFragmentSpread = buildNamedFragmentSpread( - fromFragment: fragmentSpread, - with: scope, - spreadIntoParentWithTypePath: typeInfo - ) - target.mergeIn(irFragmentSpread) - - } else { - let irTypeCaseEnclosingFragment = buildInlineFragmentSpread( - from: CompilationResult.SelectionSet( - parentType: fragmentSpread.parentType, - selections: [selection] - ), - with: scope, - inParentTypePath: typeInfo, - deferCondition: (fragmentSpread.deferCondition != nil ? .init(fragmentSpread.deferCondition!) : nil) - ) - #warning("remove force unwrap above") - - target.mergeIn(irTypeCaseEnclosingFragment) - - if matchesType { - typeInfo.entity.selectionTree.mergeIn( - selections: irTypeCaseEnclosingFragment.selectionSet.selections.direct.unsafelyUnwrapped.readOnlyView, - with: typeInfo - ) - } - } + add(fragmentSpread, from: selection, to: target, atTypePath: typeInfo) } } @@ -231,18 +190,19 @@ class RootFieldBuilder { } let inlineSelectionSet = inlineFragment.selectionSet + let matchesScope = typeInfo.scope.matches(scope) - switch (typeInfo.scope.matches(scope), inlineFragment.deferCondition) { + switch (matchesScope, inlineFragment.deferCondition) { case let (true, .some(deferCondition)): let irTypeCase = buildInlineFragmentSpread( from: inlineSelectionSet, with: scope, inParentTypePath: typeInfo, - deferCondition: .init(deferCondition) + deferCondition: DeferCondition(deferCondition) ) target.mergeIn(irTypeCase) - case (true, .none): + case (true, nil): addSelections(from: inlineSelectionSet, to: target, atTypePath: typeInfo) case (false, .some): @@ -253,7 +213,7 @@ class RootFieldBuilder { ) target.mergeIn(irTypeCase) - case (false, .none): + case (false, nil): let irTypeCase = buildInlineFragmentSpread( from: inlineSelectionSet, with: scope, @@ -263,6 +223,75 @@ class RootFieldBuilder { } } + private func add( + _ fragmentSpread: CompilationResult.FragmentSpread, + from selection: CompilationResult.Selection, + to target: DirectSelections, + atTypePath typeInfo: SelectionSet.TypeInfo + ) { + guard let scope = scopeCondition( + for: fragmentSpread, + in: typeInfo, + deferCondition: fragmentSpread.deferCondition + ) else { + return + } + + let selectionSetScope = typeInfo.scope + let matchesScope = selectionSetScope.matches(scope) + + switch (matchesScope, fragmentSpread.deferCondition) { + case let (true, .some(deferCondition)): + let irFragmentSpread = buildNamedFragmentSpread( + fromFragment: fragmentSpread, + with: scope, + spreadIntoParentWithTypePath: typeInfo, + deferCondition: DeferCondition(deferCondition) + ) + target.mergeIn(irFragmentSpread) + + case (true, nil): + let irFragmentSpread = buildNamedFragmentSpread( + fromFragment: fragmentSpread, + with: scope, + spreadIntoParentWithTypePath: typeInfo + ) + target.mergeIn(irFragmentSpread) + + case (false, .some): + let irTypeCase = buildInlineFragmentSpread( + toWrap: selection, + with: scope, + inParentTypePath: typeInfo + ) + target.mergeIn(irTypeCase) + + case (false, nil): + let irTypeCaseEnclosingFragment = buildInlineFragmentSpread( + from: CompilationResult.SelectionSet( + parentType: fragmentSpread.parentType, + selections: [selection] + ), + with: scope, + inParentTypePath: typeInfo + ) + + target.mergeIn(irTypeCaseEnclosingFragment) + + var matchesType: Bool { + guard let typeCondition = scope.type else { return true } + return selectionSetScope.matches(typeCondition) + } + + if matchesType { + typeInfo.entity.selectionTree.mergeIn( + selections: irTypeCaseEnclosingFragment.selectionSet.selections.direct.unsafelyUnwrapped.readOnlyView, + with: typeInfo + ) + } + } + } + private func scopeCondition( for conditionalSelectionSet: ConditionallyIncludable, in parentTypePath: SelectionSet.TypeInfo, @@ -363,6 +392,9 @@ class RootFieldBuilder { conditions: scopeCondition.conditions, deferCondition: deferCondition ) + + self.containsDeferredFragment = (scope.deferCondition != nil) + let typePath = enclosingTypeInfo.scopePath.mutatingLast { $0.appending(scope) } @@ -409,18 +441,25 @@ class RootFieldBuilder { private func buildNamedFragmentSpread( fromFragment fragmentSpread: CompilationResult.FragmentSpread, with scopeCondition: ScopeCondition, - spreadIntoParentWithTypePath parentTypeInfo: SelectionSet.TypeInfo + spreadIntoParentWithTypePath parentTypeInfo: SelectionSet.TypeInfo, + deferCondition: DeferCondition? = nil ) -> NamedFragmentSpread { let fragment = ir.build(fragment: fragmentSpread.fragment) referencedFragments.append(fragment) referencedFragments.append(contentsOf: fragment.referencedFragments) - self.containsDeferredFragment = fragment.containsDeferredFragment + let scope = ScopeCondition( + type: scopeCondition.type, + conditions: scopeCondition.conditions, + deferCondition: deferCondition + ) + + self.containsDeferredFragment = fragment.containsDeferredFragment || scope.deferCondition != nil let scopePath = scopeCondition.isEmpty ? parentTypeInfo.scopePath : parentTypeInfo.scopePath.mutatingLast { - $0.appending(scopeCondition) + $0.appending(scope) } let typeInfo = SelectionSet.TypeInfo( @@ -431,7 +470,7 @@ class RootFieldBuilder { let fragmentSpread = NamedFragmentSpread( fragment: fragment, typeInfo: typeInfo, - inclusionConditions: AnyOf(scopeCondition.conditions) + inclusionConditions: AnyOf(scope.conditions) ) entityStorage.mergeAllSelectionsIntoEntitySelectionTrees(from: fragmentSpread) From 97226a0e047ce346e69ecef9f4a35e579dadaa44 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Oct 2023 10:38:51 -0700 Subject: [PATCH 22/41] More contextual parameter name --- apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 3ed416fba..b7cdfba1d 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -184,7 +184,7 @@ class RootFieldBuilder { guard let scope = scopeCondition( for: inlineFragment, in: typeInfo, - deferCondition: inlineFragment.deferCondition + isDeferred: (inlineFragment.deferCondition != nil) ) else { return } @@ -232,7 +232,7 @@ class RootFieldBuilder { guard let scope = scopeCondition( for: fragmentSpread, in: typeInfo, - deferCondition: fragmentSpread.deferCondition + isDeferred: (fragmentSpread.deferCondition != nil) ) else { return } @@ -295,7 +295,7 @@ class RootFieldBuilder { private func scopeCondition( for conditionalSelectionSet: ConditionallyIncludable, in parentTypePath: SelectionSet.TypeInfo, - deferCondition: CompilationResult.DeferCondition? = nil + isDeferred: Bool = false ) -> ScopeCondition? { let inclusionResult = inclusionResult(for: conditionalSelectionSet.inclusionConditions) guard inclusionResult != .skipped else { @@ -304,7 +304,7 @@ class RootFieldBuilder { let type = ( parentTypePath.parentType == conditionalSelectionSet.parentType - && deferCondition == nil + && !isDeferred ) ? nil : conditionalSelectionSet.parentType From a20ae7c50d179b4473f12f506b07c8d633dd5240 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Oct 2023 12:41:15 -0700 Subject: [PATCH 23/41] Dump defer condition in matcher failure output --- apollo-ios-codegen/Sources/IR/IR+InlineFragmentSpread.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apollo-ios-codegen/Sources/IR/IR+InlineFragmentSpread.swift b/apollo-ios-codegen/Sources/IR/IR+InlineFragmentSpread.swift index b99292200..1035291d3 100644 --- a/apollo-ios-codegen/Sources/IR/IR+InlineFragmentSpread.swift +++ b/apollo-ios-codegen/Sources/IR/IR+InlineFragmentSpread.swift @@ -28,6 +28,9 @@ public class InlineFragmentSpread: Hashable, CustomDebugStringConvertible { if let conditions = typeInfo.inclusionConditions { string += " \(conditions.debugDescription)" } + if let deferCondition = typeInfo.deferCondition { + string += " \(deferCondition.debugDescription)" + } return string } From a1262e27e0e43cf13bc0aad7a7632e6737758507 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Oct 2023 12:52:26 -0700 Subject: [PATCH 24/41] Additional named fragment test with mocking support --- .../MockIRSubscripts.swift | 15 +++ .../CodeGenIR/IRRootFieldBuilderTests.swift | 91 ++++++++++++++++++- 2 files changed, 105 insertions(+), 1 deletion(-) diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift index c380d65ec..525b93a6b 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift @@ -46,6 +46,21 @@ extension ScopeConditionalSubscriptAccessing { return self[scope] } + public subscript( + as typeCase: String? = nil, + deferred deferCondition: IR.DeferCondition? = nil + ) -> IR.SelectionSet? { + guard let scope = self.scopeCondition( + type: typeCase, + conditions: nil, + deferCondition: deferCondition + ) else { + return nil + } + + return self[scope] + } + private func scopeCondition( type typeCase: String?, conditions conditionsResult: IR.InclusionConditions.Result?, diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 06e10fb42..9e9160ee3 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -5416,7 +5416,6 @@ class IRRootFieldBuilderTests: XCTestCase { // then let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) - let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] @@ -5450,4 +5449,94 @@ class IRRootFieldBuilderTests: XCTestCase { )) } + func test__deferredFragments__givenDeferredInlineFragment_insideNamedFragment_buildsDeferredInlineFragment_insideNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + type Dog implements Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment + } + } + + fragment DogFragment on Dog { + ... on Dog @defer(label: "root") { + species + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_DogFragment = ir.builtFragments["DogFragment"] + let allAnimals_DogFragment_AsDog = allAnimals_DogFragment?[as: "Dog", deferred: .init(label: "root")] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .fragmentSpread("DogFragment", type: Object_Dog), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_DogFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Object_Dog, label: "root"), + ] + ) + )) + + expect(allAnimals_DogFragment_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + } + } From d671de14c6100b2f2e0273b6a9989e3d20e99311 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Oct 2023 13:19:56 -0700 Subject: [PATCH 25/41] Another named fragment type case test --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 9e9160ee3..964a3cb74 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -5539,4 +5539,79 @@ class IRRootFieldBuilderTests: XCTestCase { )) } + func test__deferredFragments__givenDeferredInlineFragmentOnDifferentTypeCase_insideNamedFragment_buildsDeferredInlineFragment_insideNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + type Dog implements Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...DogFragment + } + } + + fragment DogFragment on Animal { + ... on Dog @defer(label: "root") { + species + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_DogFragment = ir.builtFragments["DogFragment"] + let allAnimals_DogFragment_AsDog = allAnimals_DogFragment?[as: "Dog"] + let allAnimals_DogFragment_AsDog_Deferred = allAnimals_DogFragment_AsDog?[as: "Dog", deferred: .init(label: "root")] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .fragmentSpread("DogFragment", type: Interface_Animal), + ] + ) + )) + + expect(allAnimals_DogFragment_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Object_Dog, label: "root"), + ] + ) + )) + + expect(allAnimals_DogFragment_AsDog_Deferred).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + } + } From 3df12f2cd9167fda4b5e4dc982bb4f2bfad38cb3 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Oct 2023 13:29:00 -0700 Subject: [PATCH 26/41] Condense case logic --- .../Sources/IR/IR+RootFieldBuilder.swift | 36 +++++++++---------- 1 file changed, 16 insertions(+), 20 deletions(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index b7cdfba1d..1171c7e88 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -193,12 +193,18 @@ class RootFieldBuilder { let matchesScope = typeInfo.scope.matches(scope) switch (matchesScope, inlineFragment.deferCondition) { - case let (true, .some(deferCondition)): + case (true, .some), (false, nil): + var deferCondition: DeferCondition? { + guard let condition = inlineFragment.deferCondition else { return nil } + + return DeferCondition(condition) + } + let irTypeCase = buildInlineFragmentSpread( from: inlineSelectionSet, with: scope, inParentTypePath: typeInfo, - deferCondition: DeferCondition(deferCondition) + deferCondition: deferCondition ) target.mergeIn(irTypeCase) @@ -212,14 +218,6 @@ class RootFieldBuilder { inParentTypePath: typeInfo ) target.mergeIn(irTypeCase) - - case (false, nil): - let irTypeCase = buildInlineFragmentSpread( - from: inlineSelectionSet, - with: scope, - inParentTypePath: typeInfo - ) - target.mergeIn(irTypeCase) } } @@ -241,20 +239,18 @@ class RootFieldBuilder { let matchesScope = selectionSetScope.matches(scope) switch (matchesScope, fragmentSpread.deferCondition) { - case let (true, .some(deferCondition)): - let irFragmentSpread = buildNamedFragmentSpread( - fromFragment: fragmentSpread, - with: scope, - spreadIntoParentWithTypePath: typeInfo, - deferCondition: DeferCondition(deferCondition) - ) - target.mergeIn(irFragmentSpread) + case (true, .some), (true, nil): + var deferCondition: DeferCondition? { + guard let condition = fragmentSpread.deferCondition else { return nil } + + return DeferCondition(condition) + } - case (true, nil): let irFragmentSpread = buildNamedFragmentSpread( fromFragment: fragmentSpread, with: scope, - spreadIntoParentWithTypePath: typeInfo + spreadIntoParentWithTypePath: typeInfo, + deferCondition: deferCondition ) target.mergeIn(irFragmentSpread) From 49e18c38542ea54e9e7cfd99e3d28a443e022fd3 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Oct 2023 17:24:04 -0700 Subject: [PATCH 27/41] Moves constants to the relevant type --- .../GraphQLCompiler/CompilationResult.swift | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift index 1b272b630..e85bd2196 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift @@ -9,11 +9,6 @@ public class CompilationResult: JavaScriptObject { static let LocalCacheMutation = "apollo_client_ios_localCacheMutation" static let Defer = "defer" } - - enum ArgumentLabels { - static let `If` = "if" - static let Label = "label" - } } public lazy var rootTypes: RootTypeDefinition = self["rootTypes"] @@ -410,6 +405,14 @@ public class CompilationResult: JavaScriptObject { } public struct DeferCondition { + /// String constants used to match JavaScriptObject instances. + fileprivate enum Constants { + enum ArgumentNames { + static let Label = "label" + static let `If` = "if" + } + } + public let label: String public let variable: String? @@ -435,14 +438,14 @@ fileprivate extension Deferrable where Self: JavaScriptObject { guard let labelArgument = directive.arguments?.first( - where: { $0.name == CompilationResult.Constants.ArgumentLabels.Label }), + where: { $0.name == CompilationResult.DeferCondition.Constants.ArgumentNames.Label }), case let .string(labelValue) = labelArgument.value else { preconditionFailure("Incorrect `label` argument. Either missing or value is not a String.") } guard let variableArgument = directive.arguments?.first( - where: { $0.name == CompilationResult.Constants.ArgumentLabels.If } + where: { $0.name == CompilationResult.DeferCondition.Constants.ArgumentNames.If } ) else { return .init(label: labelValue) } From 4e6e6036ed42894d980e0ecc88c4e073f669a001 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 16 Oct 2023 17:28:42 -0700 Subject: [PATCH 28/41] Refactored containsDeferredFragment logic --- apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift index 1533ec931..22a1393e9 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift @@ -166,13 +166,14 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { extension LinkedList where T == ScopeCondition { var containsDeferredFragment: Bool { var node: Node? = last - var deferCondition = node?.value.deferCondition - while node?.previous != nil && deferCondition == nil { + repeat { + guard node?.value.deferCondition == nil else { + return true + } node = node?.previous - deferCondition = node?.value.deferCondition - } - - return deferCondition != nil + } while node != nil + + return false } } From b5c4be2fe4098d00a9b9a225f1ce507bdfe9b0f7 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 17 Oct 2023 14:08:28 -0700 Subject: [PATCH 29/41] More inline fragment tests --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 305 +++++++++++++++++- 1 file changed, 302 insertions(+), 3 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 964a3cb74..4d793da26 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4625,7 +4625,107 @@ class IRRootFieldBuilderTests: XCTestCase { // MARK: Deferred Fragments - Inline Fragments - func test__deferredFragments__givenDeferredInlineFragment_buildsDeferredInlineFragment() throws { + func test__deferredFragments__givenDeferredInlineFragmentWithoutTypeCase_buildsDeferredInlineFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + } + + type Dog implements Animal { + id: String! + species: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... @defer(label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + + let allAnimals = self.subject[field: "allAnimals"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .deferred(Interface_Animal, label: "root"), + ] + ) + )) + } + + func test__deferredFragments__givenDeferredInlineFragmentOnSameTypeCase_buildsDeferredInlineFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String! + species: String! + } + + type Dog implements Animal { + id: String! + species: String! + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Animal @defer(label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Scalar_String = try XCTUnwrap(schema[scalar: "String"]) + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + + let allAnimals = self.subject[field: "allAnimals"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .nonNull(.scalar(Scalar_String))), + .deferred(Interface_Animal, label: "root"), + ] + ) + )) + } + + func test__deferredFragments__givenDeferredInlineFragmentOnDifferentTypeCase_buildsDeferredInlineFragment() throws { // given schemaSDL = """ type Query { @@ -4956,7 +5056,7 @@ class IRRootFieldBuilderTests: XCTestCase { expect(allAnimals_AsDog_Deferred).to(beNil()) } - func test__deferredFragments__givenSiblingSelectionSetIsSameObjectType_doesNotMergesDeferredFragments() throws { + func test__deferredFragments__givenSiblingDeferredInlineFragmentsOnSameTypeCase_doesNotMergeDeferredFragments() throws { // given schemaSDL = """ type Query { @@ -5062,7 +5162,7 @@ class IRRootFieldBuilderTests: XCTestCase { )) } - func test__deferredFragments__givenSiblingSelectionSetIsDifferentObjectType_doesNotMergesDeferredFragments() throws { + func test__deferredFragments__givenSiblingDeferredInlineFragmentsOnDifferentTypeCase_doesNotMergeDeferredFragments() throws { // given schemaSDL = """ type Query { @@ -5157,6 +5257,205 @@ class IRRootFieldBuilderTests: XCTestCase { )) } + func test__deferredFragments__givenDeferredInlineFragmentWithSiblingOnSameTypeCase_doesNotMergeDeferredFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + + type Dog implements Animal { + id: String + species: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "root") { + species + } + ... on Dog { + name + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_Deferred_AsRoot = allAnimals_AsDog?[deferredAs: "root"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("name", type: .string()), + .deferred(Object_Dog, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_AsDog_Deferred_AsRoot).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .string()), + ], + mergedSelections: [ + .field("id", type: .string()), + .field("name", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_AsDog), + ] + ) + )) + } + + func test__deferredFragments__givenDeferredInlineFragmentWithSiblingOnDifferentTypeCase_doesNotMergeDeferredFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + name: String + } + + type Dog implements Animal { + id: String + species: String + name: String + } + + type Pet implements Animal { + id: String + species: String + name: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "root") { + species + } + ... on Pet { + name + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Object_Pet = try XCTUnwrap(schema[object: "Pet"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsPet = allAnimals?[as: "Pet"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_Deferred_AsRoot = allAnimals_AsDog?[deferredAs: "root"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + .inlineFragment(parentType: Object_Pet), + ] + ) + )) + + expect(allAnimals_AsPet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Pet, + directSelections: [ + .field("name", type: .string()), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Object_Dog, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_AsDog_Deferred_AsRoot).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .string()), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } // MARK: Deferred Fragments - Named Fragments func test__deferredFragments__givenDeferredNamedFragment_buildsDeferredNamedFragment() throws { From a95ef00ce127cd8a8c8017074c7d9c54468b5714 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 19 Oct 2023 12:15:31 -0700 Subject: [PATCH 30/41] Removing duplicate DeferCondition type --- .../MockIRSubscripts.swift | 6 ++--- .../TestHelpers/IRMatchers.swift | 20 +++++++-------- .../GraphQLCompiler/CompilationResult.swift | 13 ++++++++-- .../Sources/IR/IR+DeferCondition.swift | 25 ------------------- .../Sources/IR/IR+RootFieldBuilder.swift | 12 ++++----- .../Sources/IR/IR+ScopeDescriptor.swift | 4 +-- .../Sources/IR/IR+SelectionSet.swift | 8 ++++-- 7 files changed, 38 insertions(+), 50 deletions(-) delete mode 100644 apollo-ios-codegen/Sources/IR/IR+DeferCondition.swift diff --git a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift index 525b93a6b..09a5dd084 100644 --- a/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift +++ b/Tests/ApolloCodegenInternalTestHelpers/MockIRSubscripts.swift @@ -48,7 +48,7 @@ extension ScopeConditionalSubscriptAccessing { public subscript( as typeCase: String? = nil, - deferred deferCondition: IR.DeferCondition? = nil + deferred deferCondition: CompilationResult.DeferCondition? = nil ) -> IR.SelectionSet? { guard let scope = self.scopeCondition( type: typeCase, @@ -64,7 +64,7 @@ extension ScopeConditionalSubscriptAccessing { private func scopeCondition( type typeCase: String?, conditions conditionsResult: IR.InclusionConditions.Result?, - deferCondition: DeferCondition? = nil + deferCondition: CompilationResult.DeferCondition? = nil ) -> IR.ScopeCondition? { let type: GraphQLCompositeType? if let typeCase = typeCase { @@ -169,7 +169,7 @@ extension IR.SelectionSet: ScopeConditionalSubscriptAccessing { let scope = ScopeCondition( type: self.parentType, conditions: self.inclusionConditions, - deferCondition: DeferCondition(label: label, variable: variable) + deferCondition: CompilationResult.DeferCondition(label: label, variable: variable) ) return selections[scope] } diff --git a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift index cd207f4a6..6e410e80f 100644 --- a/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift +++ b/Tests/ApolloCodegenTests/TestHelpers/IRMatchers.swift @@ -266,7 +266,7 @@ public enum ShallowSelectionMatcher { .shallowInlineFragment(ShallowInlineFragmentMatcher( parentType: parentType, inclusionConditions: nil, - deferCondition: IR.DeferCondition(label: label, variable: variable) + deferCondition: CompilationResult.DeferCondition(label: label, variable: variable) )) } @@ -279,7 +279,7 @@ public enum ShallowSelectionMatcher { name: fragment.name, type: fragment.type, inclusionConditions: nil, - deferCondition: DeferCondition(label: label, variable: variable) + deferCondition: CompilationResult.DeferCondition(label: label, variable: variable) )) } } @@ -379,12 +379,12 @@ fileprivate func shallowlyMatch(expected: ShallowFieldMatcher, actual: IR.Field) public struct ShallowInlineFragmentMatcher: Equatable, CustomDebugStringConvertible { let parentType: GraphQLCompositeType let inclusionConditions: IR.InclusionConditions? - let deferCondition: IR.DeferCondition? + let deferCondition: CompilationResult.DeferCondition? init( parentType: GraphQLCompositeType, inclusionConditions: [IR.InclusionCondition]?, - deferCondition: IR.DeferCondition? + deferCondition: CompilationResult.DeferCondition? ) { self.parentType = parentType self.deferCondition = deferCondition @@ -399,7 +399,7 @@ public struct ShallowInlineFragmentMatcher: Equatable, CustomDebugStringConverti public static func mock( parentType: GraphQLCompositeType, inclusionConditions: [IR.InclusionCondition]? = nil, - deferCondition: IR.DeferCondition? = nil + deferCondition: CompilationResult.DeferCondition? = nil ) -> ShallowInlineFragmentMatcher { self.init( parentType: parentType, @@ -464,13 +464,13 @@ public struct ShallowFragmentSpreadMatcher: Equatable, CustomDebugStringConverti let name: String let type: GraphQLCompositeType let inclusionConditions: AnyOf? - let deferCondition: IR.DeferCondition? + let deferCondition: CompilationResult.DeferCondition? init( name: String, type: GraphQLCompositeType, inclusionConditions: [IR.InclusionCondition]?, - deferCondition: IR.DeferCondition? + deferCondition: CompilationResult.DeferCondition? ) { self.name = name self.type = type @@ -489,7 +489,7 @@ public struct ShallowFragmentSpreadMatcher: Equatable, CustomDebugStringConverti name: String, type: GraphQLCompositeType, inclusionConditions: AnyOf?, - deferCondition: IR.DeferCondition? + deferCondition: CompilationResult.DeferCondition? ) { self.name = name self.type = type @@ -501,7 +501,7 @@ public struct ShallowFragmentSpreadMatcher: Equatable, CustomDebugStringConverti _ name: String, type: GraphQLCompositeType, inclusionConditions: AnyOf? = nil, - deferCondition: IR.DeferCondition? = nil + deferCondition: CompilationResult.DeferCondition? = nil ) -> ShallowFragmentSpreadMatcher { self.init( name: name, @@ -514,7 +514,7 @@ public struct ShallowFragmentSpreadMatcher: Equatable, CustomDebugStringConverti public static func mock( _ fragment: CompilationResult.FragmentDefinition, inclusionConditions: AnyOf? = nil, - deferCondition: IR.DeferCondition? = nil + deferCondition: CompilationResult.DeferCondition? = nil ) -> ShallowFragmentSpreadMatcher { self.init( name: fragment.name, diff --git a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift index e85bd2196..678ebd53f 100644 --- a/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift +++ b/apollo-ios-codegen/Sources/GraphQLCompiler/CompilationResult.swift @@ -404,7 +404,7 @@ public class CompilationResult: JavaScriptObject { } } - public struct DeferCondition { + public struct DeferCondition: Hashable, CustomDebugStringConvertible { /// String constants used to match JavaScriptObject instances. fileprivate enum Constants { enum ArgumentNames { @@ -416,10 +416,19 @@ public class CompilationResult: JavaScriptObject { public let label: String public let variable: String? - init(label: String, variable: String? = nil) { + public init(label: String, variable: String? = nil) { self.label = label self.variable = variable } + + public var debugDescription: String { + var string = "Defer \"\(label)\"" + if let variable { + string += " - if \"\(variable)\"" + } + + return string + } } } diff --git a/apollo-ios-codegen/Sources/IR/IR+DeferCondition.swift b/apollo-ios-codegen/Sources/IR/IR+DeferCondition.swift deleted file mode 100644 index 3f8d1bc2c..000000000 --- a/apollo-ios-codegen/Sources/IR/IR+DeferCondition.swift +++ /dev/null @@ -1,25 +0,0 @@ -import GraphQLCompiler - -// TODO: Documentation for this to be completed in issue #3141 -public struct DeferCondition: Hashable, CustomDebugStringConvertible { - public let label: String - public let variable: String? - - init(label: String, variable: String? = nil) { - self.label = label - self.variable = variable - } - - init(_ compilationResult: CompilationResult.DeferCondition) { - self.init(label: compilationResult.label, variable: compilationResult.variable) - } - - public var debugDescription: String { - var string = "Defer \"\(label)\"" - if let variable { - string += " - if \"\(variable)\"" - } - - return string - } -} diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 1171c7e88..259524940 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -194,10 +194,10 @@ class RootFieldBuilder { switch (matchesScope, inlineFragment.deferCondition) { case (true, .some), (false, nil): - var deferCondition: DeferCondition? { + var deferCondition: CompilationResult.DeferCondition? { guard let condition = inlineFragment.deferCondition else { return nil } - return DeferCondition(condition) + return condition } let irTypeCase = buildInlineFragmentSpread( @@ -240,10 +240,10 @@ class RootFieldBuilder { switch (matchesScope, fragmentSpread.deferCondition) { case (true, .some), (true, nil): - var deferCondition: DeferCondition? { + var deferCondition: CompilationResult.DeferCondition? { guard let condition = fragmentSpread.deferCondition else { return nil } - return DeferCondition(condition) + return condition } let irFragmentSpread = buildNamedFragmentSpread( @@ -381,7 +381,7 @@ class RootFieldBuilder { from selectionSet: CompilationResult.SelectionSet?, with scopeCondition: ScopeCondition, inParentTypePath enclosingTypeInfo: SelectionSet.TypeInfo, - deferCondition: DeferCondition? = nil + deferCondition: CompilationResult.DeferCondition? = nil ) -> InlineFragmentSpread { let scope = ScopeCondition( type: scopeCondition.type, @@ -438,7 +438,7 @@ class RootFieldBuilder { fromFragment fragmentSpread: CompilationResult.FragmentSpread, with scopeCondition: ScopeCondition, spreadIntoParentWithTypePath parentTypeInfo: SelectionSet.TypeInfo, - deferCondition: DeferCondition? = nil + deferCondition: CompilationResult.DeferCondition? = nil ) -> NamedFragmentSpread { let fragment = ir.build(fragment: fragmentSpread.fragment) referencedFragments.append(fragment) diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index 00d1139e9..2b94f7879 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -9,12 +9,12 @@ import Utilities public struct ScopeCondition: Hashable, CustomDebugStringConvertible { public let type: GraphQLCompositeType? public let conditions: InclusionConditions? - public let deferCondition: DeferCondition? + public let deferCondition: CompilationResult.DeferCondition? init( type: GraphQLCompositeType? = nil, conditions: InclusionConditions? = nil, - deferCondition: DeferCondition? = nil + deferCondition: CompilationResult.DeferCondition? = nil ) { self.type = type self.conditions = conditions diff --git a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift index 22a1393e9..4fcc602e6 100644 --- a/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift +++ b/apollo-ios-codegen/Sources/IR/IR+SelectionSet.swift @@ -21,9 +21,13 @@ public class SelectionSet: Hashable, CustomDebugStringConvertible { public var parentType: GraphQLCompositeType { scope.type } - public var inclusionConditions: InclusionConditions? { scope.scopePath.last.value.conditions } + public var inclusionConditions: InclusionConditions? { + scope.scopePath.last.value.conditions + } - public var deferCondition: DeferCondition? { scope.scopePath.last.value.deferCondition } + public var deferCondition: CompilationResult.DeferCondition? { + scope.scopePath.last.value.deferCondition + } /// Indicates if the `SelectionSet` represents a root selection set. /// If `true`, the `SelectionSet` belongs to a field directly. From 8b4ee156c32efd3c6d81f87e87fe579d63bc4e63 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 20 Oct 2023 12:49:38 -0700 Subject: [PATCH 31/41] Include defer condition in scope condition when merging selections --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 12 ------------ .../Sources/IR/IR+EntitySelectionTree.swift | 7 +++++-- 2 files changed, 5 insertions(+), 14 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 4d793da26..52c73afb2 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -5500,7 +5500,6 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5520,15 +5519,12 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .string()), - .field("species", type: .string()), // wrong ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_DogFragment), // wrong ] ) )) - #warning("fix these 'wrong' merges") } func test__deferredFragments__givenDeferredNamedFragmentWithVariableCondition_buildsDeferredNamedFragmentWithVariable() throws { @@ -5573,7 +5569,6 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5593,15 +5588,12 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .string()), - .field("species", type: .string()), // wrong ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_DogFragment), // wrong ] ) )) - #warning("fix these 'wrong' merges") } func test__deferredFragments__givenDeferredNamedFragmentWithTrueCondition_buildsDeferredNamedFragment() throws { @@ -5646,7 +5638,6 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5666,15 +5657,12 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSelections: [ .field("id", type: .string()), - .field("species", type: .string()), // wrong ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_DogFragment), // wrong ] ) )) - #warning("fix these 'wrong' merges") } func test__deferredFragments__givenDeferredNamedFragmentWithFalseCondition_doesNotBuildDeferredNamedFragment() throws { diff --git a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift index 35f97e84b..05775b54d 100644 --- a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift +++ b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift @@ -255,8 +255,11 @@ class EntitySelectionTree { fileprivate func scopeConditionNode(for condition: ScopeCondition) -> EntityNode { let nodeCondition = ScopeCondition( - type: condition.type == self.type ? nil : condition.type, - conditions: condition.conditions + type: (condition.type == self.type && condition.deferCondition == nil) + ? nil + : condition.type, + conditions: condition.conditions, + deferCondition: condition.deferCondition ) func createNode() -> EntityNode { From e49b2e86713fb51909ae60abb67bd1239d40f4dd Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sat, 21 Oct 2023 22:07:39 -0700 Subject: [PATCH 32/41] Disables deferred fragment merging --- .../Sources/IR/IR+EntitySelectionTree.swift | 8 +++++--- apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift index 05775b54d..113e9c15e 100644 --- a/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift +++ b/apollo-ios-codegen/Sources/IR/IR+EntitySelectionTree.swift @@ -215,6 +215,8 @@ class EntitySelectionTree { if let conditionalScopes = scopeConditions { for (condition, node) in conditionalScopes { + guard !node.scope.isDeferred else { continue } + if scopePathNode.value.matches(condition) { node.mergeSelections(matchingScopePath: scopePathNode, into: targetSelections) @@ -229,6 +231,8 @@ class EntitySelectionTree { if let scopeConditions = scopeConditions { for (condition, node) in scopeConditions { + guard !node.scope.isDeferred else { continue } + if scopePathNode.value.matches(condition) { node.mergeSelections(matchingScopePath: scopePathNode, into: targetSelections) } @@ -255,9 +259,7 @@ class EntitySelectionTree { fileprivate func scopeConditionNode(for condition: ScopeCondition) -> EntityNode { let nodeCondition = ScopeCondition( - type: (condition.type == self.type && condition.deferCondition == nil) - ? nil - : condition.type, + type: condition.type == self.type ? nil : condition.type, conditions: condition.conditions, deferCondition: condition.deferCondition ) diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index 2b94f7879..ab7651985 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -211,6 +211,10 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { return false } + public func matches(_ otherDeferCondition: CompilationResult.DeferCondition) -> Bool { + otherDeferCondition == self.scopePath.last.value.deferCondition + } + /// Indicates if the receiver is of the given type. If the receiver matches a given type, /// then selections for a `SelectionSet` of that type can be merged in to the receiver's /// `SelectionSet`. @@ -223,6 +227,10 @@ public struct ScopeDescriptor: Hashable, CustomDebugStringConvertible { return false } + if let deferConditions = condition.deferCondition, !self.matches(deferConditions) { + return false + } + return true } From 5bda10d0e6a47352edb0f40247f9ebb0c59feb99 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sat, 21 Oct 2023 22:08:33 -0700 Subject: [PATCH 33/41] Fixes deferred fragment logic for same type case condition --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 50 ++++++++++++++++++- .../Sources/IR/IR+NamedFragmentSpread.swift | 3 ++ .../Sources/IR/IR+RootFieldBuilder.swift | 2 +- 3 files changed, 53 insertions(+), 2 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 52c73afb2..6bf125490 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -5458,7 +5458,55 @@ class IRRootFieldBuilderTests: XCTestCase { } // MARK: Deferred Fragments - Named Fragments - func test__deferredFragments__givenDeferredNamedFragment_buildsDeferredNamedFragment() throws { + func test__deferredFragments__givenDeferredNamedFragmentOnSameTypeCase_buildsDeferredNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ...AnimalFragment @defer(label: "root") + } + } + + fragment AnimalFragment on Animal { + species + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Fragment_AnimalFragment = try XCTUnwrap(ir.compilationResult[fragment: "AnimalFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AnimalFragment = allAnimals?[fragment: "AnimalFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .deferred(Fragment_AnimalFragment, label: "root"), + ] + ) + )) + } + + func test__deferredFragments__givenDeferredNamedFragmentOnDifferentTypeCase_buildsDeferredNamedFragment() throws { // given schemaSDL = """ type Query { diff --git a/apollo-ios-codegen/Sources/IR/IR+NamedFragmentSpread.swift b/apollo-ios-codegen/Sources/IR/IR+NamedFragmentSpread.swift index c85e2f6c4..25efab05b 100644 --- a/apollo-ios-codegen/Sources/IR/IR+NamedFragmentSpread.swift +++ b/apollo-ios-codegen/Sources/IR/IR+NamedFragmentSpread.swift @@ -51,6 +51,9 @@ public class NamedFragmentSpread: Hashable, CustomDebugStringConvertible { if let inclusionConditions = inclusionConditions { description += " \(inclusionConditions.debugDescription)" } + if let deferCondition = typeInfo.deferCondition { + description += " \(deferCondition.debugDescription)" + } return description } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 259524940..04774e085 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -452,7 +452,7 @@ class RootFieldBuilder { self.containsDeferredFragment = fragment.containsDeferredFragment || scope.deferCondition != nil - let scopePath = scopeCondition.isEmpty ? + let scopePath = scope.isEmpty ? parentTypeInfo.scopePath : parentTypeInfo.scopePath.mutatingLast { $0.appending(scope) From 29d404e55d6c89cf33bb0c972d5adf7a111db2a2 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sat, 21 Oct 2023 22:08:52 -0700 Subject: [PATCH 34/41] Test cleanup --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 6bf125490..c0e50c12e 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4686,11 +4686,6 @@ class IRRootFieldBuilderTests: XCTestCase { id: String! species: String! } - - type Dog implements Animal { - id: String! - species: String! - } """ document = """ @@ -5828,7 +5823,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] let allAnimals_DogFragment = ir.builtFragments["DogFragment"] - let allAnimals_DogFragment_AsDog = allAnimals_DogFragment?[as: "Dog", deferred: .init(label: "root")] + let allAnimals_DogFragment_AsDog_Deferred = allAnimals_DogFragment?[as: "Dog", deferred: .init(label: "root")] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5864,7 +5859,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_DogFragment_AsDog).to(shallowlyMatch( + expect(allAnimals_DogFragment_AsDog_Deferred).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ From 48535e95945acf67bac06a86676b8e9a7b09d2af Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sat, 21 Oct 2023 22:09:56 -0700 Subject: [PATCH 35/41] Add missing convenience property --- apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift index ab7651985..0ab10db05 100644 --- a/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift +++ b/apollo-ios-codegen/Sources/IR/IR+ScopeDescriptor.swift @@ -30,6 +30,8 @@ public struct ScopeCondition: Hashable, CustomDebugStringConvertible { var isEmpty: Bool { type == nil && (conditions?.isEmpty ?? true) && deferCondition == nil } + + var isDeferred: Bool { deferCondition != nil } } public typealias TypeScope = OrderedSet From 50691e1cabb493fd5723015f525bd516c58c1ac4 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sun, 22 Oct 2023 16:24:52 -0700 Subject: [PATCH 36/41] Test cleanup --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index c0e50c12e..0507e7823 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4763,7 +4763,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -4790,7 +4790,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred).to(shallowlyMatch( + expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -4849,7 +4849,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root", withVariable: "a"] + let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root", withVariable: "a"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -4876,7 +4876,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred).to(shallowlyMatch( + expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -4935,7 +4935,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -4962,7 +4962,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred).to(shallowlyMatch( + expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5021,7 +5021,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_Deferred = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5048,7 +5048,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred).to(beNil()) + expect(allAnimals_AsDog_DeferredAsRoot).to(beNil()) } func test__deferredFragments__givenSiblingDeferredInlineFragmentsOnSameTypeCase_doesNotMergeDeferredFragments() throws { @@ -5097,8 +5097,8 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_Deferred_AsOne = allAnimals_AsDog?[deferredAs: "one"] - let allAnimals_AsDog_Deferred_AsTwo = allAnimals_AsDog?[deferredAs: "two"] + let allAnimals_AsDog_DeferredAsOne = allAnimals_AsDog?[deferredAs: "one"] + let allAnimals_AsDog_DeferredAsTwo = allAnimals_AsDog?[deferredAs: "two"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5126,7 +5126,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred_AsOne).to(shallowlyMatch( + expect(allAnimals_AsDog_DeferredAsOne).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5141,7 +5141,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred_AsTwo).to(shallowlyMatch( + expect(allAnimals_AsDog_DeferredAsTwo).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5202,8 +5202,8 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsBird = allAnimals?[as: "Bird"] let allAnimals_AsCat = allAnimals?[as: "Cat"] - let allAnimals_AsBird_Deferred_AsBird = allAnimals_AsBird?[deferredAs: "bird"] - let allAnimals_AsCat_Deferred_AsCat = allAnimals_AsCat?[deferredAs: "cat"] + let allAnimals_AsBird_DeferredAsBird = allAnimals_AsBird?[deferredAs: "bird"] + let allAnimals_AsCat_DeferredAsCat = allAnimals_AsCat?[deferredAs: "cat"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5233,7 +5233,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsBird_Deferred_AsBird).to(shallowlyMatch( + expect(allAnimals_AsBird_DeferredAsBird).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Bird, directSelections: [ @@ -5242,7 +5242,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsCat_Deferred_AsCat).to(shallowlyMatch( + expect(allAnimals_AsCat_DeferredAsCat).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Cat, directSelections: [ @@ -5295,7 +5295,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_Deferred_AsRoot = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5323,7 +5323,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred_AsRoot).to(shallowlyMatch( + expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5393,7 +5393,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsPet = allAnimals?[as: "Pet"] let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_Deferred_AsRoot = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5436,7 +5436,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_Deferred_AsRoot).to(shallowlyMatch( + expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5823,7 +5823,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_AsDog = allAnimals?[as: "Dog"] let allAnimals_DogFragment = ir.builtFragments["DogFragment"] - let allAnimals_DogFragment_AsDog_Deferred = allAnimals_DogFragment?[as: "Dog", deferred: .init(label: "root")] + let allAnimals_DogFragment_AsDog_DeferredAsRoot = allAnimals_DogFragment?[as: "Dog", deferred: .init(label: "root")] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5859,7 +5859,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_DogFragment_AsDog_Deferred).to(shallowlyMatch( + expect(allAnimals_DogFragment_AsDog_DeferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5913,7 +5913,7 @@ class IRRootFieldBuilderTests: XCTestCase { let allAnimals = self.subject[field: "allAnimals"] let allAnimals_DogFragment = ir.builtFragments["DogFragment"] let allAnimals_DogFragment_AsDog = allAnimals_DogFragment?[as: "Dog"] - let allAnimals_DogFragment_AsDog_Deferred = allAnimals_DogFragment_AsDog?[as: "Dog", deferred: .init(label: "root")] + let allAnimals_DogFragment_AsDog_DeferredAsRoot = allAnimals_DogFragment_AsDog?[as: "Dog", deferred: .init(label: "root")] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5934,7 +5934,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_DogFragment_AsDog_Deferred).to(shallowlyMatch( + expect(allAnimals_DogFragment_AsDog_DeferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ From d596aa237b18451c731beca10cf43366bbfed8ef Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Sun, 22 Oct 2023 17:20:41 -0700 Subject: [PATCH 37/41] Adds nested defer inline fragment test --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 0507e7823..a12fb7680 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -5451,6 +5451,124 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) } + + func test__deferredFragments__givenNestedDeferredInlineFragments_buildsNestedDeferredFragments_doesNotMergeDeferredFragments() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + friend: Animal + } + + type Cat implements Animal { + id: String + species: String + genus: String + } + """ + + document = """ + query TestOperation { + allAnimals { + __typename + id + ... on Dog @defer(label: "outer") { + friend { + ... on Cat @defer(label: "inner") { + species + } + } + } + } + } + """ + + // when + try buildSubjectRootField() + + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Object_Cat = try XCTUnwrap(schema[object: "Cat"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_AsDog_DeferredAsOuter = allAnimals_AsDog?[deferredAs: "outer"] + let allAnimals_AsDog_DeferredAsOuter_AsCat = allAnimals_AsDog_DeferredAsOuter?[field: "friend"]?[as: "Cat"] + let allAnimals_AsDog_DeferredAsOuter_AsCat_DeferredAsInner = allAnimals_AsDog_DeferredAsOuter_AsCat?[deferredAs: "inner"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Object_Dog), + ] + ) + )) + + expect(allAnimals_AsDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Object_Dog, label: "outer"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_AsDog_DeferredAsOuter).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("friend", type: .entity(Interface_Animal)), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_AsDog_DeferredAsOuter_AsCat).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Cat, + directSelections: [ + .deferred(Object_Cat, label: "inner"), + ] + ) + )) + + expect(allAnimals_AsDog_DeferredAsOuter_AsCat_DeferredAsInner).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Cat, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + } + + #warning("Need tests here with @include and @skip") + // MARK: Deferred Fragments - Named Fragments func test__deferredFragments__givenDeferredNamedFragmentOnSameTypeCase_buildsDeferredNamedFragment() throws { @@ -5944,4 +6062,6 @@ class IRRootFieldBuilderTests: XCTestCase { )) } + #warning("Need tests here with @include and @skip") + } From 7d2dd4af8e23f969bd8544ab24d8a79562764920 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 23 Oct 2023 13:59:15 -0700 Subject: [PATCH 38/41] Fix and test scope conditions with both include and defer --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 510 +++++++++++++++++- .../Sources/IR/IR+RootFieldBuilder.swift | 2 +- 2 files changed, 510 insertions(+), 2 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index a12fb7680..04f870b0a 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -5567,7 +5567,515 @@ class IRRootFieldBuilderTests: XCTestCase { )) } - #warning("Need tests here with @include and @skip") + // MARK: Deferred Fragments - Inline Fragments (with @include/@skip) + + func test__deferredFragments__givenDeferredInlineFragment_withIncludeDirective_buildsInclusionTypeCaseWithNestedDeferredInlineFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @include(if: $a) @defer(label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_ifA = allAnimals?[as: "Animal", if: "a"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.include(if: "a")]), + ] + ) + )) + + expect(allAnimals_ifA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.include(if: "a")], + directSelections: [ + .deferred(Interface_Animal, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } + + func test__deferredFragments__givenBothDeferAndIncludeDirectives_directivesOrderShouldNotAffectGeneratedFragments() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + """ + + document = """ + query IncludeFirst($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @include(if: $a) @defer(label: "root") { + species + } + } + } + + query DeferFirst($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @defer(label: "root") @include(if: $a) { + species + } + } + } + """ + + // when + let operations = ["IncludeFirst", "DeferFirst"] + + ir = try .mock(schema: schemaSDL, document: document) + + for operationName in operations { + operation = try XCTUnwrap(ir.compilationResult.operations.first( + where: { $0.name == operationName } + )) + + result = IR.RootFieldBuilder.buildRootEntityField( + forRootField: .mock( + "query", + type: .nonNull(.entity(operation.rootType)), + selectionSet: operation.selectionSet + ), + onRootEntity: IR.Entity(source: .operation(operation)), + inIR: ir + ) + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_ifA = allAnimals?[as: "Animal", if: "a"] + + let expectedInclusion = SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.include(if: "a")]), + ] + ) + + let expectedDefer = SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.include(if: "a")], + directSelections: [ + .deferred(Interface_Animal, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + + expect(allAnimals?.selectionSet).to(shallowlyMatch(expectedInclusion)) + expect(allAnimals_ifA).to(shallowlyMatch(expectedDefer)) + } + } + + func test__deferredFragments__givenBothDeferAndIncludeDirectives_onDifferentTypeCases_shouldNotNestFragments() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @include(if: $a) { + species + } + ... on Dog @defer(label: "root") { + genus + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_ifA = allAnimals?[if: "a"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals?[as: "Dog"]?[deferredAs: "root"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.include(if: "a")]), + .inlineFragment(parentType: Object_Dog) + ] + ) + )) + + expect(allAnimals_ifA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.include(if: "a")], + directSelections: [ + .field("species", type: .string()), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_asDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Object_Dog, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_asDog_deferredAsRoot).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("genus", type: .string()), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } + + func test__deferredFragments__givenDeferredInlineFragment_withSkipDirective_buildsInclusionTypeCaseWithNestedDeferredInlineFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @skip(if: $a) @defer(label: "root") { + species + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_skipIfA = allAnimals?[as: "Animal", if: !"a"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.skip(if: "a")]), + ] + ) + )) + + expect(allAnimals_skipIfA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.skip(if: "a")], + directSelections: [ + .deferred(Interface_Animal, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } + + func test__deferredFragments__givenBothDeferAndSkipDirectives_directivesOrderShouldNotAffectGeneratedFragments() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + """ + + document = """ + query IncludeFirst($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @skip(if: $a) @defer(label: "root") { + species + } + } + } + + query DeferFirst($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @defer(label: "root") @skip(if: $a) { + species + } + } + } + """ + + // when + let operations = ["IncludeFirst", "DeferFirst"] + + ir = try .mock(schema: schemaSDL, document: document) + + for operationName in operations { + operation = try XCTUnwrap(ir.compilationResult.operations.first( + where: { $0.name == operationName } + )) + + result = IR.RootFieldBuilder.buildRootEntityField( + forRootField: .mock( + "query", + type: .nonNull(.entity(operation.rootType)), + selectionSet: operation.selectionSet + ), + onRootEntity: IR.Entity(source: .operation(operation)), + inIR: ir + ) + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_skipIfA = allAnimals?[as: "Animal", if: !"a"] + + let expectedInclusion = SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.skip(if: "a")]), + ] + ) + + let expectedDefer = SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.skip(if: "a")], + directSelections: [ + .deferred(Interface_Animal, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + + expect(allAnimals?.selectionSet).to(shallowlyMatch(expectedInclusion)) + expect(allAnimals_skipIfA).to(shallowlyMatch(expectedDefer)) + } + } + + func test__deferredFragments__givenBothDeferAndSkipDirectives_onDifferentTypeCases_shouldNotNestFragments() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ... on Animal @skip(if: $a) { + species + } + ... on Dog @defer(label: "root") { + genus + } + } + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_skipIfA = allAnimals?[if: !"a"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals?[as: "Dog"]?[deferredAs: "root"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.skip(if: "a")]), + .inlineFragment(parentType: Object_Dog) + ] + ) + )) + + expect(allAnimals_skipIfA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.skip(if: "a")], + directSelections: [ + .field("species", type: .string()), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_asDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Object_Dog, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_asDog_deferredAsRoot).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("genus", type: .string()), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + } // MARK: Deferred Fragments - Named Fragments diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 04774e085..7c085ab77 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -385,7 +385,7 @@ class RootFieldBuilder { ) -> InlineFragmentSpread { let scope = ScopeCondition( type: scopeCondition.type, - conditions: scopeCondition.conditions, + conditions: (deferCondition == nil ? scopeCondition.conditions : nil), deferCondition: deferCondition ) From 614b114bf7c03916c4527a5be2eeac6e99b81681 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 24 Oct 2023 13:24:14 -0700 Subject: [PATCH 39/41] Fix deferred named fragment merging --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 78 +++++++++++++++++++ .../Sources/IR/IR+RootFieldBuilder.swift | 14 ++-- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 04f870b0a..3ccec4751 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -6570,6 +6570,84 @@ class IRRootFieldBuilderTests: XCTestCase { )) } + // MARK: Deferred Fragments - Named Fragments (with @include/@skip) + + func test__deferredFragments__givenDeferredNamedFragment_withIncludeDirective_onSameTypeCase_buildsNestedDeferredNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ...AnimalFragment @include(if: $a) @defer(label: "root") + } + } + + fragment AnimalFragment on Animal { + species + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Fragment_AnimalFragment = try XCTUnwrap(ir.compilationResult[fragment: "AnimalFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_ifA = allAnimals?[as: "Animal", if: "a"] + let animalFragment = ir.builtFragments["AnimalFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.include(if: "a")]), + ], + mergedSelections: [ + ] + ) + )) + + expect(allAnimals_ifA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.include(if: "a")], + directSelections: [ + .deferred(Fragment_AnimalFragment, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(animalFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + } + #warning("Need tests here with @include and @skip") } diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 7c085ab77..2cc7287ff 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -446,17 +446,15 @@ class RootFieldBuilder { let scope = ScopeCondition( type: scopeCondition.type, - conditions: scopeCondition.conditions, + conditions: (deferCondition == nil ? scopeCondition.conditions : nil), deferCondition: deferCondition ) self.containsDeferredFragment = fragment.containsDeferredFragment || scope.deferCondition != nil - let scopePath = scope.isEmpty ? - parentTypeInfo.scopePath : - parentTypeInfo.scopePath.mutatingLast { - $0.appending(scope) - } + let scopePath = scope.isEmpty + ? parentTypeInfo.scopePath + : parentTypeInfo.scopePath.mutatingLast { $0.appending(scope) } let typeInfo = SelectionSet.TypeInfo( entity: parentTypeInfo.entity, @@ -469,7 +467,9 @@ class RootFieldBuilder { inclusionConditions: AnyOf(scope.conditions) ) - entityStorage.mergeAllSelectionsIntoEntitySelectionTrees(from: fragmentSpread) + if fragmentSpread.typeInfo.deferCondition == nil { + entityStorage.mergeAllSelectionsIntoEntitySelectionTrees(from: fragmentSpread) + } return fragmentSpread } From 2aa8c368180018a5d2ed8fbed6d822b632249eed Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 24 Oct 2023 14:41:20 -0700 Subject: [PATCH 40/41] Additional tests + cleanup --- .../CodeGenIR/IRRootFieldBuilderTests.swift | 511 +++++++++++++++--- 1 file changed, 436 insertions(+), 75 deletions(-) diff --git a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift index 3ccec4751..61a3e71ef 100644 --- a/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift +++ b/Tests/ApolloCodegenTests/CodeGenIR/IRRootFieldBuilderTests.swift @@ -4762,8 +4762,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals_asDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -4775,7 +4775,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -4790,7 +4790,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -4848,8 +4848,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root", withVariable: "a"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals_asDog?[deferredAs: "root", withVariable: "a"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -4861,7 +4861,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -4876,7 +4876,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -4934,8 +4934,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals_asDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -4947,7 +4947,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -4962,7 +4962,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5020,8 +5020,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals_asDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5033,7 +5033,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5048,7 +5048,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsRoot).to(beNil()) + expect(allAnimals_asDog_deferredAsRoot).to(beNil()) } func test__deferredFragments__givenSiblingDeferredInlineFragmentsOnSameTypeCase_doesNotMergeDeferredFragments() throws { @@ -5096,9 +5096,9 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsOne = allAnimals_AsDog?[deferredAs: "one"] - let allAnimals_AsDog_DeferredAsTwo = allAnimals_AsDog?[deferredAs: "two"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsOne = allAnimals_asDog?[deferredAs: "one"] + let allAnimals_asDog_deferredAsTwo = allAnimals_asDog?[deferredAs: "two"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5110,7 +5110,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5126,7 +5126,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsOne).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsOne).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5141,7 +5141,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsTwo).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsTwo).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5200,10 +5200,10 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Cat = try XCTUnwrap(schema[object: "Cat"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsBird = allAnimals?[as: "Bird"] - let allAnimals_AsCat = allAnimals?[as: "Cat"] - let allAnimals_AsBird_DeferredAsBird = allAnimals_AsBird?[deferredAs: "bird"] - let allAnimals_AsCat_DeferredAsCat = allAnimals_AsCat?[deferredAs: "cat"] + let allAnimals_asBird = allAnimals?[as: "Bird"] + let allAnimals_asCat = allAnimals?[as: "Cat"] + let allAnimals_asBird_deferredAsBird = allAnimals_asBird?[deferredAs: "bird"] + let allAnimals_asCat_deferredAsCat = allAnimals_asCat?[deferredAs: "cat"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5215,7 +5215,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsBird).to(shallowlyMatch( + expect(allAnimals_asBird).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Bird, directSelections: [ @@ -5224,7 +5224,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsCat).to(shallowlyMatch( + expect(allAnimals_asCat).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Cat, directSelections: [ @@ -5233,7 +5233,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsBird_DeferredAsBird).to(shallowlyMatch( + expect(allAnimals_asBird_deferredAsBird).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Bird, directSelections: [ @@ -5242,7 +5242,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsCat_DeferredAsCat).to(shallowlyMatch( + expect(allAnimals_asCat_deferredAsCat).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Cat, directSelections: [ @@ -5294,8 +5294,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals_asDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5307,7 +5307,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5323,7 +5323,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5335,7 +5335,7 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog), + try .mock(allAnimals_asDog), ] ) )) @@ -5391,9 +5391,9 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Pet = try XCTUnwrap(schema[object: "Pet"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsPet = allAnimals?[as: "Pet"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsRoot = allAnimals_AsDog?[deferredAs: "root"] + let allAnimals_asPet = allAnimals?[as: "Pet"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsRoot = allAnimals_asDog?[deferredAs: "root"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5406,7 +5406,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsPet).to(shallowlyMatch( + expect(allAnimals_asPet).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Pet, directSelections: [ @@ -5421,7 +5421,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5436,7 +5436,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsRoot).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5503,10 +5503,10 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Cat = try XCTUnwrap(schema[object: "Cat"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DeferredAsOuter = allAnimals_AsDog?[deferredAs: "outer"] - let allAnimals_AsDog_DeferredAsOuter_AsCat = allAnimals_AsDog_DeferredAsOuter?[field: "friend"]?[as: "Cat"] - let allAnimals_AsDog_DeferredAsOuter_AsCat_DeferredAsInner = allAnimals_AsDog_DeferredAsOuter_AsCat?[deferredAs: "inner"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_deferredAsOuter = allAnimals_asDog?[deferredAs: "outer"] + let allAnimals_asDog_deferredAsOuter_asCat = allAnimals_asDog_deferredAsOuter?[field: "friend"]?[as: "Cat"] + let allAnimals_asDog_deferredAsOuter_asCat_deferredAsInner = allAnimals_asDog_deferredAsOuter_asCat?[deferredAs: "inner"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -5518,7 +5518,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5533,7 +5533,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsOuter).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsOuter).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -5548,7 +5548,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsOuter_AsCat).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsOuter_asCat).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Cat, directSelections: [ @@ -5557,7 +5557,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog_DeferredAsOuter_AsCat_DeferredAsInner).to(shallowlyMatch( + expect(allAnimals_asDog_deferredAsOuter_asCat_deferredAsInner).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Cat, directSelections: [ @@ -5569,7 +5569,7 @@ class IRRootFieldBuilderTests: XCTestCase { // MARK: Deferred Fragments - Inline Fragments (with @include/@skip) - func test__deferredFragments__givenDeferredInlineFragment_withIncludeDirective_buildsInclusionTypeCaseWithNestedDeferredInlineFragment() throws { + func test__deferredFragments__givenBothDeferAndIncludeDirectives_onSameTypeCase_buildsInclusionTypeCaseWithNestedDeferredInlineFragment() throws { // given schemaSDL = """ type Query { @@ -5823,7 +5823,7 @@ class IRRootFieldBuilderTests: XCTestCase { )) } - func test__deferredFragments__givenDeferredInlineFragment_withSkipDirective_buildsInclusionTypeCaseWithNestedDeferredInlineFragment() throws { + func test__deferredFragments__givenBothDeferAndSkipDirectives_onSameTypeCase_buildsInclusionTypeCaseWithNestedDeferredInlineFragment() throws { // given schemaSDL = """ type Query { @@ -6114,7 +6114,7 @@ class IRRootFieldBuilderTests: XCTestCase { let Fragment_AnimalFragment = try XCTUnwrap(ir.compilationResult[fragment: "AnimalFragment"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AnimalFragment = allAnimals?[fragment: "AnimalFragment"] + let animalFragment = ir.builtFragments["AnimalFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -6125,6 +6125,15 @@ class IRRootFieldBuilderTests: XCTestCase { ] ) )) + + expect(animalFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) } func test__deferredFragments__givenDeferredNamedFragmentOnDifferentTypeCase_buildsDeferredNamedFragment() throws { @@ -6168,7 +6177,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let dogFragment = ir.builtFragments["DogFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -6180,7 +6190,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6194,6 +6204,15 @@ class IRRootFieldBuilderTests: XCTestCase { ] ) )) + + expect(dogFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) } func test__deferredFragments__givenDeferredNamedFragmentWithVariableCondition_buildsDeferredNamedFragmentWithVariable() throws { @@ -6237,7 +6256,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let dogFragment = ir.builtFragments["DogFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -6249,7 +6269,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6263,6 +6283,15 @@ class IRRootFieldBuilderTests: XCTestCase { ] ) )) + + expect(dogFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) } func test__deferredFragments__givenDeferredNamedFragmentWithTrueCondition_buildsDeferredNamedFragment() throws { @@ -6306,7 +6335,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let dogFragment = ir.builtFragments["DogFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -6318,7 +6348,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6332,6 +6362,15 @@ class IRRootFieldBuilderTests: XCTestCase { ] ) )) + + expect(dogFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) } func test__deferredFragments__givenDeferredNamedFragmentWithFalseCondition_doesNotBuildDeferredNamedFragment() throws { @@ -6374,8 +6413,8 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_AsDog_DogFragment = allAnimals_AsDog?[fragment: "DogFragment"] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let allAnimals_asDog_DogFragment = allAnimals_asDog?[fragment: "DogFragment"] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -6387,7 +6426,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6399,7 +6438,7 @@ class IRRootFieldBuilderTests: XCTestCase { ], mergedSources: [ try .mock(allAnimals), - try .mock(allAnimals_AsDog_DogFragment), + try .mock(allAnimals_asDog_DogFragment), ] ) )) @@ -6447,9 +6486,9 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_AsDog = allAnimals?[as: "Dog"] - let allAnimals_DogFragment = ir.builtFragments["DogFragment"] - let allAnimals_DogFragment_AsDog_DeferredAsRoot = allAnimals_DogFragment?[as: "Dog", deferred: .init(label: "root")] + let allAnimals_asDog = allAnimals?[as: "Dog"] + let dogFragment = ir.builtFragments["DogFragment"] + let dogFragment_asDog_deferredAsRoot = dogFragment?[as: "Dog", deferred: .init(label: "root")] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -6461,7 +6500,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_AsDog).to(shallowlyMatch( + expect(allAnimals_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6476,7 +6515,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_DogFragment?.rootField.selectionSet).to(shallowlyMatch( + expect(dogFragment?.rootField.selectionSet).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6485,7 +6524,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_DogFragment_AsDog_DeferredAsRoot).to(shallowlyMatch( + expect(dogFragment_asDog_deferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6537,9 +6576,9 @@ class IRRootFieldBuilderTests: XCTestCase { let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) let allAnimals = self.subject[field: "allAnimals"] - let allAnimals_DogFragment = ir.builtFragments["DogFragment"] - let allAnimals_DogFragment_AsDog = allAnimals_DogFragment?[as: "Dog"] - let allAnimals_DogFragment_AsDog_DeferredAsRoot = allAnimals_DogFragment_AsDog?[as: "Dog", deferred: .init(label: "root")] + let dogFragment = ir.builtFragments["DogFragment"] + let dogFragment_asDog = dogFragment?[as: "Dog"] + let dogFragment_asDog_deferredAsRoot = dogFragment_asDog?[as: "Dog", deferred: .init(label: "root")] expect(allAnimals?.selectionSet).to(shallowlyMatch( SelectionSetMatcher( @@ -6551,7 +6590,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_DogFragment_AsDog).to(shallowlyMatch( + expect(dogFragment_asDog).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6560,7 +6599,7 @@ class IRRootFieldBuilderTests: XCTestCase { ) )) - expect(allAnimals_DogFragment_AsDog_DeferredAsRoot).to(shallowlyMatch( + expect(dogFragment_asDog_deferredAsRoot).to(shallowlyMatch( SelectionSetMatcher( parentType: Object_Dog, directSelections: [ @@ -6572,7 +6611,7 @@ class IRRootFieldBuilderTests: XCTestCase { // MARK: Deferred Fragments - Named Fragments (with @include/@skip) - func test__deferredFragments__givenDeferredNamedFragment_withIncludeDirective_onSameTypeCase_buildsNestedDeferredNamedFragment() throws { + func test__deferredFragments__givenBothDeferAndIncludeDirectives_onSameNamedFragment_buildsNestedDeferredNamedFragment() throws { // given schemaSDL = """ type Query { @@ -6616,8 +6655,105 @@ class IRRootFieldBuilderTests: XCTestCase { directSelections: [ .field("id", type: .string()), .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.include(if: "a")]), + ] + ) + )) + + expect(allAnimals_ifA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.include(if: "a")], + directSelections: [ + .deferred(Fragment_AnimalFragment, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(animalFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + } + + func test__deferredFragments__givenBothDeferAndIncludeDirectives_onDifferentNamedFragment_shouldNotNestFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ...AnimalFragment @include(if: $a) + ...DogFragment @defer(label: "root") + } + } + + fragment AnimalFragment on Animal { + species + } + + fragment DogFragment on Dog { + genus + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Fragment_AnimalFragment = try XCTUnwrap(ir.compilationResult[fragment: "AnimalFragment"]) + let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_animalFragment = allAnimals?[fragment: "AnimalFragment"] + let allAnimals_ifA = allAnimals?[if: "a"] + let animalFragment = ir.builtFragments["AnimalFragment"] + + let allAnimals_asDog = allAnimals?[as: "Dog"] + let dogFragment = ir.builtFragments["DogFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.include(if: "a")]), + .inlineFragment(parentType: Object_Dog), ], mergedSelections: [ + .fragmentSpread(Fragment_AnimalFragment, inclusionConditions: [.include(if: "a")]), + ], + mergedSources: [ + try .mock(allAnimals), ] ) )) @@ -6626,6 +6762,107 @@ class IRRootFieldBuilderTests: XCTestCase { SelectionSetMatcher( parentType: Interface_Animal, inclusionConditions: [.include(if: "a")], + directSelections: [ + .fragmentSpread(Fragment_AnimalFragment, inclusionConditions: [.include(if: "a")]), + ], + mergedSelections: [ + .field("id", type: .string()), + .field("species", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_animalFragment), + ] + ) + )) + + expect(animalFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + + expect(allAnimals_asDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Fragment_DogFragment, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + .fragmentSpread(Fragment_AnimalFragment, inclusionConditions: [.include(if: "a")]), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(dogFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("genus", type: .string()), + ] + ) + )) + } + + func test__deferredFragments__givenBothDeferAndSkipDirectives_onSameNamedFragment_buildsNestedDeferredNamedFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ...AnimalFragment @skip(if: $a) @defer(label: "root") + } + } + + fragment AnimalFragment on Animal { + species + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Fragment_AnimalFragment = try XCTUnwrap(ir.compilationResult[fragment: "AnimalFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_ifA = allAnimals?[as: "Animal", if: !"a"] + let animalFragment = ir.builtFragments["AnimalFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.skip(if: "a")]), + ] + ) + )) + + expect(allAnimals_ifA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.skip(if: "a")], directSelections: [ .deferred(Fragment_AnimalFragment, label: "root"), ], @@ -6648,6 +6885,130 @@ class IRRootFieldBuilderTests: XCTestCase { )) } - #warning("Need tests here with @include and @skip") + func test__deferredFragments__givenBothDeferAndSkipDirectives_onDifferentNamedFragment_shouldNotNestFragment() throws { + // given + schemaSDL = """ + type Query { + allAnimals: [Animal!] + } + + interface Animal { + id: String + species: String + genus: String + } + + type Dog implements Animal { + id: String + species: String + genus: String + } + """ + + document = """ + query TestOperation($a: Boolean) { + allAnimals { + __typename + id + ...AnimalFragment @skip(if: $a) + ...DogFragment @defer(label: "root") + } + } + + fragment AnimalFragment on Animal { + species + } + + fragment DogFragment on Dog { + genus + } + """ + + // when + try buildSubjectRootField() + + // then + let Interface_Animal = try XCTUnwrap(schema[interface: "Animal"]) + let Object_Dog = try XCTUnwrap(schema[object: "Dog"]) + let Fragment_AnimalFragment = try XCTUnwrap(ir.compilationResult[fragment: "AnimalFragment"]) + let Fragment_DogFragment = try XCTUnwrap(ir.compilationResult[fragment: "DogFragment"]) + + let allAnimals = self.subject[field: "allAnimals"] + let allAnimals_animalFragment = allAnimals?[fragment: "AnimalFragment"] + let allAnimals_ifA = allAnimals?[if: !"a"] + let animalFragment = ir.builtFragments["AnimalFragment"] + + let allAnimals_asDog = allAnimals?[as: "Dog"] + let dogFragment = ir.builtFragments["DogFragment"] + + expect(allAnimals?.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("id", type: .string()), + .inlineFragment(parentType: Interface_Animal, inclusionConditions: [.skip(if: "a")]), + .inlineFragment(parentType: Object_Dog), + ], + mergedSelections: [ + .fragmentSpread(Fragment_AnimalFragment, inclusionConditions: [.skip(if: "a")]), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(allAnimals_ifA).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + inclusionConditions: [.skip(if: "a")], + directSelections: [ + .fragmentSpread(Fragment_AnimalFragment, inclusionConditions: [.skip(if: "a")]), + ], + mergedSelections: [ + .field("id", type: .string()), + .field("species", type: .string()), + ], + mergedSources: [ + try .mock(allAnimals), + try .mock(allAnimals_animalFragment), + ] + ) + )) + + expect(animalFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Interface_Animal, + directSelections: [ + .field("species", type: .string()), + ] + ) + )) + + expect(allAnimals_asDog).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .deferred(Fragment_DogFragment, label: "root"), + ], + mergedSelections: [ + .field("id", type: .string()), + .fragmentSpread(Fragment_AnimalFragment, inclusionConditions: [.skip(if: "a")]), + ], + mergedSources: [ + try .mock(allAnimals), + ] + ) + )) + + expect(dogFragment?.rootField.selectionSet).to(shallowlyMatch( + SelectionSetMatcher( + parentType: Object_Dog, + directSelections: [ + .field("genus", type: .string()), + ] + ) + )) + } } From ae4ce883f4e4819398e168d2008eb3ee1795dc66 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Wed, 25 Oct 2023 15:48:07 -0700 Subject: [PATCH 41/41] Adds inline comment on scope condition behaviour --- apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift index 2cc7287ff..bb71780dc 100644 --- a/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift +++ b/apollo-ios-codegen/Sources/IR/IR+RootFieldBuilder.swift @@ -299,6 +299,10 @@ class RootFieldBuilder { } let type = ( + // We must specify the type whenever there is a defer condition because even when the type + // case matches that of the parent it must be evaluted as an entirely separate scope because + // deferred fragments are treated as isolated selections and must not be merged into any + // other selection. parentTypePath.parentType == conditionalSelectionSet.parentType && !isDeferred )