Skip to content

Commit ecacc66

Browse files
authored
Handle coercion of AnyHashable (#61)
1 parent 3246526 commit ecacc66

File tree

5 files changed

+96
-43
lines changed

5 files changed

+96
-43
lines changed

Tests/ApolloInternalTestHelpers/SelectionSet+TestHelpers.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ public extension SelectionSet {
99
guard let value = self.__data._data[key] else {
1010
return false
1111
}
12-
return value == DataDict.NullValue
12+
return value == DataDict._NullValue
1313
}
1414

1515
}

Tests/ApolloTests/SelectionSetTests.swift

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,12 +1436,20 @@ class SelectionSetTests: XCTestCase {
14361436
return
14371437
}
14381438
expect(nameValue).to(beNil())
1439-
1440-
guard let nameValue = nameValue as? String? else {
1441-
fail("name should be Optional.some(Optional.none).")
1442-
return
1439+
1440+
if DataDict._AnyHashableCanBeCoerced {
1441+
guard let nameValue = nameValue as? String? else {
1442+
fail("name should be Optional.some(Optional.none).")
1443+
return
1444+
}
1445+
expect(nameValue).to(beNil())
1446+
} else {
1447+
guard let nameValue = nameValue.base as? String? else {
1448+
fail("name should be Optional.some(Optional.none).")
1449+
return
1450+
}
1451+
expect(nameValue).to(beNil())
14431452
}
1444-
expect(nameValue).to(beNil())
14451453
}
14461454

14471455
func test__selectionInitializer_givenOptionalEntityField__fieldIsPresentWithOptionalNilValue() {
@@ -1510,11 +1518,20 @@ class SelectionSetTests: XCTestCase {
15101518
}
15111519
expect(childValue).to(beNil())
15121520

1513-
guard let childValue = childValue as? Hero.Child? else {
1514-
fail("child should be Optional.some(Optional.none).")
1515-
return
1521+
if DataDict._AnyHashableCanBeCoerced {
1522+
guard let childValue = childValue as? Hero.Child? else {
1523+
fail("child should be Optional.some(Optional.none).")
1524+
return
1525+
}
1526+
expect(childValue).to(beNil())
1527+
1528+
} else {
1529+
guard let childValue = childValue.base as? Hero.Child? else {
1530+
fail("child should be Optional.some(Optional.none).")
1531+
return
1532+
}
1533+
expect(childValue).to(beNil())
15161534
}
1517-
expect(childValue).to(beNil())
15181535
}
15191536

15201537
func test__selectionInitializer_givenOptionalListOfOptionalEntitiesField__setsFieldDataCorrectly() {

apollo-ios/Sources/Apollo/GraphQLSelectionSetMapper.swift

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ final class GraphQLSelectionSetMapper<T: SelectionSet>: GraphQLResultAccumulator
4545
}
4646

4747
func acceptNullValue(info: FieldExecutionInfo) -> AnyHashable? {
48-
return DataDict.NullValue
48+
return DataDict._NullValue
4949
}
5050

5151
func acceptMissingValue(info: FieldExecutionInfo) throws -> AnyHashable? {
@@ -86,11 +86,3 @@ final class GraphQLSelectionSetMapper<T: SelectionSet>: GraphQLResultAccumulator
8686
return T.init(_dataDict: rootValue)
8787
}
8888
}
89-
90-
// MARK: - Null Value Definition
91-
extension DataDict {
92-
/// A common value used to represent a null value in a `DataDict`.
93-
///
94-
/// This value can be cast to `NSNull` and will bridge automatically.
95-
static let NullValue = AnyHashable(Optional<AnyHashable>.none)
96-
}

apollo-ios/Sources/ApolloAPI/DataDict.swift

