From 9fa47dd610893ea67e874758ebf867b409f0c2db Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Thu, 25 Jan 2018 16:33:32 +0100 Subject: [PATCH 1/9] Add skip geo and reference flag --- CodableFirebase/Decoder.swift | 1 + CodableFirebase/FirebaseDecoder.swift | 1 + CodableFirebase/FirestoreDecoder.swift | 7 ++++++- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CodableFirebase/Decoder.swift b/CodableFirebase/Decoder.swift index 02839a9..4ca29fa 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] } 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/FirestoreDecoder.swift b/CodableFirebase/FirestoreDecoder.swift index 8463fba..a647e77 100644 --- a/CodableFirebase/FirestoreDecoder.swift +++ b/CodableFirebase/FirestoreDecoder.swift @@ -14,7 +14,12 @@ open class FirestoreDecoder { 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")) From 7aff81a267fad97cb22cb5e8f0309616b08e87a7 Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Thu, 25 Jan 2018 17:14:11 +0100 Subject: [PATCH 2/9] Add geopointtype and documentreference protocols --- CodableFirebase/Decoder.swift | 2 ++ CodableFirebase/FirestoreDecoder.swift | 41 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/CodableFirebase/Decoder.swift b/CodableFirebase/Decoder.swift index 4ca29fa..6e66198 100644 --- a/CodableFirebase/Decoder.swift +++ b/CodableFirebase/Decoder.swift @@ -1230,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 || T.self is DocumentReferenceType) { + decoded = value as! T } else { self.storage.push(container: value) decoded = try T(from: self) diff --git a/CodableFirebase/FirestoreDecoder.swift b/CodableFirebase/FirestoreDecoder.swift index a647e77..40ffb7c 100644 --- a/CodableFirebase/FirestoreDecoder.swift +++ b/CodableFirebase/FirestoreDecoder.swift @@ -8,6 +8,14 @@ import Foundation +protocol GeoPointType: Codable { + var latitude: Double { get } + var longitude: Double { get } + init(latitude: Double, longitude: Double) +} + +protocol DocumentReferenceType: Codable {} + open class FirestoreDecoder { public init() {} @@ -28,3 +36,36 @@ 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 +} + +extension DocumentReferenceType { + public init(from decoder: Decoder) throws { + throw DocumentReferenceError.typeIsNotSupported + } + + public func encode(to encoder: Encoder) throws { + throw DocumentReferenceError.typeIsNotSupported + } +} From f3eed29a7d6bca4e738dd26c970601cedfd5b51f Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Thu, 25 Jan 2018 17:18:29 +0100 Subject: [PATCH 3/9] Add encoding the geo point and document reference --- CodableFirebase/Encoder.swift | 6 ++++++ CodableFirebase/FirebaseEncoder.swift | 1 + CodableFirebase/FirestoreDecoder.swift | 1 + CodableFirebase/FirestoreEncoder.swift | 7 ++++++- 4 files changed, 14 insertions(+), 1 deletion(-) 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/FirebaseEncoder.swift b/CodableFirebase/FirebaseEncoder.swift index d257441..8935075 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: true, userInfo: userInfo ) let encoder = _FirebaseEncoder(options: options) diff --git a/CodableFirebase/FirestoreDecoder.swift b/CodableFirebase/FirestoreDecoder.swift index 40ffb7c..62e4e6d 100644 --- a/CodableFirebase/FirestoreDecoder.swift +++ b/CodableFirebase/FirestoreDecoder.swift @@ -58,6 +58,7 @@ extension GeoPointType { enum DocumentReferenceError: Error { case typeIsNotSupported + case typeIsNotNSObject } extension DocumentReferenceType { diff --git a/CodableFirebase/FirestoreEncoder.swift b/CodableFirebase/FirestoreEncoder.swift index c1c576e..1874ce5 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: false, + userInfo: userInfo + ) let encoder = _FirebaseEncoder(options: options) guard let topLevel = try encoder.box_(value) else { throw EncodingError.invalidValue(value, From 04facd2f6df98bcfc395c5ae0f7fb2fd5ba73134 Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Fri, 26 Jan 2018 10:19:44 +0100 Subject: [PATCH 4/9] Fix skipping encoding --- CodableFirebase/FirebaseEncoder.swift | 2 +- CodableFirebase/FirestoreEncoder.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CodableFirebase/FirebaseEncoder.swift b/CodableFirebase/FirebaseEncoder.swift index 8935075..526d072 100644 --- a/CodableFirebase/FirebaseEncoder.swift +++ b/CodableFirebase/FirebaseEncoder.swift @@ -57,7 +57,7 @@ open class FirebaseEncoder { let options = _FirebaseEncoder._Options( dateEncodingStrategy: dateEncodingStrategy, dataEncodingStrategy: dataEncodingStrategy, - skipGeoPointAndReference: true, + skipGeoPointAndReference: false, userInfo: userInfo ) let encoder = _FirebaseEncoder(options: options) diff --git a/CodableFirebase/FirestoreEncoder.swift b/CodableFirebase/FirestoreEncoder.swift index 1874ce5..76ebdec 100644 --- a/CodableFirebase/FirestoreEncoder.swift +++ b/CodableFirebase/FirestoreEncoder.swift @@ -29,7 +29,7 @@ open class FirestoreEncoder { let options = _FirebaseEncoder._Options( dateEncodingStrategy: nil, dataEncodingStrategy: nil, - skipGeoPointAndReference: false, + skipGeoPointAndReference: true, userInfo: userInfo ) let encoder = _FirebaseEncoder(options: options) From 587db265cdd7486ae70137256f3b43f618fa5478 Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Fri, 26 Jan 2018 10:20:03 +0100 Subject: [PATCH 5/9] Make protocols public --- CodableFirebase/FirestoreDecoder.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CodableFirebase/FirestoreDecoder.swift b/CodableFirebase/FirestoreDecoder.swift index 62e4e6d..b395b1a 100644 --- a/CodableFirebase/FirestoreDecoder.swift +++ b/CodableFirebase/FirestoreDecoder.swift @@ -8,13 +8,13 @@ import Foundation -protocol GeoPointType: Codable { +public protocol GeoPointType: Codable { var latitude: Double { get } var longitude: Double { get } init(latitude: Double, longitude: Double) } -protocol DocumentReferenceType: Codable {} +public protocol DocumentReferenceType: Codable {} open class FirestoreDecoder { public init() {} From 2528453f9bc4c1790985051c3f2d7ac09e81c3cf Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Fri, 26 Jan 2018 10:20:19 +0100 Subject: [PATCH 6/9] Cover firebase database geopoint and reference with tests --- .../TestCodableFirebase.swift | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CodableFirebaseTests/TestCodableFirebase.swift b/CodableFirebaseTests/TestCodableFirebase.swift index d3bfdc4..e2f444b 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: - GeioPoint +fileprivate 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 { From 3c61d54654a2e02b2f5374c44ae930229c6f94e8 Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Fri, 26 Jan 2018 10:55:02 +0100 Subject: [PATCH 7/9] Fix checking for protocol conformance --- CodableFirebase/Decoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodableFirebase/Decoder.swift b/CodableFirebase/Decoder.swift index 6e66198..19446bc 100644 --- a/CodableFirebase/Decoder.swift +++ b/CodableFirebase/Decoder.swift @@ -1230,7 +1230,7 @@ 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 || T.self is DocumentReferenceType) { + } else if options.skipGeoPointAndReference && (T.self is GeoPointType.Type || T.self is DocumentReferenceType.Type) { decoded = value as! T } else { self.storage.push(container: value) From 4cfd13702be33cb8fe521428ff52b1806fc0b225 Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Fri, 26 Jan 2018 11:12:33 +0100 Subject: [PATCH 8/9] Cover firestore types with tests --- .../TestCodableFirebase.swift | 4 +-- .../TestCodableFirestore.swift | 33 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/CodableFirebaseTests/TestCodableFirebase.swift b/CodableFirebaseTests/TestCodableFirebase.swift index e2f444b..1e2b654 100644 --- a/CodableFirebaseTests/TestCodableFirebase.swift +++ b/CodableFirebaseTests/TestCodableFirebase.swift @@ -428,8 +428,8 @@ class TestCodableFirebase: XCTestCase { // MARK: - Test Types /* FIXME: Import from %S/Inputs/Coding/SharedTypes.swift somehow. */ -// MARK: - GeioPoint -fileprivate struct Point: GeoPointType, Equatable { +// MARK: - GeoPoint +struct Point: GeoPointType, Equatable { let latitude: Double let longitude: Double 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 {} From 3a08c9e65025520b994afd3502365825e7c6dae8 Mon Sep 17 00:00:00 2001 From: Oleksii Dykan Date: Fri, 26 Jan 2018 11:42:35 +0100 Subject: [PATCH 9/9] Update README --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) 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+)