Skip to content

Commit a1dde8e

Browse files
Require explicit typing for DocumentSnapshot decoding. DocumentReference decoding. (#9101)
1 parent fe1b04d commit a1dde8e

File tree

5 files changed

+144
-34
lines changed

5 files changed

+144
-34
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/*
2+
* Copyright 2021 Google LLC
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import Foundation
18+
import FirebaseFirestore
19+
20+
public extension DocumentReference {
21+
/// Fetches and decodes the document referenced by this `DocumentReference`.
22+
///
23+
/// This allows users to retrieve a Firestore document and have it decoded to
24+
/// an instance of caller-specified type as follows:
25+
/// ```swift
26+
/// ref.getDocument(as: Book.self) { result in
27+
/// do {
28+
/// let book = try result.get()
29+
/// } catch {
30+
/// // Handle error
31+
/// }
32+
/// }
33+
/// ```
34+
///
35+
/// This method attempts to provide up-to-date data when possible by waiting
36+
/// for data from the server, but it may return cached data or fail if you are
37+
/// offline and the server cannot be reached. If `T` denotes an optional
38+
/// type, the method returns a successful status with a value of `nil` for
39+
/// non-existing documents.
40+
///
41+
/// - Parameters:
42+
/// - as: A `Decodable` type to convert the document fields to.
43+
/// - serverTimestampBehavior: Configures how server timestamps that have
44+
/// not yet been set to their final value are returned from the snapshot.
45+
/// - decoder: The decoder to use to convert the document. Defaults to use
46+
/// the default decoder.
47+
/// - completion: The closure to call when the document snapshot has been
48+
/// fetched and decoded.
49+
func getDocument<T: Decodable>(as type: T.Type,
50+
with serverTimestampBehavior: ServerTimestampBehavior =
51+
.none,
52+
decoder: Firestore.Decoder = .init(),
53+
completion: @escaping (Result<T, Error>) -> Void) {
54+
getDocument { snapshot, error in
55+
guard let snapshot = snapshot else {
56+
/**
57+
* Force unwrapping here is fine since this logic corresponds to the auto-synthesized
58+
* async/await wrappers for Objective-C functions with callbacks taking an object and an error
59+
* parameter. The API should (and does) guarantee that either object or error is set, but never both.
60+
* For more details see:
61+
* https://github.com/firebase/firebase-ios-sdk/pull/9101#discussion_r809117034
62+
*/
63+
completion(.failure(error!))
64+
return
65+
}
66+
let result = Result {
67+
try snapshot.data(as: T.self,
68+
with: serverTimestampBehavior,
69+
decoder: decoder)
70+
}
71+
completion(result)
72+
}
73+
}
74+
75+
#if compiler(>=5.5) && canImport(_Concurrency)
76+
/// Fetches and decodes the document referenced by this `DocumentReference`.
77+
///
78+
/// This allows users to retrieve a Firestore document and have it decoded
79+
/// to an instance of caller-specified type as follows:
80+
/// ```swift
81+
/// do {
82+
/// let book = try await ref.getDocument(as: Book.self)
83+
/// } catch {
84+
/// // Handle error
85+
/// }
86+
/// ```
87+
///
88+
/// This method attempts to provide up-to-date data when possible by waiting
89+
/// for data from the server, but it may return cached data or fail if you
90+
/// are offline and the server cannot be reached. If `T` denotes
91+
/// an optional type, the method returns a successful status with a value
92+
/// of `nil` for non-existing documents.
93+
///
94+
/// - Parameters:
95+
/// - as: A `Decodable` type to convert the document fields to.
96+
/// - serverTimestampBehavior: Configures how server timestamps that have
97+
/// not yet been set to their final value are returned from the
98+
/// snapshot.
99+
/// - decoder: The decoder to use to convert the document. Defaults to use
100+
/// the default decoder.
101+
/// - Returns: This instance of the supplied `Decodable` type `T`.
102+
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
103+
func getDocument<T: Decodable>(as type: T.Type,
104+
with serverTimestampBehavior: ServerTimestampBehavior =
105+
.none,
106+
decoder: Firestore.Decoder = .init()) async throws -> T {
107+
let snapshot = try await getDocument()
108+
return try snapshot.data(as: T.self,
109+
with: serverTimestampBehavior,
110+
decoder: decoder)
111+
}
112+
#endif
113+
}

