From 719cf6d8904d517d9cdab57d27a71a3f79f2d820 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 13 Sep 2024 09:16:43 -0700 Subject: [PATCH] fix: Cherry pick `ObjectData` type check to `ListData` (#473) --- .../ObjectDataTransformerTests.swift | 150 ++++++++++++++++++ apollo-ios/Sources/ApolloAPI/ObjectData.swift | 32 ++-- 2 files changed, 169 insertions(+), 13 deletions(-) create mode 100644 Tests/ApolloTests/ObjectDataTransformerTests.swift diff --git a/Tests/ApolloTests/ObjectDataTransformerTests.swift b/Tests/ApolloTests/ObjectDataTransformerTests.swift new file mode 100644 index 000000000..daee1ec6e --- /dev/null +++ b/Tests/ApolloTests/ObjectDataTransformerTests.swift @@ -0,0 +1,150 @@ +import XCTest +import Nimble +import ApolloAPI + +class ObjectDataTransformerTests: XCTestCase { + + fileprivate struct DataTransformer: _ObjectData_Transformer { + func transform(_ value: AnyHashable) -> (any ScalarType)? { + switch value { + case let scalar as any ScalarType: + return scalar + default: + return nil + } + } + + // Empty until needed in tests + func transform(_ value: AnyHashable) -> ObjectData? { return nil } + func transform(_ value: AnyHashable) -> ListData? { return nil } + } + + // MARK: ObjectData Tests + + func test__ObjectData_subscript_ScalarType__givenData_asInt_equalToBoolFalse_shouldReturnIntType() { + // given + let dataTransformer = ObjectData( + _transformer: DataTransformer(), + _rawData: ["intKey": 0] + ) + + // when + let actual = dataTransformer["intKey"] + + // then + expect(actual).to(beAnInstanceOf(Int.self)) + } + + func test__ObjectData_subscript_ScalarType__givenData_asInt_equalToBoolTrue_shouldReturnIntType() { + // given + let dataTransformer = ObjectData( + _transformer: DataTransformer(), + _rawData: ["intKey": 1] + ) + + // when + let actual = dataTransformer["intKey"] + + // then + expect(actual).to(beAnInstanceOf(Int.self)) + } + + func test__ObjectData_subscript_ScalarType__givenData_asInt_outsideBoolRange_shouldReturnIntType() { + // given + let dataTransformer = ObjectData( + _transformer: DataTransformer(), + _rawData: ["intKey": 2] + ) + + // when + let actual = dataTransformer["intKey"] + + // then + expect(actual).to(beAnInstanceOf(Int.self)) + } + + func test__ObjectData_subscript_ScalarType__givenData_asBool_true_shouldReturnBoolType() { + // given + let dataTransformer = ObjectData( + _transformer: DataTransformer(), + _rawData: ["boolKey": true] + ) + + // when + let actual = dataTransformer["boolKey"] + + // then + expect(actual).to(beAnInstanceOf(Bool.self)) + } + + func test__ObjectData_subscript_ScalarType__givenData_asBool_false_shouldReturnBoolType() { + // given + let dataTransformer = ObjectData( + _transformer: DataTransformer(), + _rawData: ["boolKey": false] + ) + + // when + let actual = dataTransformer["boolKey"] + + // then + expect(actual).to(beAnInstanceOf(Bool.self)) + } + + // MARK: ListData Tests + + func test__ListData_subscript_ScalarType__givenData_asInt_equalToBoolFalse_shouldReturnIntType() { + // given + let dataTransformer = ListData(_transformer: DataTransformer(), _rawData: [0]) + + // when + let actual = dataTransformer[0] + + // then + expect(actual).to(beAnInstanceOf(Int.self)) + } + + func test__ListData_subscript_ScalarType__givenData_asInt_equalToBoolTrue_shouldReturnIntType() { + // given + let dataTransformer = ListData(_transformer: DataTransformer(), _rawData: [1]) + + // when + let actual = dataTransformer[0] + + // then + expect(actual).to(beAnInstanceOf(Int.self)) + } + + func test__ListData_subscript_ScalarType__givenData_asInt_outsideBoolRange_shouldReturnIntType() { + // given + let dataTransformer = ListData(_transformer: DataTransformer(), _rawData: [2]) + + // when + let actual = dataTransformer[0] + + // then + expect(actual).to(beAnInstanceOf(Int.self)) + } + + func test__ListData_subscript_ScalarType__givenData_asBool_true_shouldReturnBoolType() { + // given + let dataTransformer = ListData(_transformer: DataTransformer(), _rawData: [true]) + + // when + let actual = dataTransformer[0] + + // then + expect(actual).to(beAnInstanceOf(Bool.self)) + } + + func test__ListData_subscript_ScalarType__givenData_asBool_false_shouldReturnBoolType() { + // given + let dataTransformer = ListData(_transformer: DataTransformer(), _rawData: [false]) + + // when + let actual = dataTransformer[0] + + // then + expect(actual).to(beAnInstanceOf(Bool.self)) + } +} diff --git a/apollo-ios/Sources/ApolloAPI/ObjectData.swift b/apollo-ios/Sources/ApolloAPI/ObjectData.swift index db2781616..b057b9266 100644 --- a/apollo-ios/Sources/ApolloAPI/ObjectData.swift +++ b/apollo-ios/Sources/ApolloAPI/ObjectData.swift @@ -11,9 +11,6 @@ public struct ObjectData { public let _transformer: any _ObjectData_Transformer public let _rawData: [String: AnyHashable] - @usableFromInline internal static let _boolTrue = AnyHashable(true) - @usableFromInline internal static let _boolFalse = AnyHashable(false) - public init( _transformer: any _ObjectData_Transformer, _rawData: [String: AnyHashable] @@ -29,7 +26,7 @@ public struct ObjectData { // This check is based on AnyHashable using a canonical representation of the type-erased value so // instances wrapping the same value of any type compare as equal. Therefore while Int(1) and Int(0) // might be representable as Bool they will never equal Bool(true) nor Bool(false). - if let boolVal = value as? Bool, (value == Self._boolTrue || value == Self._boolFalse) { + if let boolVal = value as? Bool, value.isCanonicalBool { value = boolVal // Cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting @@ -72,17 +69,17 @@ public struct ListData { @inlinable public subscript(_ key: Int) -> (any ScalarType)? { var value: AnyHashable = _rawData[key] - // Attempting cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting, - // also need to attempt `Bool` cast first to ensure a bool doesn't get inadvertently converted to `Int` - switch value { - case let boolVal as Bool: + // This check is based on AnyHashable using a canonical representation of the type-erased value so + // instances wrapping the same value of any type compare as equal. Therefore while Int(1) and Int(0) + // might be representable as Bool they will never equal Bool(true) nor Bool(false). + if let boolVal = value as? Bool, value.isCanonicalBool { value = boolVal - case let intVal as Int: - value = intVal - default: - break + + // Cast to `Int` to ensure we always use `Int` vs `Int32` or `Int64` for consistency and ScalarType casting + } else if let intValue = value as? Int { + value = intValue } - + return _transformer.transform(value) } @@ -96,3 +93,12 @@ public struct ListData { return _transformer.transform(_rawData[key]) } } + +extension AnyHashable { + fileprivate static let boolTrue = AnyHashable(true) + fileprivate static let boolFalse = AnyHashable(false) + + @usableFromInline var isCanonicalBool: Bool { + self == Self.boolTrue || self == Self.boolFalse + } +}