From 1743eb22bfa8fce54e343b1fd03331774595ed42 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 3 Jan 2019 08:57:14 -0800 Subject: [PATCH 01/24] Squash #2178 to continue Firestore Codable (#2228) --- .../FDLBuilderTestAppObjCEarlGrey.xcscheme | 42 +- FirebaseFirestoreSwift.podspec | 2 +- .../Firestore.xcodeproj/project.pbxproj | 28 +- .../xcschemes/Firestore_Example_iOS.xcscheme | 10 + Firestore/Example/Tests/API/FIRQueryTests.mm | 1 + Firestore/Example/Tests/API/FSTAPIHelpers.h | 15 +- Firestore/Example/Tests/API/FSTAPIHelpers.mm | 9 +- .../Swift/Source/Codable/CodableErrors.swift | 23 + .../Codable/DocumentReference+Codable.swift | 39 + .../Source/Codable/FieldValue+Codable.swift | 35 + .../Source/Codable/FirestoreDecoder.swift | 1062 +++++++++++++++++ .../Source/Codable/FirestoreEncoder.swift | 612 ++++++++++ ...eGeoPoint.swift => GeoPoint+Codable.swift} | 0 .../Source/Codable/Timestamp+Codable.swift | 45 + Firestore/Swift/Tests/BridgingHeader.h | 22 + .../Tests/Codable/CodableDocumentTests.swift | 403 +++++++ 16 files changed, 2332 insertions(+), 16 deletions(-) create mode 100644 Firestore/Swift/Source/Codable/CodableErrors.swift create mode 100644 Firestore/Swift/Source/Codable/DocumentReference+Codable.swift create mode 100644 Firestore/Swift/Source/Codable/FieldValue+Codable.swift create mode 100644 Firestore/Swift/Source/Codable/FirestoreDecoder.swift create mode 100644 Firestore/Swift/Source/Codable/FirestoreEncoder.swift rename Firestore/Swift/Source/Codable/{CodableGeoPoint.swift => GeoPoint+Codable.swift} (100%) create mode 100644 Firestore/Swift/Source/Codable/Timestamp+Codable.swift create mode 100644 Firestore/Swift/Tests/BridgingHeader.h create mode 100644 Firestore/Swift/Tests/Codable/CodableDocumentTests.swift diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme index af5583c4a78..22feddbc9f9 100644 --- a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme @@ -7,8 +7,11 @@ buildImplicitDependencies = "YES"> + buildForProfiling = "YES" + buildForArchiving = "YES" + buildForAnalyzing = "YES"> + + + + + + + + @@ -49,6 +71,15 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> + + + + @@ -58,6 +89,15 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> + + + + diff --git a/FirebaseFirestoreSwift.podspec b/FirebaseFirestoreSwift.podspec index 684307065f7..d6b3fd69171 100644 --- a/FirebaseFirestoreSwift.podspec +++ b/FirebaseFirestoreSwift.podspec @@ -5,7 +5,7 @@ Pod::Spec.new do |s| s.name = 'FirebaseFirestoreSwift' - s.version = '0.1' + s.version = '0.2.0' s.summary = 'Google Cloud Firestore for iOS Swift Extensions' s.description = <<-DESC diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index 0f49daf6717..f1a0030be46 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -229,10 +229,13 @@ DE03B2D61F2149D600A30B9C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6003F58D195388D20070C39A /* Foundation.framework */; }; DE03B2DD1F2149D600A30B9C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 6003F5B8195388D20070C39A /* InfoPlist.strings */; }; DE03B3631F215E1A00A30B9C /* CAcert.pem in Resources */ = {isa = PBXBuildFile; fileRef = DE03B3621F215E1600A30B9C /* CAcert.pem */; }; + DE26C61621C15FCC00DE141A /* CodableDocumentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE26C61521C15FCC00DE141A /* CodableDocumentTests.swift */; }; DE2EF0851F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */; }; DE2EF0861F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */; }; DE2EF0871F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0821F3D0B6E003D0CDC /* FSTImmutableSortedSet+Testing.m */; }; DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; }; + DE84BBB421C18F060048A176 /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; + DE84BBB921C1A6ED0048A176 /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; EBFC611B1BF195D0EC710AF4 /* app_testing.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FB07203E6A44009C9584 /* app_testing.mm */; }; /* End PBXBuildFile section */ @@ -545,6 +548,8 @@ DE03B2E91F2149D600A30B9C /* Firestore_IntegrationTests_iOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = Firestore_IntegrationTests_iOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = ""; }; DE0761F61F2FE68D003233AF /* BasicCompileTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicCompileTests.swift; sourceTree = ""; }; + DE26C61521C15FCC00DE141A /* CodableDocumentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableDocumentTests.swift; sourceTree = ""; }; + DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTArraySortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m; sourceTree = ""; }; DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedDictionary+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h"; sourceTree = ""; }; DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedDictionary+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m"; sourceTree = ""; }; @@ -557,6 +562,7 @@ DE51B1981F0D48AC0013853F /* FSTSpecTests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSpecTests.h; sourceTree = ""; }; DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = ""; }; DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; + DE84BBB821C1951E0048A176 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = ""; }; ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED4B3E3EA0EBF3ED19A07060 /* grpc_stream_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = grpc_stream_tester.h; sourceTree = ""; }; @@ -757,6 +763,7 @@ 5495EB012040E90200EBA509 /* Codable */ = { isa = PBXGroup; children = ( + DE26C61521C15FCC00DE141A /* CodableDocumentTests.swift */, 5495EB022040E90200EBA509 /* CodableGeoPointTests.swift */, ); path = Codable; @@ -779,6 +786,7 @@ 544A20ED20F6C046004E52CD /* API */, 5495EB012040E90200EBA509 /* Codable */, 54C9EDF52040E16300A969CD /* Info.plist */, + DE84BBB821C1951E0048A176 /* BridgingHeader.h */, ); name = SwiftTests; path = ../Swift/Tests; @@ -1399,6 +1407,7 @@ TargetAttributes = { 54C9EDF02040E16300A969CD = { CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1010; ProvisioningStyle = Automatic; TestTargetID = 6003F589195388D20070C39A; }; @@ -1831,8 +1840,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + DE84BBB921C1A6ED0048A176 /* FSTHelpers.mm in Sources */, 544A20EE20F6C10C004E52CD /* BasicCompileTests.swift in Sources */, 5495EB032040E90200EBA509 /* CodableGeoPointTests.swift in Sources */, + DE84BBB421C18F060048A176 /* FSTAPIHelpers.mm in Sources */, + DE26C61621C15FCC00DE141A /* CodableDocumentTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2138,6 +2150,11 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../..\"", + "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", + ); INFOPLIST_FILE = ../Swift/Tests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -2145,8 +2162,9 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.google.Firestore-SwiftTests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OBJC_BRIDGING_HEADER = ../Swift/Tests/BridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example_iOS.app/Firestore_Example_iOS"; }; @@ -2177,14 +2195,20 @@ ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; + HEADER_SEARCH_PATHS = ( + "$(inherited)", + "\"${PODS_ROOT}/../../..\"", + "\"${PODS_ROOT}/../../../Firestore/third_party/abseil-cpp\"", + ); INFOPLIST_FILE = ../Swift/Tests/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 11.2; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.google.Firestore-SwiftTests-iOS"; PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_OBJC_BRIDGING_HEADER = ../Swift/Tests/BridgingHeader.h; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; - SWIFT_VERSION = 4.0; + SWIFT_VERSION = 4.2; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Firestore_Example_iOS.app/Firestore_Example_iOS"; }; diff --git a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme index 8390f30fd00..5190072c475 100644 --- a/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme +++ b/Firestore/Example/Firestore.xcodeproj/xcshareddata/xcschemes/Firestore_Example_iOS.xcscheme @@ -48,6 +48,16 @@ ReferencedContainer = "container:Firestore.xcodeproj"> + + + + -#import "Firestore/Example/Tests/Util/FSTHelpers.h" - -#include "absl/strings/string_view.h" - @class FIRCollectionReference; @class FIRDocumentReference; @class FIRDocumentSnapshot; @class FIRFirestore; @class FIRQuerySnapshot; +/** Allow tests to just use an int literal for versions. */ +typedef int64_t FSTTestSnapshotVersion; + NS_ASSUME_NONNULL_BEGIN #if __cplusplus @@ -36,17 +35,17 @@ extern "C" { FIRFirestore *FSTTestFirestore(); /** A convenience method for creating a doc snapshot for tests. */ -FIRDocumentSnapshot *FSTTestDocSnapshot(const absl::string_view path, +FIRDocumentSnapshot *FSTTestDocSnapshot(const char *path, FSTTestSnapshotVersion version, NSDictionary *_Nullable data, BOOL hasMutations, BOOL fromCache); /** A convenience method for creating a collection reference from a path string. */ -FIRCollectionReference *FSTTestCollectionRef(const absl::string_view path); +FIRCollectionReference *FSTTestCollectionRef(const char *path); /** A convenience method for creating a document reference from a path string. */ -FIRDocumentReference *FSTTestDocRef(const absl::string_view path); +FIRDocumentReference *FSTTestDocRef(const char *path); /** * A convenience method for creating a particular query snapshot for tests. @@ -63,7 +62,7 @@ FIRDocumentReference *FSTTestDocRef(const absl::string_view path); * @returns A query snapshot that consists of both sets of documents. */ FIRQuerySnapshot *FSTTestQuerySnapshot( - const absl::string_view path, + const char *path, NSDictionary *> *oldDocs, NSDictionary *> *docsToAdd, BOOL hasPendingWrites, diff --git a/Firestore/Example/Tests/API/FSTAPIHelpers.mm b/Firestore/Example/Tests/API/FSTAPIHelpers.mm index 8e783c364b2..1ea4993bf78 100644 --- a/Firestore/Example/Tests/API/FSTAPIHelpers.mm +++ b/Firestore/Example/Tests/API/FSTAPIHelpers.mm @@ -22,6 +22,7 @@ #include +#import "Firestore/Example/Tests/Util/FSTHelpers.h" #import "Firestore/Source/API/FIRCollectionReference+Internal.h" #import "Firestore/Source/API/FIRDocumentReference+Internal.h" #import "Firestore/Source/API/FIRDocumentSnapshot+Internal.h" @@ -59,7 +60,7 @@ return sharedInstance; } -FIRDocumentSnapshot *FSTTestDocSnapshot(const absl::string_view path, +FIRDocumentSnapshot *FSTTestDocSnapshot(const char *path, FSTTestSnapshotVersion version, NSDictionary *_Nullable data, BOOL hasMutations, @@ -75,19 +76,19 @@ hasPendingWrites:hasMutations]; } -FIRCollectionReference *FSTTestCollectionRef(const absl::string_view path) { +FIRCollectionReference *FSTTestCollectionRef(const char *path) { return [FIRCollectionReference referenceWithPath:testutil::Resource(path) firestore:FSTTestFirestore()]; } -FIRDocumentReference *FSTTestDocRef(const absl::string_view path) { +FIRDocumentReference *FSTTestDocRef(const char *path) { return [FIRDocumentReference referenceWithPath:testutil::Resource(path) firestore:FSTTestFirestore()]; } /** A convenience method for creating a query snapshots for tests. */ FIRQuerySnapshot *FSTTestQuerySnapshot( - const absl::string_view path, + const char *path, NSDictionary *> *oldDocs, NSDictionary *> *docsToAdd, BOOL hasPendingWrites, diff --git a/Firestore/Swift/Source/Codable/CodableErrors.swift b/Firestore/Swift/Source/Codable/CodableErrors.swift new file mode 100644 index 00000000000..63d76622d86 --- /dev/null +++ b/Firestore/Swift/Source/Codable/CodableErrors.swift @@ -0,0 +1,23 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +enum FirestoreDecodingError: Error { + case decodingIsNotSupported +} + +enum FirestoreEncodingError: Error { + case encodingIsNotSupported +} diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift new file mode 100644 index 00000000000..395e5ba6b89 --- /dev/null +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -0,0 +1,39 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore + +/** + * A protocol describing the encodable properties of a DocumentReference. + * + * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class was + * extended directly to conform to Codable, the methods implementing the protcol would be need to be + * marked required but that can't be done in an extension. Declaring the extension on the protocol + * sidesteps this issue. + */ +fileprivate protocol CodableDocumentReference: Codable {} + +extension CodableDocumentReference { + public init(from decoder: Decoder) throws { + throw FirestoreDecodingError.decodingIsNotSupported + } + + public func encode(to encoder: Encoder) throws { + throw FirestoreEncodingError.encodingIsNotSupported + } +} + +extension DocumentReference: CodableDocumentReference {} diff --git a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift new file mode 100644 index 00000000000..9f72c94f16a --- /dev/null +++ b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift @@ -0,0 +1,35 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore + +/** + * A protocol describing the encodable properties of a FirebaseFirestore. + * + * Note: this protocol exists as a workaround for the Swift compiler: if the FieldValue class was + * extended directly to conform to Codable, the methods implementing the protcol would be need to be + * marked required but that can't be done in an extension. Declaring the extension on the protocol + * sidesteps this issue. + */ +fileprivate protocol CodableFieldValue: Encodable {} + +extension CodableFieldValue { + public func encode(to encoder: Encoder) throws { + throw FirestoreEncodingError.encodingIsNotSupported + } +} + +extension FieldValue: CodableFieldValue {} diff --git a/Firestore/Swift/Source/Codable/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/FirestoreDecoder.swift new file mode 100644 index 00000000000..095efddac54 --- /dev/null +++ b/Firestore/Swift/Source/Codable/FirestoreDecoder.swift @@ -0,0 +1,1062 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import FirebaseFirestore + +// Add CollectionReference? + +extension DocumentSnapshot { + public func data(as type: T.Type) throws -> T { + guard let dict = data() else { + throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "Data was empty")) + } + return try Firestore.Decoder().decode(T.self, from: dict) + } +} + +// Is there a better way to do this without the Firestore.Decoder wrapper? + +extension Firestore { + struct Decoder { + func decode(_ type: T.Type, from container: [String: Any]) throws -> T { + let decoder = _FirestoreDecoder(referencing: container) + 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")) + } + + return value + } + } +} + +class _FirestoreDecoder: Decoder { + /// Options set on the top-level encoder to pass down the decoding hierarchy. + + // MARK: Properties + + /// The decoder's storage. + + fileprivate var storage: _FirestoreDecodingStorage + + /// The path to the current point in encoding. + public fileprivate(set) var codingPath: [CodingKey] + + /// Contextual user-provided information for use during encoding. + public var userInfo: [CodingUserInfoKey: Any] = [:] + + // MARK: - Initialization + + /// Initializes `self` with the given top-level container and options. + init(referencing container: Any, at codingPath: [CodingKey] = []) { + storage = _FirestoreDecodingStorage() + storage.push(container: container) + self.codingPath = codingPath + } + + // MARK: - Decoder Methods + + public func container(keyedBy type: Key.Type) throws -> KeyedDecodingContainer { + guard !(storage.topContainer is NSNull) else { + throw DecodingError.valueNotFound(KeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, + debugDescription: "Cannot get keyed decoding container -- found null value instead.")) + } + + guard let topContainer = self.storage.topContainer as? [String: Any] else { + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Not a dictionary") + throw DecodingError.typeMismatch([String: Any].self, context) + } + + let container = _FirestoreKeyedDecodingContainer(referencing: self, wrapping: topContainer) + return KeyedDecodingContainer(container) + } + + public func unkeyedContainer() throws -> UnkeyedDecodingContainer { + guard !(storage.topContainer is NSNull) else { + throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, + debugDescription: "Cannot get unkeyed decoding container -- found null value instead.")) + } + + guard let topContainer = self.storage.topContainer as? [Any] else { + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Not an array") + throw DecodingError.typeMismatch([Any].self, context) + } + + return _FirestoreUnkeyedDecodingContainer(referencing: self, wrapping: topContainer) + } + + public func singleValueContainer() throws -> SingleValueDecodingContainer { + return self + } +} + +fileprivate struct _FirestoreDecodingStorage { + // MARK: Properties + + /// The container stack. + /// Elements may be any one of the plist types (NSNumber, Date, String, Array, [String : Any]). + fileprivate private(set) var containers: [Any] = [] + + // MARK: - Initialization + + /// Initializes `self` with no containers. + fileprivate init() {} + + // MARK: - Modifying the Stack + + fileprivate var count: Int { + return containers.count + } + + fileprivate var topContainer: Any { + precondition(containers.count > 0, "Empty container stack.") + return containers.last! + } + + fileprivate mutating func push(container: Any) { + containers.append(container) + } + + fileprivate mutating func popContainer() { + precondition(containers.count > 0, "Empty container stack.") + containers.removeLast() + } +} + +fileprivate struct _FirestoreKeyedDecodingContainer: KeyedDecodingContainerProtocol { + typealias Key = K + + // MARK: Properties + + /// A reference to the decoder we're reading from. + private let decoder: _FirestoreDecoder + + /// A reference to the container we're reading from. + private let container: [String: Any] + + /// The path of coding keys taken to get to this point in decoding. + public private(set) var codingPath: [CodingKey] + + // MARK: - Initialization + + /// Initializes `self` by referencing the given decoder and container. + fileprivate init(referencing decoder: _FirestoreDecoder, wrapping container: [String: Any]) { + self.decoder = decoder + self.container = container + codingPath = decoder.codingPath + } + + // MARK: - KeyedDecodingContainerProtocol Methods + + public var allKeys: [Key] { + return container.keys.compactMap { Key(stringValue: $0) } + } + + public func contains(_ key: Key) -> Bool { + return container[key.stringValue] != nil + } + + public func decodeNil(forKey key: Key) throws -> Bool { + let entry = try require(key: key) + return entry is NSNull + } + + public func decode(_ type: Bool.Type, forKey key: Key) throws -> Bool { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Bool.self) + return try require(value: value) + } + + public func decode(_ type: Int.Type, forKey key: Key) throws -> Int { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Int.self) + return try require(value: value) + } + + public func decode(_ type: Int8.Type, forKey key: Key) throws -> Int8 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Int8.self) + return try require(value: value) + } + + public func decode(_ type: Int16.Type, forKey key: Key) throws -> Int16 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Int16.self) + return try require(value: value) + } + + public func decode(_ type: Int32.Type, forKey key: Key) throws -> Int32 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Int32.self) + return try require(value: value) + } + + public func decode(_ type: Int64.Type, forKey key: Key) throws -> Int64 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Int64.self) + return try require(value: value) + } + + public func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: UInt.self) + return try require(value: value) + } + + public func decode(_ type: UInt8.Type, forKey key: Key) throws -> UInt8 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: UInt8.self) + return try require(value: value) + } + + public func decode(_ type: UInt16.Type, forKey key: Key) throws -> UInt16 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: UInt16.self) + return try require(value: value) + } + + public func decode(_ type: UInt32.Type, forKey key: Key) throws -> UInt32 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: UInt32.self) + return try require(value: value) + } + + public func decode(_ type: UInt64.Type, forKey key: Key) throws -> UInt64 { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: UInt64.self) + return try require(value: value) + } + + public func decode(_ type: Float.Type, forKey key: Key) throws -> Float { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Float.self) + return try require(value: value) + } + + public func decode(_ type: Double.Type, forKey key: Key) throws -> Double { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: Double.self) + return try require(value: value) + } + + public func decode(_ type: String.Type, forKey key: Key) throws -> String { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: String.self) + return try require(value: value) + } + + public func decode(_ type: T.Type, forKey key: Key) throws -> T { + let entry = try require(key: key) + + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value = try decoder.unbox(entry, as: T.self) + return try require(value: value) + } + + private func require(key: Key) throws -> Any { + if let entry = self.container[key.stringValue] { + return entry + } + + let description = "No value associated with key \(key) (\"\(key.stringValue)\")." + let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: description) + throw DecodingError.keyNotFound(key, context) + } + + private func require(value: T?) throws -> T { + if let value = value { + return value + } + + let message = "Expected \(T.self) value but found null instead." + let context = DecodingError.Context(codingPath: decoder.codingPath, debugDescription: message) + throw DecodingError.valueNotFound(T.self, context) + } + + public func nestedContainer(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer { + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let value = self.container[key.stringValue] else { + throw DecodingError.valueNotFound(KeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, + debugDescription: "Cannot get nested keyed container -- no value found for key \"\(key.stringValue)\"")) + } + + guard let dictionary = value as? [String: Any] else { + throw DecodingError._typeMismatch(at: codingPath, expectation: [String: Any].self, reality: value) + } + + let container = _FirestoreKeyedDecodingContainer(referencing: decoder, wrapping: dictionary) + return KeyedDecodingContainer(container) + } + + public func nestedUnkeyedContainer(forKey key: Key) throws -> UnkeyedDecodingContainer { + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + guard let value = self.container[key.stringValue] else { + throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, + DecodingError.Context(codingPath: codingPath, + debugDescription: "Cannot get nested unkeyed container -- no value found for key \"\(key.stringValue)\"")) + } + + guard let array = value as? [Any] else { + let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Not an array") + throw DecodingError.typeMismatch([Any].self, context) + } + + return _FirestoreUnkeyedDecodingContainer(referencing: decoder, wrapping: array) + } + + private func _superDecoder(forKey key: CodingKey) throws -> Decoder { + decoder.codingPath.append(key) + defer { self.decoder.codingPath.removeLast() } + + let value: Any = container[key.stringValue] ?? NSNull() + return _FirestoreDecoder(referencing: value, at: decoder.codingPath) + } + + public func superDecoder() throws -> Decoder { + return try _superDecoder(forKey: _FirestoreKey.super) + } + + public func superDecoder(forKey key: Key) throws -> Decoder { + return try _superDecoder(forKey: key) + } +} + +fileprivate struct _FirestoreUnkeyedDecodingContainer: UnkeyedDecodingContainer { + // MARK: Properties + + /// A reference to the decoder we're reading from. + private let decoder: _FirestoreDecoder + + /// A reference to the container we're reading from. + private let container: [Any] + + /// The path of coding keys taken to get to this point in decoding. + public private(set) var codingPath: [CodingKey] + + /// The index of the element we're about to decode. + public private(set) var currentIndex: Int + + // MARK: - Initialization + + /// Initializes `self` by referencing the given decoder and container. + fileprivate init(referencing decoder: _FirestoreDecoder, wrapping container: [Any]) { + self.decoder = decoder + self.container = container + codingPath = decoder.codingPath + currentIndex = 0 + } + + // MARK: - UnkeyedDecodingContainer Methods + + public var count: Int? { + return container.count + } + + public var isAtEnd: Bool { + return currentIndex >= count! + } + + public mutating func decodeNil() throws -> Bool { + try expectNotAtEnd() + + if container[currentIndex] is NSNull { + currentIndex += 1 + return true + } else { + return false + } + } + + public mutating func decode(_ type: Bool.Type) throws -> Bool { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Bool.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: Int.Type) throws -> Int { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Int.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: Int8.Type) throws -> Int8 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Int8.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: Int16.Type) throws -> Int16 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Int16.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: Int32.Type) throws -> Int32 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Int32.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: Int64.Type) throws -> Int64 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Int64.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: UInt.Type) throws -> UInt { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: UInt.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: UInt8.Type) throws -> UInt8 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: UInt8.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: UInt16.Type) throws -> UInt16 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: UInt16.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: UInt32.Type) throws -> UInt32 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: UInt32.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: UInt64.Type) throws -> UInt64 { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: UInt64.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: Float.Type) throws -> Float { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Float.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: Double.Type) throws -> Double { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: Double.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: String.Type) throws -> String { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: String.self) + return try require(value: decoded) + } + + public mutating func decode(_ type: T.Type) throws -> T { + try expectNotAtEnd() + + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + let decoded = try decoder.unbox(container[currentIndex], as: T.self) + return try require(value: decoded) + } + + public mutating func nestedContainer(keyedBy type: NestedKey.Type) throws -> KeyedDecodingContainer { + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + try expectNotAtEnd() + + let value = self.container[self.currentIndex] + try requireNotNSNull(value) + + guard let dictionary = value as? [String: Any] else { + throw DecodingError._typeMismatch(at: codingPath, expectation: [String: Any].self, reality: value) + } + + currentIndex += 1 + let container = _FirestoreKeyedDecodingContainer(referencing: decoder, wrapping: dictionary) + return KeyedDecodingContainer(container) + } + + public mutating func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + try expectNotAtEnd() + + let value = container[self.currentIndex] + try requireNotNSNull(value) + + guard let array = value as? [Any] else { + throw DecodingError._typeMismatch(at: codingPath, expectation: [Any].self, reality: value) + } + + currentIndex += 1 + return _FirestoreUnkeyedDecodingContainer(referencing: decoder, wrapping: array) + } + + public mutating func superDecoder() throws -> Decoder { + decoder.codingPath.append(_FirestoreKey(index: currentIndex)) + defer { self.decoder.codingPath.removeLast() } + + try expectNotAtEnd() + + let value = container[self.currentIndex] + currentIndex += 1 + return _FirestoreDecoder(referencing: value, at: decoder.codingPath) + } + + private func expectNotAtEnd() throws { + guard !isAtEnd else { + throw DecodingError.valueNotFound(Any?.self, DecodingError.Context(codingPath: decoder.codingPath + [_FirestoreKey(index: self.currentIndex)], debugDescription: "Unkeyed container is at end.")) + } + } + + private func requireNotNSNull(_ value: Any) throws { + if !(value is NSNull) { + return + } + + let description = "Cannot get keyed decoding container -- found null value instead." + let context = DecodingError.Context(codingPath: codingPath, debugDescription: description) + throw DecodingError.valueNotFound(UnkeyedDecodingContainer.self, context) + } + + private mutating func require(value: T?) throws -> T { + guard let value = value else { + let message = "Expected \(T.self) value but found null instead." + let context = DecodingError.Context(codingPath: decoder.codingPath + [_FirestoreKey(index: currentIndex)], debugDescription: message) + throw DecodingError.valueNotFound(T.self, context) + } + + currentIndex += 1 + return value + } +} + +extension _FirestoreDecoder: SingleValueDecodingContainer { + // MARK: SingleValueDecodingContainer Methods + + private func expectNonNull(_ type: T.Type) throws { + guard !decodeNil() else { + throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: codingPath, debugDescription: "Expected \(type) but found null value instead.")) + } + } + + public func decodeNil() -> Bool { + return storage.topContainer is NSNull + } + + public func decode(_ type: Bool.Type) throws -> Bool { + try expectNonNull(Bool.self) + return try unbox(storage.topContainer, as: Bool.self)! + } + + public func decode(_ type: Int.Type) throws -> Int { + try expectNonNull(Int.self) + return try unbox(storage.topContainer, as: Int.self)! + } + + public func decode(_ type: Int8.Type) throws -> Int8 { + try expectNonNull(Int8.self) + return try unbox(storage.topContainer, as: Int8.self)! + } + + public func decode(_ type: Int16.Type) throws -> Int16 { + try expectNonNull(Int16.self) + return try unbox(storage.topContainer, as: Int16.self)! + } + + public func decode(_ type: Int32.Type) throws -> Int32 { + try expectNonNull(Int32.self) + return try unbox(storage.topContainer, as: Int32.self)! + } + + public func decode(_ type: Int64.Type) throws -> Int64 { + try expectNonNull(Int64.self) + return try unbox(storage.topContainer, as: Int64.self)! + } + + public func decode(_ type: UInt.Type) throws -> UInt { + try expectNonNull(UInt.self) + return try unbox(storage.topContainer, as: UInt.self)! + } + + public func decode(_ type: UInt8.Type) throws -> UInt8 { + try expectNonNull(UInt8.self) + return try unbox(storage.topContainer, as: UInt8.self)! + } + + public func decode(_ type: UInt16.Type) throws -> UInt16 { + try expectNonNull(UInt16.self) + return try unbox(storage.topContainer, as: UInt16.self)! + } + + public func decode(_ type: UInt32.Type) throws -> UInt32 { + try expectNonNull(UInt32.self) + return try unbox(storage.topContainer, as: UInt32.self)! + } + + public func decode(_ type: UInt64.Type) throws -> UInt64 { + try expectNonNull(UInt64.self) + return try unbox(storage.topContainer, as: UInt64.self)! + } + + public func decode(_ type: Float.Type) throws -> Float { + try expectNonNull(Float.self) + return try unbox(storage.topContainer, as: Float.self)! + } + + public func decode(_ type: Double.Type) throws -> Double { + try expectNonNull(Double.self) + return try unbox(storage.topContainer, as: Double.self)! + } + + public func decode(_ type: String.Type) throws -> String { + try expectNonNull(String.self) + return try unbox(storage.topContainer, as: String.self)! + } + + public func decode(_ type: T.Type) throws -> T { + try expectNonNull(T.self) + return try unbox(storage.topContainer, as: T.self)! + } +} + +extension _FirestoreDecoder { + /// Returns the given value unboxed from a container. + func unbox(_ value: Any, as type: Bool.Type) throws -> Bool? { + guard !(value is NSNull) else { return nil } + + if let number = value as? NSNumber { + // TODO: Add a flag to coerce non-boolean numbers into Bools? + if number === kCFBooleanTrue as NSNumber { + return true + } else if number === kCFBooleanFalse as NSNumber { + return false + } + + /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: + } else if let bool = value as? Bool { + return bool + */ + } + + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + func unbox(_ value: Any, as type: Int.Type) throws -> Int? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let int = number.intValue + guard NSNumber(value: int) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return int + } + + func unbox(_ value: Any, as type: Int8.Type) throws -> Int8? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let int8 = number.int8Value + guard NSNumber(value: int8) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return int8 + } + + func unbox(_ value: Any, as type: Int16.Type) throws -> Int16? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let int16 = number.int16Value + guard NSNumber(value: int16) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return int16 + } + + func unbox(_ value: Any, as type: Int32.Type) throws -> Int32? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let int32 = number.int32Value + guard NSNumber(value: int32) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return int32 + } + + func unbox(_ value: Any, as type: Int64.Type) throws -> Int64? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let int64 = number.int64Value + guard NSNumber(value: int64) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return int64 + } + + func unbox(_ value: Any, as type: UInt.Type) throws -> UInt? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let uint = number.uintValue + guard NSNumber(value: uint) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return uint + } + + func unbox(_ value: Any, as type: UInt8.Type) throws -> UInt8? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let uint8 = number.uint8Value + guard NSNumber(value: uint8) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return uint8 + } + + func unbox(_ value: Any, as type: UInt16.Type) throws -> UInt16? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let uint16 = number.uint16Value + guard NSNumber(value: uint16) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return uint16 + } + + func unbox(_ value: Any, as type: UInt32.Type) throws -> UInt32? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let uint32 = number.uint32Value + guard NSNumber(value: uint32) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return uint32 + } + + func unbox(_ value: Any, as type: UInt64.Type) throws -> UInt64? { + guard !(value is NSNull) else { return nil } + + guard let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + let uint64 = number.uint64Value + guard NSNumber(value: uint64) == number else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number <\(number)> does not fit in \(type).")) + } + + return uint64 + } + + func unbox(_ value: Any, as type: Float.Type) throws -> Float? { + guard !(value is NSNull) else { return nil } + + if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse { + // We are willing to return a Float by losing precision: + // * If the original value was integral, + // * and the integral value was > Float.greatestFiniteMagnitude, we will fail + // * and the integral value was <= Float.greatestFiniteMagnitude, we are willing to lose precision past 2^24 + // * If it was a Float, you will get back the precise value + // * If it was a Double or Decimal, you will get back the nearest approximation if it will fit + let double = number.doubleValue + guard abs(double) <= Double(Float.greatestFiniteMagnitude) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Decoded number \(number) does not fit in \(type).")) + } + + return Float(double) + + /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: + } else if let double = value as? Double { + if abs(double) <= Double(Float.max) { + return Float(double) + } + overflow = true + } else if let int = value as? Int { + if let float = Float(exactly: int) { + return float + } + overflow = true + */ + } + + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + func unbox(_ value: Any, as type: Double.Type) throws -> Double? { + guard !(value is NSNull) else { return nil } + + if let number = value as? NSNumber, number !== kCFBooleanTrue, number !== kCFBooleanFalse { + // We are always willing to return the number as a Double: + // * If the original value was integral, it is guaranteed to fit in a Double; we are willing to lose precision past 2^53 if you encoded a UInt64 but requested a Double + // * If it was a Float or Double, you will get back the precise value + // * If it was Decimal, you will get back the nearest approximation + return number.doubleValue + + /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: + } else if let double = value as? Double { + return double + } else if let int = value as? Int { + if let double = Double(exactly: int) { + return double + } + overflow = true + */ + } + + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + func unbox(_ value: Any, as type: String.Type) throws -> String? { + guard !(value is NSNull) else { return nil } + + guard let string = value as? String else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + + return string + } + + func unbox(_ value: Any, as type: Date.Type) throws -> Date? { + guard !(value is NSNull) else { return nil } + guard let date = value as? Date else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + return date + } + + func unbox(_ value: Any, as type: Data.Type) throws -> Data? { + guard !(value is NSNull) else { return nil } + guard let data = value as? Data else { + throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) + } + return data + } + + func unbox(_ value: Any, as type: Decimal.Type) throws -> Decimal? { + guard !(value is NSNull) else { return nil } + + // Attempt to bridge from NSDecimalNumber. + if let decimal = value as? Decimal { + return decimal + } else { + let doubleValue = try unbox(value, as: Double.self)! + return Decimal(doubleValue) + } + } + + func unbox(_ value: Any, as type: T.Type) throws -> T? { + let decoded: T + if T.self == Date.self || T.self == NSDate.self { + guard let date = try self.unbox(value, as: Date.self) else { return nil } + decoded = date as! T + } else if T.self == Data.self || T.self == NSData.self { + guard let data = try self.unbox(value, as: Data.self) else { return nil } + decoded = data as! T + } else if T.self == URL.self || T.self == NSURL.self { + guard let urlString = try self.unbox(value, as: String.self) else { + return nil + } + + guard let url = URL(string: urlString) else { + throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, + debugDescription: "Invalid URL string.")) + } + + decoded = (url as! T) + } 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 T.self == GeoPoint.self || T.self == DocumentReference.self { + // All the native types that should not be encoded + decoded = value as! T + } else { + storage.push(container: value) + decoded = try T(from: self) + storage.popContainer() + } + + return decoded + } +} diff --git a/Firestore/Swift/Source/Codable/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/FirestoreEncoder.swift new file mode 100644 index 00000000000..eba68545af8 --- /dev/null +++ b/Firestore/Swift/Source/Codable/FirestoreEncoder.swift @@ -0,0 +1,612 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import FirebaseFirestore + +extension CollectionReference { + public func addDocument(_ item: T) throws -> DocumentReference { + do { + return addDocument(data: try Firestore.Encoder().encode(item)) + } catch let error { + throw (error) + } + } + + public func addDocument(_ item: T, _ completion: ((Error?) -> Void)?) throws -> DocumentReference { + do { + let encoded = try Firestore.Encoder().encode(item) + return addDocument(data: encoded, completion: completion) + } catch let error { + throw (error) + } + } +} + +extension DocumentReference { + public func setData(value: T) throws { + do { + setData(try Firestore.Encoder().encode(value)) + } catch let error { + throw (error) + } + } + + public func setData(value: T, _ completion: ((Error?) -> Void)?) throws { + do { + let encoded = try Firestore.Encoder().encode(value) + setData(encoded, completion: completion) + } catch let error { + throw error + } + } +} + +extension Transaction { + public func setData(value: T, forDocument: DocumentReference) throws { + do { + setData(try Firestore.Encoder().encode(value), forDocument: forDocument) + } catch let error { + throw (error) + } + } +} + +extension WriteBatch { + public func setData(value: T, forDocument: DocumentReference) throws { + do { + setData(try Firestore.Encoder().encode(value), forDocument: forDocument) + } catch let error { + throw (error) + } + } +} + +extension Firestore { + struct Encoder { + func encode(_ value: T) throws -> [String: Any] { + guard let topLevel = try _FirestoreEncoder().box_(value) else { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-level \(T.self) did not encode any values.")) + } + + // This is O(n) check. We might get rid of it once we refactor to internal + guard let dict = topLevel as? [String: Any] else { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-level \(T.self) encoded not as dictionary.")) + } + + return dict + } + } +} + +fileprivate class _FirestoreEncoder: Encoder { + fileprivate var storage: _FirestoreEncodingStorage + public fileprivate(set) var codingPath: [CodingKey] + public var userInfo: [CodingUserInfoKey: Any] = [:] + + init(codingPath: [CodingKey] = []) { + storage = _FirestoreEncodingStorage() + self.codingPath = codingPath + } + + /// Returns whether a new element can be encoded at this coding path. + /// + /// `true` if an element has not yet been encoded at this coding path; `false` otherwise. + fileprivate var canEncodeNewValue: Bool { + // Every time a new value gets encoded, the key it's encoded for is pushed onto the coding path (even if it's a nil key from an unkeyed container). + // At the same time, every time a container is requested, a new value gets pushed onto the storage stack. + // If there are more values on the storage stack than on the coding path, it means the value is requesting more than one container, which violates the precondition. + // + // This means that anytime something that can request a new container goes onto the stack, we MUST push a key onto the coding path. + // Things which will not request containers do not need to have the coding path extended for them (but it doesn't matter if it is, because they will not reach here). + return storage.count == codingPath.count + } + + // MARK: - Encoder Methods + + public func container(keyedBy: Key.Type) -> KeyedEncodingContainer { + // If an existing keyed container was already requested, return that one. + let topContainer: NSMutableDictionary + if canEncodeNewValue { + // We haven't yet pushed a container at this level; do so here. + topContainer = storage.pushKeyedContainer() + } else { + guard let container = self.storage.containers.last as? NSMutableDictionary else { + preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") + } + + topContainer = container + } + + let container = _FirestoreKeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) + return KeyedEncodingContainer(container) + } + + public func unkeyedContainer() -> UnkeyedEncodingContainer { + // If an existing unkeyed container was already requested, return that one. + let topContainer: NSMutableArray + if canEncodeNewValue { + // We haven't yet pushed a container at this level; do so here. + topContainer = storage.pushUnkeyedContainer() + } else { + guard let container = self.storage.containers.last as? NSMutableArray else { + preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") + } + + topContainer = container + } + + return _FirestoreUnkeyedEncodingContainer(referencing: self, codingPath: codingPath, wrapping: topContainer) + } + + public func singleValueContainer() -> SingleValueEncodingContainer { + return self + } +} + +fileprivate struct _FirestoreEncodingStorage { + // MARK: Properties + + /// The container stack. + /// Elements may be any one of the plist types (NSNumber, NSString, NSDate, NSArray, NSDictionary). + fileprivate private(set) var containers: [NSObject] = [] + + // MARK: - Initialization + + /// Initializes `self` with no containers. + fileprivate init() {} + + // MARK: - Modifying the Stack + + fileprivate var count: Int { + return containers.count + } + + fileprivate mutating func pushKeyedContainer() -> NSMutableDictionary { + let dictionary = NSMutableDictionary() + containers.append(dictionary) + return dictionary + } + + fileprivate mutating func pushUnkeyedContainer() -> NSMutableArray { + let array = NSMutableArray() + containers.append(array) + return array + } + + fileprivate mutating func push(container: NSObject) { + containers.append(container) + } + + fileprivate mutating func popContainer() -> NSObject { + precondition(containers.count > 0, "Empty container stack.") + return containers.popLast()! + } +} + +fileprivate struct _FirestoreKeyedEncodingContainer: KeyedEncodingContainerProtocol { + typealias Key = K + + // MARK: Properties + + /// A reference to the encoder we're writing to. + private let encoder: _FirestoreEncoder + + /// A reference to the container we're writing to. + private let container: NSMutableDictionary + + /// The path of coding keys taken to get to this point in encoding. + public private(set) var codingPath: [CodingKey] + + // MARK: - Initialization + + /// Initializes `self` with the given references. + fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableDictionary) { + self.encoder = encoder + self.codingPath = codingPath + self.container = container + } + + // MARK: - KeyedEncodingContainerProtocol Methods + + public mutating func encodeNil(forKey key: Key) throws { container[key.stringValue] = NSNull() } + public mutating func encode(_ value: Bool, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Int64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt8, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt16, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt32, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: UInt64, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: String, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Float, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + public mutating func encode(_ value: Double, forKey key: Key) throws { container[key.stringValue] = encoder.box(value) } + + public mutating func encode(_ value: T, forKey key: Key) throws { + encoder.codingPath.append(key) + defer { encoder.codingPath.removeLast() } + container[key.stringValue] = try encoder.box(value) + } + + public mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + let dictionary = NSMutableDictionary() + self.container[key.stringValue] = dictionary + + codingPath.append(key) + defer { codingPath.removeLast() } + + let container = _FirestoreKeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: dictionary) + return KeyedEncodingContainer(container) + } + + public mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let array = NSMutableArray() + container[key.stringValue] = array + + codingPath.append(key) + defer { codingPath.removeLast() } + return _FirestoreUnkeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: array) + } + + public mutating func superEncoder() -> Encoder { + return _FirestoreReferencingEncoder(referencing: encoder, at: _FirestoreKey.super, wrapping: container) + } + + public mutating func superEncoder(forKey key: Key) -> Encoder { + return _FirestoreReferencingEncoder(referencing: encoder, at: key, wrapping: container) + } +} + +fileprivate struct _FirestoreUnkeyedEncodingContainer: UnkeyedEncodingContainer { + // MARK: Properties + + /// A reference to the encoder we're writing to. + private let encoder: _FirestoreEncoder + + /// A reference to the container we're writing to. + private let container: NSMutableArray + + /// The path of coding keys taken to get to this point in encoding. + public private(set) var codingPath: [CodingKey] + + /// The number of elements encoded into the container. + public var count: Int { + return container.count + } + + // MARK: - Initialization + + /// Initializes `self` with the given references. + fileprivate init(referencing encoder: _FirestoreEncoder, codingPath: [CodingKey], wrapping container: NSMutableArray) { + self.encoder = encoder + self.codingPath = codingPath + self.container = container + } + + // MARK: - UnkeyedEncodingContainer Methods + + public mutating func encodeNil() throws { container.add(NSNull()) } + public mutating func encode(_ value: Bool) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: Int) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: Int8) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: Int16) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: Int32) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: Int64) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: UInt) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: UInt8) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: UInt16) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: UInt32) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: UInt64) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: Float) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: Double) throws { container.add(encoder.box(value)) } + public mutating func encode(_ value: String) throws { container.add(encoder.box(value)) } + + public mutating func encode(_ value: T) throws { + encoder.codingPath.append(_FirestoreKey(index: count)) + defer { encoder.codingPath.removeLast() } + container.add(try encoder.box(value)) + } + + public mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + codingPath.append(_FirestoreKey(index: count)) + defer { self.codingPath.removeLast() } + + let dictionary = NSMutableDictionary() + self.container.add(dictionary) + + let container = _FirestoreKeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: dictionary) + return KeyedEncodingContainer(container) + } + + public mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + codingPath.append(_FirestoreKey(index: count)) + defer { self.codingPath.removeLast() } + + let array = NSMutableArray() + container.add(array) + return _FirestoreUnkeyedEncodingContainer(referencing: encoder, codingPath: codingPath, wrapping: array) + } + + public mutating func superEncoder() -> Encoder { + return _FirestoreReferencingEncoder(referencing: encoder, at: container.count, wrapping: container) + } +} + +struct _FirestoreKey: CodingKey { + public var stringValue: String + public var intValue: Int? + + public init?(stringValue: String) { + self.stringValue = stringValue + intValue = nil + } + + public init?(intValue: Int) { + stringValue = "\(intValue)" + self.intValue = intValue + } + + init(index: Int) { + stringValue = "Index \(index)" + intValue = index + } + + static let `super` = _FirestoreKey(stringValue: "super")! +} + +extension _FirestoreEncoder { + /// Returns the given value boxed in a container appropriate for pushing onto the container stack. + fileprivate func box(_ value: Bool) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int8) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int16) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int32) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Int64) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt8) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt16) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt32) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: UInt64) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Float) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: Double) -> NSObject { return NSNumber(value: value) } + fileprivate func box(_ value: String) -> NSObject { return NSString(string: value) } + + fileprivate func box(_ value: T) throws -> NSObject { + return try box_(value) ?? NSDictionary() + } + + func box_(_ value: T) throws -> NSObject? { + if T.self == Date.self || T.self == NSDate.self { + return (value as! NSDate) + } else if T.self == Data.self || T.self == NSData.self { + return (value as! NSData) + } else if T.self == URL.self || T.self == NSURL.self { + return box((value as! URL).absoluteString) + } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { + return (value as! NSDecimalNumber) + } else if T.self == GeoPoint.self || T.self == DocumentReference.self || T.self == FieldValue.self { + // These are all native _Firestore types that we don't need to Encode + return (value as! NSObject) + } + + // The value should request a container from the _FirestoreEncoder. + let depth = storage.count + do { + try value.encode(to: self) + } catch { + // If the value pushed a container before throwing, pop it back off to restore state. + if storage.count > depth { + _ = storage.popContainer() + } + + throw error + } + + // The top container should be a new container. + guard storage.count > depth else { + return nil + } + + return storage.popContainer() + } +} + +extension _FirestoreEncoder: SingleValueEncodingContainer { + // MARK: - SingleValueEncodingContainer Methods + + private func assertCanEncodeNewValue() { + precondition(canEncodeNewValue, "Attempt to encode value through single value container when previously value already encoded.") + } + + public func encodeNil() throws { + assertCanEncodeNewValue() + storage.push(container: NSNull()) + } + + public func encode(_ value: Bool) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Int64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt8) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt16) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt32) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: UInt64) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: String) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Float) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: Double) throws { + assertCanEncodeNewValue() + storage.push(container: box(value)) + } + + public func encode(_ value: T) throws { + assertCanEncodeNewValue() + try storage.push(container: box(value)) + } +} + +fileprivate class _FirestoreReferencingEncoder: _FirestoreEncoder { + // MARK: Reference types. + + /// The type of container we're referencing. + private enum Reference { + /// Referencing a specific index in an array container. + case array(NSMutableArray, Int) + + /// Referencing a specific key in a dictionary container. + case dictionary(NSMutableDictionary, String) + } + + // MARK: - Properties + + /// The encoder we're referencing. + private let encoder: _FirestoreEncoder + + /// The container reference itself. + private let reference: Reference + + // MARK: - Initialization + + /// Initializes `self` by referencing the given array container in the given encoder. + fileprivate init(referencing encoder: _FirestoreEncoder, at index: Int, wrapping array: NSMutableArray) { + self.encoder = encoder + reference = .array(array, index) + super.init(codingPath: encoder.codingPath) + + codingPath.append(_FirestoreKey(index: index)) + } + + /// Initializes `self` by referencing the given dictionary container in the given encoder. + fileprivate init(referencing encoder: _FirestoreEncoder, at key: CodingKey, wrapping dictionary: NSMutableDictionary) { + self.encoder = encoder + reference = .dictionary(dictionary, key.stringValue) + super.init(codingPath: encoder.codingPath) + codingPath.append(key) + } + + // MARK: - Coding Path Operations + + fileprivate override var canEncodeNewValue: Bool { + // With a regular encoder, the storage and coding path grow together. + // A referencing encoder, however, inherits its parents coding path, as well as the key it was created for. + // We have to take this into account. + return storage.count == codingPath.count - encoder.codingPath.count - 1 + } + + // MARK: - Deinitialization + + // Finalizes `self` by writing the contents of our storage to the referenced encoder's storage. + deinit { + let value: Any + switch storage.count { + case 0: value = NSDictionary() + case 1: value = self.storage.popContainer() + default: fatalError("Referencing encoder deallocated with multiple containers on stack.") + } + + switch self.reference { + case let .array(array, index): + array.insert(value, at: index) + + case let .dictionary(dictionary, key): + dictionary[NSString(string: key)] = value + } + } +} + +extension DecodingError { + static func _typeMismatch(at path: [CodingKey], expectation: Any.Type, reality: Any) -> DecodingError { + let description = "Expected to decode \(expectation) but found \(_typeDescription(of: reality)) instead." + return .typeMismatch(expectation, Context(codingPath: path, debugDescription: description)) + } + + fileprivate static func _typeDescription(of value: Any) -> String { + if value is NSNull { + return "a null value" + } else if value is NSNumber /* FIXME: If swift-corelibs-foundation isn't updated to use NSNumber, this check will be necessary: || value is Int || value is Double */ { + return "a number" + } else if value is String { + return "a string/data" + } else if value is [Any] { + return "an array" + } else if value is [String: Any] { + return "a dictionary" + } else { + return "\(type(of: value))" + } + } +} diff --git a/Firestore/Swift/Source/Codable/CodableGeoPoint.swift b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift similarity index 100% rename from Firestore/Swift/Source/Codable/CodableGeoPoint.swift rename to Firestore/Swift/Source/Codable/GeoPoint+Codable.swift diff --git a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift new file mode 100644 index 00000000000..44896c84fde --- /dev/null +++ b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift @@ -0,0 +1,45 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore + +/** + * A protocol describing the encodable properties of a DocumentSnapshot. + * + * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentSnapshot class was + * extended directly to conform to Codable, the methods implementing the protcol would be need to be + * marked required but that can't be done in an extension. Declaring the extension on the protocol + * sidesteps this issue. + */ +public protocol CodableTimestamp: Codable { + init(date: Date) + func dateValue() -> Date +} + +extension CodableTimestamp { + var date: Date { return dateValue() } + public init(from decoder: Decoder) throws { + 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(dateValue()) + } +} + +extension Timestamp: CodableTimestamp {} diff --git a/Firestore/Swift/Tests/BridgingHeader.h b/Firestore/Swift/Tests/BridgingHeader.h new file mode 100644 index 00000000000..d8f451d7b0a --- /dev/null +++ b/Firestore/Swift/Tests/BridgingHeader.h @@ -0,0 +1,22 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef BridgingHeader_h +#define BridgingHeader_h + +#import "FSTAPIHelpers.h" + +#endif /* BridgingHeader_h */ diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift new file mode 100644 index 00000000000..3f78632e357 --- /dev/null +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -0,0 +1,403 @@ +/* + * Copyright 2018 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore +@testable import FirebaseFirestoreSwift +import Foundation +import XCTest + +class CodableDocumentTests: XCTestCase { + func roundTrip(input: X, expected: [String: Any], doTest: Bool = true) -> X where X: Codable { + var encoded = [String: Any]() + do { + encoded = try Firestore.Encoder().encode(input) + if doTest { + XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) + } + } catch { + XCTFail("Failed to encode \(X.self): error: \(error)") + } + do { + let decoded = try Firestore.Decoder().decode(X.self, from: encoded) + return decoded + } catch { + XCTFail("Failed to decode \(X.self): \(error)") + } + return input // After failure + } + + func testInt() { + struct Model: Codable { + let x: Int + } + let model = Model(x: 42) + let dict = ["x": 42] + XCTAssertEqual(model.x, roundTrip(input: model, expected: dict).x) + } + + func testEmpty() { + struct Model: Codable {} + let model = Model() + let dict = [String: Any]() + XCTAssertEqual((try Firestore.Encoder().encode(model)) as NSDictionary, dict as NSDictionary) + } + + func testNil() { + struct Model: Codable { + let x: Int? + } + let model = Model(x: nil) + let dict = ["x": nil] as [String: Int?] + let encodedDict = try! Firestore.Encoder().encode(model) + XCTAssertNil(encodedDict["x"]) + let model2 = try? Firestore.Decoder().decode(Model.self, from: dict as [String: Any]) + XCTAssertNil(model2) + } + + func testOptional() { + struct Model: Codable { + let x: Int + let opt: Int? + } + let dict = ["x": 42] + let model = Model(x: 42, opt: nil) + XCTAssertEqual(model.x, roundTrip(input: model, expected: dict).x) + + let model2 = Model(x: 42, opt: 7) + let expected = ["x": 42, "opt": 7] + let encoded = try! Firestore.Encoder().encode(model2) + XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) + let decoded = try! Firestore.Decoder().decode(Model.self, from: expected) + XCTAssertEqual(decoded.x, model2.x) + XCTAssertEqual(decoded.opt, model2.opt) + } + + func testOptionalTimestamp() { + class FirestoreDummy { + /// Partial keypath can represent the property name + func setObject(_ object: T, fieldValues: [PartialKeyPath: FieldValue] = [:]) { + // Encode, check if any timestamps are nil or not, and if so use FieldValue.serverTimestamp() + } + } + struct Model: Codable { + let value: Int + let timestamp: Timestamp? + } + let c = Model(value: 10, timestamp: nil) + let fs = FirestoreDummy() + // If no custom field values need to be set: + fs.setObject(c) + + // Or, overriding custom field values: + fs.setObject(c, fieldValues: [\Model.timestamp: FieldValue.serverTimestamp(), + \Model.value: FieldValue.delete()]) + } + + func testEnum() { + enum MyEnum: Codable, Equatable { + case num(number: Int) + case text(String) + case timestamp(Timestamp) + + private enum CodingKeys: String, CodingKey { + case num + case text + case timestamp + } + + private enum DecodingError: Error { + case decoding(String) + } + + init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + if let value = try? values.decode(Int.self, forKey: .num) { + self = .num(number: value) + return + } + if let value = try? values.decode(String.self, forKey: .text) { + self = .text(value) + return + } + if let value = try? values.decode(Timestamp.self, forKey: .timestamp) { + self = .timestamp(value) + return + } + throw DecodingError.decoding("Decoding error: \(dump(values))") + } + + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + switch self { + case let .num(number): + try container.encode(number, forKey: .num) + case let .text(value): + try container.encode(value, forKey: .text) + case let .timestamp(stamp): + try container.encode(stamp, forKey: .timestamp) + } + } + } + struct Model: Codable { + let x: Int + let e: MyEnum + } + let model = Model(x: 42, e: MyEnum.num(number: 4)) + let output = roundTrip(input: model, expected: [:], doTest: false) + XCTAssertEqual(model.x, output.x) + XCTAssertEqual(model.e, output.e) + + let model2 = Model(x: 43, e: MyEnum.text("abc")) + let output2 = roundTrip(input: model2, expected: [:], doTest: false) + XCTAssertEqual(model2.x, output2.x) + XCTAssertEqual(model2.e, output2.e) + + let model3 = Model(x: 43, e: MyEnum.timestamp(Timestamp(date: Date()))) + let output3 = roundTrip(input: model3, expected: [:], doTest: false) + XCTAssertEqual(model3.x, output3.x) + XCTAssertEqual(model3.e, output3.e) + } + + func testGeoPoint() { + struct Model: Codable { + let p: GeoPoint + } + let model = Model(p: GeoPoint(latitude: 1, longitude: -2)) + let dict = ["p": GeoPoint(latitude: 1, longitude: -2)] + XCTAssertEqual(model.p, roundTrip(input: model, expected: dict).p) + } + + func testDate() { + struct Model: Codable { + let date: Date + } + let d = Date(timeIntervalSinceReferenceDate: 0) + let model = Model(date: d) + let dict = ["date": d] + XCTAssertEqual(model.date, roundTrip(input: model, expected: dict).date) + } + + func testDocumentReference() { + struct Model: Codable { + let doc: DocumentReference + } + let d = FSTTestDocRef("abc/xyz") + let model = Model(doc: d) + let dict = ["doc": d] + XCTAssertEqual(model.doc, roundTrip(input: model, expected: dict).doc) + } + + func testTimestamp() { + struct Model: Codable { + let timestamp: Timestamp + } + let t = Timestamp(date: Date()) + let model = Model(timestamp: t) + let encoded = (try! Firestore.Encoder().encode(model)) + let model2 = try! Firestore.Decoder().decode(Model.self, from: encoded) + XCTAssertEqual(model.timestamp, model2.timestamp) + } + + func testBadValue() { + struct Model: Codable { + let x: Int + } + let dict = ["x": "abc"] + var didThrow = false + do { + _ = try Firestore.Decoder().decode(Model.self, from: dict) + } catch { + didThrow = true + } + XCTAssertTrue(didThrow) + } + + func testValueTooBig() { + struct Model: Codable { + let x: CChar + } + let dict = ["x": 12345] + let model = try? Firestore.Decoder().decode(Model.self, from: dict) + XCTAssertNil(model) + + let dict2 = ["x": 12] + let model2 = try? Firestore.Decoder().decode(Model.self, from: dict2) + XCTAssertNotNil(model2) + } + + // Inspired by https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java + func testBeans() { + struct Model: Codable { + let s: String + let d: Double + let f: Float + let l: CLongLong + let i: Int + let b: Bool + let sh: CShort + let byte: CChar + let uchar: CUnsignedChar + let ai: [Int] + let si: [String] + let caseSensitive: String + let casESensitive: String + let casESensitivE: String + } + let model = Model( + s: "abc", + d: 123, + f: -4.321, + l: 1_234_567_890_123, + i: -4444, + b: false, + sh: 123, + byte: 45, + uchar: 44, + ai: [1, 2, 3, 4], + si: ["abc", "def"], + caseSensitive: "aaa", + casESensitive: "bbb", + casESensitivE: "ccc" + ) + let dict = [ + "s": "abc", + "d": 123, + "f": -4.321, + "l": 1_234_567_890_123, + "i": -4444, + "b": false, + "sh": 123, + "byte": 45, + "uchar": 44, + "ai": [1, 2, 3, 4], + "si": ["abc", "def"], + "caseSensitive": "aaa", + "casESensitive": "bbb", + "casESensitivE": "ccc", + ] as [String: Any] + + let model2 = try! Firestore.Decoder().decode(Model.self, from: dict) + XCTAssertEqual(model.s, model2.s) + XCTAssertEqual(model.d, model2.d) + XCTAssertEqual(model.f, model2.f) + XCTAssertEqual(model.l, model2.l) + XCTAssertEqual(model.i, model2.i) + XCTAssertEqual(model.b, model2.b) + XCTAssertEqual(model.sh, model2.sh) + XCTAssertEqual(model.byte, model2.byte) + XCTAssertEqual(model.uchar, model2.uchar) + XCTAssertEqual(model.ai, model2.ai) + XCTAssertEqual(model.si, model2.si) + XCTAssertEqual(model.caseSensitive, model2.caseSensitive) + XCTAssertEqual(model.casESensitive, model2.casESensitive) + XCTAssertEqual(model.casESensitivE, model2.casESensitivE) + + let encodedDict = try! Firestore.Encoder().encode(model) + XCTAssertEqual(encodedDict["s"] as! String, "abc") + XCTAssertEqual(encodedDict["d"] as! Double, 123) + XCTAssertEqual(encodedDict["f"] as! Float, -4.321) + XCTAssertEqual(encodedDict["l"] as! CLongLong, 1_234_567_890_123) + XCTAssertEqual(encodedDict["i"] as! Int, -4444) + XCTAssertEqual(encodedDict["b"] as! Bool, false) + XCTAssertEqual(encodedDict["sh"] as! CShort, 123) + XCTAssertEqual(encodedDict["byte"] as! CChar, 45) + XCTAssertEqual(encodedDict["uchar"] as! CUnsignedChar, 44) + XCTAssertEqual(encodedDict["ai"] as! [Int], [1, 2, 3, 4]) + XCTAssertEqual(encodedDict["si"] as! [String], ["abc", "def"]) + XCTAssertEqual(encodedDict["caseSensitive"] as! String, "aaa") + XCTAssertEqual(encodedDict["casESensitive"] as! String, "bbb") + XCTAssertEqual(encodedDict["casESensitivE"] as! String, "ccc") + } + + func testCodingKeys() { + struct Model: Codable { + var s: String + var ms: String + var d: Double + var md: Double + var i: Int + var mi: Int + var b: Bool + var mb: Bool + + enum CodingKeys: String, CodingKey { + case s + case d + case i + case b + } + + public init(from decoder: Decoder) throws { + let values = try decoder.container(keyedBy: CodingKeys.self) + s = try values.decode(String.self, forKey: .s) + d = try values.decode(Double.self, forKey: .d) + i = try values.decode(Int.self, forKey: .i) + b = try values.decode(Bool.self, forKey: .b) + ms = "filler" + md = 42.42 + mi = -9 + mb = false + } + + public init(ins: String, inms: String, ind: Double, inmd: Double, ini: Int, inmi: Int, inb: Bool, inmb: Bool) { + s = ins + d = ind + i = ini + b = inb + ms = inms + md = inmd + mi = inmi + mb = inmb + } + } + let model = Model( + ins: "abc", + inms: "dummy", + ind: 123.3, + inmd: 0, + ini: -4444, + inmi: 0, + inb: true, + inmb: true + ) + let dict = [ + "s": "abc", + "d": 123.3, + "i": -4444, + "b": true, + ] as [String: Any] + + let model2 = try! Firestore.Decoder().decode(Model.self, from: dict) + XCTAssertEqual(model.s, model2.s) + XCTAssertEqual(model.d, model2.d) + XCTAssertEqual(model.i, model2.i) + XCTAssertEqual(model.b, model2.b) + XCTAssertEqual(model2.ms, "filler") + XCTAssertEqual(model2.md, 42.42) + XCTAssertEqual(model2.mi, -9) + XCTAssertEqual(model2.mb, false) + + let encodedDict = try! Firestore.Encoder().encode(model) + XCTAssertEqual(encodedDict["s"] as! String, "abc") + XCTAssertEqual(encodedDict["d"] as! Double, 123.3) + XCTAssertEqual(encodedDict["i"] as! Int, -4444) + XCTAssertEqual(encodedDict["b"] as! Bool, true) + XCTAssertNil(encodedDict["ms"]) + XCTAssertNil(encodedDict["md"]) + XCTAssertNil(encodedDict["mi"]) + XCTAssertNil(encodedDict["mb"]) + } +} From e8fbb2ad2bcdec3ca313d147a367b08370bde22e Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 3 Jan 2019 11:26:03 -0800 Subject: [PATCH 02/24] Fix bad merge --- .../FDLBuilderTestAppObjCEarlGrey.xcscheme | 42 +------------------ 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme index 22feddbc9f9..af5583c4a78 100644 --- a/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme +++ b/Example/Firebase.xcodeproj/xcshareddata/xcschemes/FDLBuilderTestAppObjCEarlGrey.xcscheme @@ -7,11 +7,8 @@ buildImplicitDependencies = "YES"> + buildForTesting = "YES"> - - - - - - - - @@ -71,15 +49,6 @@ debugDocumentVersioning = "YES" debugServiceExtension = "internal" allowLocationSimulation = "YES"> - - - - @@ -89,15 +58,6 @@ savedToolIdentifier = "" useCustomWorkingDirectory = "NO" debugDocumentVersioning = "YES"> - - - - From 35365ac3e4d01421ddec5c5e6de17b3d3246ec9f Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 18 Jan 2019 17:31:35 -0800 Subject: [PATCH 03/24] Remove throws from decodable APIs --- .../Source/Codable/FirestoreDecoder.swift | 4 ++- .../Source/Codable/FirestoreEncoder.swift | 34 ++++++++++++------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Firestore/Swift/Source/Codable/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/FirestoreDecoder.swift index 095efddac54..c32d373df7d 100644 --- a/Firestore/Swift/Source/Codable/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/FirestoreDecoder.swift @@ -22,7 +22,9 @@ import FirebaseFirestore extension DocumentSnapshot { public func data(as type: T.Type) throws -> T { guard let dict = data() else { - throw DecodingError.valueNotFound(T.self, DecodingError.Context(codingPath: [], debugDescription: "Data was empty")) + throw DecodingError.valueNotFound(T.self, + DecodingError.Context(codingPath: [], + debugDescription: "Data was empty")) } return try Firestore.Decoder().decode(T.self, from: dict) } diff --git a/Firestore/Swift/Source/Codable/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/FirestoreEncoder.swift index eba68545af8..a472691a011 100644 --- a/Firestore/Swift/Source/Codable/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/FirestoreEncoder.swift @@ -18,59 +18,69 @@ import Foundation import FirebaseFirestore extension CollectionReference { - public func addDocument(_ item: T) throws -> DocumentReference { + public func addDocument(_ item: T) -> DocumentReference { do { return addDocument(data: try Firestore.Encoder().encode(item)) } catch let error { - throw (error) + Firestore.firestore().settings.dispatchQueue.sync { + fatalError("Encoding error \(error)") + } } } - public func addDocument(_ item: T, _ completion: ((Error?) -> Void)?) throws -> DocumentReference { + public func addDocument(_ item: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { do { let encoded = try Firestore.Encoder().encode(item) return addDocument(data: encoded, completion: completion) } catch let error { - throw (error) + Firestore.firestore().settings.dispatchQueue.sync { + fatalError("Encoding error \(error)") + } } } } extension DocumentReference { - public func setData(value: T) throws { + public func setData(_ value: T) { do { setData(try Firestore.Encoder().encode(value)) } catch let error { - throw (error) + fatalError("TODO \(error)") } } - public func setData(value: T, _ completion: ((Error?) -> Void)?) throws { + public func setData(_ value: T, _ completion: ((Error?) -> Void)?) { do { let encoded = try Firestore.Encoder().encode(value) setData(encoded, completion: completion) } catch let error { - throw error + Firestore.firestore().settings.dispatchQueue.sync { + fatalError("Encoding error \(error)") + } } } } extension Transaction { - public func setData(value: T, forDocument: DocumentReference) throws { + public func setData(_ value: T, forDocument: DocumentReference) { do { setData(try Firestore.Encoder().encode(value), forDocument: forDocument) } catch let error { - throw (error) + Firestore.firestore().settings.dispatchQueue.sync { + fatalError("Encoding error \(error)") + } } } } extension WriteBatch { - public func setData(value: T, forDocument: DocumentReference) throws { + public func setData(_ value: T, forDocument: DocumentReference) { do { setData(try Firestore.Encoder().encode(value), forDocument: forDocument) } catch let error { - throw (error) + Firestore.firestore().settings.dispatchQueue.sync { + fatalError("Encoding error \(error)") + } } } } From 1533b288cd472b244eedfe5c52a458c31e3a8187 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 23 Jan 2019 11:10:10 -0800 Subject: [PATCH 04/24] Address review feedback --- .../Codable/DocumentReference+Codable.swift | 8 +-- .../Source/Codable/FieldValue+Codable.swift | 8 +-- .../Source/Codable/GeoPoint+Codable.swift | 8 +-- .../Source/Codable/Timestamp+Codable.swift | 8 +-- .../{ => third_party}/FirestoreDecoder.swift | 60 +++++-------------- .../{ => third_party}/FirestoreEncoder.swift | 50 +++++++--------- 6 files changed, 53 insertions(+), 89 deletions(-) rename Firestore/Swift/Source/Codable/{ => third_party}/FirestoreDecoder.swift (95%) rename Firestore/Swift/Source/Codable/{ => third_party}/FirestoreEncoder.swift (94%) diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index 395e5ba6b89..4e7f02f1051 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -19,10 +19,10 @@ import FirebaseFirestore /** * A protocol describing the encodable properties of a DocumentReference. * - * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class was - * extended directly to conform to Codable, the methods implementing the protcol would be need to be - * marked required but that can't be done in an extension. Declaring the extension on the protocol - * sidesteps this issue. + * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class + * was extended directly to conform to Codable, the methods implementing the protocol would be need + * to be marked required but that can't be done in an extension. Declaring the extension on the + * protocol sidesteps this issue. */ fileprivate protocol CodableDocumentReference: Codable {} diff --git a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift index 9f72c94f16a..57c19cac510 100644 --- a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift +++ b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift @@ -19,10 +19,10 @@ import FirebaseFirestore /** * A protocol describing the encodable properties of a FirebaseFirestore. * - * Note: this protocol exists as a workaround for the Swift compiler: if the FieldValue class was - * extended directly to conform to Codable, the methods implementing the protcol would be need to be - * marked required but that can't be done in an extension. Declaring the extension on the protocol - * sidesteps this issue. + * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class + * was extended directly to conform to Codable, the methods implementing the protocol would be need + * to be marked required but that can't be done in an extension. Declaring the extension on the + * protocol sidesteps this issue. */ fileprivate protocol CodableFieldValue: Encodable {} diff --git a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift index fa563403c8d..2e3f64200a2 100644 --- a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift +++ b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift @@ -19,10 +19,10 @@ import FirebaseFirestore /** * A protocol describing the encodable properties of a GeoPoint. * - * Note: this protocol exists as a workaround for the Swift compiler: if the GeoPoint class was - * extended directly to conform to Codable, the methods implementing the protcol would be need to be - * marked required but that can't be done in an extension. Declaring the extension on the protocol - * sidesteps this issue. + * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class + * was extended directly to conform to Codable, the methods implementing the protocol would be need + * to be marked required but that can't be done in an extension. Declaring the extension on the + * protocol sidesteps this issue. */ fileprivate protocol CodableGeoPoint: Codable { var latitude: Double { get } diff --git a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift index 44896c84fde..27d2c308d71 100644 --- a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift +++ b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift @@ -19,10 +19,10 @@ import FirebaseFirestore /** * A protocol describing the encodable properties of a DocumentSnapshot. * - * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentSnapshot class was - * extended directly to conform to Codable, the methods implementing the protcol would be need to be - * marked required but that can't be done in an extension. Declaring the extension on the protocol - * sidesteps this issue. + * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class + * was extended directly to conform to Codable, the methods implementing the protocol would be need + * to be marked required but that can't be done in an extension. Declaring the extension on the + * protocol sidesteps this issue. */ public protocol CodableTimestamp: Codable { init(date: Date) diff --git a/Firestore/Swift/Source/Codable/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift similarity index 95% rename from Firestore/Swift/Source/Codable/FirestoreDecoder.swift rename to Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index c32d373df7d..bf133177cd5 100644 --- a/Firestore/Swift/Source/Codable/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -1,23 +1,22 @@ -/* - * Copyright 2018 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// This file is derived from swift/stdlib/public/SDK/Foundation/JSONEncoder.swift +// and swift/stdlib/public/SDK/Foundation/PlistEncoder.swift + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// import Foundation import FirebaseFirestore -// Add CollectionReference? +// TODO: Consider adding CollectionReference extension DocumentSnapshot { public func data(as type: T.Type) throws -> T { @@ -51,7 +50,6 @@ class _FirestoreDecoder: Decoder { // MARK: Properties /// The decoder's storage. - fileprivate var storage: _FirestoreDecodingStorage /// The path to the current point in encoding. @@ -772,11 +770,6 @@ extension _FirestoreDecoder { } else if number === kCFBooleanFalse as NSNumber { return false } - - /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: - } else if let bool = value as? Bool { - return bool - */ } throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) @@ -948,19 +941,6 @@ extension _FirestoreDecoder { } return Float(double) - - /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: - } else if let double = value as? Double { - if abs(double) <= Double(Float.max) { - return Float(double) - } - overflow = true - } else if let int = value as? Int { - if let float = Float(exactly: int) { - return float - } - overflow = true - */ } throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) @@ -975,16 +955,6 @@ extension _FirestoreDecoder { // * If it was a Float or Double, you will get back the precise value // * If it was Decimal, you will get back the nearest approximation return number.doubleValue - - /* FIXME: If swift-corelibs-foundation doesn't change to use NSNumber, this code path will need to be included and tested: - } else if let double = value as? Double { - return double - } else if let int = value as? Int { - if let double = Double(exactly: int) { - return double - } - overflow = true - */ } throw DecodingError._typeMismatch(at: codingPath, expectation: type, reality: value) diff --git a/Firestore/Swift/Source/Codable/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift similarity index 94% rename from Firestore/Swift/Source/Codable/FirestoreEncoder.swift rename to Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index a472691a011..0523b87c3c6 100644 --- a/Firestore/Swift/Source/Codable/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -1,18 +1,17 @@ -/* - * Copyright 2018 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ +// This file is derived from swift/stdlib/public/SDK/Foundation/JSONEncoder.swift +// and swift/stdlib/public/SDK/Foundation/PlistEncoder.swift + +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2017 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// import Foundation import FirebaseFirestore @@ -22,9 +21,7 @@ extension CollectionReference { do { return addDocument(data: try Firestore.Encoder().encode(item)) } catch let error { - Firestore.firestore().settings.dispatchQueue.sync { - fatalError("Encoding error \(error)") - } + fatalError("Unable to encode data with Firestore encoder: \(error)") } } @@ -34,8 +31,9 @@ extension CollectionReference { return addDocument(data: encoded, completion: completion) } catch let error { Firestore.firestore().settings.dispatchQueue.sync { - fatalError("Encoding error \(error)") + completion!(error) } + return document() // Is there something better to return after the error? } } } @@ -45,7 +43,7 @@ extension DocumentReference { do { setData(try Firestore.Encoder().encode(value)) } catch let error { - fatalError("TODO \(error)") + fatalError("Unable to encode data with Firestore encoder: \(error)") } } @@ -55,7 +53,7 @@ extension DocumentReference { setData(encoded, completion: completion) } catch let error { Firestore.firestore().settings.dispatchQueue.sync { - fatalError("Encoding error \(error)") + completion!(error) } } } @@ -66,9 +64,7 @@ extension Transaction { do { setData(try Firestore.Encoder().encode(value), forDocument: forDocument) } catch let error { - Firestore.firestore().settings.dispatchQueue.sync { - fatalError("Encoding error \(error)") - } + fatalError("Unable to encode data with Firestore encoder: \(error)") } } } @@ -78,9 +74,7 @@ extension WriteBatch { do { setData(try Firestore.Encoder().encode(value), forDocument: forDocument) } catch let error { - Firestore.firestore().settings.dispatchQueue.sync { - fatalError("Encoding error \(error)") - } + fatalError("Unable to encode data with Firestore encoder: \(error)") } } } @@ -94,7 +88,7 @@ extension Firestore { debugDescription: "Top-level \(T.self) did not encode any values.")) } - // This is O(n) check. We might get rid of it once we refactor to internal + // This is O(n) check. Consider refactorking box_ to return [String: Any]. guard let dict = topLevel as? [String: Any] else { throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], From 280bbf666412a1fda022cc77b3077e6b707925d4 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Wed, 23 Jan 2019 15:03:04 -0800 Subject: [PATCH 05/24] Address test review feedback --- .../Source/Codable/FieldValue+Codable.swift | 2 +- .../Source/Codable/GeoPoint+Codable.swift | 2 +- .../Source/Codable/Timestamp+Codable.swift | 2 +- .../third_party/FirestoreEncoder.swift | 2 +- .../Tests/Codable/CodableDocumentTests.swift | 54 ++++++++++--------- 5 files changed, 32 insertions(+), 30 deletions(-) diff --git a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift index 57c19cac510..aac26d242c7 100644 --- a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift +++ b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift @@ -19,7 +19,7 @@ import FirebaseFirestore /** * A protocol describing the encodable properties of a FirebaseFirestore. * - * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class + * Note: this protocol exists as a workaround for the Swift compiler: if the FieldValue class * was extended directly to conform to Codable, the methods implementing the protocol would be need * to be marked required but that can't be done in an extension. Declaring the extension on the * protocol sidesteps this issue. diff --git a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift index 2e3f64200a2..bc2994cf904 100644 --- a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift +++ b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift @@ -19,7 +19,7 @@ import FirebaseFirestore /** * A protocol describing the encodable properties of a GeoPoint. * - * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class + * Note: this protocol exists as a workaround for the Swift compiler: if the GeoPoint class * was extended directly to conform to Codable, the methods implementing the protocol would be need * to be marked required but that can't be done in an extension. Declaring the extension on the * protocol sidesteps this issue. diff --git a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift index 27d2c308d71..0ee26351b48 100644 --- a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift +++ b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift @@ -19,7 +19,7 @@ import FirebaseFirestore /** * A protocol describing the encodable properties of a DocumentSnapshot. * - * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class + * Note: this protocol exists as a workaround for the Swift compiler: if the Timestamp class * was extended directly to conform to Codable, the methods implementing the protocol would be need * to be marked required but that can't be done in an extension. Declaring the extension on the * protocol sidesteps this issue. diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index 0523b87c3c6..775979d9a6e 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -88,7 +88,7 @@ extension Firestore { debugDescription: "Top-level \(T.self) did not encode any values.")) } - // This is O(n) check. Consider refactorking box_ to return [String: Any]. + // This is O(n) check. Consider refactoring box_ to return [String: Any]. guard let dict = topLevel as? [String: Any] else { throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: [], diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index 3f78632e357..f3a9bba0083 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -20,12 +20,12 @@ import Foundation import XCTest class CodableDocumentTests: XCTestCase { - func roundTrip(input: X, expected: [String: Any], doTest: Bool = true) -> X where X: Codable { + func roundTrip(input: X, expected: [String: Any]? = nil) -> X where X: Codable { var encoded = [String: Any]() do { encoded = try Firestore.Encoder().encode(input) - if doTest { - XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) + if expected != nil { + XCTAssertEqual(encoded as NSDictionary, expected! as NSDictionary) } } catch { XCTFail("Failed to encode \(X.self): error: \(error)") @@ -51,8 +51,9 @@ class CodableDocumentTests: XCTestCase { func testEmpty() { struct Model: Codable {} let model = Model() - let dict = [String: Any]() - XCTAssertEqual((try Firestore.Encoder().encode(model)) as NSDictionary, dict as NSDictionary) + let dict = [String: Any]() as NSDictionary + let encoded = try! Firestore.Encoder().encode(model) as NSDictionary + XCTAssertEqual(encoded, dict) } func testNil() { @@ -86,16 +87,19 @@ class CodableDocumentTests: XCTestCase { } func testOptionalTimestamp() { - class FirestoreDummy { - /// Partial keypath can represent the property name - func setObject(_ object: T, fieldValues: [PartialKeyPath: FieldValue] = [:]) { - // Encode, check if any timestamps are nil or not, and if so use FieldValue.serverTimestamp() - } - } struct Model: Codable { let value: Int let timestamp: Timestamp? } + class FirestoreDummy { + var visited = 0 + func setObject(_ object: T, fieldValues: [PartialKeyPath: FieldValue] = [:]) { + let obj = object as! Model + XCTAssertEqual(obj.value, 10) + XCTAssertNil(obj.timestamp) + visited += 1 + } + } let c = Model(value: 10, timestamp: nil) let fs = FirestoreDummy() // If no custom field values need to be set: @@ -104,6 +108,7 @@ class CodableDocumentTests: XCTestCase { // Or, overriding custom field values: fs.setObject(c, fieldValues: [\Model.timestamp: FieldValue.serverTimestamp(), \Model.value: FieldValue.delete()]) + XCTAssert(fs.visited == 2) } func testEnum() { @@ -156,17 +161,17 @@ class CodableDocumentTests: XCTestCase { let e: MyEnum } let model = Model(x: 42, e: MyEnum.num(number: 4)) - let output = roundTrip(input: model, expected: [:], doTest: false) + let output = roundTrip(input: model) XCTAssertEqual(model.x, output.x) XCTAssertEqual(model.e, output.e) let model2 = Model(x: 43, e: MyEnum.text("abc")) - let output2 = roundTrip(input: model2, expected: [:], doTest: false) + let output2 = roundTrip(input: model2) XCTAssertEqual(model2.x, output2.x) XCTAssertEqual(model2.e, output2.e) let model3 = Model(x: 43, e: MyEnum.timestamp(Timestamp(date: Date()))) - let output3 = roundTrip(input: model3, expected: [:], doTest: false) + let output3 = roundTrip(input: model3) XCTAssertEqual(model3.x, output3.x) XCTAssertEqual(model3.e, output3.e) } @@ -206,7 +211,7 @@ class CodableDocumentTests: XCTestCase { } let t = Timestamp(date: Date()) let model = Model(timestamp: t) - let encoded = (try! Firestore.Encoder().encode(model)) + let encoded = try! Firestore.Encoder().encode(model) let model2 = try! Firestore.Decoder().decode(Model.self, from: encoded) XCTAssertEqual(model.timestamp, model2.timestamp) } @@ -215,23 +220,16 @@ class CodableDocumentTests: XCTestCase { struct Model: Codable { let x: Int } - let dict = ["x": "abc"] - var didThrow = false - do { - _ = try Firestore.Decoder().decode(Model.self, from: dict) - } catch { - didThrow = true - } - XCTAssertTrue(didThrow) + let dict = ["x": "abc"] // Wrong type; + XCTAssertThrowsError(try Firestore.Decoder().decode(Model.self, from: dict)) } func testValueTooBig() { struct Model: Codable { let x: CChar } - let dict = ["x": 12345] - let model = try? Firestore.Decoder().decode(Model.self, from: dict) - XCTAssertNil(model) + let dict = ["x": 12345] // Overflow + XCTAssertThrowsError(try Firestore.Decoder().decode(Model.self, from: dict)) let dict2 = ["x": 12] let model2 = try? Firestore.Decoder().decode(Model.self, from: dict2) @@ -320,6 +318,9 @@ class CodableDocumentTests: XCTestCase { XCTAssertEqual(encodedDict["caseSensitive"] as! String, "aaa") XCTAssertEqual(encodedDict["casESensitive"] as! String, "bbb") XCTAssertEqual(encodedDict["casESensitivE"] as! String, "ccc") + + let model3 = try? Firestore.Decoder().decode(Model.self, from: encodedDict as [String: Any]) + XCTAssertNotNil(model3) } func testCodingKeys() { @@ -333,6 +334,7 @@ class CodableDocumentTests: XCTestCase { var b: Bool var mb: Bool + // Use CodingKeys to only encode part of the struct. enum CodingKeys: String, CodingKey { case s case d From ee349c29949184e4f0eba9d3f65e8703d76a2ac2 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 24 Jan 2019 07:47:48 -0800 Subject: [PATCH 06/24] Remove force unwrap --- Firestore/Swift/Tests/Codable/CodableDocumentTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index f3a9bba0083..4a2d508d1d1 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -24,8 +24,8 @@ class CodableDocumentTests: XCTestCase { var encoded = [String: Any]() do { encoded = try Firestore.Encoder().encode(input) - if expected != nil { - XCTAssertEqual(encoded as NSDictionary, expected! as NSDictionary) + if let expected = expected { + XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) } } catch { XCTFail("Failed to encode \(X.self): error: \(error)") From a858b9a4d93564a7da2913299b2dc29827a62297 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Thu, 24 Jan 2019 16:47:27 -0800 Subject: [PATCH 07/24] Codable Timestamp --- .../Firestore.xcodeproj/project.pbxproj | 5 ++- .../Source/Codable/Timestamp+Codable.swift | 35 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/Firestore/Example/Firestore.xcodeproj/project.pbxproj b/Firestore/Example/Firestore.xcodeproj/project.pbxproj index f1a0030be46..05d948fcbeb 100644 --- a/Firestore/Example/Firestore.xcodeproj/project.pbxproj +++ b/Firestore/Example/Firestore.xcodeproj/project.pbxproj @@ -236,6 +236,7 @@ DE2EF0881F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m in Sources */ = {isa = PBXBuildFile; fileRef = DE2EF0841F3D0B6E003D0CDC /* FSTTreeSortedDictionaryTests.m */; }; DE84BBB421C18F060048A176 /* FSTAPIHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E04E202154AA00B64F25 /* FSTAPIHelpers.mm */; }; DE84BBB921C1A6ED0048A176 /* FSTHelpers.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5492E03A2021401F00B64F25 /* FSTHelpers.mm */; }; + DE958C0521FA82D200E6C1C5 /* CodableTimestampTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE958C0421FA82D200E6C1C5 /* CodableTimestampTests.swift */; }; EBFC611B1BF195D0EC710AF4 /* app_testing.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5467FB07203E6A44009C9584 /* app_testing.mm */; }; /* End PBXBuildFile section */ @@ -549,7 +550,6 @@ DE03B3621F215E1600A30B9C /* CAcert.pem */ = {isa = PBXFileReference; lastKnownFileType = text; path = CAcert.pem; sourceTree = ""; }; DE0761F61F2FE68D003233AF /* BasicCompileTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BasicCompileTests.swift; sourceTree = ""; }; DE26C61521C15FCC00DE141A /* CodableDocumentTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableDocumentTests.swift; sourceTree = ""; }; - DE2EF07E1F3D0B6E003D0CDC /* FSTArraySortedDictionaryTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FSTArraySortedDictionaryTests.m; path = ../../third_party/Immutable/Tests/FSTArraySortedDictionaryTests.m; sourceTree = ""; }; DE2EF07F1F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "FSTImmutableSortedDictionary+Testing.h"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.h"; sourceTree = ""; }; DE2EF0801F3D0B6E003D0CDC /* FSTImmutableSortedDictionary+Testing.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = "FSTImmutableSortedDictionary+Testing.m"; path = "../../third_party/Immutable/Tests/FSTImmutableSortedDictionary+Testing.m"; sourceTree = ""; }; @@ -563,6 +563,7 @@ DE51B19A1F0D48AC0013853F /* FSTSyncEngineTestDriver.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FSTSyncEngineTestDriver.h; sourceTree = ""; }; DE51B1A71F0D48AC0013853F /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; DE84BBB821C1951E0048A176 /* BridgingHeader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BridgingHeader.h; sourceTree = ""; }; + DE958C0421FA82D200E6C1C5 /* CodableTimestampTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CodableTimestampTests.swift; sourceTree = ""; }; E592181BFD7C53C305123739 /* Pods-Firestore_Tests_iOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Firestore_Tests_iOS.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Firestore_Tests_iOS/Pods-Firestore_Tests_iOS.debug.xcconfig"; sourceTree = ""; }; ECEBABC7E7B693BE808A1052 /* Pods_Firestore_IntegrationTests_iOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Firestore_IntegrationTests_iOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; ED4B3E3EA0EBF3ED19A07060 /* grpc_stream_tester.h */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.c.h; path = grpc_stream_tester.h; sourceTree = ""; }; @@ -765,6 +766,7 @@ children = ( DE26C61521C15FCC00DE141A /* CodableDocumentTests.swift */, 5495EB022040E90200EBA509 /* CodableGeoPointTests.swift */, + DE958C0421FA82D200E6C1C5 /* CodableTimestampTests.swift */, ); path = Codable; sourceTree = ""; @@ -1845,6 +1847,7 @@ 5495EB032040E90200EBA509 /* CodableGeoPointTests.swift in Sources */, DE84BBB421C18F060048A176 /* FSTAPIHelpers.mm in Sources */, DE26C61621C15FCC00DE141A /* CodableDocumentTests.swift in Sources */, + DE958C0521FA82D200E6C1C5 /* CodableTimestampTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift index 0ee26351b48..f9e7941e586 100644 --- a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift +++ b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift @@ -17,29 +17,46 @@ import FirebaseFirestore /** - * A protocol describing the encodable properties of a DocumentSnapshot. + * A protocol describing the encodable properties of a Timestamp. * * Note: this protocol exists as a workaround for the Swift compiler: if the Timestamp class * was extended directly to conform to Codable, the methods implementing the protocol would be need * to be marked required but that can't be done in an extension. Declaring the extension on the * protocol sidesteps this issue. */ -public protocol CodableTimestamp: Codable { - init(date: Date) - func dateValue() -> Date +fileprivate protocol CodableTimestamp: Codable { + var seconds: Int64 { get } + var nanoseconds: Int32 { get } + + init(seconds: Int64, nanoseconds: Int32) +} + +/** The keys in a GeoPoint. Must match the properties of CodableGeoPoint. */ +fileprivate enum TimestampKeys: String, CodingKey { + case seconds + case nanoseconds } +/** + * An extension of GeoPoint that implements the behavior of the Codable protocol. + * + * Note: this is implemented manually here because the Swift compiler can't synthesize these methods + * when declaring an extension to conform to Codable. + */ extension CodableTimestamp { - var date: Date { return dateValue() } public init(from decoder: Decoder) throws { - let container = try decoder.singleValueContainer() - self.init(date: try container.decode(Date.self)) + let container = try decoder.container(keyedBy: TimestampKeys.self) + let seconds = try container.decode(Int64.self, forKey: .seconds) + let nanoseconds = try container.decode(Int32.self, forKey: .nanoseconds) + self.init(seconds: seconds, nanoseconds: nanoseconds) } public func encode(to encoder: Encoder) throws { - var container = encoder.singleValueContainer() - try container.encode(dateValue()) + var container = encoder.container(keyedBy: TimestampKeys.self) + try container.encode(seconds, forKey: .seconds) + try container.encode(nanoseconds, forKey: .nanoseconds) } } +/** Extends GeoPoint to conform to Codable. */ extension Timestamp: CodableTimestamp {} From d79806b7c5e265f14d395bcbcba1ba62eb6020a4 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 25 Jan 2019 15:41:36 -0800 Subject: [PATCH 08/24] Extension to separate files, Timestamp pass-through, match param labels, 2019 --- .../Swift/Source/Codable/CodableErrors.swift | 2 +- .../Codable/CollectionReference+Codable.swift | 36 ++++++++++ .../Codable/DocumentReference+Codable.swift | 21 +++++- .../Codable/DocumentSnapshot+Codable.swift | 28 ++++++++ .../Source/Codable/FieldValue+Codable.swift | 2 +- .../Source/Codable/GeoPoint+Codable.swift | 2 +- .../Source/Codable/Timestamp+Codable.swift | 2 +- .../Source/Codable/Transaction+Codable.swift | 27 ++++++++ .../Source/Codable/WriteBatch+Codable.swift | 27 ++++++++ .../third_party/FirestoreDecoder.swift | 15 ---- .../third_party/FirestoreEncoder.swift | 69 ++----------------- .../Tests/Codable/CodableDocumentTests.swift | 2 +- .../Tests/Codable/CodableGeoPointTests.swift | 2 +- .../Tests/Codable/CodableTimestampTests.swift | 49 +++++++++++++ 14 files changed, 196 insertions(+), 88 deletions(-) create mode 100644 Firestore/Swift/Source/Codable/CollectionReference+Codable.swift create mode 100644 Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift create mode 100644 Firestore/Swift/Source/Codable/Transaction+Codable.swift create mode 100644 Firestore/Swift/Source/Codable/WriteBatch+Codable.swift create mode 100644 Firestore/Swift/Tests/Codable/CodableTimestampTests.swift diff --git a/Firestore/Swift/Source/Codable/CodableErrors.swift b/Firestore/Swift/Source/Codable/CodableErrors.swift index 63d76622d86..ae231a6310c 100644 --- a/Firestore/Swift/Source/Codable/CodableErrors.swift +++ b/Firestore/Swift/Source/Codable/CodableErrors.swift @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift new file mode 100644 index 00000000000..d186cea3e2a --- /dev/null +++ b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift @@ -0,0 +1,36 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore + +extension CollectionReference { + public func addDocument(data: T) -> DocumentReference { + do { + return addDocument(data: try Firestore.Encoder().encode(data)) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } + } + + public func addDocument(data: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { + do { + let encoded = try Firestore.Encoder().encode(data) + return addDocument(data: encoded, completion: completion) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } + } +} diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index 4e7f02f1051..c7488d58439 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,4 +36,21 @@ extension CodableDocumentReference { } } -extension DocumentReference: CodableDocumentReference {} +extension DocumentReference: CodableDocumentReference { + public func setData(_ value: T) { + do { + setData(try Firestore.Encoder().encode(value)) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } + } + + public func setData(_ value: T, _ completion: ((Error?) -> Void)?) { + do { + let encoded = try Firestore.Encoder().encode(value) + setData(encoded, completion: completion) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } + } +} diff --git a/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift b/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift new file mode 100644 index 00000000000..dce57bf0bb4 --- /dev/null +++ b/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift @@ -0,0 +1,28 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore + +extension DocumentSnapshot { + public func data(as type: T.Type) throws -> T { + guard let dict = data() else { + throw DecodingError.valueNotFound(T.self, + DecodingError.Context(codingPath: [], + debugDescription: "Data was empty")) + } + return try Firestore.Decoder().decode(T.self, from: dict) + } +} diff --git a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift index aac26d242c7..4925209ca26 100644 --- a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift +++ b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift index bc2994cf904..1c422d54a70 100644 --- a/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift +++ b/Firestore/Swift/Source/Codable/GeoPoint+Codable.swift @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift index f9e7941e586..1581caaf14d 100644 --- a/Firestore/Swift/Source/Codable/Timestamp+Codable.swift +++ b/Firestore/Swift/Source/Codable/Timestamp+Codable.swift @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Firestore/Swift/Source/Codable/Transaction+Codable.swift b/Firestore/Swift/Source/Codable/Transaction+Codable.swift new file mode 100644 index 00000000000..cf3df8fbcd9 --- /dev/null +++ b/Firestore/Swift/Source/Codable/Transaction+Codable.swift @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore + +extension Transaction { + public func setData(_ value: T, forDocument: DocumentReference) { + do { + setData(try Firestore.Encoder().encode(value), forDocument: forDocument) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } + } +} diff --git a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift new file mode 100644 index 00000000000..666f4b75473 --- /dev/null +++ b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift @@ -0,0 +1,27 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore + +extension WriteBatch { + public func setData(_ value: T, forDocument: DocumentReference) { + do { + setData(try Firestore.Encoder().encode(value), forDocument: forDocument) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } + } +} diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index bf133177cd5..1c107856b2a 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -16,21 +16,6 @@ import Foundation import FirebaseFirestore -// TODO: Consider adding CollectionReference - -extension DocumentSnapshot { - public func data(as type: T.Type) throws -> T { - guard let dict = data() else { - throw DecodingError.valueNotFound(T.self, - DecodingError.Context(codingPath: [], - debugDescription: "Data was empty")) - } - return try Firestore.Decoder().decode(T.self, from: dict) - } -} - -// Is there a better way to do this without the Firestore.Decoder wrapper? - extension Firestore { struct Decoder { func decode(_ type: T.Type, from container: [String: Any]) throws -> T { diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index 775979d9a6e..72210414ad0 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -16,69 +16,6 @@ import Foundation import FirebaseFirestore -extension CollectionReference { - public func addDocument(_ item: T) -> DocumentReference { - do { - return addDocument(data: try Firestore.Encoder().encode(item)) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } - } - - public func addDocument(_ item: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { - do { - let encoded = try Firestore.Encoder().encode(item) - return addDocument(data: encoded, completion: completion) - } catch let error { - Firestore.firestore().settings.dispatchQueue.sync { - completion!(error) - } - return document() // Is there something better to return after the error? - } - } -} - -extension DocumentReference { - public func setData(_ value: T) { - do { - setData(try Firestore.Encoder().encode(value)) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } - } - - public func setData(_ value: T, _ completion: ((Error?) -> Void)?) { - do { - let encoded = try Firestore.Encoder().encode(value) - setData(encoded, completion: completion) - } catch let error { - Firestore.firestore().settings.dispatchQueue.sync { - completion!(error) - } - } - } -} - -extension Transaction { - public func setData(_ value: T, forDocument: DocumentReference) { - do { - setData(try Firestore.Encoder().encode(value), forDocument: forDocument) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } - } -} - -extension WriteBatch { - public func setData(_ value: T, forDocument: DocumentReference) { - do { - setData(try Firestore.Encoder().encode(value), forDocument: forDocument) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } - } -} - extension Firestore { struct Encoder { func encode(_ value: T) throws -> [String: Any] { @@ -94,7 +31,6 @@ extension Firestore { EncodingError.Context(codingPath: [], debugDescription: "Top-level \(T.self) encoded not as dictionary.")) } - return dict } } @@ -408,7 +344,10 @@ extension _FirestoreEncoder { return box((value as! URL).absoluteString) } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { return (value as! NSDecimalNumber) - } else if T.self == GeoPoint.self || T.self == DocumentReference.self || T.self == FieldValue.self { + } else if T.self == GeoPoint.self || + T.self == DocumentReference.self || + T.self == FieldValue.self || + T.self == Timestamp.self { // These are all native _Firestore types that we don't need to Encode return (value as! NSObject) } diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index 4a2d508d1d1..0a7fb929f16 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Firestore/Swift/Tests/Codable/CodableGeoPointTests.swift b/Firestore/Swift/Tests/Codable/CodableGeoPointTests.swift index 6b1dce432a0..51a99d854ea 100644 --- a/Firestore/Swift/Tests/Codable/CodableGeoPointTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableGeoPointTests.swift @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/Firestore/Swift/Tests/Codable/CodableTimestampTests.swift b/Firestore/Swift/Tests/Codable/CodableTimestampTests.swift new file mode 100644 index 00000000000..f59546e26a4 --- /dev/null +++ b/Firestore/Swift/Tests/Codable/CodableTimestampTests.swift @@ -0,0 +1,49 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import FirebaseFirestore +import FirebaseFirestoreSwift +import Foundation +import XCTest + +class CodableTimestampTests: XCTestCase { + func testTimestampEncodes() { + let timestamp = Timestamp(seconds: 37, nanoseconds: 123) + + let jsonData = try! JSONEncoder().encode(timestamp) + let json = String(data: jsonData, encoding: .utf8)! + + // The ordering of attributes in the JSON output is not guaranteed, nor is the rounding of + // the values so just verify that each required property is present and that the value + // starts as expected. + XCTAssert(json.contains("\"seconds\":37")) + XCTAssert(json.contains("\"nanoseconds\":123")) + } + + func testTimestampDecodes() { + let json = """ + { + "seconds": 37, + "nanoseconds": 122 + } + """ + let jsonData: Data = json.data(using: .utf8)! + + let timestamp = try! JSONDecoder().decode(Timestamp.self, from: jsonData) + XCTAssertEqual(37, timestamp.seconds) + XCTAssertEqual(122, timestamp.nanoseconds) + } +} From 162e775e4a5f1defa9e22bd24d056f83f327f4e0 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 25 Jan 2019 16:20:35 -0800 Subject: [PATCH 09/24] Firestore.encode() and Firestore.decode() --- .../Codable/CollectionReference+Codable.swift | 4 +-- .../Codable/DocumentReference+Codable.swift | 4 +-- .../Codable/DocumentSnapshot+Codable.swift | 2 +- .../Source/Codable/Transaction+Codable.swift | 2 +- .../Source/Codable/WriteBatch+Codable.swift | 2 +- .../third_party/FirestoreDecoder.swift | 19 ++++++----- .../third_party/FirestoreEncoder.swift | 26 +++++++------- .../Tests/Codable/CodableDocumentTests.swift | 34 +++++++++---------- 8 files changed, 46 insertions(+), 47 deletions(-) diff --git a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift index d186cea3e2a..ae1155dd646 100644 --- a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift @@ -19,7 +19,7 @@ import FirebaseFirestore extension CollectionReference { public func addDocument(data: T) -> DocumentReference { do { - return addDocument(data: try Firestore.Encoder().encode(data)) + return addDocument(data: try Firestore.encode(data)) } catch let error { fatalError("Unable to encode data with Firestore encoder: \(error)") } @@ -27,7 +27,7 @@ extension CollectionReference { public func addDocument(data: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { do { - let encoded = try Firestore.Encoder().encode(data) + let encoded = try Firestore.encode(data) return addDocument(data: encoded, completion: completion) } catch let error { fatalError("Unable to encode data with Firestore encoder: \(error)") diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index c7488d58439..4520f5dd872 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -39,7 +39,7 @@ extension CodableDocumentReference { extension DocumentReference: CodableDocumentReference { public func setData(_ value: T) { do { - setData(try Firestore.Encoder().encode(value)) + setData(try Firestore.encode(value)) } catch let error { fatalError("Unable to encode data with Firestore encoder: \(error)") } @@ -47,7 +47,7 @@ extension DocumentReference: CodableDocumentReference { public func setData(_ value: T, _ completion: ((Error?) -> Void)?) { do { - let encoded = try Firestore.Encoder().encode(value) + let encoded = try Firestore.encode(value) setData(encoded, completion: completion) } catch let error { fatalError("Unable to encode data with Firestore encoder: \(error)") diff --git a/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift b/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift index dce57bf0bb4..7aab2ce89d0 100644 --- a/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift @@ -23,6 +23,6 @@ extension DocumentSnapshot { DecodingError.Context(codingPath: [], debugDescription: "Data was empty")) } - return try Firestore.Decoder().decode(T.self, from: dict) + return try Firestore.decode(T.self, from: dict) } } diff --git a/Firestore/Swift/Source/Codable/Transaction+Codable.swift b/Firestore/Swift/Source/Codable/Transaction+Codable.swift index cf3df8fbcd9..8fecdd7c54a 100644 --- a/Firestore/Swift/Source/Codable/Transaction+Codable.swift +++ b/Firestore/Swift/Source/Codable/Transaction+Codable.swift @@ -19,7 +19,7 @@ import FirebaseFirestore extension Transaction { public func setData(_ value: T, forDocument: DocumentReference) { do { - setData(try Firestore.Encoder().encode(value), forDocument: forDocument) + setData(try Firestore.encode(value), forDocument: forDocument) } catch let error { fatalError("Unable to encode data with Firestore encoder: \(error)") } diff --git a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift index 666f4b75473..0bd547117f8 100644 --- a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift +++ b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift @@ -19,7 +19,7 @@ import FirebaseFirestore extension WriteBatch { public func setData(_ value: T, forDocument: DocumentReference) { do { - setData(try Firestore.Encoder().encode(value), forDocument: forDocument) + setData(try Firestore.encode(value), forDocument: forDocument) } catch let error { fatalError("Unable to encode data with Firestore encoder: \(error)") } diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index 1c107856b2a..5fecb815f86 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -17,15 +17,13 @@ import Foundation import FirebaseFirestore extension Firestore { - struct Decoder { - func decode(_ type: T.Type, from container: [String: Any]) throws -> T { - let decoder = _FirestoreDecoder(referencing: container) - 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")) - } - - return value + public static func decode(_ type: T.Type, from container: [String: Any]) throws -> T { + let decoder = _FirestoreDecoder(referencing: container) + 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")) } + + return value } } @@ -1005,7 +1003,10 @@ extension _FirestoreDecoder { } 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 T.self == GeoPoint.self || T.self == DocumentReference.self { + } else if T.self == GeoPoint.self || + T.self == DocumentReference.self || + T.self == FieldValue.self || + T.self == Timestamp.self { // All the native types that should not be encoded decoded = value as! T } else { diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index 72210414ad0..c4154502a3a 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -17,22 +17,20 @@ import Foundation import FirebaseFirestore extension Firestore { - struct Encoder { - func encode(_ value: T) throws -> [String: Any] { - guard let topLevel = try _FirestoreEncoder().box_(value) else { - throw EncodingError.invalidValue(value, - EncodingError.Context(codingPath: [], - debugDescription: "Top-level \(T.self) did not encode any values.")) - } + public static func encode(_ value: T) throws -> [String: Any] { + guard let topLevel = try _FirestoreEncoder().box_(value) else { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-level \(T.self) did not encode any values.")) + } - // This is O(n) check. Consider refactoring box_ to return [String: Any]. - guard let dict = topLevel as? [String: Any] else { - throw EncodingError.invalidValue(value, - EncodingError.Context(codingPath: [], - debugDescription: "Top-level \(T.self) encoded not as dictionary.")) - } - return dict + // This is O(n) check. Consider refactoring box_ to return [String: Any]. + guard let dict = topLevel as? [String: Any] else { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-level \(T.self) encoded not as dictionary.")) } + return dict } } diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index 0a7fb929f16..eb2a3c0c4d7 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -23,7 +23,7 @@ class CodableDocumentTests: XCTestCase { func roundTrip(input: X, expected: [String: Any]? = nil) -> X where X: Codable { var encoded = [String: Any]() do { - encoded = try Firestore.Encoder().encode(input) + encoded = try Firestore.encode(input) if let expected = expected { XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) } @@ -31,7 +31,7 @@ class CodableDocumentTests: XCTestCase { XCTFail("Failed to encode \(X.self): error: \(error)") } do { - let decoded = try Firestore.Decoder().decode(X.self, from: encoded) + let decoded = try Firestore.decode(X.self, from: encoded) return decoded } catch { XCTFail("Failed to decode \(X.self): \(error)") @@ -52,7 +52,7 @@ class CodableDocumentTests: XCTestCase { struct Model: Codable {} let model = Model() let dict = [String: Any]() as NSDictionary - let encoded = try! Firestore.Encoder().encode(model) as NSDictionary + let encoded = try! Firestore.encode(model) as NSDictionary XCTAssertEqual(encoded, dict) } @@ -62,9 +62,9 @@ class CodableDocumentTests: XCTestCase { } let model = Model(x: nil) let dict = ["x": nil] as [String: Int?] - let encodedDict = try! Firestore.Encoder().encode(model) + let encodedDict = try! Firestore.encode(model) XCTAssertNil(encodedDict["x"]) - let model2 = try? Firestore.Decoder().decode(Model.self, from: dict as [String: Any]) + let model2 = try? Firestore.decode(Model.self, from: dict as [String: Any]) XCTAssertNil(model2) } @@ -79,9 +79,9 @@ class CodableDocumentTests: XCTestCase { let model2 = Model(x: 42, opt: 7) let expected = ["x": 42, "opt": 7] - let encoded = try! Firestore.Encoder().encode(model2) + let encoded = try! Firestore.encode(model2) XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) - let decoded = try! Firestore.Decoder().decode(Model.self, from: expected) + let decoded = try! Firestore.decode(Model.self, from: expected) XCTAssertEqual(decoded.x, model2.x) XCTAssertEqual(decoded.opt, model2.opt) } @@ -211,8 +211,8 @@ class CodableDocumentTests: XCTestCase { } let t = Timestamp(date: Date()) let model = Model(timestamp: t) - let encoded = try! Firestore.Encoder().encode(model) - let model2 = try! Firestore.Decoder().decode(Model.self, from: encoded) + let encoded = try! Firestore.encode(model) + let model2 = try! Firestore.decode(Model.self, from: encoded) XCTAssertEqual(model.timestamp, model2.timestamp) } @@ -221,7 +221,7 @@ class CodableDocumentTests: XCTestCase { let x: Int } let dict = ["x": "abc"] // Wrong type; - XCTAssertThrowsError(try Firestore.Decoder().decode(Model.self, from: dict)) + XCTAssertThrowsError(try Firestore.decode(Model.self, from: dict)) } func testValueTooBig() { @@ -229,10 +229,10 @@ class CodableDocumentTests: XCTestCase { let x: CChar } let dict = ["x": 12345] // Overflow - XCTAssertThrowsError(try Firestore.Decoder().decode(Model.self, from: dict)) + XCTAssertThrowsError(try Firestore.decode(Model.self, from: dict)) let dict2 = ["x": 12] - let model2 = try? Firestore.Decoder().decode(Model.self, from: dict2) + let model2 = try? Firestore.decode(Model.self, from: dict2) XCTAssertNotNil(model2) } @@ -287,7 +287,7 @@ class CodableDocumentTests: XCTestCase { "casESensitivE": "ccc", ] as [String: Any] - let model2 = try! Firestore.Decoder().decode(Model.self, from: dict) + let model2 = try! Firestore.decode(Model.self, from: dict) XCTAssertEqual(model.s, model2.s) XCTAssertEqual(model.d, model2.d) XCTAssertEqual(model.f, model2.f) @@ -303,7 +303,7 @@ class CodableDocumentTests: XCTestCase { XCTAssertEqual(model.casESensitive, model2.casESensitive) XCTAssertEqual(model.casESensitivE, model2.casESensitivE) - let encodedDict = try! Firestore.Encoder().encode(model) + let encodedDict = try! Firestore.encode(model) XCTAssertEqual(encodedDict["s"] as! String, "abc") XCTAssertEqual(encodedDict["d"] as! Double, 123) XCTAssertEqual(encodedDict["f"] as! Float, -4.321) @@ -319,7 +319,7 @@ class CodableDocumentTests: XCTestCase { XCTAssertEqual(encodedDict["casESensitive"] as! String, "bbb") XCTAssertEqual(encodedDict["casESensitivE"] as! String, "ccc") - let model3 = try? Firestore.Decoder().decode(Model.self, from: encodedDict as [String: Any]) + let model3 = try? Firestore.decode(Model.self, from: encodedDict as [String: Any]) XCTAssertNotNil(model3) } @@ -382,7 +382,7 @@ class CodableDocumentTests: XCTestCase { "b": true, ] as [String: Any] - let model2 = try! Firestore.Decoder().decode(Model.self, from: dict) + let model2 = try! Firestore.decode(Model.self, from: dict) XCTAssertEqual(model.s, model2.s) XCTAssertEqual(model.d, model2.d) XCTAssertEqual(model.i, model2.i) @@ -392,7 +392,7 @@ class CodableDocumentTests: XCTestCase { XCTAssertEqual(model2.mi, -9) XCTAssertEqual(model2.mb, false) - let encodedDict = try! Firestore.Encoder().encode(model) + let encodedDict = try! Firestore.encode(model) XCTAssertEqual(encodedDict["s"] as! String, "abc") XCTAssertEqual(encodedDict["d"] as! Double, 123.3) XCTAssertEqual(encodedDict["i"] as! Int, -4444) From 0de7c2595ed4f1bdef70c6899f667d6bcc3e1270 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 26 Jan 2019 18:01:46 -0800 Subject: [PATCH 10/24] Add Function for checking Firestore pass-through types --- .../Codable/CodablePassThroughTypes.swift | 26 ++++++++++++ .../third_party/FirestoreDecoder.swift | 40 +++++++++---------- .../third_party/FirestoreEncoder.swift | 5 +-- 3 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift diff --git a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift new file mode 100644 index 00000000000..5a8fb01db7f --- /dev/null +++ b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift @@ -0,0 +1,26 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import FirebaseFirestore + +func isCodablePassThroughType(_ value: T) -> Bool { + return + T.self == GeoPoint.self || + T.self == DocumentReference.self || + T.self == FieldValue.self || + T.self == Timestamp.self +} diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index 5fecb815f86..1838f42a695 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -982,39 +982,37 @@ extension _FirestoreDecoder { } func unbox(_ value: Any, as type: T.Type) throws -> T? { - let decoded: T if T.self == Date.self || T.self == NSDate.self { guard let date = try self.unbox(value, as: Date.self) else { return nil } - decoded = date as! T - } else if T.self == Data.self || T.self == NSData.self { + return (date as! T) + } + if T.self == Data.self || T.self == NSData.self { guard let data = try self.unbox(value, as: Data.self) else { return nil } - decoded = data as! T - } else if T.self == URL.self || T.self == NSURL.self { + return (data as! T) + } + if T.self == URL.self || T.self == NSURL.self { guard let urlString = try self.unbox(value, as: String.self) else { return nil } - guard let url = URL(string: urlString) else { throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: codingPath, debugDescription: "Invalid URL string.")) } - - decoded = (url as! T) - } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { + return (url as! T) + } + 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 T.self == GeoPoint.self || - T.self == DocumentReference.self || - T.self == FieldValue.self || - T.self == Timestamp.self { - // All the native types that should not be encoded - decoded = value as! T - } else { - storage.push(container: value) - decoded = try T(from: self) - storage.popContainer() + return (decimal as! T) } - + if let _ = value as? Codable { + if isCodablePassThroughType(value as! T) { + // All the native Firestore types that should not be encoded + return (value as! T) + } + } + storage.push(container: value) + let decoded = try T(from: self) + storage.popContainer() return decoded } } diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index c4154502a3a..a94b346651a 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -342,10 +342,7 @@ extension _FirestoreEncoder { return box((value as! URL).absoluteString) } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { return (value as! NSDecimalNumber) - } else if T.self == GeoPoint.self || - T.self == DocumentReference.self || - T.self == FieldValue.self || - T.self == Timestamp.self { + } else if isCodablePassThroughType(value) { // These are all native _Firestore types that we don't need to Encode return (value as! NSObject) } From c980da083b62e1b9d41a5c2d27f81ca275f61c79 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 28 Jan 2019 09:43:42 -0800 Subject: [PATCH 11/24] encodeOrDie --- .../Codable/CollectionReference+Codable.swift | 15 +++-------- .../Codable/DocumentReference+Codable.swift | 15 +++-------- .../Swift/Source/Codable/EncodeOrDie.swift | 26 +++++++++++++++++++ .../Source/Codable/Transaction+Codable.swift | 7 ++--- .../Source/Codable/WriteBatch+Codable.swift | 7 ++--- 5 files changed, 38 insertions(+), 32 deletions(-) create mode 100644 Firestore/Swift/Source/Codable/EncodeOrDie.swift diff --git a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift index ae1155dd646..2b297284df2 100644 --- a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift @@ -18,19 +18,12 @@ import FirebaseFirestore extension CollectionReference { public func addDocument(data: T) -> DocumentReference { - do { - return addDocument(data: try Firestore.encode(data)) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } + let encoded = encodeOrDie(data) + return addDocument(data: encoded) } public func addDocument(data: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { - do { - let encoded = try Firestore.encode(data) - return addDocument(data: encoded, completion: completion) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } + let encoded = encodeOrDie(data) + return addDocument(data: encoded, completion: completion) } } diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index 4520f5dd872..c8708e71205 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -38,19 +38,12 @@ extension CodableDocumentReference { extension DocumentReference: CodableDocumentReference { public func setData(_ value: T) { - do { - setData(try Firestore.encode(value)) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } + let encoded = encodeOrDie(value) + setData(encoded) } public func setData(_ value: T, _ completion: ((Error?) -> Void)?) { - do { - let encoded = try Firestore.encode(value) - setData(encoded, completion: completion) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } + let encoded = encodeOrDie(value) + setData(encoded, completion: completion) } } diff --git a/Firestore/Swift/Source/Codable/EncodeOrDie.swift b/Firestore/Swift/Source/Codable/EncodeOrDie.swift new file mode 100644 index 00000000000..c3ae7088403 --- /dev/null +++ b/Firestore/Swift/Source/Codable/EncodeOrDie.swift @@ -0,0 +1,26 @@ +/* + * Copyright 2019 Google + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import Foundation +import FirebaseFirestore + +internal func encodeOrDie(_ value: T) -> [String : Any] { + do { + return try Firestore.encode(value) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } +} diff --git a/Firestore/Swift/Source/Codable/Transaction+Codable.swift b/Firestore/Swift/Source/Codable/Transaction+Codable.swift index 8fecdd7c54a..0cf7cd7f46e 100644 --- a/Firestore/Swift/Source/Codable/Transaction+Codable.swift +++ b/Firestore/Swift/Source/Codable/Transaction+Codable.swift @@ -18,10 +18,7 @@ import FirebaseFirestore extension Transaction { public func setData(_ value: T, forDocument: DocumentReference) { - do { - setData(try Firestore.encode(value), forDocument: forDocument) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } + let encoded = encodeOrDie(value) + setData(encoded, forDocument: forDocument) } } diff --git a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift index 0bd547117f8..9f0aa4e790c 100644 --- a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift +++ b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift @@ -18,10 +18,7 @@ import FirebaseFirestore extension WriteBatch { public func setData(_ value: T, forDocument: DocumentReference) { - do { - setData(try Firestore.encode(value), forDocument: forDocument) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } + let encoded = encodeOrDie(value) + setData(encoded, forDocument: forDocument) } } From ef120f954281819b02d6c636cfc80d331a0072d8 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 28 Jan 2019 10:18:01 -0800 Subject: [PATCH 12/24] Fix unused build warning --- Firestore/Swift/Tests/API/BasicCompileTests.swift | 2 +- Firestore/Swift/Tests/Codable/CodableDocumentTests.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Firestore/Swift/Tests/API/BasicCompileTests.swift b/Firestore/Swift/Tests/API/BasicCompileTests.swift index 2191de2cf2d..00232243215 100644 --- a/Firestore/Swift/Tests/API/BasicCompileTests.swift +++ b/Firestore/Swift/Tests/API/BasicCompileTests.swift @@ -199,7 +199,7 @@ func writeDocuments(at docRef: DocumentReference, database db: Firestore) { } func addDocument(to collectionRef: CollectionReference) { - collectionRef.addDocument(data: ["foo": 42]) + _ = collectionRef.addDocument(data: ["foo": 42]) // or collectionRef.document().setData(["foo": 42]) } diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index eb2a3c0c4d7..0e196dec657 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -14,9 +14,9 @@ * limitations under the License. */ +import Foundation import FirebaseFirestore @testable import FirebaseFirestoreSwift -import Foundation import XCTest class CodableDocumentTests: XCTestCase { From cca950314004a7cbef10b003a3265596bbef3f60 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 28 Jan 2019 13:26:26 -0800 Subject: [PATCH 13/24] Stop overriding in extensions --- .../Source/Codable/CollectionReference+Codable.swift | 8 ++++---- .../Swift/Source/Codable/DocumentReference+Codable.swift | 8 ++++---- Firestore/Swift/Source/Codable/Transaction+Codable.swift | 4 ++-- Firestore/Swift/Source/Codable/WriteBatch+Codable.swift | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift index 2b297284df2..9f429a13316 100644 --- a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift @@ -17,13 +17,13 @@ import FirebaseFirestore extension CollectionReference { - public func addDocument(data: T) -> DocumentReference { - let encoded = encodeOrDie(data) + public func addDocument(encoderInput: T) -> DocumentReference { + let encoded = encodeOrDie(encoderInput) return addDocument(data: encoded) } - public func addDocument(data: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { - let encoded = encodeOrDie(data) + public func addDocument(encoderInput: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { + let encoded = encodeOrDie(encoderInput) return addDocument(data: encoded, completion: completion) } } diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index c8708e71205..a40d69ee0a9 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -37,13 +37,13 @@ extension CodableDocumentReference { } extension DocumentReference: CodableDocumentReference { - public func setData(_ value: T) { - let encoded = encodeOrDie(value) + public func setData(encoderInput : T) { + let encoded = encodeOrDie(encoderInput) setData(encoded) } - public func setData(_ value: T, _ completion: ((Error?) -> Void)?) { - let encoded = encodeOrDie(value) + public func setData(encoderInput: T, _ completion: ((Error?) -> Void)?) { + let encoded = encodeOrDie(encoderInput) setData(encoded, completion: completion) } } diff --git a/Firestore/Swift/Source/Codable/Transaction+Codable.swift b/Firestore/Swift/Source/Codable/Transaction+Codable.swift index 0cf7cd7f46e..b4ceb60157e 100644 --- a/Firestore/Swift/Source/Codable/Transaction+Codable.swift +++ b/Firestore/Swift/Source/Codable/Transaction+Codable.swift @@ -17,8 +17,8 @@ import FirebaseFirestore extension Transaction { - public func setData(_ value: T, forDocument: DocumentReference) { - let encoded = encodeOrDie(value) + public func setData(encoderInput: T, forDocument: DocumentReference) { + let encoded = encodeOrDie(encoderInput) setData(encoded, forDocument: forDocument) } } diff --git a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift index 9f0aa4e790c..eee1c0be364 100644 --- a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift +++ b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift @@ -17,8 +17,8 @@ import FirebaseFirestore extension WriteBatch { - public func setData(_ value: T, forDocument: DocumentReference) { - let encoded = encodeOrDie(value) + public func setData(encoderInput: T, forDocument: DocumentReference) { + let encoded = encodeOrDie(encoderInput) setData(encoded, forDocument: forDocument) } } From 7bd089d39fdb7ab6f8d1d19fd8edd9657404c7ee Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 28 Jan 2019 14:03:52 -0800 Subject: [PATCH 14/24] One more 2019 --- Firestore/Swift/Tests/BridgingHeader.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firestore/Swift/Tests/BridgingHeader.h b/Firestore/Swift/Tests/BridgingHeader.h index d8f451d7b0a..7becc83f2e7 100644 --- a/Firestore/Swift/Tests/BridgingHeader.h +++ b/Firestore/Swift/Tests/BridgingHeader.h @@ -1,5 +1,5 @@ /* - * Copyright 2018 Google + * Copyright 2019 Google * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. From be05ca6fc67192e7b52ce5d1faa0c09846d6d34f Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Mon, 28 Jan 2019 14:06:21 -0800 Subject: [PATCH 15/24] style --- Firestore/Swift/Source/Codable/DocumentReference+Codable.swift | 2 +- Firestore/Swift/Source/Codable/EncodeOrDie.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index a40d69ee0a9..9e6581c334f 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -37,7 +37,7 @@ extension CodableDocumentReference { } extension DocumentReference: CodableDocumentReference { - public func setData(encoderInput : T) { + public func setData(encoderInput: T) { let encoded = encodeOrDie(encoderInput) setData(encoded) } diff --git a/Firestore/Swift/Source/Codable/EncodeOrDie.swift b/Firestore/Swift/Source/Codable/EncodeOrDie.swift index c3ae7088403..80f223b7ddd 100644 --- a/Firestore/Swift/Source/Codable/EncodeOrDie.swift +++ b/Firestore/Swift/Source/Codable/EncodeOrDie.swift @@ -17,7 +17,7 @@ import Foundation import FirebaseFirestore -internal func encodeOrDie(_ value: T) -> [String : Any] { +internal func encodeOrDie(_ value: T) -> [String: Any] { do { return try Firestore.encode(value) } catch let error { From 7dd421e3bb53e5d6a78d4d7d81c0dc97c716a1db Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Tue, 29 Jan 2019 14:16:27 -0800 Subject: [PATCH 16/24] from argument label --- .../Source/Codable/CollectionReference+Codable.swift | 8 ++++---- .../Swift/Source/Codable/DocumentReference+Codable.swift | 8 ++++---- Firestore/Swift/Source/Codable/Transaction+Codable.swift | 4 ++-- Firestore/Swift/Source/Codable/WriteBatch+Codable.swift | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift index 9f429a13316..d0f2bb491e8 100644 --- a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift @@ -17,13 +17,13 @@ import FirebaseFirestore extension CollectionReference { - public func addDocument(encoderInput: T) -> DocumentReference { - let encoded = encodeOrDie(encoderInput) + public func addDocument(from encodable: T) -> DocumentReference { + let encoded = encodeOrDie(encodable) return addDocument(data: encoded) } - public func addDocument(encoderInput: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { - let encoded = encodeOrDie(encoderInput) + public func addDocument(from encodable: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { + let encoded = encodeOrDie(encodable) return addDocument(data: encoded, completion: completion) } } diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift index 9e6581c334f..bbf80bb55be 100644 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift @@ -37,13 +37,13 @@ extension CodableDocumentReference { } extension DocumentReference: CodableDocumentReference { - public func setData(encoderInput: T) { - let encoded = encodeOrDie(encoderInput) + public func setData(from encodable: T) { + let encoded = encodeOrDie(encodable) setData(encoded) } - public func setData(encoderInput: T, _ completion: ((Error?) -> Void)?) { - let encoded = encodeOrDie(encoderInput) + public func setData(from encodable: T, _ completion: ((Error?) -> Void)?) { + let encoded = encodeOrDie(encodable) setData(encoded, completion: completion) } } diff --git a/Firestore/Swift/Source/Codable/Transaction+Codable.swift b/Firestore/Swift/Source/Codable/Transaction+Codable.swift index b4ceb60157e..839771780ad 100644 --- a/Firestore/Swift/Source/Codable/Transaction+Codable.swift +++ b/Firestore/Swift/Source/Codable/Transaction+Codable.swift @@ -17,8 +17,8 @@ import FirebaseFirestore extension Transaction { - public func setData(encoderInput: T, forDocument: DocumentReference) { - let encoded = encodeOrDie(encoderInput) + public func setData(from encodable: T, forDocument: DocumentReference) { + let encoded = encodeOrDie(encodable) setData(encoded, forDocument: forDocument) } } diff --git a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift index eee1c0be364..ddfe5308647 100644 --- a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift +++ b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift @@ -17,8 +17,8 @@ import FirebaseFirestore extension WriteBatch { - public func setData(encoderInput: T, forDocument: DocumentReference) { - let encoded = encodeOrDie(encoderInput) + public func setData(from encodable: T, forDocument: DocumentReference) { + let encoded = encodeOrDie(encodable) setData(encoded, forDocument: forDocument) } } From b98e874b9300a27dfdabc996de0046ad16bdd9bf Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 2 Feb 2019 09:12:07 -0800 Subject: [PATCH 17/24] Address code review feedback (not including tests) --- .../Swift/Source/Codable/CodableErrors.swift | 4 +-- .../Codable/CodablePassThroughTypes.swift | 2 +- .../Swift/Source/Codable/EncodeOrDie.swift | 26 ------------------- .../third_party/FirestoreDecoder.swift | 2 +- .../third_party/FirestoreEncoder.swift | 10 ++++++- 5 files changed, 13 insertions(+), 31 deletions(-) delete mode 100644 Firestore/Swift/Source/Codable/EncodeOrDie.swift diff --git a/Firestore/Swift/Source/Codable/CodableErrors.swift b/Firestore/Swift/Source/Codable/CodableErrors.swift index ae231a6310c..4412304f346 100644 --- a/Firestore/Swift/Source/Codable/CodableErrors.swift +++ b/Firestore/Swift/Source/Codable/CodableErrors.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -enum FirestoreDecodingError: Error { +internal enum FirestoreDecodingError: Error { case decodingIsNotSupported } -enum FirestoreEncodingError: Error { +internal enum FirestoreEncodingError: Error { case encodingIsNotSupported } diff --git a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift index 5a8fb01db7f..2808d075485 100644 --- a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift +++ b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift @@ -17,7 +17,7 @@ import Foundation import FirebaseFirestore -func isCodablePassThroughType(_ value: T) -> Bool { +internal func isFirestorePassthroughType(_ value: T) -> Bool { return T.self == GeoPoint.self || T.self == DocumentReference.self || diff --git a/Firestore/Swift/Source/Codable/EncodeOrDie.swift b/Firestore/Swift/Source/Codable/EncodeOrDie.swift deleted file mode 100644 index 80f223b7ddd..00000000000 --- a/Firestore/Swift/Source/Codable/EncodeOrDie.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import Foundation -import FirebaseFirestore - -internal func encodeOrDie(_ value: T) -> [String: Any] { - do { - return try Firestore.encode(value) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } -} diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index 1838f42a695..58f0c040b70 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -1005,7 +1005,7 @@ extension _FirestoreDecoder { return (decimal as! T) } if let _ = value as? Codable { - if isCodablePassThroughType(value as! T) { + if isFirestorePassthroughType(value as! T) { // All the native Firestore types that should not be encoded return (value as! T) } diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index a94b346651a..4034213eae5 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -16,6 +16,14 @@ import Foundation import FirebaseFirestore +internal func encodeOrDie(_ value: T) -> [String: Any] { + do { + return try Firestore.encode(value) + } catch let error { + fatalError("Unable to encode data with Firestore encoder: \(error)") + } +} + extension Firestore { public static func encode(_ value: T) throws -> [String: Any] { guard let topLevel = try _FirestoreEncoder().box_(value) else { @@ -342,7 +350,7 @@ extension _FirestoreEncoder { return box((value as! URL).absoluteString) } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { return (value as! NSDecimalNumber) - } else if isCodablePassThroughType(value) { + } else if isFirestorePassthroughType(value) { // These are all native _Firestore types that we don't need to Encode return (value as! NSObject) } From 4be21cd3641b7595e8d6ab8cf2a1452feed3d5b3 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 2 Feb 2019 11:15:18 -0800 Subject: [PATCH 18/24] wip --- .../third_party/FirestoreDecoder.swift | 4 +- .../Tests/Codable/CodableDocumentTests.swift | 127 +++++++++--------- 2 files changed, 67 insertions(+), 64 deletions(-) diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index 58f0c040b70..cb3ce6a6b76 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -1004,8 +1004,8 @@ extension _FirestoreDecoder { guard let decimal = try self.unbox(value, as: Decimal.self) else { return nil } return (decimal as! T) } - if let _ = value as? Codable { - if isFirestorePassthroughType(value as! T) { + if let v = value as? T { + if isFirestorePassthroughType(v) { // All the native Firestore types that should not be encoded return (value as! T) } diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index 0e196dec657..f33be45a20c 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -19,33 +19,49 @@ import FirebaseFirestore @testable import FirebaseFirestoreSwift import XCTest -class CodableDocumentTests: XCTestCase { - func roundTrip(input: X, expected: [String: Any]? = nil) -> X where X: Codable { - var encoded = [String: Any]() - do { - encoded = try Firestore.encode(input) - if let expected = expected { - XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) - } - } catch { - XCTFail("Failed to encode \(X.self): error: \(error)") - } - do { - let decoded = try Firestore.decode(X.self, from: encoded) - return decoded - } catch { - XCTFail("Failed to decode \(X.self): \(error)") - } - return input // After failure +fileprivate func assertRoundTrip(model: X, encoded: [String: Any]) -> Void { + let enc = assertEncodes(model, encoded: encoded) + assertDecodes(enc, encoded: model) +} + +fileprivate func assertEncodes(_ model: X, encoded: [String: Any]) -> [String: Any] { + do { + let enc = try Firestore.encode(model) + XCTAssertEqual(enc as NSDictionary, encoded as NSDictionary) + return enc + } catch { + XCTFail("Failed to encode \(X.self): error: \(error)") } + return ["" : -1] +} + +fileprivate func assertDecodes(_ model: [String: Any], encoded: X) -> Void { + do { + let decoded = try Firestore.decode(X.self, from: model) + XCTAssertEqual(decoded, encoded) + } catch { + XCTFail("Failed to decode \(X.self): \(error)") + } +} + +fileprivate func assertDecodingThrows(_ model: [String: Any], encoded: X) -> Void { + do { + let _ = try Firestore.decode(X.self, from: model) + } catch { + return + } + XCTFail("Failed to throw") +} + +class CodableDocumentTests: XCTestCase { func testInt() { - struct Model: Codable { + struct Model: Codable, Equatable { let x: Int } let model = Model(x: 42) let dict = ["x": 42] - XCTAssertEqual(model.x, roundTrip(input: model, expected: dict).x) + assertRoundTrip(model: model, encoded: dict) } func testEmpty() { @@ -69,21 +85,17 @@ class CodableDocumentTests: XCTestCase { } func testOptional() { - struct Model: Codable { + struct Model: Codable, Equatable { let x: Int let opt: Int? } - let dict = ["x": 42] - let model = Model(x: 42, opt: nil) - XCTAssertEqual(model.x, roundTrip(input: model, expected: dict).x) - - let model2 = Model(x: 42, opt: 7) - let expected = ["x": 42, "opt": 7] - let encoded = try! Firestore.encode(model2) - XCTAssertEqual(encoded as NSDictionary, expected as NSDictionary) - let decoded = try! Firestore.decode(Model.self, from: expected) - XCTAssertEqual(decoded.x, model2.x) - XCTAssertEqual(decoded.opt, model2.opt) + assertRoundTrip(model: Model(x: 42, opt: nil), encoded: ["x": 42]) + assertRoundTrip(model: Model(x: 42, opt: 7), encoded: ["x": 42, "opt": 7]) + assertDecodes(["x": 42, "opt": 5], encoded: Model(x: 42, opt: 5)) + assertDecodingThrows(["x": 42, "opt": true], encoded: Model(x: 42, opt: nil)) + assertDecodingThrows(["x": 42, "opt": "abc"], encoded: Model(x: 42, opt: nil)) + assertDecodingThrows(["x": 45.55, "opt": 5], encoded: Model(x: 42, opt: nil)) + assertDecodingThrows(["opt": 5], encoded: Model(x: 42, opt: nil)) } func testOptionalTimestamp() { @@ -156,64 +168,54 @@ class CodableDocumentTests: XCTestCase { } } } - struct Model: Codable { + struct Model: Codable, Equatable { let x: Int let e: MyEnum } - let model = Model(x: 42, e: MyEnum.num(number: 4)) - let output = roundTrip(input: model) - XCTAssertEqual(model.x, output.x) - XCTAssertEqual(model.e, output.e) + let model = Model(x: 42, e: MyEnum.num(number: 4)) + assertRoundTrip(model: model, encoded: ["x" : 42, "e": [ "num" : 4] ]) let model2 = Model(x: 43, e: MyEnum.text("abc")) - let output2 = roundTrip(input: model2) - XCTAssertEqual(model2.x, output2.x) - XCTAssertEqual(model2.e, output2.e) - - let model3 = Model(x: 43, e: MyEnum.timestamp(Timestamp(date: Date()))) - let output3 = roundTrip(input: model3) - XCTAssertEqual(model3.x, output3.x) - XCTAssertEqual(model3.e, output3.e) + assertRoundTrip(model: model2, encoded: ["x" : 43, "e": [ "text" : "abc"] ]) + let timestamp = Timestamp(date: Date()) + let model3 = Model(x: 43, e: MyEnum.timestamp(timestamp)) + assertRoundTrip(model: model3, encoded: ["x" : 43, "e": [ "timestamp" : timestamp] ]) } func testGeoPoint() { - struct Model: Codable { + struct Model: Codable, Equatable { let p: GeoPoint } - let model = Model(p: GeoPoint(latitude: 1, longitude: -2)) - let dict = ["p": GeoPoint(latitude: 1, longitude: -2)] - XCTAssertEqual(model.p, roundTrip(input: model, expected: dict).p) + let geopoint = GeoPoint(latitude: 1, longitude: -2) + let model = Model(p: geopoint) + assertRoundTrip(model: model, encoded: ["p": geopoint]) } func testDate() { - struct Model: Codable { + struct Model: Codable, Equatable { let date: Date } - let d = Date(timeIntervalSinceReferenceDate: 0) - let model = Model(date: d) - let dict = ["date": d] - XCTAssertEqual(model.date, roundTrip(input: model, expected: dict).date) + let date = Date(timeIntervalSinceReferenceDate: 0) + let model = Model(date: date) + assertRoundTrip(model: model, encoded: ["date": date]) } func testDocumentReference() { - struct Model: Codable { + struct Model: Codable, Equatable { let doc: DocumentReference } let d = FSTTestDocRef("abc/xyz") let model = Model(doc: d) - let dict = ["doc": d] - XCTAssertEqual(model.doc, roundTrip(input: model, expected: dict).doc) + assertRoundTrip(model: model, encoded: ["doc": d]) } func testTimestamp() { - struct Model: Codable { + struct Model: Codable, Equatable { let timestamp: Timestamp } let t = Timestamp(date: Date()) let model = Model(timestamp: t) - let encoded = try! Firestore.encode(model) - let model2 = try! Firestore.decode(Model.self, from: encoded) - XCTAssertEqual(model.timestamp, model2.timestamp) + assertRoundTrip(model: model, encoded: ["timestamp": t]) } func testBadValue() { @@ -225,7 +227,7 @@ class CodableDocumentTests: XCTestCase { } func testValueTooBig() { - struct Model: Codable { + struct Model: Codable, Equatable { let x: CChar } let dict = ["x": 12345] // Overflow @@ -233,6 +235,7 @@ class CodableDocumentTests: XCTestCase { let dict2 = ["x": 12] let model2 = try? Firestore.decode(Model.self, from: dict2) + assertRoundTrip(model: model2, encoded: ["x" : 12]) XCTAssertNotNil(model2) } From f1a94b81383beb027c7e2940a0823a865dea5b51 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 2 Feb 2019 11:21:15 -0800 Subject: [PATCH 19/24] style --- .../Swift/Tests/Codable/CodableDocumentTests.swift | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index f33be45a20c..b612248f81e 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -32,7 +32,7 @@ fileprivate func assertEncodes(_ model: X, encoded: [Str } catch { XCTFail("Failed to encode \(X.self): error: \(error)") } - return ["" : -1] + return ["": -1] } fileprivate func assertDecodes(_ model: [String: Any], encoded: X) -> Void { @@ -46,7 +46,7 @@ fileprivate func assertDecodes(_ model: [String: Any], e fileprivate func assertDecodingThrows(_ model: [String: Any], encoded: X) -> Void { do { - let _ = try Firestore.decode(X.self, from: model) + _ = try Firestore.decode(X.self, from: model) } catch { return } @@ -54,7 +54,6 @@ fileprivate func assertDecodingThrows(_ model: [String: } class CodableDocumentTests: XCTestCase { - func testInt() { struct Model: Codable, Equatable { let x: Int @@ -174,12 +173,12 @@ class CodableDocumentTests: XCTestCase { } let model = Model(x: 42, e: MyEnum.num(number: 4)) - assertRoundTrip(model: model, encoded: ["x" : 42, "e": [ "num" : 4] ]) + assertRoundTrip(model: model, encoded: ["x": 42, "e": ["num": 4]]) let model2 = Model(x: 43, e: MyEnum.text("abc")) - assertRoundTrip(model: model2, encoded: ["x" : 43, "e": [ "text" : "abc"] ]) + assertRoundTrip(model: model2, encoded: ["x": 43, "e": ["text": "abc"]]) let timestamp = Timestamp(date: Date()) let model3 = Model(x: 43, e: MyEnum.timestamp(timestamp)) - assertRoundTrip(model: model3, encoded: ["x" : 43, "e": [ "timestamp" : timestamp] ]) + assertRoundTrip(model: model3, encoded: ["x": 43, "e": ["timestamp": timestamp]]) } func testGeoPoint() { @@ -235,7 +234,7 @@ class CodableDocumentTests: XCTestCase { let dict2 = ["x": 12] let model2 = try? Firestore.decode(Model.self, from: dict2) - assertRoundTrip(model: model2, encoded: ["x" : 12]) + assertRoundTrip(model: model2, encoded: ["x": 12]) XCTAssertNotNil(model2) } From 75d2bbbcd3e73cc901575270668f9f42a2179f5d Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 2 Feb 2019 16:30:22 -0800 Subject: [PATCH 20/24] checkpoint --- .../Swift/Source/Codable/CodableErrors.swift | 4 +- .../Tests/Codable/CodableDocumentTests.swift | 104 +++++------------- 2 files changed, 29 insertions(+), 79 deletions(-) diff --git a/Firestore/Swift/Source/Codable/CodableErrors.swift b/Firestore/Swift/Source/Codable/CodableErrors.swift index 4412304f346..0b5dcf070ff 100644 --- a/Firestore/Swift/Source/Codable/CodableErrors.swift +++ b/Firestore/Swift/Source/Codable/CodableErrors.swift @@ -14,10 +14,10 @@ * limitations under the License. */ -internal enum FirestoreDecodingError: Error { +public enum FirestoreDecodingError: Error { case decodingIsNotSupported } -internal enum FirestoreEncodingError: Error { +public enum FirestoreEncodingError: Error { case encodingIsNotSupported } diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index b612248f81e..65fec640d0d 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -64,15 +64,12 @@ class CodableDocumentTests: XCTestCase { } func testEmpty() { - struct Model: Codable {} - let model = Model() - let dict = [String: Any]() as NSDictionary - let encoded = try! Firestore.encode(model) as NSDictionary - XCTAssertEqual(encoded, dict) + struct Model: Codable, Equatable {} + _ = assertEncodes(Model(), encoded: [String: Any]()) } func testNil() { - struct Model: Codable { + struct Model: Codable, Equatable { let x: Int? } let model = Model(x: nil) @@ -97,31 +94,6 @@ class CodableDocumentTests: XCTestCase { assertDecodingThrows(["opt": 5], encoded: Model(x: 42, opt: nil)) } - func testOptionalTimestamp() { - struct Model: Codable { - let value: Int - let timestamp: Timestamp? - } - class FirestoreDummy { - var visited = 0 - func setObject(_ object: T, fieldValues: [PartialKeyPath: FieldValue] = [:]) { - let obj = object as! Model - XCTAssertEqual(obj.value, 10) - XCTAssertNil(obj.timestamp) - visited += 1 - } - } - let c = Model(value: 10, timestamp: nil) - let fs = FirestoreDummy() - // If no custom field values need to be set: - fs.setObject(c) - - // Or, overriding custom field values: - fs.setObject(c, fieldValues: [\Model.timestamp: FieldValue.serverTimestamp(), - \Model.value: FieldValue.delete()]) - XCTAssert(fs.visited == 2) - } - func testEnum() { enum MyEnum: Codable, Equatable { case num(number: Int) @@ -208,6 +180,19 @@ class CodableDocumentTests: XCTestCase { assertRoundTrip(model: model, encoded: ["doc": d]) } + // DocumentReference is not Codable unless embedded in a Firestore object. + func testDocumentReferenceEncodes() { + let doc = FSTTestDocRef("abc/xyz") + do { + let _ = try JSONEncoder().encode(doc) + XCTFail("Failed to throw") + } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported { + return + } catch { + XCTFail("Unrecognized error: \(error)") + } + } + func testTimestamp() { struct Model: Codable, Equatable { let timestamp: Timestamp @@ -218,11 +203,12 @@ class CodableDocumentTests: XCTestCase { } func testBadValue() { - struct Model: Codable { + struct Model: Codable, Equatable { let x: Int } let dict = ["x": "abc"] // Wrong type; - XCTAssertThrowsError(try Firestore.decode(Model.self, from: dict)) + let model = Model(x: 42) + assertDecodingThrows(dict, encoded: model) } func testValueTooBig() { @@ -230,17 +216,14 @@ class CodableDocumentTests: XCTestCase { let x: CChar } let dict = ["x": 12345] // Overflow - XCTAssertThrowsError(try Firestore.decode(Model.self, from: dict)) - - let dict2 = ["x": 12] - let model2 = try? Firestore.decode(Model.self, from: dict2) - assertRoundTrip(model: model2, encoded: ["x": 12]) - XCTAssertNotNil(model2) + let model = Model(x: 42) + assertDecodingThrows(dict, encoded: model) + assertRoundTrip(model: model, encoded: ["x": 42]) } // Inspired by https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java func testBeans() { - struct Model: Codable { + struct Model: Codable, Equatable { let s: String let d: Double let f: Float @@ -259,7 +242,7 @@ class CodableDocumentTests: XCTestCase { let model = Model( s: "abc", d: 123, - f: -4.321, + f: -4, l: 1_234_567_890_123, i: -4444, b: false, @@ -275,7 +258,7 @@ class CodableDocumentTests: XCTestCase { let dict = [ "s": "abc", "d": 123, - "f": -4.321, + "f": -4, "l": 1_234_567_890_123, "i": -4444, "b": false, @@ -289,44 +272,11 @@ class CodableDocumentTests: XCTestCase { "casESensitivE": "ccc", ] as [String: Any] - let model2 = try! Firestore.decode(Model.self, from: dict) - XCTAssertEqual(model.s, model2.s) - XCTAssertEqual(model.d, model2.d) - XCTAssertEqual(model.f, model2.f) - XCTAssertEqual(model.l, model2.l) - XCTAssertEqual(model.i, model2.i) - XCTAssertEqual(model.b, model2.b) - XCTAssertEqual(model.sh, model2.sh) - XCTAssertEqual(model.byte, model2.byte) - XCTAssertEqual(model.uchar, model2.uchar) - XCTAssertEqual(model.ai, model2.ai) - XCTAssertEqual(model.si, model2.si) - XCTAssertEqual(model.caseSensitive, model2.caseSensitive) - XCTAssertEqual(model.casESensitive, model2.casESensitive) - XCTAssertEqual(model.casESensitivE, model2.casESensitivE) - - let encodedDict = try! Firestore.encode(model) - XCTAssertEqual(encodedDict["s"] as! String, "abc") - XCTAssertEqual(encodedDict["d"] as! Double, 123) - XCTAssertEqual(encodedDict["f"] as! Float, -4.321) - XCTAssertEqual(encodedDict["l"] as! CLongLong, 1_234_567_890_123) - XCTAssertEqual(encodedDict["i"] as! Int, -4444) - XCTAssertEqual(encodedDict["b"] as! Bool, false) - XCTAssertEqual(encodedDict["sh"] as! CShort, 123) - XCTAssertEqual(encodedDict["byte"] as! CChar, 45) - XCTAssertEqual(encodedDict["uchar"] as! CUnsignedChar, 44) - XCTAssertEqual(encodedDict["ai"] as! [Int], [1, 2, 3, 4]) - XCTAssertEqual(encodedDict["si"] as! [String], ["abc", "def"]) - XCTAssertEqual(encodedDict["caseSensitive"] as! String, "aaa") - XCTAssertEqual(encodedDict["casESensitive"] as! String, "bbb") - XCTAssertEqual(encodedDict["casESensitivE"] as! String, "ccc") - - let model3 = try? Firestore.decode(Model.self, from: encodedDict as [String: Any]) - XCTAssertNotNil(model3) + assertRoundTrip(model: model, encoded: dict) } func testCodingKeys() { - struct Model: Codable { + struct Model: Codable, Equatable { var s: String var ms: String var d: Double From 55585970ae6bccee267901cf2f50155d347635d5 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sat, 2 Feb 2019 17:26:23 -0800 Subject: [PATCH 21/24] style --- Firestore/Swift/Tests/Codable/CodableDocumentTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index 65fec640d0d..0d7b3258c53 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -184,7 +184,7 @@ class CodableDocumentTests: XCTestCase { func testDocumentReferenceEncodes() { let doc = FSTTestDocRef("abc/xyz") do { - let _ = try JSONEncoder().encode(doc) + _ = try JSONEncoder().encode(doc) XCTFail("Failed to throw") } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported { return From a3eaadcaf92a2d5cd609ebb124412a951beb77d4 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Sun, 3 Feb 2019 11:36:42 -0800 Subject: [PATCH 22/24] Test and comments about nil encoding challenge --- .../Codable/third_party/FirestoreEncoder.swift | 2 +- .../Tests/Codable/CodableDocumentTests.swift | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index 4034213eae5..c5abb0afa49 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -351,7 +351,7 @@ extension _FirestoreEncoder { } else if T.self == Decimal.self || T.self == NSDecimalNumber.self { return (value as! NSDecimalNumber) } else if isFirestorePassthroughType(value) { - // These are all native _Firestore types that we don't need to Encode + // This is a native Firestore types that we don't need to encode. return (value as! NSObject) } diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index 0d7b3258c53..e775312d277 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -80,6 +80,23 @@ class CodableDocumentTests: XCTestCase { XCTAssertNil(model2) } + func testIntNilString() { + struct Model: Codable, Equatable { + let i: Int + let x: Int? + let s: String + } + let model = Model(i: 7, x: nil, s: "abc") + let encodedDict = try! Firestore.encode(model) + XCTAssertNil(encodedDict["x"]) + XCTAssertTrue(encodedDict.keys.contains("i")) + + // TODO: - handle encoding keys with nil values + // See https://stackoverflow.com/questions/47266862/encode-nil-value-as-null-with-jsonencoder + // and https://bugs.swift.org/browse/SR-9232 + // XCTAssertTrue(encodedDict.keys.contains("x")) + } + func testOptional() { struct Model: Codable, Equatable { let x: Int From 7c6ee0f027461c4e5b1f40cde7f8688669ec66d1 Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 8 Feb 2019 12:04:50 -0800 Subject: [PATCH 23/24] Back to encode/decode instance methods and remove some extensions --- .../Codable/CodablePassThroughTypes.swift | 2 - .../Codable/CollectionReference+Codable.swift | 29 --------- .../Codable/DocumentReference+Codable.swift | 49 --------------- .../Codable/DocumentSnapshot+Codable.swift | 28 --------- .../Source/Codable/FieldValue+Codable.swift | 35 ----------- .../Source/Codable/Transaction+Codable.swift | 24 -------- .../Source/Codable/WriteBatch+Codable.swift | 24 -------- .../third_party/FirestoreDecoder.swift | 14 +++-- .../third_party/FirestoreEncoder.swift | 35 +++++------ .../Tests/Codable/CodableDocumentTests.swift | 60 ++++++++++--------- 10 files changed, 54 insertions(+), 246 deletions(-) delete mode 100644 Firestore/Swift/Source/Codable/CollectionReference+Codable.swift delete mode 100644 Firestore/Swift/Source/Codable/DocumentReference+Codable.swift delete mode 100644 Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift delete mode 100644 Firestore/Swift/Source/Codable/FieldValue+Codable.swift delete mode 100644 Firestore/Swift/Source/Codable/Transaction+Codable.swift delete mode 100644 Firestore/Swift/Source/Codable/WriteBatch+Codable.swift diff --git a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift index 2808d075485..d946f8999cf 100644 --- a/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift +++ b/Firestore/Swift/Source/Codable/CodablePassThroughTypes.swift @@ -20,7 +20,5 @@ import FirebaseFirestore internal func isFirestorePassthroughType(_ value: T) -> Bool { return T.self == GeoPoint.self || - T.self == DocumentReference.self || - T.self == FieldValue.self || T.self == Timestamp.self } diff --git a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift b/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift deleted file mode 100644 index d0f2bb491e8..00000000000 --- a/Firestore/Swift/Source/Codable/CollectionReference+Codable.swift +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import FirebaseFirestore - -extension CollectionReference { - public func addDocument(from encodable: T) -> DocumentReference { - let encoded = encodeOrDie(encodable) - return addDocument(data: encoded) - } - - public func addDocument(from encodable: T, _ completion: ((Error?) -> Void)?) -> DocumentReference { - let encoded = encodeOrDie(encodable) - return addDocument(data: encoded, completion: completion) - } -} diff --git a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift b/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift deleted file mode 100644 index bbf80bb55be..00000000000 --- a/Firestore/Swift/Source/Codable/DocumentReference+Codable.swift +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import FirebaseFirestore - -/** - * A protocol describing the encodable properties of a DocumentReference. - * - * Note: this protocol exists as a workaround for the Swift compiler: if the DocumentReference class - * was extended directly to conform to Codable, the methods implementing the protocol would be need - * to be marked required but that can't be done in an extension. Declaring the extension on the - * protocol sidesteps this issue. - */ -fileprivate protocol CodableDocumentReference: Codable {} - -extension CodableDocumentReference { - public init(from decoder: Decoder) throws { - throw FirestoreDecodingError.decodingIsNotSupported - } - - public func encode(to encoder: Encoder) throws { - throw FirestoreEncodingError.encodingIsNotSupported - } -} - -extension DocumentReference: CodableDocumentReference { - public func setData(from encodable: T) { - let encoded = encodeOrDie(encodable) - setData(encoded) - } - - public func setData(from encodable: T, _ completion: ((Error?) -> Void)?) { - let encoded = encodeOrDie(encodable) - setData(encoded, completion: completion) - } -} diff --git a/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift b/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift deleted file mode 100644 index 7aab2ce89d0..00000000000 --- a/Firestore/Swift/Source/Codable/DocumentSnapshot+Codable.swift +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import FirebaseFirestore - -extension DocumentSnapshot { - public func data(as type: T.Type) throws -> T { - guard let dict = data() else { - throw DecodingError.valueNotFound(T.self, - DecodingError.Context(codingPath: [], - debugDescription: "Data was empty")) - } - return try Firestore.decode(T.self, from: dict) - } -} diff --git a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift b/Firestore/Swift/Source/Codable/FieldValue+Codable.swift deleted file mode 100644 index 4925209ca26..00000000000 --- a/Firestore/Swift/Source/Codable/FieldValue+Codable.swift +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import FirebaseFirestore - -/** - * A protocol describing the encodable properties of a FirebaseFirestore. - * - * Note: this protocol exists as a workaround for the Swift compiler: if the FieldValue class - * was extended directly to conform to Codable, the methods implementing the protocol would be need - * to be marked required but that can't be done in an extension. Declaring the extension on the - * protocol sidesteps this issue. - */ -fileprivate protocol CodableFieldValue: Encodable {} - -extension CodableFieldValue { - public func encode(to encoder: Encoder) throws { - throw FirestoreEncodingError.encodingIsNotSupported - } -} - -extension FieldValue: CodableFieldValue {} diff --git a/Firestore/Swift/Source/Codable/Transaction+Codable.swift b/Firestore/Swift/Source/Codable/Transaction+Codable.swift deleted file mode 100644 index 839771780ad..00000000000 --- a/Firestore/Swift/Source/Codable/Transaction+Codable.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import FirebaseFirestore - -extension Transaction { - public func setData(from encodable: T, forDocument: DocumentReference) { - let encoded = encodeOrDie(encodable) - setData(encoded, forDocument: forDocument) - } -} diff --git a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift b/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift deleted file mode 100644 index ddfe5308647..00000000000 --- a/Firestore/Swift/Source/Codable/WriteBatch+Codable.swift +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2019 Google - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import FirebaseFirestore - -extension WriteBatch { - public func setData(from encodable: T, forDocument: DocumentReference) { - let encoded = encodeOrDie(encodable) - setData(encoded, forDocument: forDocument) - } -} diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift index cb3ce6a6b76..bbc1f7beee5 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreDecoder.swift @@ -17,13 +17,15 @@ import Foundation import FirebaseFirestore extension Firestore { - public static func decode(_ type: T.Type, from container: [String: Any]) throws -> T { - let decoder = _FirestoreDecoder(referencing: container) - 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")) + public struct Decoder { + public init() {} + public func decode(_ type: T.Type, from container: [String: Any]) throws -> T { + let decoder = _FirestoreDecoder(referencing: container) + 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")) + } + return value } - - return value } } diff --git a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift index c5abb0afa49..6b4c9b1410a 100644 --- a/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift +++ b/Firestore/Swift/Source/Codable/third_party/FirestoreEncoder.swift @@ -16,29 +16,24 @@ import Foundation import FirebaseFirestore -internal func encodeOrDie(_ value: T) -> [String: Any] { - do { - return try Firestore.encode(value) - } catch let error { - fatalError("Unable to encode data with Firestore encoder: \(error)") - } -} - extension Firestore { - public static func encode(_ value: T) throws -> [String: Any] { - guard let topLevel = try _FirestoreEncoder().box_(value) else { - throw EncodingError.invalidValue(value, - EncodingError.Context(codingPath: [], - debugDescription: "Top-level \(T.self) did not encode any values.")) - } + public struct Encoder { + public init() {} + public func encode(_ value: T) throws -> [String: Any] { + guard let topLevel = try _FirestoreEncoder().box_(value) else { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-level \(T.self) did not encode any values.")) + } - // This is O(n) check. Consider refactoring box_ to return [String: Any]. - guard let dict = topLevel as? [String: Any] else { - throw EncodingError.invalidValue(value, - EncodingError.Context(codingPath: [], - debugDescription: "Top-level \(T.self) encoded not as dictionary.")) + // This is O(n) check. Consider refactoring box_ to return [String: Any]. + guard let dict = topLevel as? [String: Any] else { + throw EncodingError.invalidValue(value, + EncodingError.Context(codingPath: [], + debugDescription: "Top-level \(T.self) encoded not as dictionary.")) + } + return dict } - return dict } } diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index e775312d277..0b62e460dc9 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -26,7 +26,7 @@ fileprivate func assertRoundTrip(model: X, encoded: [Str fileprivate func assertEncodes(_ model: X, encoded: [String: Any]) -> [String: Any] { do { - let enc = try Firestore.encode(model) + let enc = try Firestore.Encoder().encode(model) XCTAssertEqual(enc as NSDictionary, encoded as NSDictionary) return enc } catch { @@ -37,7 +37,7 @@ fileprivate func assertEncodes(_ model: X, encoded: [Str fileprivate func assertDecodes(_ model: [String: Any], encoded: X) -> Void { do { - let decoded = try Firestore.decode(X.self, from: model) + let decoded = try Firestore.Decoder().decode(X.self, from: model) XCTAssertEqual(decoded, encoded) } catch { XCTFail("Failed to decode \(X.self): \(error)") @@ -46,7 +46,7 @@ fileprivate func assertDecodes(_ model: [String: Any], e fileprivate func assertDecodingThrows(_ model: [String: Any], encoded: X) -> Void { do { - _ = try Firestore.decode(X.self, from: model) + _ = try Firestore.Decoder().decode(X.self, from: model) } catch { return } @@ -74,9 +74,9 @@ class CodableDocumentTests: XCTestCase { } let model = Model(x: nil) let dict = ["x": nil] as [String: Int?] - let encodedDict = try! Firestore.encode(model) + let encodedDict = try! Firestore.Encoder().encode(model) XCTAssertNil(encodedDict["x"]) - let model2 = try? Firestore.decode(Model.self, from: dict as [String: Any]) + let model2 = try? Firestore.Decoder().decode(Model.self, from: dict as [String: Any]) XCTAssertNil(model2) } @@ -87,7 +87,7 @@ class CodableDocumentTests: XCTestCase { let s: String } let model = Model(i: 7, x: nil, s: "abc") - let encodedDict = try! Firestore.encode(model) + let encodedDict = try! Firestore.Encoder().encode(model) XCTAssertNil(encodedDict["x"]) XCTAssertTrue(encodedDict.keys.contains("i")) @@ -188,27 +188,29 @@ class CodableDocumentTests: XCTestCase { assertRoundTrip(model: model, encoded: ["date": date]) } - func testDocumentReference() { - struct Model: Codable, Equatable { - let doc: DocumentReference - } - let d = FSTTestDocRef("abc/xyz") - let model = Model(doc: d) - assertRoundTrip(model: model, encoded: ["doc": d]) - } - - // DocumentReference is not Codable unless embedded in a Firestore object. - func testDocumentReferenceEncodes() { - let doc = FSTTestDocRef("abc/xyz") - do { - _ = try JSONEncoder().encode(doc) - XCTFail("Failed to throw") - } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported { - return - } catch { - XCTFail("Unrecognized error: \(error)") - } - } +// Uncomment if we decide to reenable embedded DocumentReference's +// +// func testDocumentReference() { +// struct Model: Codable, Equatable { +// let doc: DocumentReference +// } +// let d = FSTTestDocRef("abc/xyz") +// let model = Model(doc: d) +// assertRoundTrip(model: model, encoded: ["doc": d]) +// } +// +// // DocumentReference is not Codable unless embedded in a Firestore object. +// func testDocumentReferenceEncodes() { +// let doc = FSTTestDocRef("abc/xyz") +// do { +// _ = try JSONEncoder().encode(doc) +// XCTFail("Failed to throw") +// } catch FirebaseFirestoreSwift.FirestoreEncodingError.encodingIsNotSupported { +// return +// } catch { +// XCTFail("Unrecognized error: \(error)") +// } +// } func testTimestamp() { struct Model: Codable, Equatable { @@ -351,7 +353,7 @@ class CodableDocumentTests: XCTestCase { "b": true, ] as [String: Any] - let model2 = try! Firestore.decode(Model.self, from: dict) + let model2 = try! Firestore.Decoder().decode(Model.self, from: dict) XCTAssertEqual(model.s, model2.s) XCTAssertEqual(model.d, model2.d) XCTAssertEqual(model.i, model2.i) @@ -361,7 +363,7 @@ class CodableDocumentTests: XCTestCase { XCTAssertEqual(model2.mi, -9) XCTAssertEqual(model2.mb, false) - let encodedDict = try! Firestore.encode(model) + let encodedDict = try! Firestore.Encoder().encode(model) XCTAssertEqual(encodedDict["s"] as! String, "abc") XCTAssertEqual(encodedDict["d"] as! Double, 123.3) XCTAssertEqual(encodedDict["i"] as! Int, -4444) From 90673ea4297431435fec6213aa4f2858cad065dd Mon Sep 17 00:00:00 2001 From: Paul Beusterien Date: Fri, 8 Feb 2019 12:13:13 -0800 Subject: [PATCH 24/24] style --- Firestore/Swift/Tests/Codable/CodableDocumentTests.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift index 0b62e460dc9..ec6b7c331db 100644 --- a/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift +++ b/Firestore/Swift/Tests/Codable/CodableDocumentTests.swift @@ -188,7 +188,7 @@ class CodableDocumentTests: XCTestCase { assertRoundTrip(model: model, encoded: ["date": date]) } -// Uncomment if we decide to reenable embedded DocumentReference's + // Uncomment if we decide to reenable embedded DocumentReference's // // func testDocumentReference() { // struct Model: Codable, Equatable {