Firestore/Swift/Source/Codable/DocumentReference+WriteEncodable.swift

+6-6
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ public extension DocumentReference {
2727
/// - Parameters:
2828
/// - value: An instance of `Encodable` to be encoded to a document.
2929
/// - encoder: An encoder instance to use to run the encoding.
30-
/// - completion: A block to execute once the document has been successfully
31-
/// written to the server. This block will not be called while
30+
/// - completion: A closure to execute once the document has been successfully
31+
/// written to the server. This closure will not be called while
3232
/// the client is offline, though local changes will be visible
3333
/// immediately.
3434
func setData<T: Encodable>(from value: T,
@@ -49,8 +49,8 @@ public extension DocumentReference {
4949
/// - merge: Whether to merge the provided `Encodable` into any existing
5050
/// document.
5151
/// - encoder: An encoder instance to use to run the encoding.
52-
/// - completion: A block to execute once the document has been successfully
53-
/// written to the server. This block will not be called while
52+
/// - completion: A closure to execute once the document has been successfully
53+
/// written to the server. This closure will not be called while
5454
/// the client is offline, though local changes will be visible
5555
/// immediately.
5656
func setData<T: Encodable>(from value: T,
@@ -76,8 +76,8 @@ public extension DocumentReference {
7676
/// merge. Fields can contain dots to reference nested fields within the
7777
/// document.
7878
/// - encoder: An encoder instance to use to run the encoding.
79-
/// - completion: A block to execute once the document has been successfully
80-
/// written to the server. This block will not be called while
79+
/// - completion: A closure to execute once the document has been successfully
80+
/// written to the server. This closure will not be called while
8181
/// the client is offline, though local changes will be visible
8282
/// immediately.
8383
func setData<T: Encodable>(from value: T,

Firestore/Swift/Source/Codable/DocumentSnapshot+ReadDecodable.swift

+7-10
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,24 @@ import FirebaseFirestore
1919

2020
public extension DocumentSnapshot {
2121
/// Retrieves all fields in a document and converts them to an instance of
22-
/// caller-specified type. Returns `nil` if the document does not exist.
22+
/// caller-specified type.
2323
///
2424
/// By default, server-provided timestamps that have not yet been set to their
2525
/// final value will be returned as `NSNull`. Pass `serverTimestampBehavior`
26-
/// configure this behavior.
26+
/// to configure this behavior.
2727
///
2828
/// See `Firestore.Decoder` for more details about the decoding process.
2929
///
3030
/// - Parameters
3131
/// - type: The type to convert the document fields to.
3232
/// - serverTimestampBehavior: Configures how server timestamps that have
3333
/// not yet been set to their final value are returned from the snapshot.
34-
/// - decoder: The decoder to use to convert the document. `nil` to use
35-
/// default decoder.
34+
/// - decoder: The decoder to use to convert the document. Defaults to use
35+
/// the default decoder.
3636
func data<T: Decodable>(as type: T.Type,
3737
with serverTimestampBehavior: ServerTimestampBehavior = .none,
38-
decoder: Firestore.Decoder? = nil) throws -> T? {
39-
let d = decoder ?? Firestore.Decoder()
40-
if let data = data(with: serverTimestampBehavior) {
41-
return try d.decode(T.self, from: data, in: reference)
42-
}
43-
return nil
38+
decoder: Firestore.Decoder = .init()) throws -> T {
39+
let data: Any = data(with: serverTimestampBehavior) ?? NSNull()
40+
return try decoder.decode(T.self, from: data, in: reference)
4441
}
4542
}

Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift

+17-17
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
9696

9797
let readAfterWrite = try readDocument(forRef: docToWrite).data(as: Model.self)
9898

99-
XCTAssertEqual(readAfterWrite!, model, "Failed with flavor \(flavor)")
99+
XCTAssertEqual(readAfterWrite, model, "Failed with flavor \(flavor)")
100100
}
101101
}
102102

@@ -113,8 +113,8 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
113113

114114
let decoded = try readDocument(forRef: docToWrite).data(as: Model.self)
115115

116-
XCTAssertNotNil(decoded?.ts, "Failed with flavor \(flavor)")
117-
if let ts = decoded?.ts {
116+
XCTAssertNotNil(decoded.ts, "Failed with flavor \(flavor)")
117+
if let ts = decoded.ts {
118118
XCTAssertGreaterThan(ts.seconds, 1_500_000_000, "Failed with flavor \(flavor)")
119119
} else {
120120
XCTFail("Expect server timestamp is set, but getting .pending")
@@ -145,17 +145,17 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
145145

146146
let snapshot = readDocument(forRef: docToWrite)
147147
var decoded = try snapshot.data(as: Model.self, with: .none)
148-
XCTAssertNil(decoded?.ts)
148+
XCTAssertNil(decoded.ts)
149149

150150
decoded = try snapshot.data(as: Model.self, with: .estimate)
151-
XCTAssertNotNil(decoded?.ts)
152-
XCTAssertNotNil(decoded?.ts?.seconds)
153-
XCTAssertGreaterThanOrEqual(decoded!.ts!.seconds, now)
151+
XCTAssertNotNil(decoded.ts)
152+
XCTAssertNotNil(decoded.ts?.seconds)
153+
XCTAssertGreaterThanOrEqual(decoded.ts!.seconds, now)
154154

155155
decoded = try snapshot.data(as: Model.self, with: .previous)
156-
XCTAssertNotNil(decoded?.ts)
157-
XCTAssertNotNil(decoded?.ts?.seconds)
158-
XCTAssertEqual(decoded!.ts!.seconds, pastTimestamp.seconds)
156+
XCTAssertNotNil(decoded.ts)
157+
XCTAssertNotNil(decoded.ts?.seconds)
158+
XCTAssertEqual(decoded.ts!.seconds, pastTimestamp.seconds)
159159

160160
enableNetwork()
161161
awaitExpectations()
@@ -230,7 +230,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
230230

231231
// Decoded result has "docId" auto-populated.
232232
let decoded = try readDocument(forRef: docToWrite).data(as: Model.self)
233-
XCTAssertEqual(decoded!, Model(name: "name", docId: docToWrite))
233+
XCTAssertEqual(decoded, Model(name: "name", docId: docToWrite))
234234
}
235235

236236
func testSelfDocumentIDWithCustomCodable() throws {
@@ -277,7 +277,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
277277

278278
// Decoded result has "docId" auto-populated.
279279
let decoded = try readDocument(forRef: docToWrite).data(as: Model.self)
280-
XCTAssertEqual(decoded!, Model(name: "name", docId: docToWrite))
280+
XCTAssertEqual(decoded, Model(name: "name", docId: docToWrite))
281281
}
282282

283283
func testSetThenMerge() throws {
@@ -298,18 +298,18 @@ class CodableIntegrationTests: FSTIntegrationTestCase {
298298

299299
var readAfterUpdate = try readDocument(forRef: docToWrite).data(as: Model.self)
300300

301-
XCTAssertEqual(readAfterUpdate!, Model(name: "test",
302-
age: 43, hobby: "No"), "Failed with flavor \(flavor)")
301+
XCTAssertEqual(readAfterUpdate, Model(name: "test",
302+
age: 43, hobby: "No"), "Failed with flavor \(flavor)")
303303

304304
let newUpdate = Model(name: "xxxx", age: 10, hobby: "Play")
305305
// Note 'name' is not updated.
306306
try setData(from: newUpdate, forDocument: docToWrite, withFlavor: flavor,
307307
mergeFields: ["age", FieldPath(["hobby"])])
308308

309309
readAfterUpdate = try readDocument(forRef: docToWrite).data(as: Model.self)
310-
XCTAssertEqual(readAfterUpdate!, Model(name: "test",
311-
age: 10,
312-
hobby: "Play"), "Failed with flavor \(flavor)")
310+
XCTAssertEqual(readAfterUpdate, Model(name: "test",
311+
age: 10,
312+
hobby: "Play"), "Failed with flavor \(flavor)")
313313
}
314314
}
315315

Firestore/third_party/FirestoreEncoder/FirestoreDecoder.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public extension Firestore {
3838
/// decoded.
3939
/// - Returns: An instance of specified type by the first parameter.
4040
public func decode<T: Decodable>(_: T.Type,
41-
from container: [String: Any],
41+
from container: Any,
4242
in document: DocumentReference? = nil) throws -> T {
4343
let decoder = _FirestoreDecoder(referencing: container)
4444
if let doc = document {

0 commit comments

Comments
 (0)