diff --git a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift index 7b831cd2d..bbb9edf5e 100644 --- a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift @@ -27,6 +27,20 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ) } + private func readValues( + _ selectionSet: T.Type, + in operation: Operation.Type, + from object: JSONObject, + variables: GraphQLOperation.Variables? = nil + ) throws -> T { + return try GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.executor.execute( + selectionSet: selectionSet, + in: operation, + on: object, + accumulator: GraphQLSelectionSetMapper() + ) + } + // MARK: - Tests // MARK: Nonnull Scalar @@ -952,7 +966,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { // MARK: Deferred Inline Fragments - func test__deferredInlineFragment__whenExecutingOnDeferredInlineFragment_selectsFieldsAndFulfillsFragment() throws { + func test__deferredInlineFragment__givenPartialDataForSelection_withConditionEvaluatingTrue_collectsDeferredFragment() throws { // given class AnAnimal: MockSelectionSet { typealias Schema = MockSchemaMetadata @@ -967,7 +981,174 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { override class var __selections: [Selection] {[ .field("__typename", String.self), .field("name", String.self), - .deferred(DeferredSpecies.self, label: "deferreSpecies"), + .deferred(if: "varA", DeferredSpecies.self, label: "deferredSpecies"), + ]} + + struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _deferredSpecies = Deferred(_dataDict: _dataDict) + } + + @Deferred var deferredSpecies: DeferredSpecies? + } + + class DeferredSpecies: MockTypeCase { + override class var __selections: [Selection] {[ + .field("species", String.self), + ]} + } + } + } + + let object: JSONObject = [ + "animal": [ + "__typename": "Animal", + "name": "Lassie" + ] + ] + + // when + let data = try readValues(AnAnimal.self, from: object, variables: ["varA": true]) + + // then + XCTAssertEqual(data.animal.__typename, "Animal") + XCTAssertEqual(data.animal.name, "Lassie") + + XCTAssertEqual(data.animal.__data._deferredFragments, [ObjectIdentifier(AnAnimal.Animal.DeferredSpecies.self)]) + XCTAssertEqual(data.animal.__data._fulfilledFragments, [ObjectIdentifier(AnAnimal.Animal.self)]) + } + + func test__deferredInlineFragment__givenPartialDataForSelection_withConditionEvaluatingFalse_doesCollectFulfilledFragmentAndFields() throws { + // given + class AnAnimal: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("animal", Animal.self), + ]} + + var animal: Animal { __data["animal"] } + + class Animal: AbstractMockSelectionSet { + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("name", String.self), + .deferred(if: "varA", DeferredSpecies.self, label: "deferredSpecies"), + ]} + + struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _deferredSpecies = Deferred(_dataDict: _dataDict) + } + + @Deferred var deferredSpecies: DeferredSpecies? + } + + class DeferredSpecies: MockTypeCase { + override class var __selections: [Selection] {[ + .field("species", String.self), + ]} + } + } + } + + let object: JSONObject = [ + "animal": [ + "__typename": "Animal", + "name": "Lassie", + "species": "Canis familiaris", + ] + ] + + // when + let data = try readValues(AnAnimal.self, from: object, variables: ["varA": false]) + + // then + XCTAssertEqual(data.animal.__typename, "Animal") + XCTAssertEqual(data.animal.name, "Lassie") + XCTAssertEqual(data.animal.fragments.deferredSpecies?.species, "Canis familiaris") + + XCTAssertTrue(data.animal.__data._deferredFragments.isEmpty) + XCTAssertEqual(data.animal.__data._fulfilledFragments, [ + ObjectIdentifier(AnAnimal.Animal.self), + ObjectIdentifier(AnAnimal.Animal.DeferredSpecies.self) + ]) + } + + func test__deferredInlineFragment__givenPartialDataForSelection_withConditionEvaluatingFalse_whenMissingDeferredIncrementalData_shouldThrow() throws { + // given + class AnAnimal: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("animal", Animal.self), + ]} + + var animal: Animal { __data["animal"] } + + class Animal: AbstractMockSelectionSet { + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("name", String.self), + .deferred(if: "varA", DeferredSpecies.self, label: "deferredSpecies"), + ]} + + struct Fragments: FragmentContainer { + public let __data: DataDict + public init(_dataDict: DataDict) { + __data = _dataDict + _deferredSpecies = Deferred(_dataDict: _dataDict) + } + + @Deferred var deferredSpecies: DeferredSpecies? + } + + class DeferredSpecies: MockTypeCase { + override class var __selections: [Selection] {[ + .field("species", String.self), + ]} + } + } + } + + let object: JSONObject = [ + "animal": [ + "__typename": "Animal", + "name": "Lassie" + ] + ] + + // when + then + XCTAssertThrowsError(try readValues(AnAnimal.self, from: object, variables: ["varA": false])) { error in + guard + let error = error as? GraphQLExecutionError, + case JSONDecodingError.missingValue = error.underlying + else { return fail("Incorrect error type") } + + XCTAssertEqual(error.path, ResponsePath("animal.species")) + } + } + + func test__deferredInlineFragment__givenIncrementalDataForDeferredSelection_selectsFieldsAndFulfillsFragment() throws { + // given + class AnAnimal: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __selections: [Selection] {[ + .field("animal", Animal.self), + ]} + + var animal: Animal { __data["animal"] } + + class Animal: AbstractMockSelectionSet { + override class var __selections: [Selection] {[ + .field("__typename", String.self), + .field("name", String.self), + .deferred(DeferredSpecies.self, label: "deferredSpecies"), ]} struct Fragments: FragmentContainer { @@ -993,17 +1174,13 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ] // when - let data = try GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.executor.execute( - selectionSet: AnAnimal.Animal.DeferredSpecies.self, - in: MockQuery.self, - on: object, - accumulator: GraphQLSelectionSetMapper() - ) + let data = try readValues(AnAnimal.Animal.DeferredSpecies.self, in: MockQuery.self, from: object) // then XCTAssertEqual(data.species, "Canis familiaris") XCTAssertEqual(data.__data._fulfilledFragments, [ObjectIdentifier(AnAnimal.Animal.DeferredSpecies.self)]) + XCTAssertTrue(data.__data._deferredFragments.isEmpty) } // MARK: - Fragments diff --git a/apollo-ios/Sources/Apollo/FieldSelectionCollector.swift b/apollo-ios/Sources/Apollo/FieldSelectionCollector.swift index bb9cba567..f0835d468 100644 --- a/apollo-ios/Sources/Apollo/FieldSelectionCollector.swift +++ b/apollo-ios/Sources/Apollo/FieldSelectionCollector.swift @@ -93,15 +93,30 @@ struct DefaultFieldSelectionCollector: FieldSelectionCollector { info: info) } - case let .deferred(_, typeCase, _): + case let .deferred(condition, typeCase, _): // In Apollo's implementation (Router + Server) of deferSpec=20220824 ALL defer directives // will be honoured and sent as separate incremental responses. This means deferred - // selection fields never need to be collected because they are parsed with the incremental - // data, at which time they are no longer deferred. + // selection fields only need to be collected when they are parsed with the incremental + // data, at which time they are no longer deferred. The deferred fragment identifiers still + // need to be collected becuase that is how the user determines the state of the deferred + // fragment via the @Deferred property wrapper. // - // The deferred fragment identifiers still need to be collected becuase that is how the - // user determines the state of the deferred fragment via the @Deferred property wrapper. - groupedFields.addDeferredFragment(typeCase) + // If the defer condition evaluates to false though, the fragment is considered to be fulfilled + // and and the fields must be collected. + let isDeferred: Bool = { + if let condition, !condition.evaluate(with: info.variables) { + return false + } + return true + }() + + if isDeferred { + groupedFields.addDeferredFragment(typeCase) + + } else { + groupedFields.addFulfilledFragment(typeCase) + try collectFields(from: typeCase.__selections, into: &groupedFields, for: object, info: info) + } case let .fragment(fragment): groupedFields.addFulfilledFragment(fragment) diff --git a/apollo-ios/Sources/ApolloAPI/Selection+Conditions.swift b/apollo-ios/Sources/ApolloAPI/Selection+Conditions.swift index f19756c22..63eff2bf2 100644 --- a/apollo-ios/Sources/ApolloAPI/Selection+Conditions.swift +++ b/apollo-ios/Sources/ApolloAPI/Selection+Conditions.swift @@ -114,7 +114,7 @@ fileprivate extension Array where Element == Selection.Condition { } // MARK: Conditions - Individual -fileprivate extension Selection.Condition { +public extension Selection.Condition { func evaluate(with variables: GraphQLOperation.Variables?) -> Bool { switch self { case let .value(value):