Lines changed: 51 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import Foundation
2+
13
/// A structure that wraps the underlying data for a ``SelectionSet``.
24
public struct DataDict: Hashable {
35
@usableFromInline var _storage: _Storage
@@ -51,11 +53,16 @@ public struct DataDict: Hashable {
5153

5254
@inlinable public subscript<T: AnyScalarType & Hashable>(_ key: String) -> T {
5355
get {
54-
#if swift(>=5.4)
55-
_data[key] as! T
56-
#else
57-
_data[key]?.base as! T
58-
#endif
56+
if DataDict._AnyHashableCanBeCoerced {
57+
return _data[key] as! T
58+
} else {
59+
let value = _data[key]
60+
if value == DataDict._NullValue {
61+
return (Optional<T>.none as Any) as! T
62+
} else {
63+
return (value?.base as? T) ?? (value._asAnyHashable as! T)
64+
}
65+
}
5966
}
6067
set {
6168
_data[key] = newValue
@@ -126,6 +133,34 @@ public struct DataDict: Hashable {
126133
}
127134
}
128135

136+
// MARK: - Null Value Definition
137+
extension DataDict {
138+
/// A common value used to represent a null value in a `DataDict`.
139+
///
140+
/// This value can be cast to `NSNull` and will bridge automatically.
141+
public static let _NullValue = {
142+
if DataDict._AnyHashableCanBeCoerced {
143+
return AnyHashable(Optional<AnyHashable>.none)
144+
} else {
145+
return NSNull()
146+
}
147+
}()
148+
149+
/// Indicates if `AnyHashable` can be coerced via casting into its underlying type.
150+
///
151+
/// In iOS versions 14.4 and lower, `AnyHashable` coercion does not work. On these platforms,
152+
/// we need to do some additional unwrapping and casting of the values to avoid crashes and other
153+
/// run time bugs.
154+
public static var _AnyHashableCanBeCoerced: Bool {
155+
if #available(iOS 14.5, *) {
156+
return true
157+
} else {
158+
return false
159+
}
160+
}
161+
162+
}
163+
129164
// MARK: - Value Conversion Helpers
130165

131166
public protocol SelectionSetEntityValue {
@@ -160,11 +195,13 @@ extension Optional: SelectionSetEntityValue where Wrapped: SelectionSetEntityVal
160195
case .none:
161196
self = .none
162197
case .some(let hashable):
163-
if let optional = hashable.base as? Optional<AnyHashable>, optional == nil {
164-
self = .none
165-
return
166-
}
198+
if DataDict._AnyHashableCanBeCoerced && hashable == DataDict._NullValue {
199+
self = .none
200+
} else if let optional = hashable.base as? Optional<AnyHashable>, optional == nil {
201+
self = .none
202+
} else {
167203
self = .some(Wrapped.init(_fieldData: data))
204+
}
168205
}
169206
}
170207

@@ -179,11 +216,11 @@ extension Array: SelectionSetEntityValue where Element: SelectionSetEntityValue
179216
fatalError("\(Self.self) expected list of data for entity.")
180217
}
181218
self = data.map {
182-
#if swift(>=5.4)
183-
Element.init(_fieldData:$0)
184-
#else
185-
Element.init(_fieldData:$0?.base as? AnyHashable)
186-
#endif
219+
if DataDict._AnyHashableCanBeCoerced {
220+
return Element.init(_fieldData:$0)
221+
} else {
222+
return Element.init(_fieldData:$0?.base as? AnyHashable)
223+
}
187224
}
188225
}
189226

apollo-ios/Sources/ApolloTestSupport/TestMock.swift

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public class Mock<O: MockObject>: AnyMock, Hashable {
9797

9898
public var _selectionSetMockData: JSONObject {
9999
_data.mapValues {
100-
if let mock = $0 as? AnyMock {
100+
if let mock = $0.base as? AnyMock {
101101
return mock._selectionSetMockData
102102
}
103103
if let mockArray = $0 as? Array<Any> {
@@ -188,20 +188,27 @@ fileprivate extension Array {
188188
}
189189

190190
func _unsafelyConvertToSelectionSetData() -> [AnyHashable?] {
191-
map { element in
192-
switch element {
193-
case let element as AnyMock:
194-
return element._selectionSetMockData
191+
map(_unsafelyConvertToSelectionSetData(element:))
192+
}
195193

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

199-
case let element as AnyHashable:
199+
case let innerArray as Array<Any>:
200+
return innerArray._unsafelyConvertToSelectionSetData()
201+
202+
case let element as AnyHashable:
203+
if DataDict._AnyHashableCanBeCoerced {
200204
return element
201205

202-
default:
203-
return nil
206+
} else {
207+
return _unsafelyConvertToSelectionSetData(element: element.base)
204208
}
209+
210+
default:
211+
return nil
205212
}
206213
}
207214
}

0 commit comments

Comments
 (0)