diff --git a/CodableFirebase/DecodeStrategy.swift b/CodableFirebase/DecodeStrategy.swift index ec6a363..e93b7ad 100644 --- a/CodableFirebase/DecodeStrategy.swift +++ b/CodableFirebase/DecodeStrategy.swift @@ -13,6 +13,8 @@ public enum DateDecodingStrategy { /// Defer to `Date` for decoding. This is the default strategy. case deferredToDate + case deferredToTimestamp + /// Decode the `Date` as a UNIX timestamp from a JSON number. case secondsSince1970 @@ -43,13 +45,17 @@ public enum DataDecodingStrategy { } public enum FirestoreTypeDecodingStrategy { - case byProtocol + case deferredToPtotocol + case custom((_ value: Any) throws -> Any) } extension CodingUserInfoKey { public static let dateDecodingStrategy: CodingUserInfoKey = CodingUserInfoKey(rawValue: "dateDecodingStrategy")! + public static let dataDecodingStrategy: CodingUserInfoKey = CodingUserInfoKey(rawValue: "dataDecodingStrategy")! + + public static let firestoreTypeDecodingStrategy: CodingUserInfoKey = CodingUserInfoKey(rawValue: "firestoreTypeDecodingStrategy")! } extension Dictionary where Key == CodingUserInfoKey, Value == Any { @@ -60,4 +66,12 @@ extension Dictionary where Key == CodingUserInfoKey, Value == Any { var dataDecodingStrategy: DataDecodingStrategy? { return self[.dataDecodingStrategy] as? DataDecodingStrategy } + + var firestoreTypeDecodingStrategy: FirestoreTypeDecodingStrategy { + if let strategy = self[.firestoreTypeDecodingStrategy] as? FirestoreTypeDecodingStrategy { + return strategy + } + + return FirestoreTypeDecodingStrategy.deferredToPtotocol + } } diff --git a/CodableFirebase/Decoder.swift b/CodableFirebase/Decoder.swift index 200f9e0..78d54a0 100644 --- a/CodableFirebase/Decoder.swift +++ b/CodableFirebase/Decoder.swift @@ -1104,6 +1104,9 @@ extension _FirebaseDecoder { } switch options { + case .deferredToTimestamp: + let timestamp = value as! TimestampType + return timestamp.dateValue() case .deferredToDate: self.storage.push(container: value) let date = try Date(from: self) @@ -1219,7 +1222,13 @@ extension _FirebaseDecoder { decoded = decimal as! T } else if userInfo.skipFirestoreTypes && (T.self is FirestoreDecodable.Type) { - decoded = value as! T + let strategy = userInfo.firestoreTypeDecodingStrategy + switch strategy { + case .deferredToPtotocol: + decoded = value as! T + case .custom(let decodeFunc): + decoded = try decodeFunc(value) as! T + } } else { self.storage.push(container: value) diff --git a/CodableFirebase/EncodeStrategy.swift b/CodableFirebase/EncodeStrategy.swift index a007605..6a1ce5d 100644 --- a/CodableFirebase/EncodeStrategy.swift +++ b/CodableFirebase/EncodeStrategy.swift @@ -13,6 +13,8 @@ public enum DateEncodingStrategy { /// Defer to `Date` for choosing an encoding. This is the default strategy. case deferredToDate + case deferredToTimestamp((Date) -> TimestampType) + /// Encode the `Date` as a UNIX timestamp (as a JSON number). case secondsSince1970 @@ -46,11 +48,19 @@ public enum DataEncodingStrategy { case custom((Data, Encoder) throws -> Void) } +public enum FirestoreTypeEncodingStrategy { + case deferredToPtotocol + case custom((_ value: Any) throws -> Any) +} + extension CodingUserInfoKey { public static let dateEncodingStrategy: CodingUserInfoKey = CodingUserInfoKey(rawValue: "dateEncodingStrategy")! + public static let dataEncodingStrategy: CodingUserInfoKey = CodingUserInfoKey(rawValue: "dataEncodingStrategy")! public static let skipFirestoreTypes: CodingUserInfoKey = CodingUserInfoKey(rawValue: "skipFirestoreTypes")! + + public static let firestoreTypeEncodingStrategy: CodingUserInfoKey = CodingUserInfoKey(rawValue: "firestoreTypeEncodingStrategy")! } extension Dictionary where Key == CodingUserInfoKey, Value == Any { @@ -68,4 +78,12 @@ extension Dictionary where Key == CodingUserInfoKey, Value == Any { } return false } + + var firestoreTypeEncodingStrategy: FirestoreTypeEncodingStrategy { + if let strategy = self[.firestoreTypeEncodingStrategy] as? FirestoreTypeEncodingStrategy { + return strategy + } + + return FirestoreTypeEncodingStrategy.deferredToPtotocol + } } diff --git a/CodableFirebase/Encoder.swift b/CodableFirebase/Encoder.swift index 059436b..946aa25 100644 --- a/CodableFirebase/Encoder.swift +++ b/CodableFirebase/Encoder.swift @@ -306,6 +306,8 @@ extension _FirebaseEncoder { guard let options = userInfo.dateEncodingStrategy else { return date as NSDate } switch options { + case .deferredToTimestamp(let converter): + return converter(date) as! NSObject case .deferredToDate: // Must be called with a surrounding with(pushedKey:) call. try date.encode(to: self) @@ -377,10 +379,18 @@ extension _FirebaseEncoder { } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { return (value as! NSDecimalNumber) } else if userInfo.skipFirestoreTypes && (value is FirestoreEncodable) { - guard let value = value as? NSObject else { + let target: Any + switch userInfo.firestoreTypeDecodingStrategy { + case .deferredToPtotocol: + target = value + case .custom(let encodeFunc): + target = try encodeFunc(value) + } + + guard let result = target as? NSObject else { throw DocumentReferenceError.typeIsNotNSObject } - return value + return result } // The value should request a container from the _FirebaseEncoder. diff --git a/CodableFirebase/FirestoreDecoder.swift b/CodableFirebase/FirestoreDecoder.swift index 023d7c7..4f73d77 100644 --- a/CodableFirebase/FirestoreDecoder.swift +++ b/CodableFirebase/FirestoreDecoder.swift @@ -26,9 +26,11 @@ public protocol TimestampType: FirestoreDecodable, FirestoreEncodable { } open class FirestoreDecoder { - public init() {} + public init(userInfo: [CodingUserInfoKey: Any] = [.skipFirestoreTypes: true]) { + self.userInfo = userInfo + } - open var userInfo: [CodingUserInfoKey : Any] = [.skipFirestoreTypes: true] + public let userInfo: [CodingUserInfoKey: Any] open func decode(_ type: T.Type, from container: [String: Any]) throws -> T { let decoder = _FirebaseDecoder(referencing: container, diff --git a/CodableFirebase/FirestoreEncoder.swift b/CodableFirebase/FirestoreEncoder.swift index fa7d85e..ffb4036 100644 --- a/CodableFirebase/FirestoreEncoder.swift +++ b/CodableFirebase/FirestoreEncoder.swift @@ -9,10 +9,12 @@ import Foundation open class FirestoreEncoder { - public init() {} - - open var userInfo: [CodingUserInfoKey : Any] = [.skipFirestoreTypes: true] + public init(userInfo: [CodingUserInfoKey: Any] = [.skipFirestoreTypes: true]) { + self.userInfo = userInfo + } + public let userInfo: [CodingUserInfoKey: Any] + open func encode(_ value: Value) throws -> [String: Any] { let topLevel = try encodeToTopLevelContainer(value) switch topLevel { diff --git a/CodableFirebaseTests/TestCodableFirestore.swift b/CodableFirebaseTests/TestCodableFirestore.swift index 74a7147..d61b381 100644 --- a/CodableFirebaseTests/TestCodableFirestore.swift +++ b/CodableFirebaseTests/TestCodableFirestore.swift @@ -128,14 +128,36 @@ class TestCodableFirestore: XCTestCase { XCTAssertEqual((try? FirestoreEncoder().encode(val)) as NSDictionary?, ["value": val.value]) XCTAssertEqual(try? FirestoreDecoder().decode(TopLevelWrapper.self, from: ["value": val.value]), val) } - + func testEncodingTimestamp() { let timestamp = Timestamp(date: Date()) let wrapper = TopLevelWrapper(timestamp) XCTAssertEqual((try? FirestoreEncoder().encode(wrapper)) as NSDictionary?, ["value": timestamp]) XCTAssertEqual(try? FirestoreDecoder().decode(TopLevelWrapper.self, from: ["value": timestamp]), wrapper) } - + + func testCustomEncodingTimestamp() { + let date = Date() + let timestamp = Timestamp(date: date) + + // encode date to Timestamp + let encodeWrapper = TopLevelWrapper(date) + let encodeResult = (try? FirestoreEncoder(userInfo: [CodingUserInfoKey.dateEncodingStrategy: DateEncodingStrategy.deferredToTimestamp({ date in + return Timestamp(date: date) + })]) + .encode(encodeWrapper)) as NSDictionary? + XCTAssertEqual(encodeResult, ["value": timestamp]) + + // decode timestamp to date + let decodeWrapper = TopLevelWrapper(timestamp) + let decoder = FirestoreDecoder(userInfo: [ + CodingUserInfoKey.dateDecodingStrategy: DateDecodingStrategy.deferredToTimestamp + ]) + + let decodeResult = (try? decoder.decode(TopLevelWrapper.self, from: ["value": timestamp])) + XCTAssertEqual(decodeResult, decodeWrapper) + } + private func _testEncodeFailure(of value: T) { do { let _ = try FirestoreEncoder().encode(value) @@ -214,17 +236,21 @@ fileprivate class DocumentReference: NSObject, DocumentReferenceType {} // MARK: - Timestamp fileprivate class Timestamp: NSObject, TimestampType { let date: Date - + required init(date: Date) { self.date = date } - + func dateValue() -> Date { return date } - + override func isEqual(_ object: Any?) -> Bool { guard let other = object.flatMap({ $0 as? Timestamp }) else { return false } return date == other.date } } + +extension Date: FirestoreEncodable { + +}