Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove stripping of null values from Selection Set models #25

Merged
merged 1 commit into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions Tests/ApolloInternalTestHelpers/SelectionSet+TestHelpers.swift
Original file line number Diff line number Diff line change
@@ -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
}

}
70 changes: 69 additions & 1 deletion Tests/ApolloTests/Cache/ReadWriteFromStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
}
}
Expand Down Expand Up @@ -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<Data>()
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<Data>()
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase {
selectionSet: selectionSet,
on: object,
variables: variables,
accumulator: GraphQLSelectionSetMapper<T>(stripNullValues: true)
accumulator: GraphQLSelectionSetMapper<T>()
)
}

Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
1 change: 0 additions & 1 deletion apollo-ios/Sources/Apollo/ApolloStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,6 @@ public class ApolloStore {
withKey: key,
variables: variables,
accumulator: GraphQLSelectionSetMapper<SelectionSet>(
stripNullValues: false,
handleMissingValues: .allowForOptionalFields
)
)
Expand Down
13 changes: 9 additions & 4 deletions apollo-ios/Sources/Apollo/GraphQLSelectionSetMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ final class GraphQLSelectionSetMapper<T: SelectionSet>: GraphQLResultAccumulator

let requiresCacheKeyComputation: Bool = false

let stripNullValues: Bool
let handleMissingValues: HandleMissingValues

enum HandleMissingValues {
Expand All @@ -19,10 +18,8 @@ final class GraphQLSelectionSetMapper<T: SelectionSet>: GraphQLResultAccumulator
}

init(
stripNullValues: Bool = true,
handleMissingValues: HandleMissingValues = .disallow
) {
self.stripNullValues = stripNullValues
self.handleMissingValues = handleMissingValues
}

Expand All @@ -48,7 +45,7 @@ final class GraphQLSelectionSetMapper<T: SelectionSet>: GraphQLResultAccumulator
}

func acceptNullValue(info: FieldExecutionInfo) -> AnyHashable? {
return stripNullValues ? nil : Optional<AnyHashable>.none
return DataDict.NullValue
}

func acceptMissingValue(info: FieldExecutionInfo) throws -> AnyHashable? {
Expand Down Expand Up @@ -89,3 +86,11 @@ final class GraphQLSelectionSetMapper<T: SelectionSet>: 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<AnyHashable>.none)
}
4 changes: 1 addition & 3 deletions apollo-ios/Sources/ApolloAPI/DataDict.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading