Skip to content

Commit

Permalink
Merge pull request #12 from insidegui/enum-support
Browse files Browse the repository at this point in the history
Add support for encoding/decoding String and Int enum properties
  • Loading branch information
insidegui authored Apr 13, 2024
2 parents 1ccbccd + 85d19d6 commit 1e44cf5
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 9 deletions.
6 changes: 0 additions & 6 deletions Sources/CloudKitCodable/CloudKitCodable.swift

This file was deleted.

35 changes: 33 additions & 2 deletions Sources/CloudKitCodable/CloudKitRecordDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Sources/CloudKitCodable/CloudKitRecordEncoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
12 changes: 12 additions & 0 deletions Sources/CloudKitCodable/CustomCloudKitEncodable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 { }
11 changes: 11 additions & 0 deletions Tests/CloudKitCodableTests/CloudKitRecordDecoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

}
24 changes: 23 additions & 1 deletion Tests/CloudKitCodableTests/CloudKitRecordEncoderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
}

}
37 changes: 37 additions & 0 deletions Tests/CloudKitCodableTests/TestTypes/TestModelWithEnum.swift
Original file line number Diff line number Diff line change
@@ -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
)
}

0 comments on commit 1e44cf5

Please sign in to comment.