diff --git a/Tests/ApolloInternalTestHelpers/SelectionSet+TestHelpers.swift b/Tests/ApolloInternalTestHelpers/SelectionSet+TestHelpers.swift new file mode 100644 index 000000000..61076edaa --- /dev/null +++ b/Tests/ApolloInternalTestHelpers/SelectionSet+TestHelpers.swift @@ -0,0 +1,15 @@ +@testable import ApolloAPI +@testable import Apollo + +public extension SelectionSet { + + var _rawData: [String: AnyHashable] { self.__data._data } + + func hasNullValue(forKey key: String) -> Bool { + guard let value = self.__data._data[key] else { + return false + } + return value == DataDict.NullValue + } + +} diff --git a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift index ffc07c0c7..de407c782 100644 --- a/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift +++ b/Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift @@ -503,7 +503,8 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { let data = try XCTUnwrap(graphQLResult.data) XCTAssertEqual(data.hero.name, "Artoo") - XCTAssertNil(data.hero.nickname) + expect(data.hero.nickname).to(beNil()) + expect(data.hero.hasNullValue(forKey: "nickname")).to(beTrue()) } } } @@ -1696,6 +1697,73 @@ class ReadWriteFromStoreTests: XCTestCase, CacheDependentTesting, StoreLoading { } } + // MARK: - Write w/Selection Set Initializers + + func test_writeDataForOperation_givenSelectionSetManuallyInitialized_withNullValueForField_fieldHasNullValue() throws { + // given + struct Types { + static let Query = Object(typename: "Query", implementedInterfaces: []) + } + + MockSchemaMetadata.stub_objectTypeForTypeName = { + switch $0 { + case "Query": return Types.Query + default: XCTFail(); return nil + } + } + + class Data: MockSelectionSet { + typealias Schema = MockSchemaMetadata + + override class var __parentType: ParentType { Types.Query } + override class var __selections: [Selection] {[ + .field("name", String?.self) + ]} + + public var name: String? { __data["name"] } + + convenience init( + name: String? = nil + ) { + self.init(_dataDict: DataDict(data: [ + "__typename": Types.Query.typename, + "name": name + ], fulfilledFragments: [ObjectIdentifier(Self.self)])) + } + } + + // when + let writeCompletedExpectation = expectation(description: "Write completed") + + store.withinReadWriteTransaction({ transaction in + let data = Data(name: nil) + let query = MockQuery() + try transaction.write(data: data, for: query) + + }, completion: { result in + defer { writeCompletedExpectation.fulfill() } + XCTAssertSuccessResult(result) + }) + + self.wait(for: [writeCompletedExpectation], timeout: Self.defaultWaitTimeout) + + let readCompletedExpectation = expectation(description: "Read completed") + + store.withinReadTransaction({ transaction in + let query = MockQuery() + let resultData = try transaction.read(query: query) + + expect(resultData.name).to(beNil()) + expect(resultData.hasNullValue(forKey: "name")).to(beTrue()) + + }, completion: { result in + defer { readCompletedExpectation.fulfill() } + XCTAssertSuccessResult(result) + }) + + self.wait(for: [readCompletedExpectation], timeout: Self.defaultWaitTimeout) + } + func test_writeDataForOperation_givenSelectionSetManuallyInitializedWithInclusionConditions_writesFieldsForInclusionConditions() throws { // given struct Types { diff --git a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift index dbd0331e9..dbb87b717 100644 --- a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift @@ -23,7 +23,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { selectionSet: selectionSet, on: object, variables: variables, - accumulator: GraphQLSelectionSetMapper(stripNullValues: true) + accumulator: GraphQLSelectionSetMapper() ) } @@ -238,7 +238,8 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let data = try readValues(GivenSelectionSet.self, from: object) // then - XCTAssertNil(data.name) + expect(data.name).to(beNil()) + expect(data.hasNullValue(forKey: "name")).to(beTrue()) } func test__optional_scalar__givenDataWithTypeConvertibleToFieldType_getsConvertedValue() throws { @@ -689,7 +690,8 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let data = try readValues(GivenSelectionSet.self, from: object) // then - XCTAssertEqual(data.favorites! as [String?], ["Red", nil, "Bird"]) + expect(data.favorites! as [String?]).to(equal(["Red", nil, "Bird"])) + expect((data._rawData["favorites"] as? [AnyHashable])?[1]).to(equal(DataDict.NullValue)) } // MARK: Optional List Of Optional Scalar diff --git a/apollo-ios/Sources/Apollo/ApolloStore.swift b/apollo-ios/Sources/Apollo/ApolloStore.swift index e2ad5862e..cc4ea7c3c 100644 --- a/apollo-ios/Sources/Apollo/ApolloStore.swift +++ b/apollo-ios/Sources/Apollo/ApolloStore.swift @@ -278,7 +278,6 @@ public class ApolloStore { withKey: key, variables: variables, accumulator: GraphQLSelectionSetMapper( - stripNullValues: false, handleMissingValues: .allowForOptionalFields ) ) diff --git a/apollo-ios/Sources/Apollo/GraphQLSelectionSetMapper.swift b/apollo-ios/Sources/Apollo/GraphQLSelectionSetMapper.swift index 458367449..d67907574 100644 --- a/apollo-ios/Sources/Apollo/GraphQLSelectionSetMapper.swift +++ b/apollo-ios/Sources/Apollo/GraphQLSelectionSetMapper.swift @@ -7,7 +7,6 @@ final class GraphQLSelectionSetMapper: GraphQLResultAccumulator let requiresCacheKeyComputation: Bool = false - let stripNullValues: Bool let handleMissingValues: HandleMissingValues enum HandleMissingValues { @@ -19,10 +18,8 @@ final class GraphQLSelectionSetMapper: GraphQLResultAccumulator } init( - stripNullValues: Bool = true, handleMissingValues: HandleMissingValues = .disallow ) { - self.stripNullValues = stripNullValues self.handleMissingValues = handleMissingValues } @@ -48,7 +45,7 @@ final class GraphQLSelectionSetMapper: GraphQLResultAccumulator } func acceptNullValue(info: FieldExecutionInfo) -> AnyHashable? { - return stripNullValues ? nil : Optional.none + return DataDict.NullValue } func acceptMissingValue(info: FieldExecutionInfo) throws -> AnyHashable? { @@ -89,3 +86,11 @@ final class GraphQLSelectionSetMapper: GraphQLResultAccumulator return T.init(_dataDict: rootValue) } } + +// MARK: - Null Value Definition +extension DataDict { + /// A common value used to represent a null value in a `DataDict`. + /// + /// This value can be cast to `NSNull` and will bridge automatically. + static let NullValue = AnyHashable(Optional.none) +} diff --git a/apollo-ios/Sources/ApolloAPI/DataDict.swift b/apollo-ios/Sources/ApolloAPI/DataDict.swift index c8651384b..d5e458df2 100644 --- a/apollo-ios/Sources/ApolloAPI/DataDict.swift +++ b/apollo-ios/Sources/ApolloAPI/DataDict.swift @@ -126,9 +126,7 @@ public struct DataDict: Hashable { } } - - -// MARK: Value Conversion Helpers +// MARK: - Value Conversion Helpers public protocol SelectionSetEntityValue { /// - Warning: This function is not supported for external use.