From 85d19d698415189158d919282edc754d02925acd Mon Sep 17 00:00:00 2001 From: Gui Rambo Date: Sat, 14 Oct 2023 16:07:18 -0300 Subject: [PATCH] Add support for encoding/decoding String and Int enum properties --- Sources/CloudKitCodable/CloudKitCodable.swift | 6 --- .../CloudKitRecordDecoder.swift | 35 +++++++++++++++++- .../CloudKitRecordEncoder.swift | 4 ++ .../CustomCloudKitEncodable.swift | 12 ++++++ .../CloudKitRecordDecoderTests.swift | 11 ++++++ .../CloudKitRecordEncoderTests.swift | 24 +++++++++++- .../TestTypes/TestModelWithEnum.swift | 37 +++++++++++++++++++ 7 files changed, 120 insertions(+), 9 deletions(-) delete mode 100644 Sources/CloudKitCodable/CloudKitCodable.swift create mode 100644 Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift diff --git a/Sources/CloudKitCodable/CloudKitCodable.swift b/Sources/CloudKitCodable/CloudKitCodable.swift deleted file mode 100644 index 822c639..0000000 --- a/Sources/CloudKitCodable/CloudKitCodable.swift +++ /dev/null @@ -1,6 +0,0 @@ -public struct CloudKitCodable { - public private(set) var text = "Hello, World!" - - public init() { - } -} diff --git a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift index ede4dba..f23d683 100644 --- a/Sources/CloudKitCodable/CloudKitRecordDecoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordDecoder.swift @@ -135,9 +135,40 @@ extension _CloudKitRecordDecoder.KeyedContainer: KeyedDecodingContainerProtocol return try decodeURL(forKey: key) as! T } + func typeMismatch(_ message: String) -> DecodingError { + let context = DecodingError.Context( + codingPath: codingPath, + debugDescription: message + ) + return DecodingError.typeMismatch(type, context) + } + + if let stringEnumType = T.self as? any CloudKitStringEnum.Type { + guard let stringValue = record[key.stringValue] as? String else { + throw typeMismatch("Expected to decode a rawValue String for \"\(String(describing: type))\"") + } + guard let enumValue = stringEnumType.init(rawValue: stringValue) ?? stringEnumType.cloudKitFallbackCase else { + #if DEBUG + throw typeMismatch("Failed to construct enum \"\(String(describing: type))\" from String \"\(stringValue)\"") + #else + throw typeMismatch("Failed to construct enum \"\(String(describing: type))\" from String value") + #endif + } + return enumValue as! T + } + + if let intEnumType = T.self as? any CloudKitIntEnum.Type { + guard let intValue = record[key.stringValue] as? Int else { + throw typeMismatch("Expected to decode a rawValue Int for \"\(String(describing: type))\"") + } + guard let enumValue = intEnumType.init(rawValue: intValue) ?? intEnumType.cloudKitFallbackCase else { + throw typeMismatch("Failed to construct enum \"\(String(describing: type))\" from value \"\(intValue)\"") + } + return enumValue as! T + } + guard let value = record[key.stringValue] as? T else { - let context = DecodingError.Context(codingPath: codingPath, debugDescription: "CKRecordValue couldn't be converted to \(String(describing: type))'") - throw DecodingError.typeMismatch(type, context) + throw typeMismatch("CKRecordValue couldn't be converted to \"\(String(describing: type))\"") } return value diff --git a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift index cbb7304..9ff9f19 100644 --- a/Sources/CloudKitCodable/CloudKitRecordEncoder.swift +++ b/Sources/CloudKitCodable/CloudKitRecordEncoder.swift @@ -181,6 +181,10 @@ extension _CloudKitRecordEncoder.KeyedContainer: KeyedEncodingContainerProtocol throw CloudKitRecordEncodingError.referencesNotSupported(key.stringValue) } else if let ckValue = value as? CKRecordValue { return ckValue + } else if let stringValue = (value as? any CloudKitStringEnum)?.rawValue { + return stringValue as NSString + } else if let intValue = (value as? any CloudKitIntEnum)?.rawValue { + return NSNumber(value: Int(intValue)) } else { throw CloudKitRecordEncodingError.unsupportedValueForKey(key.stringValue) } diff --git a/Sources/CloudKitCodable/CustomCloudKitEncodable.swift b/Sources/CloudKitCodable/CustomCloudKitEncodable.swift index 2296a8f..e35660c 100644 --- a/Sources/CloudKitCodable/CustomCloudKitEncodable.swift +++ b/Sources/CloudKitCodable/CustomCloudKitEncodable.swift @@ -37,3 +37,15 @@ public protocol CustomCloudKitDecodable: CloudKitRecordRepresentable & Decodable } public protocol CustomCloudKitCodable: CustomCloudKitEncodable & CustomCloudKitDecodable { } + +public protocol CloudKitEnum { + static var cloudKitFallbackCase: Self? { get } +} + +public extension CloudKitEnum where Self: CaseIterable { + static var cloudKitFallbackCase: Self? { allCases.first } +} + +public protocol CloudKitStringEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == String { } + +public protocol CloudKitIntEnum: Codable, RawRepresentable, CloudKitEnum where RawValue == Int { } diff --git a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift index a7cf856..58f05db 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift @@ -53,4 +53,15 @@ final class CloudKitRecordDecoderTests: XCTestCase { XCTAssert(samePersonDecoded.cloudKitIdentifier == "MY-ID") } + func testEnumRoundtrip() throws { + let model = TestModelWithEnum.allEnumsPopulated + + let record = try CloudKitRecordEncoder().encode(model) + + var sameModelDecoded = try CloudKitRecordDecoder().decode(TestModelWithEnum.self, from: record) + sameModelDecoded.cloudKitSystemFields = nil + + XCTAssertEqual(sameModelDecoded, model) + } + } diff --git a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift index 588ed19..532bd98 100644 --- a/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift +++ b/Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift @@ -49,5 +49,27 @@ final class CloudKitRecordEncoderTests: XCTestCase { XCTAssert(record.recordID.zoneID == zoneID) XCTAssert(record.recordID.recordName == "MY-ID") } - + + func testEnumEncoding() throws { + let model = TestModelWithEnum.allEnumsPopulated + + let record = try CloudKitRecordEncoder().encode(model) + + XCTAssertEqual(record["enumProperty"], "enumCase3") + XCTAssertEqual(record["optionalEnumProperty"], "enumCase2") + XCTAssertEqual(record["intEnumProperty"], 1) + XCTAssertEqual(record["optionalIntEnumProperty"], 2) + } + + func testEnumEncodingNilValue() throws { + let model = TestModelWithEnum.optionalEnumNil + + let record = try CloudKitRecordEncoder().encode(model) + + XCTAssertEqual(record["enumProperty"], "enumCase3") + XCTAssertNil(record["optionalEnumProperty"]) + XCTAssertEqual(record["intEnumProperty"], 1) + XCTAssertNil(record["optionalIntEnumProperty"]) + } + } diff --git a/Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift b/Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift new file mode 100644 index 0000000..ced8a24 --- /dev/null +++ b/Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift @@ -0,0 +1,37 @@ +import Foundation +import CloudKitCodable + +struct TestModelWithEnum: CustomCloudKitCodable, Hashable { + enum MyStringEnum: String, CloudKitStringEnum, CaseIterable { + case enumCase0 + case enumCase1 + case enumCase2 + case enumCase3 + } + enum MyIntEnum: Int, CloudKitIntEnum, CaseIterable { + case enumCase0 + case enumCase1 + case enumCase2 + case enumCase3 + } + var cloudKitSystemFields: Data? + var enumProperty: MyStringEnum + var optionalEnumProperty: MyStringEnum? + var intEnumProperty: MyIntEnum + var optionalIntEnumProperty: MyIntEnum? +} + +extension TestModelWithEnum { + static let allEnumsPopulated = TestModelWithEnum( + enumProperty: .enumCase3, + optionalEnumProperty: .enumCase2, + intEnumProperty: .enumCase1, + optionalIntEnumProperty: .enumCase2 + ) + static let optionalEnumNil = TestModelWithEnum( + enumProperty: .enumCase3, + optionalEnumProperty: nil, + intEnumProperty: .enumCase1, + optionalIntEnumProperty: nil + ) +}