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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ public extension SelectionSet {
guard let value = self.__data._data[key] else {
return false
}
return value == DataDict.NullValue
return value == DataDict._NullValue
}

}
35 changes: 26 additions & 9 deletions Tests/ApolloTests/SelectionSetTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1436,12 +1436,20 @@ class SelectionSetTests: XCTestCase {
return
}
expect(nameValue).to(beNil())

guard let nameValue = nameValue as? String? else {
fail("name should be Optional.some(Optional.none).")
return

if DataDict._AnyHashableCanBeCoerced {
guard let nameValue = nameValue as? String? else {
fail("name should be Optional.some(Optional.none).")
return
}
expect(nameValue).to(beNil())
} else {
guard let nameValue = nameValue.base as? String? else {
fail("name should be Optional.some(Optional.none).")
return
}
expect(nameValue).to(beNil())
}
expect(nameValue).to(beNil())
}

func test__selectionInitializer_givenOptionalEntityField__fieldIsPresentWithOptionalNilValue() {
Expand Down Expand Up @@ -1510,11 +1518,20 @@ class SelectionSetTests: XCTestCase {
}
expect(childValue).to(beNil())

guard let childValue = childValue as? Hero.Child? else {
fail("child should be Optional.some(Optional.none).")
return
if DataDict._AnyHashableCanBeCoerced {
guard let childValue = childValue as? Hero.Child? else {
fail("child should be Optional.some(Optional.none).")
return
}
expect(childValue).to(beNil())

} else {
guard let childValue = childValue.base as? Hero.Child? else {
fail("child should be Optional.some(Optional.none).")
return
}
expect(childValue).to(beNil())
}
expect(childValue).to(beNil())
}

func test__selectionInitializer_givenOptionalListOfOptionalEntitiesField__setsFieldDataCorrectly() {
Expand Down
10 changes: 1 addition & 9 deletions apollo-ios/Sources/Apollo/GraphQLSelectionSetMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ final class GraphQLSelectionSetMapper<T: SelectionSet>: GraphQLResultAccumulator
}

func acceptNullValue(info: FieldExecutionInfo) -> AnyHashable? {
return DataDict.NullValue
return DataDict._NullValue
}

func acceptMissingValue(info: FieldExecutionInfo) throws -> AnyHashable? {
Expand Down Expand Up @@ -86,11 +86,3 @@ 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)
}
65 changes: 51 additions & 14 deletions apollo-ios/Sources/ApolloAPI/DataDict.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Foundation

/// A structure that wraps the underlying data for a ``SelectionSet``.
public struct DataDict: Hashable {
@usableFromInline var _storage: _Storage
Expand Down Expand Up @@ -51,11 +53,16 @@ public struct DataDict: Hashable {

@inlinable public subscript<T: AnyScalarType & Hashable>(_ key: String) -> T {
get {
#if swift(>=5.4)
_data[key] as! T
#else
_data[key]?.base as! T
#endif
if DataDict._AnyHashableCanBeCoerced {
return _data[key] as! T
} else {
let value = _data[key]
if value == DataDict._NullValue {
return (Optional<T>.none as Any) as! T
} else {
return (value?.base as? T) ?? (value._asAnyHashable as! T)
}
}
}
set {
_data[key] = newValue
Expand Down Expand Up @@ -126,6 +133,34 @@ public struct DataDict: Hashable {
}
}

// 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.
public static let _NullValue = {
if DataDict._AnyHashableCanBeCoerced {
return AnyHashable(Optional<AnyHashable>.none)
} else {
return NSNull()
}
}()

/// Indicates if `AnyHashable` can be coerced via casting into its underlying type.
///
/// In iOS versions 14.4 and lower, `AnyHashable` coercion does not work. On these platforms,
/// we need to do some additional unwrapping and casting of the values to avoid crashes and other
/// run time bugs.
public static var _AnyHashableCanBeCoerced: Bool {
if #available(iOS 14.5, *) {
return true
} else {
return false
}
}

}

// MARK: - Value Conversion Helpers

public protocol SelectionSetEntityValue {
Expand Down Expand Up @@ -160,11 +195,13 @@ extension Optional: SelectionSetEntityValue where Wrapped: SelectionSetEntityVal
case .none:
self = .none
case .some(let hashable):
if let optional = hashable.base as? Optional<AnyHashable>, optional == nil {
self = .none
return
}
if DataDict._AnyHashableCanBeCoerced && hashable == DataDict._NullValue {
self = .none
} else if let optional = hashable.base as? Optional<AnyHashable>, optional == nil {
self = .none
} else {
self = .some(Wrapped.init(_fieldData: data))
}
}
}

Expand All @@ -179,11 +216,11 @@ extension Array: SelectionSetEntityValue where Element: SelectionSetEntityValue
fatalError("\(Self.self) expected list of data for entity.")
}
self = data.map {
#if swift(>=5.4)
Element.init(_fieldData:$0)
#else
Element.init(_fieldData:$0?.base as? AnyHashable)
#endif
if DataDict._AnyHashableCanBeCoerced {
return Element.init(_fieldData:$0)
} else {
return Element.init(_fieldData:$0?.base as? AnyHashable)
}
}
}

Expand Down
27 changes: 17 additions & 10 deletions apollo-ios/Sources/ApolloTestSupport/TestMock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ public class Mock<O: MockObject>: AnyMock, Hashable {

public var _selectionSetMockData: JSONObject {
_data.mapValues {
if let mock = $0 as? AnyMock {
if let mock = $0.base as? AnyMock {
return mock._selectionSetMockData
}
if let mockArray = $0 as? Array<Any> {
Expand Down Expand Up @@ -188,20 +188,27 @@ fileprivate extension Array {
}

func _unsafelyConvertToSelectionSetData() -> [AnyHashable?] {
map { element in
switch element {
case let element as AnyMock:
return element._selectionSetMockData
map(_unsafelyConvertToSelectionSetData(element:))
}

case let innerArray as Array<Any>:
return innerArray._unsafelyConvertToSelectionSetData()
private func _unsafelyConvertToSelectionSetData(element: Any) -> AnyHashable? {
switch element {
case let element as AnyMock:
return element._selectionSetMockData

case let element as AnyHashable:
case let innerArray as Array<Any>:
return innerArray._unsafelyConvertToSelectionSetData()

case let element as AnyHashable:
if DataDict._AnyHashableCanBeCoerced {
return element

default:
return nil
} else {
return _unsafelyConvertToSelectionSetData(element: element.base)
}

default:
return nil
}
}
}