diff --git a/CodableFirebase/Decoder.swift b/CodableFirebase/Decoder.swift index 02839a9..19446bc 100644 --- a/CodableFirebase/Decoder.swift +++ b/CodableFirebase/Decoder.swift @@ -13,6 +13,7 @@ class _FirebaseDecoder : Decoder { struct _Options { let dateDecodingStrategy: FirebaseDecoder.DateDecodingStrategy? let dataDecodingStrategy: FirebaseDecoder.DataDecodingStrategy? + let skipGeoPointAndReference: Bool let userInfo: [CodingUserInfoKey : Any] } @@ -1229,6 +1230,8 @@ 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.skipGeoPointAndReference && (T.self is GeoPointType.Type || T.self is DocumentReferenceType.Type) { + decoded = value as! T } else { self.storage.push(container: value) decoded = try T(from: self) diff --git a/CodableFirebase/Encoder.swift b/CodableFirebase/Encoder.swift index c9f3076..5419094 100644 --- a/CodableFirebase/Encoder.swift +++ b/CodableFirebase/Encoder.swift @@ -13,6 +13,7 @@ class _FirebaseEncoder : Encoder { struct _Options { let dateEncodingStrategy: FirebaseEncoder.DateEncodingStrategy? let dataEncodingStrategy: FirebaseEncoder.DataEncodingStrategy? + let skipGeoPointAndReference: Bool let userInfo: [CodingUserInfoKey : Any] } @@ -382,6 +383,11 @@ extension _FirebaseEncoder { return try self.box((value as! Data)) } else if T.self == URL.self || T.self == NSURL.self { return self.box((value as! URL).absoluteString) + } else if options.skipGeoPointAndReference && (value is GeoPointType || value is DocumentReferenceType) { + guard let value = value as? NSObject else { + throw DocumentReferenceError.typeIsNotNSObject + } + return value } // The value should request a container from the _FirebaseEncoder. diff --git a/CodableFirebase/FirebaseDecoder.swift b/CodableFirebase/FirebaseDecoder.swift index 8869cc9..8db7fa5 100644 --- a/CodableFirebase/FirebaseDecoder.swift +++ b/CodableFirebase/FirebaseDecoder.swift @@ -53,6 +53,7 @@ open class FirebaseDecoder { let options = _FirebaseDecoder._Options( dateDecodingStrategy: dateDecodingStrategy, dataDecodingStrategy: dataDecodingStrategy, + skipGeoPointAndReference: false, userInfo: userInfo ) let decoder = _FirebaseDecoder(referencing: container, options: options) diff --git a/CodableFirebase/FirebaseEncoder.swift b/CodableFirebase/FirebaseEncoder.swift index d257441..526d072 100644 --- a/CodableFirebase/FirebaseEncoder.swift +++ b/CodableFirebase/FirebaseEncoder.swift @@ -57,6 +57,7 @@ open class FirebaseEncoder { let options = _FirebaseEncoder._Options( dateEncodingStrategy: dateEncodingStrategy, dataEncodingStrategy: dataEncodingStrategy, + skipGeoPointAndReference: false, userInfo: userInfo ) let encoder = _FirebaseEncoder(options: options) diff --git a/CodableFirebase/FirestoreDecoder.swift b/CodableFirebase/FirestoreDecoder.swift index 8463fba..b395b1a 100644 --- a/CodableFirebase/FirestoreDecoder.swift +++ b/CodableFirebase/FirestoreDecoder.swift @@ -8,13 +8,26 @@ import Foundation +public protocol GeoPointType: Codable { + var latitude: Double { get } + var longitude: Double { get } + init(latitude: Double, longitude: Double) +} + +public protocol DocumentReferenceType: Codable {} + open class FirestoreDecoder { public init() {} open var userInfo: [CodingUserInfoKey : Any] = [:] open func decode(_ type: T.Type, from container: [String: Any]) throws -> T { - let options = _FirebaseDecoder._Options(dateDecodingStrategy: nil, dataDecodingStrategy: nil, userInfo: userInfo) + let options = _FirebaseDecoder._Options( + dateDecodingStrategy: nil, + dataDecodingStrategy: nil, + skipGeoPointAndReference: true, + userInfo: userInfo + ) let decoder = _FirebaseDecoder(referencing: container, options: options) 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")) @@ -23,3 +36,37 @@ open class FirestoreDecoder { return value } } + +enum GeoPointKeys: CodingKey { + case latitude, longitude +} + +extension GeoPointType { + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: GeoPointKeys.self) + let latitude = try container.decode(Double.self, forKey: .latitude) + let longitude = try container.decode(Double.self, forKey: .longitude) + self.init(latitude: latitude, longitude: longitude) + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: GeoPointKeys.self) + try container.encode(latitude, forKey: .latitude) + try container.encode(longitude, forKey: .longitude) + } +} + +enum DocumentReferenceError: Error { + case typeIsNotSupported + case typeIsNotNSObject +} + +extension DocumentReferenceType { + public init(from decoder: Decoder) throws { + throw DocumentReferenceError.typeIsNotSupported + } + + public func encode(to encoder: Encoder) throws { + throw DocumentReferenceError.typeIsNotSupported + } +} diff --git a/CodableFirebase/FirestoreEncoder.swift b/CodableFirebase/FirestoreEncoder.swift index c1c576e..76ebdec 100644 --- a/CodableFirebase/FirestoreEncoder.swift +++ b/CodableFirebase/FirestoreEncoder.swift @@ -26,7 +26,12 @@ open class FirestoreEncoder { } internal func encodeToTopLevelContainer(_ value: Value) throws -> Any { - let options = _FirebaseEncoder._Options(dateEncodingStrategy: nil, dataEncodingStrategy: nil, userInfo: userInfo) + let options = _FirebaseEncoder._Options( + dateEncodingStrategy: nil, + dataEncodingStrategy: nil, + skipGeoPointAndReference: true, + userInfo: userInfo + ) let encoder = _FirebaseEncoder(options: options) guard let topLevel = try encoder.box_(value) else { throw EncodingError.invalidValue(value, diff --git a/CodableFirebaseTests/TestCodableFirebase.swift b/CodableFirebaseTests/TestCodableFirebase.swift index d3bfdc4..1e2b654 100644 --- a/CodableFirebaseTests/TestCodableFirebase.swift +++ b/CodableFirebaseTests/TestCodableFirebase.swift @@ -369,6 +369,19 @@ class TestCodableFirebase: XCTestCase { _testRoundTrip(of: 3 as Double) } + // MARK: - GeoPoint + func testEncodingGeoPoint() { + let point = Point(latitude: 2, longitude: 2) + XCTAssertEqual((try? FirebaseEncoder().encode(point)) as? NSDictionary, ["latitude": 2, "longitude": 2]) + XCTAssertEqual(try? FirebaseDecoder().decode(Point.self, from: ["latitude": 2, "longitude": 2]), point) + } + + // MARK: - Document Reference + func testEncodingDocumentReference() { + XCTAssertThrowsError(try FirebaseEncoder().encode(DocumentReference())) + XCTAssertThrowsError(try FirebaseDecoder().decode(DocumentReference.self, from: [])) + } + // MARK: - Helper Functions private var _emptyDictionary: [String: Any] = [:] @@ -415,6 +428,19 @@ class TestCodableFirebase: XCTestCase { // MARK: - Test Types /* FIXME: Import from %S/Inputs/Coding/SharedTypes.swift somehow. */ +// MARK: - GeoPoint +struct Point: GeoPointType, Equatable { + let latitude: Double + let longitude: Double + + static func == (lhs: Point, rhs: Point) -> Bool { + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude + } +} + +// MARK: - ReferenceType +fileprivate struct DocumentReference: DocumentReferenceType {} + // MARK: - Empty Types fileprivate struct EmptyStruct : Codable, Equatable { static func ==(_ lhs: EmptyStruct, _ rhs: EmptyStruct) -> Bool { diff --git a/CodableFirebaseTests/TestCodableFirestore.swift b/CodableFirebaseTests/TestCodableFirestore.swift index 93017ed..236f0ed 100644 --- a/CodableFirebaseTests/TestCodableFirestore.swift +++ b/CodableFirebaseTests/TestCodableFirestore.swift @@ -109,6 +109,21 @@ class TestCodableFirestore: XCTestCase { _testRoundTrip(of: TopLevelWrapper(date), expected: ["value": date]) } + // MARK: - GeoPoint & Document Reference + func testEncodingGeoPoint() { + let point = GeoPoint(latitude: 2, longitude: 2) + let wrapper = TopLevelWrapper(point) + XCTAssertEqual((try? FirestoreEncoder().encode(wrapper)) as NSDictionary?, ["value": point]) + XCTAssertEqual(try? FirestoreDecoder().decode(TopLevelWrapper.self, from: ["value": point]), wrapper) + XCTAssertThrowsError(try FirestoreEncoder().encode(TopLevelWrapper(Point(latitude: 2, longitude: 2)))) + } + + func testEncodingDocumentReference() { + let val = TopLevelWrapper(DocumentReference()) + XCTAssertEqual((try? FirestoreEncoder().encode(val)) as NSDictionary?, ["value": val.value]) + XCTAssertEqual(try? FirestoreDecoder().decode(TopLevelWrapper.self, from: ["value": val.value]), val) + } + private func _testEncodeFailure(of value: T) { do { let _ = try FirestoreEncoder().encode(value) @@ -164,3 +179,21 @@ func expectEqualPaths(_ lhs: [CodingKey], _ rhs: [CodingKey], _ prefix: String) XCTAssertEqual(key1.stringValue, key2.stringValue, "\(prefix) CodingKey.stringValue mismatch: \(type(of: key1))('\(key1.stringValue)') != \(type(of: key2))('\(key2.stringValue)')") } } + +// MARK: - GeioPoint +fileprivate class GeoPoint: NSObject, GeoPointType { + let latitude: Double + let longitude: Double + + required init(latitude: Double, longitude: Double) { + self.latitude = latitude + self.longitude = longitude + } + + static func == (lhs: Point, rhs: Point) -> Bool { + return lhs.latitude == rhs.latitude && lhs.longitude == rhs.longitude + } +} + +// MARK: - ReferenceType +fileprivate class DocumentReference: NSObject, DocumentReferenceType {} diff --git a/README.md b/README.md index 4a4a10c..485f457 100644 --- a/README.md +++ b/README.md @@ -86,6 +86,17 @@ Firestore.firestore().collection("data").document("one").getDocument { (document } ``` +### How to use `GeoPoint` and `DocumentRefence` in Firestore + +In order to use these 2 types with `Firestore`, you need to add the following code somewhere in your app: + +```swift +extension DocumentReference: DocumentReferenceType {} +extension GeoPoint: GeoPointType {} +``` + +and now they become `Codable` and can be used properly with `FirestoreEncoder` and `FirestoreDecoder`. + ## Integration ### CocoaPods (iOS 9+)