Skip to content

Commit

Permalink
Require explicit typing for DocumentSnapshot decoding. DocumentRefere…
Browse files Browse the repository at this point in the history
…nce decoding. (#9101)
  • Loading branch information
mortenbekditlevsen committed Feb 23, 2022
1 parent fe1b04d commit a1dde8e
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 34 deletions.
113 changes: 113 additions & 0 deletions Firestore/Swift/Source/Codable/DocumentReference+ReadDecodable.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
* Copyright 2021 Google LLC
*
* 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

public extension DocumentReference {
/// Fetches and decodes the document referenced by this `DocumentReference`.
///
/// This allows users to retrieve a Firestore document and have it decoded to
/// an instance of caller-specified type as follows:
/// ```swift
/// ref.getDocument(as: Book.self) { result in
/// do {
/// let book = try result.get()
/// } catch {
/// // Handle error
/// }
/// }
/// ```
///
/// This method attempts to provide up-to-date data when possible by waiting
/// for data from the server, but it may return cached data or fail if you are
/// offline and the server cannot be reached. If `T` denotes an optional
/// type, the method returns a successful status with a value of `nil` for
/// non-existing documents.
///
/// - Parameters:
/// - as: A `Decodable` type to convert the document fields to.
/// - serverTimestampBehavior: Configures how server timestamps that have
/// not yet been set to their final value are returned from the snapshot.
/// - decoder: The decoder to use to convert the document. Defaults to use
/// the default decoder.
/// - completion: The closure to call when the document snapshot has been
/// fetched and decoded.
func getDocument<T: Decodable>(as type: T.Type,
with serverTimestampBehavior: ServerTimestampBehavior =
.none,
decoder: Firestore.Decoder = .init(),
completion: @escaping (Result<T, Error>) -> Void) {
getDocument { snapshot, error in
guard let snapshot = snapshot else {
/**
* Force unwrapping here is fine since this logic corresponds to the auto-synthesized
* async/await wrappers for Objective-C functions with callbacks taking an object and an error
* parameter. The API should (and does) guarantee that either object or error is set, but never both.
* For more details see:
* https://github.com/firebase/firebase-ios-sdk/pull/9101#discussion_r809117034
*/
completion(.failure(error!))
return
}
let result = Result {
try snapshot.data(as: T.self,
with: serverTimestampBehavior,
decoder: decoder)
}
completion(result)
}
}

#if compiler(>=5.5) && canImport(_Concurrency)
/// Fetches and decodes the document referenced by this `DocumentReference`.
///
/// This allows users to retrieve a Firestore document and have it decoded
/// to an instance of caller-specified type as follows:
/// ```swift
/// do {
/// let book = try await ref.getDocument(as: Book.self)
/// } catch {
/// // Handle error
/// }
/// ```
///
/// This method attempts to provide up-to-date data when possible by waiting
/// for data from the server, but it may return cached data or fail if you
/// are offline and the server cannot be reached. If `T` denotes
/// an optional type, the method returns a successful status with a value
/// of `nil` for non-existing documents.
///
/// - Parameters:
/// - as: A `Decodable` type to convert the document fields to.
/// - serverTimestampBehavior: Configures how server timestamps that have
/// not yet been set to their final value are returned from the
/// snapshot.
/// - decoder: The decoder to use to convert the document. Defaults to use
/// the default decoder.
/// - Returns: This instance of the supplied `Decodable` type `T`.
@available(iOS 15, tvOS 15, macOS 12, watchOS 8, *)
func getDocument<T: Decodable>(as type: T.Type,
with serverTimestampBehavior: ServerTimestampBehavior =
.none,
decoder: Firestore.Decoder = .init()) async throws -> T {
let snapshot = try await getDocument()
return try snapshot.data(as: T.self,
with: serverTimestampBehavior,
decoder: decoder)
}
#endif
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ public extension DocumentReference {
/// - Parameters:
/// - value: An instance of `Encodable` to be encoded to a document.
/// - encoder: An encoder instance to use to run the encoding.
/// - completion: A block to execute once the document has been successfully
/// written to the server. This block will not be called while
/// - completion: A closure to execute once the document has been successfully
/// written to the server. This closure will not be called while
/// the client is offline, though local changes will be visible
/// immediately.
func setData<T: Encodable>(from value: T,
Expand All @@ -49,8 +49,8 @@ public extension DocumentReference {
/// - merge: Whether to merge the provided `Encodable` into any existing
/// document.
/// - encoder: An encoder instance to use to run the encoding.
/// - completion: A block to execute once the document has been successfully
/// written to the server. This block will not be called while
/// - completion: A closure to execute once the document has been successfully
/// written to the server. This closure will not be called while
/// the client is offline, though local changes will be visible
/// immediately.
func setData<T: Encodable>(from value: T,
Expand All @@ -76,8 +76,8 @@ public extension DocumentReference {
/// merge. Fields can contain dots to reference nested fields within the
/// document.
/// - encoder: An encoder instance to use to run the encoding.
/// - completion: A block to execute once the document has been successfully
/// written to the server. This block will not be called while
/// - completion: A closure to execute once the document has been successfully
/// written to the server. This closure will not be called while
/// the client is offline, though local changes will be visible
/// immediately.
func setData<T: Encodable>(from value: T,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,24 @@ import FirebaseFirestore

public extension DocumentSnapshot {
/// Retrieves all fields in a document and converts them to an instance of
/// caller-specified type. Returns `nil` if the document does not exist.
/// caller-specified type.
///
/// By default, server-provided timestamps that have not yet been set to their
/// final value will be returned as `NSNull`. Pass `serverTimestampBehavior`
/// configure this behavior.
/// to configure this behavior.
///
/// See `Firestore.Decoder` for more details about the decoding process.
///
/// - Parameters
/// - type: The type to convert the document fields to.
/// - serverTimestampBehavior: Configures how server timestamps that have
/// not yet been set to their final value are returned from the snapshot.
/// - decoder: The decoder to use to convert the document. `nil` to use
/// default decoder.
/// - decoder: The decoder to use to convert the document. Defaults to use
/// the default decoder.
func data<T: Decodable>(as type: T.Type,
with serverTimestampBehavior: ServerTimestampBehavior = .none,
decoder: Firestore.Decoder? = nil) throws -> T? {
let d = decoder ?? Firestore.Decoder()
if let data = data(with: serverTimestampBehavior) {
return try d.decode(T.self, from: data, in: reference)
}
return nil
decoder: Firestore.Decoder = .init()) throws -> T {
let data: Any = data(with: serverTimestampBehavior) ?? NSNull()
return try decoder.decode(T.self, from: data, in: reference)
}
}
34 changes: 17 additions & 17 deletions Firestore/Swift/Tests/Integration/CodableIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {

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

XCTAssertEqual(readAfterWrite!, model, "Failed with flavor \(flavor)")
XCTAssertEqual(readAfterWrite, model, "Failed with flavor \(flavor)")
}
}

Expand All @@ -113,8 +113,8 @@ class CodableIntegrationTests: FSTIntegrationTestCase {

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

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

let snapshot = readDocument(forRef: docToWrite)
var decoded = try snapshot.data(as: Model.self, with: .none)
XCTAssertNil(decoded?.ts)
XCTAssertNil(decoded.ts)

decoded = try snapshot.data(as: Model.self, with: .estimate)
XCTAssertNotNil(decoded?.ts)
XCTAssertNotNil(decoded?.ts?.seconds)
XCTAssertGreaterThanOrEqual(decoded!.ts!.seconds, now)
XCTAssertNotNil(decoded.ts)
XCTAssertNotNil(decoded.ts?.seconds)
XCTAssertGreaterThanOrEqual(decoded.ts!.seconds, now)

decoded = try snapshot.data(as: Model.self, with: .previous)
XCTAssertNotNil(decoded?.ts)
XCTAssertNotNil(decoded?.ts?.seconds)
XCTAssertEqual(decoded!.ts!.seconds, pastTimestamp.seconds)
XCTAssertNotNil(decoded.ts)
XCTAssertNotNil(decoded.ts?.seconds)
XCTAssertEqual(decoded.ts!.seconds, pastTimestamp.seconds)

enableNetwork()
awaitExpectations()
Expand Down Expand Up @@ -230,7 +230,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {

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

func testSelfDocumentIDWithCustomCodable() throws {
Expand Down Expand Up @@ -277,7 +277,7 @@ class CodableIntegrationTests: FSTIntegrationTestCase {

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

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

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

XCTAssertEqual(readAfterUpdate!, Model(name: "test",
age: 43, hobby: "No"), "Failed with flavor \(flavor)")
XCTAssertEqual(readAfterUpdate, Model(name: "test",
age: 43, hobby: "No"), "Failed with flavor \(flavor)")

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

readAfterUpdate = try readDocument(forRef: docToWrite).data(as: Model.self)
XCTAssertEqual(readAfterUpdate!, Model(name: "test",
age: 10,
hobby: "Play"), "Failed with flavor \(flavor)")
XCTAssertEqual(readAfterUpdate, Model(name: "test",
age: 10,
hobby: "Play"), "Failed with flavor \(flavor)")
}
}

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

0 comments on commit a1dde8e

Please sign in to comment.