diff --git a/CodableFirebase.xcodeproj/project.pbxproj b/CodableFirebase.xcodeproj/project.pbxproj index bdcb01b..2796d05 100644 --- a/CodableFirebase.xcodeproj/project.pbxproj +++ b/CodableFirebase.xcodeproj/project.pbxproj @@ -7,10 +7,12 @@ objects = { /* Begin PBXBuildFile section */ + 09D19A4B218D64F900A862A3 /* DecodeStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D19A4A218D64F900A862A3 /* DecodeStrategy.swift */; }; + 09D19A4D218D650000A862A3 /* EncodeStrategy.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D19A4C218D650000A862A3 /* EncodeStrategy.swift */; }; + 09D19A4E218D874000A862A3 /* FirestoreDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */; }; + 09D19A4F218D88A800A862A3 /* FirestoreEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */; }; CE7DD3711F9CFA81000225C5 /* CodableFirebase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE7DD3671F9CFA81000225C5 /* CodableFirebase.framework */; }; CE7DD3781F9CFA81000225C5 /* CodableFirebase.h in Headers */ = {isa = PBXBuildFile; fileRef = CE7DD36A1F9CFA81000225C5 /* CodableFirebase.h */; settings = {ATTRIBUTES = (Public, ); }; }; - CE7DD3831F9D04AE000225C5 /* FirestoreEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */; }; - CE7DD3841F9D04AE000225C5 /* FirestoreDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3821F9D04AE000225C5 /* FirestoreDecoder.swift */; }; CE7DD3861F9DE4F7000225C5 /* TestCodableFirestore.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE7DD3851F9DE4F7000225C5 /* TestCodableFirestore.swift */; }; CEFDBF821FF3B35B00745EBE /* Encoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF811FF3B35B00745EBE /* Encoder.swift */; }; CEFDBF861FF3B56200745EBE /* Decoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEFDBF851FF3B56200745EBE /* Decoder.swift */; }; @@ -31,6 +33,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 09D19A4A218D64F900A862A3 /* DecodeStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DecodeStrategy.swift; sourceTree = ""; }; + 09D19A4C218D650000A862A3 /* EncodeStrategy.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EncodeStrategy.swift; sourceTree = ""; }; CE7DD3671F9CFA81000225C5 /* CodableFirebase.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CodableFirebase.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CE7DD36A1F9CFA81000225C5 /* CodableFirebase.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = CodableFirebase.h; sourceTree = ""; }; CE7DD36B1F9CFA81000225C5 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -92,6 +96,8 @@ CE7DD3811F9D04AE000225C5 /* FirestoreEncoder.swift */, CEFDBF891FF3E24200745EBE /* FirebaseDecoder.swift */, CEFDBF8B1FF3E3CB00745EBE /* FirebaseEncoder.swift */, + 09D19A4A218D64F900A862A3 /* DecodeStrategy.swift */, + 09D19A4C218D650000A862A3 /* EncodeStrategy.swift */, CEFDBF851FF3B56200745EBE /* Decoder.swift */, CEFDBF811FF3B35B00745EBE /* Encoder.swift */, CE7DD36B1F9CFA81000225C5 /* Info.plist */, @@ -222,11 +228,13 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - CE7DD3831F9D04AE000225C5 /* FirestoreEncoder.swift in Sources */, + 09D19A4D218D650000A862A3 /* EncodeStrategy.swift in Sources */, CEFDBF8C1FF3E3CB00745EBE /* FirebaseEncoder.swift in Sources */, + 09D19A4B218D64F900A862A3 /* DecodeStrategy.swift in Sources */, + 09D19A4E218D874000A862A3 /* FirestoreDecoder.swift in Sources */, + 09D19A4F218D88A800A862A3 /* FirestoreEncoder.swift in Sources */, CEFDBF821FF3B35B00745EBE /* Encoder.swift in Sources */, CEFDBF861FF3B56200745EBE /* Decoder.swift in Sources */, - CE7DD3841F9D04AE000225C5 /* FirestoreDecoder.swift in Sources */, CEFDBF8A1FF3E24200745EBE /* FirebaseDecoder.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/CodableFirebase/DecodeStrategy.swift b/CodableFirebase/DecodeStrategy.swift new file mode 100644 index 0000000..e93b7ad --- /dev/null +++ b/CodableFirebase/DecodeStrategy.swift @@ -0,0 +1,77 @@ +// +// DecodeStrategy.swift +// CodableFirebase +// +// Created by Zitao Xiong on 11/3/18. +// Copyright © 2018 ViolentOctopus. All rights reserved. +// + +import Foundation + +/// The strategy to use for decoding `Date` values. +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 + + /// Decode the `Date` as UNIX millisecond timestamp from a JSON number. + case millisecondsSince1970 + + /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). + @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 + + /// Decode the `Date` as a string parsed by the given formatter. + case formatted(DateFormatter) + + /// Decode the `Date` as a custom value decoded by the given closure. + case custom((_ decoder: Decoder) throws -> Date) +} + +/// The strategy to use for decoding `Data` values. +public enum DataDecodingStrategy { + /// Defer to `Data` for decoding. + case deferredToData + + /// Decode the `Data` from a Base64-encoded string. This is the default strategy. + case base64 + + /// Decode the `Data` as a custom value decoded by the given closure. + case custom((_ decoder: Decoder) throws -> Data) +} + +public enum FirestoreTypeDecodingStrategy { + 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 { + var dateDecodingStrategy: DateDecodingStrategy? { + return self[.dateDecodingStrategy] as? DateDecodingStrategy + } + + 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 df4ca1f..78d54a0 100644 --- a/CodableFirebase/Decoder.swift +++ b/CodableFirebase/Decoder.swift @@ -9,35 +9,22 @@ import Foundation class _FirebaseDecoder : Decoder { - /// Options set on the top-level encoder to pass down the decoding hierarchy. - struct _Options { - let dateDecodingStrategy: FirebaseDecoder.DateDecodingStrategy? - let dataDecodingStrategy: FirebaseDecoder.DataDecodingStrategy? - let skipFirestoreTypes: Bool - let userInfo: [CodingUserInfoKey : Any] - } - // MARK: Properties /// The decoder's storage. fileprivate var storage: _FirebaseDecodingStorage - fileprivate let options: _Options - /// The path to the current point in encoding. fileprivate(set) public var codingPath: [CodingKey] - - /// Contextual user-provided information for use during encoding. - public var userInfo: [CodingUserInfoKey : Any] { - return options.userInfo - } + + let userInfo: [CodingUserInfoKey : Any] // MARK: - Initialization /// Initializes `self` with the given top-level container and options. - init(referencing container: Any, at codingPath: [CodingKey] = [], options: _Options) { + init(referencing container: Any, at codingPath: [CodingKey] = [], userInfo: [CodingUserInfoKey: Any]) { self.storage = _FirebaseDecodingStorage() self.storage.push(container: container) self.codingPath = codingPath - self.options = options + self.userInfo = userInfo } // MARK: - Decoder Methods @@ -410,7 +397,7 @@ fileprivate struct _FirebaseKeyedDecodingContainer : KeyedDecodin defer { self.decoder.codingPath.removeLast() } let value: Any = container[key.stringValue] ?? NSNull() - return _FirebaseDecoder(referencing: value, at: self.decoder.codingPath, options: decoder.options) + return _FirebaseDecoder(referencing: value, at: self.decoder.codingPath, userInfo: decoder.userInfo) } public func superDecoder() throws -> Decoder { @@ -771,7 +758,7 @@ fileprivate struct _FirebaseUnkeyedDecodingContainer : UnkeyedDecodingContainer let value = self.container[self.currentIndex] self.currentIndex += 1 - return _FirebaseDecoder(referencing: value, at: decoder.codingPath, options: decoder.options) + return _FirebaseDecoder(referencing: value, at: decoder.codingPath, userInfo: decoder.userInfo) } } @@ -1109,7 +1096,7 @@ extension _FirebaseDecoder { func unbox(_ value: Any, as type: Date.Type) throws -> Date? { guard !(value is NSNull) else { return nil } - guard let options = options.dateDecodingStrategy else { + guard let options = userInfo.dateDecodingStrategy else { guard let date = value as? Date else { throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) } @@ -1117,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) @@ -1162,7 +1152,7 @@ extension _FirebaseDecoder { func unbox(_ value: Any, as type: Data.Type) throws -> Data? { guard !(value is NSNull) else { return nil } - guard let options = options.dataDecodingStrategy else { + guard let options = userInfo.dataDecodingStrategy else { guard let data = value as? Data else { throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) } @@ -1230,9 +1220,17 @@ extension _FirebaseDecoder { } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { guard let decimal = try self.unbox(value, as: Decimal.self) else { return nil } decoded = decimal as! T - } else if options.skipFirestoreTypes && (T.self is FirestoreDecodable.Type) { - decoded = value as! T - } else { + } + else if userInfo.skipFirestoreTypes && (T.self is FirestoreDecodable.Type) { + 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) decoded = try T(from: self) self.storage.popContainer() diff --git a/CodableFirebase/EncodeStrategy.swift b/CodableFirebase/EncodeStrategy.swift new file mode 100644 index 0000000..6a1ce5d --- /dev/null +++ b/CodableFirebase/EncodeStrategy.swift @@ -0,0 +1,89 @@ +// +// EncodeStrategy.swift +// CodableFirebase +// +// Created by Zitao Xiong on 11/3/18. +// Copyright © 2018 ViolentOctopus. All rights reserved. +// + +import Foundation + +/// The strategy to use for encoding `Date` values. +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 + + /// Encode the `Date` as UNIX millisecond timestamp (as a JSON number). + case millisecondsSince1970 + + /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). + @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) + case iso8601 + + /// Encode the `Date` as a string formatted by the given formatter. + case formatted(DateFormatter) + + /// Encode the `Date` as a custom value encoded by the given closure. + /// + /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. + case custom((Date, Encoder) throws -> Void) +} + +/// The strategy to use for encoding `Data` values. +public enum DataEncodingStrategy { + /// Defer to `Data` for choosing an encoding. + case deferredToData + + /// Encoded the `Data` as a Base64-encoded string. This is the default strategy. + case base64 + + /// Encode the `Data` as a custom value encoded by the given closure. + /// + /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. + 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 { + var dateEncodingStrategy: DateEncodingStrategy? { + return self[.dateEncodingStrategy] as? DateEncodingStrategy + } + + var dataEncodingStrategy: DataEncodingStrategy? { + return self[.dataEncodingStrategy] as? DataEncodingStrategy + } + + var skipFirestoreTypes: Bool { + if let skip = self[.skipFirestoreTypes] as? Bool { + return skip + } + 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 69fe0ef..946aa25 100644 --- a/CodableFirebase/Encoder.swift +++ b/CodableFirebase/Encoder.swift @@ -10,25 +10,16 @@ import Foundation class _FirebaseEncoder : Encoder { /// Options set on the top-level encoder to pass down the encoding hierarchy. - struct _Options { - let dateEncodingStrategy: FirebaseEncoder.DateEncodingStrategy? - let dataEncodingStrategy: FirebaseEncoder.DataEncodingStrategy? - let skipFirestoreTypes: Bool - let userInfo: [CodingUserInfoKey : Any] - } - fileprivate var storage: _FirebaseEncodingStorage - fileprivate let options: _Options + fileprivate(set) public var codingPath: [CodingKey] - public var userInfo: [CodingUserInfoKey : Any] { - return options.userInfo - } + let userInfo: [CodingUserInfoKey: Any] - init(options: _Options, codingPath: [CodingKey] = []) { + init(userInfo: [CodingUserInfoKey: Any], codingPath: [CodingKey] = []) { self.storage = _FirebaseEncodingStorage() self.codingPath = codingPath - self.options = options + self.userInfo = userInfo } /// Returns whether a new element can be encoded at this coding path. @@ -312,9 +303,11 @@ extension _FirebaseEncoder { } fileprivate func box(_ date: Date) throws -> NSObject { - guard let options = options.dateEncodingStrategy else { return date as NSDate } + 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) @@ -351,7 +344,7 @@ extension _FirebaseEncoder { } fileprivate func box(_ data: Data) throws -> NSObject { - guard let options = options.dataEncodingStrategy else { return data as NSData } + guard let options = userInfo.dataEncodingStrategy else { return data as NSData } switch options { case .deferredToData: @@ -385,11 +378,19 @@ extension _FirebaseEncoder { return self.box((value as! URL).absoluteString) } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { return (value as! NSDecimalNumber) - } else if options.skipFirestoreTypes && (value is FirestoreEncodable) { - guard let value = value as? NSObject else { + } else if userInfo.skipFirestoreTypes && (value is FirestoreEncodable) { + 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. @@ -524,7 +525,7 @@ fileprivate class _FirebaseReferencingEncoder : _FirebaseEncoder { fileprivate init(referencing encoder: _FirebaseEncoder, at index: Int, wrapping array: NSMutableArray) { self.encoder = encoder self.reference = .array(array, index) - super.init(options: encoder.options, codingPath: encoder.codingPath) + super.init(userInfo: encoder.userInfo, codingPath: encoder.codingPath) self.codingPath.append(_FirebaseKey(index: index)) } @@ -533,7 +534,7 @@ fileprivate class _FirebaseReferencingEncoder : _FirebaseEncoder { fileprivate init(referencing encoder: _FirebaseEncoder, at key: CodingKey, wrapping dictionary: NSMutableDictionary) { self.encoder = encoder reference = .dictionary(dictionary, key.stringValue) - super.init(options: encoder.options, codingPath: encoder.codingPath) + super.init(userInfo: encoder.userInfo, codingPath: encoder.codingPath) codingPath.append(key) } diff --git a/CodableFirebase/FirebaseDecoder.swift b/CodableFirebase/FirebaseDecoder.swift index a6a20f0..9b44da8 100644 --- a/CodableFirebase/FirebaseDecoder.swift +++ b/CodableFirebase/FirebaseDecoder.swift @@ -9,54 +9,36 @@ import Foundation open class FirebaseDecoder { - /// The strategy to use for decoding `Date` values. - public enum DateDecodingStrategy { - /// Defer to `Date` for decoding. This is the default strategy. - case deferredToDate - - /// Decode the `Date` as a UNIX timestamp from a JSON number. - case secondsSince1970 - - /// Decode the `Date` as UNIX millisecond timestamp from a JSON number. - case millisecondsSince1970 - - /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - case iso8601 - - /// Decode the `Date` as a string parsed by the given formatter. - case formatted(DateFormatter) - - /// Decode the `Date` as a custom value decoded by the given closure. - case custom((_ decoder: Decoder) throws -> Date) - } - - /// The strategy to use for decoding `Data` values. - public enum DataDecodingStrategy { - /// Defer to `Data` for decoding. - case deferredToData - - /// Decode the `Data` from a Base64-encoded string. This is the default strategy. - case base64 - - /// Decode the `Data` as a custom value decoded by the given closure. - case custom((_ decoder: Decoder) throws -> Data) - } - public init() {} - open var userInfo: [CodingUserInfoKey : Any] = [:] - open var dateDecodingStrategy: DateDecodingStrategy = .deferredToDate - open var dataDecodingStrategy: DataDecodingStrategy = .deferredToData - + open var userInfo: [CodingUserInfoKey: Any] = [:] + + public var dateDecodingStrategy: DateDecodingStrategy { + set { + userInfo[.dateDecodingStrategy] = newValue + } + get { + if let strategy = userInfo[.dateDecodingStrategy] as? DateDecodingStrategy { + return strategy + } + return .deferredToDate + } + } + + public var dataDecodingStrategy: DataDecodingStrategy { + set { + userInfo[.dataDecodingStrategy] = newValue + } + get { + if let strategy = userInfo[.dataDecodingStrategy] as? DataDecodingStrategy { + return strategy + } + return .deferredToData + } + } + open func decode(_ type: T.Type, from container: Any) throws -> T { - let options = _FirebaseDecoder._Options( - dateDecodingStrategy: dateDecodingStrategy, - dataDecodingStrategy: dataDecodingStrategy, - skipFirestoreTypes: false, - userInfo: userInfo - ) - let decoder = _FirebaseDecoder(referencing: container, options: options) + let decoder = _FirebaseDecoder(referencing: container, userInfo: userInfo) guard let value = try decoder.unbox(container, as: T.self) else { throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "The given dictionary was invalid")) } diff --git a/CodableFirebase/FirebaseEncoder.swift b/CodableFirebase/FirebaseEncoder.swift index 49f79fc..cd4e45c 100644 --- a/CodableFirebase/FirebaseEncoder.swift +++ b/CodableFirebase/FirebaseEncoder.swift @@ -9,58 +9,12 @@ import Foundation open class FirebaseEncoder { - /// The strategy to use for encoding `Date` values. - public enum DateEncodingStrategy { - /// Defer to `Date` for choosing an encoding. This is the default strategy. - case deferredToDate - - /// Encode the `Date` as a UNIX timestamp (as a JSON number). - case secondsSince1970 - - /// Encode the `Date` as UNIX millisecond timestamp (as a JSON number). - case millisecondsSince1970 - - /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(OSX 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) - case iso8601 - - /// Encode the `Date` as a string formatted by the given formatter. - case formatted(DateFormatter) - - /// Encode the `Date` as a custom value encoded by the given closure. - /// - /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. - case custom((Date, Encoder) throws -> Void) - } - - /// The strategy to use for encoding `Data` values. - public enum DataEncodingStrategy { - /// Defer to `Data` for choosing an encoding. - case deferredToData - - /// Encoded the `Data` as a Base64-encoded string. This is the default strategy. - case base64 - - /// Encode the `Data` as a custom value encoded by the given closure. - /// - /// If the closure fails to encode a value into the given encoder, the encoder will encode an empty automatic container in its place. - case custom((Data, Encoder) throws -> Void) - } - public init() {} open var userInfo: [CodingUserInfoKey : Any] = [:] - open var dateEncodingStrategy: DateEncodingStrategy = .deferredToDate - open var dataEncodingStrategy: DataEncodingStrategy = .deferredToData - + open func encode(_ value: Value) throws -> Any { - let options = _FirebaseEncoder._Options( - dateEncodingStrategy: dateEncodingStrategy, - dataEncodingStrategy: dataEncodingStrategy, - skipFirestoreTypes: false, - userInfo: userInfo - ) - let encoder = _FirebaseEncoder(options: options) + let encoder = _FirebaseEncoder(userInfo: userInfo) guard let topLevel = try encoder.box_(value) else { throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], diff --git a/CodableFirebase/FirestoreDecoder.swift b/CodableFirebase/FirestoreDecoder.swift index bf841de..4f73d77 100644 --- a/CodableFirebase/FirestoreDecoder.swift +++ b/CodableFirebase/FirestoreDecoder.swift @@ -26,18 +26,15 @@ 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] = [:] + public let userInfo: [CodingUserInfoKey: Any] open func decode(_ type: T.Type, from container: [String: Any]) throws -> T { - let options = _FirebaseDecoder._Options( - dateDecodingStrategy: nil, - dataDecodingStrategy: nil, - skipFirestoreTypes: true, - userInfo: userInfo - ) - let decoder = _FirebaseDecoder(referencing: container, options: options) + let decoder = _FirebaseDecoder(referencing: container, + userInfo: userInfo) guard let value = try decoder.unbox(container, as: T.self) else { throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "The given dictionary was invalid")) } @@ -87,7 +84,7 @@ extension TimestampType { let container = try decoder.singleValueContainer() self.init(date: try container.decode(Date.self)) } - + public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() try container.encode(self.dateValue()) diff --git a/CodableFirebase/FirestoreEncoder.swift b/CodableFirebase/FirestoreEncoder.swift index d88e5cb..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] = [:] + 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 { @@ -26,13 +28,7 @@ open class FirestoreEncoder { } internal func encodeToTopLevelContainer(_ value: Value) throws -> Any { - let options = _FirebaseEncoder._Options( - dateEncodingStrategy: nil, - dataEncodingStrategy: nil, - skipFirestoreTypes: true, - userInfo: userInfo - ) - let encoder = _FirebaseEncoder(options: options) + let encoder = _FirebaseEncoder(userInfo: userInfo) guard let topLevel = try encoder.box_(value) else { throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], diff --git a/CodableFirebaseTests/TestCodableFirebase.swift b/CodableFirebaseTests/TestCodableFirebase.swift index ae1ea7d..e245696 100644 --- a/CodableFirebaseTests/TestCodableFirebase.swift +++ b/CodableFirebaseTests/TestCodableFirebase.swift @@ -392,15 +392,15 @@ class TestCodableFirebase: XCTestCase { private func _testRoundTrip(of value: T, expectedValue json: Any? = nil, - dateEncodingStrategy: FirebaseEncoder.DateEncodingStrategy = .deferredToDate, - dateDecodingStrategy: FirebaseDecoder.DateDecodingStrategy = .deferredToDate, - dataEncodingStrategy: FirebaseEncoder.DataEncodingStrategy = .base64, - dataDecodingStrategy: FirebaseDecoder.DataDecodingStrategy = .base64) where T : Codable, T : Equatable { + dateEncodingStrategy: DateEncodingStrategy = .deferredToDate, + dateDecodingStrategy: DateDecodingStrategy = .deferredToDate, + dataEncodingStrategy: DataEncodingStrategy = .base64, + dataDecodingStrategy: DataDecodingStrategy = .base64) where T : Codable, T : Equatable { var payload: Any! = nil do { let encoder = FirebaseEncoder() - encoder.dateEncodingStrategy = dateEncodingStrategy - encoder.dataEncodingStrategy = dataEncodingStrategy + encoder.userInfo[.dateEncodingStrategy] = dateEncodingStrategy + encoder.userInfo[.dataEncodingStrategy] = dataEncodingStrategy payload = try encoder.encode(value) } catch { XCTFail("Failed to encode \(T.self) to val: \(error)") diff --git a/CodableFirebaseTests/TestCodableFirestore.swift b/CodableFirebaseTests/TestCodableFirestore.swift index 74a7147..cd1a909 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,15 +236,15 @@ 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