mirror of
https://github.com/firebase/firebase-ios-sdk
synced 2026-11-11 22:55:59 +05:45
* Rewrite SelfDocumentID as a property wrapper Make wrapped types conform to DocumentReferenceConvertible and use that protocol to implement the conversion. * Change DocumentReferenceConvertible to DocumentIDWrappable * Make ServerTimestampWrappable use static functions Use the same form as DocumentIDWrappable. * Update extension String: ServerTimestampWrappable * Use #if compiler, not #if swift The FirebaseFirestoreSwift module is declared as conforming to Swift 4.0 so #if swift is still false, even when compiling with Swift 5.1. Using #if compiler will see the actual compiler version. Property wrappers don't depend on runtime features, so this works. Also, allow Wrappable protocols to throw during conversions.
749 lines
22 KiB
Swift
749 lines
22 KiB
Swift
/*
|
|
* 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
|
|
import FirebaseFirestoreSwift
|
|
import XCTest
|
|
|
|
class FirestoreEncoderTests: XCTestCase {
|
|
func testInt() {
|
|
struct Model: Codable, Equatable {
|
|
let x: Int
|
|
}
|
|
let model = Model(x: 42)
|
|
let dict = ["x": 42]
|
|
assertThat(model).roundTrips(to: dict)
|
|
}
|
|
|
|
func testEmpty() {
|
|
struct Model: Codable, Equatable {}
|
|
assertThat(Model()).roundTrips(to: [String: Any]())
|
|
}
|
|
|
|
func testString() throws {
|
|
struct Model: Codable, Equatable {
|
|
let s: String
|
|
}
|
|
assertThat(Model(s: "abc")).roundTrips(to: ["s": "abc"])
|
|
}
|
|
|
|
func testOptional() {
|
|
struct Model: Codable, Equatable {
|
|
let x: Int
|
|
let opt: Int?
|
|
}
|
|
assertThat(Model(x: 42, opt: nil)).roundTrips(to: ["x": 42])
|
|
assertThat(Model(x: 42, opt: 7)).roundTrips(to: ["x": 42, "opt": 7])
|
|
assertThat(["x": 42, "opt": 5]).decodes(to: Model(x: 42, opt: 5))
|
|
assertThat(["x": 42, "opt": true]).failsDecoding(to: Model.self)
|
|
assertThat(["x": 42, "opt": "abc"]).failsDecoding(to: Model.self)
|
|
assertThat(["x": 45.55, "opt": 5]).failsDecoding(to: Model.self)
|
|
assertThat(["opt": 5]).failsDecoding(to: Model.self)
|
|
|
|
// 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 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, Equatable {
|
|
let x: Int
|
|
let e: MyEnum
|
|
}
|
|
|
|
assertThat(Model(x: 42, e: MyEnum.num(number: 4)))
|
|
.roundTrips(to: ["x": 42, "e": ["num": 4]])
|
|
|
|
assertThat(Model(x: 43, e: MyEnum.text("abc")))
|
|
.roundTrips(to: ["x": 43, "e": ["text": "abc"]])
|
|
|
|
let timestamp = Timestamp(date: Date())
|
|
assertThat(Model(x: 43, e: MyEnum.timestamp(timestamp)))
|
|
.roundTrips(to: ["x": 43, "e": ["timestamp": timestamp]])
|
|
}
|
|
|
|
func testGeoPoint() {
|
|
struct Model: Codable, Equatable {
|
|
let p: GeoPoint
|
|
}
|
|
let geopoint = GeoPoint(latitude: 1, longitude: -2)
|
|
assertThat(Model(p: geopoint)).roundTrips(to: ["p": geopoint])
|
|
}
|
|
|
|
func testDate() {
|
|
struct Model: Codable, Equatable {
|
|
let date: Date
|
|
}
|
|
let date = Date(timeIntervalSinceReferenceDate: 0)
|
|
assertThat(Model(date: date)).roundTrips(to: ["date": date])
|
|
}
|
|
|
|
func testTimestampCanDecodeAsDate() {
|
|
struct EncodingModel: Codable, Equatable {
|
|
let date: Timestamp
|
|
}
|
|
struct DecodingModel: Codable, Equatable {
|
|
let date: Date
|
|
}
|
|
|
|
let date = Date(timeIntervalSinceReferenceDate: 0)
|
|
let timestamp = Timestamp(date: date)
|
|
assertThat(EncodingModel(date: timestamp))
|
|
.encodes(to: ["date": timestamp])
|
|
.decodes(to: DecodingModel(date: date))
|
|
}
|
|
|
|
func testDocumentReference() {
|
|
struct Model: Codable, Equatable {
|
|
let doc: DocumentReference
|
|
}
|
|
let d = FSTTestDocRef("abc/xyz")
|
|
assertThat(Model(doc: d)).roundTrips(to: ["doc": d])
|
|
}
|
|
|
|
func testEncodingDocumentReferenceThrowsWithJSONEncoder() {
|
|
assertThat(FSTTestDocRef("abc/xyz")).failsEncodingWithJSONEncoder()
|
|
}
|
|
|
|
func testEncodingDocumentReferenceNotEmbeddedThrows() {
|
|
assertThat(FSTTestDocRef("abc/xyz")).failsEncodingAtTopLevel()
|
|
}
|
|
|
|
func testTimestamp() {
|
|
struct Model: Codable, Equatable {
|
|
let timestamp: Timestamp
|
|
}
|
|
let t = Timestamp(date: Date())
|
|
assertThat(Model(timestamp: t)).roundTrips(to: ["timestamp": t])
|
|
}
|
|
|
|
func testBadValue() {
|
|
struct Model: Codable, Equatable {
|
|
let x: Int
|
|
}
|
|
assertThat(["x": "abc"]).failsDecoding(to: Model.self) // Wrong type
|
|
}
|
|
|
|
func testValueTooBig() {
|
|
struct Model: Codable, Equatable {
|
|
let x: CChar
|
|
}
|
|
assertThat(Model(x: 42)).roundTrips(to: ["x": 42])
|
|
assertThat(["x": 12345]).failsDecoding(to: Model.self) // Overflow
|
|
}
|
|
|
|
// 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, Equatable {
|
|
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,
|
|
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,
|
|
"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]
|
|
|
|
assertThat(model).roundTrips(to: dict)
|
|
}
|
|
|
|
func testCodingKeysCanCustomizeEncodingAndDecoding() throws {
|
|
struct Model: Codable, Equatable {
|
|
var s: String
|
|
var ms: String = "filler"
|
|
var d: Double
|
|
var md: Double = 42.42
|
|
|
|
// Use CodingKeys to only encode part of the struct.
|
|
enum CodingKeys: String, CodingKey {
|
|
case s
|
|
case d
|
|
}
|
|
}
|
|
|
|
assertThat(Model(s: "abc", ms: "dummy", d: 123.3, md: 0))
|
|
.encodes(to: ["s": "abc", "d": 123.3])
|
|
.decodes(to: Model(s: "abc", ms: "filler", d: 123.3, md: 42.42))
|
|
}
|
|
|
|
func testNestedObjects() {
|
|
struct SecondLevelNestedModel: Codable, Equatable {
|
|
var age: Int8
|
|
var weight: Double
|
|
}
|
|
struct NestedModel: Codable, Equatable {
|
|
var group: String
|
|
var groupList: [SecondLevelNestedModel]
|
|
var groupMap: [String: SecondLevelNestedModel]
|
|
var point: GeoPoint
|
|
}
|
|
struct Model: Codable, Equatable {
|
|
var id: Int64
|
|
var group: NestedModel
|
|
}
|
|
|
|
let model = Model(
|
|
id: 123,
|
|
group: NestedModel(
|
|
group: "g1",
|
|
groupList: [
|
|
SecondLevelNestedModel(age: 20, weight: 80.1),
|
|
SecondLevelNestedModel(age: 25, weight: 85.1),
|
|
],
|
|
groupMap: [
|
|
"name1": SecondLevelNestedModel(age: 30, weight: 64.2),
|
|
"name2": SecondLevelNestedModel(age: 35, weight: 79.2),
|
|
],
|
|
point: GeoPoint(latitude: 12.0, longitude: 9.1)
|
|
)
|
|
)
|
|
|
|
let dict = [
|
|
"group": [
|
|
"group": "g1",
|
|
"point": GeoPoint(latitude: 12.0, longitude: 9.1),
|
|
"groupList": [
|
|
[
|
|
"age": 20,
|
|
"weight": 80.1,
|
|
],
|
|
[
|
|
"age": 25,
|
|
"weight": 85.1,
|
|
],
|
|
],
|
|
"groupMap": [
|
|
"name1": [
|
|
"age": 30,
|
|
"weight": 64.2,
|
|
],
|
|
"name2": [
|
|
"age": 35,
|
|
"weight": 79.2,
|
|
],
|
|
],
|
|
],
|
|
"id": 123,
|
|
] as [String: Any]
|
|
|
|
assertThat(model).roundTrips(to: dict)
|
|
}
|
|
|
|
func testCollapsingNestedObjects() {
|
|
// The model is flat but the document has a nested Map.
|
|
struct Model: Codable, Equatable {
|
|
var id: Int64
|
|
var name: String
|
|
|
|
init(id: Int64, name: String) {
|
|
self.id = id
|
|
self.name = name
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id
|
|
case nested
|
|
}
|
|
|
|
private enum NestedCodingKeys: String, CodingKey {
|
|
case name
|
|
}
|
|
|
|
init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
try id = container.decode(Int64.self, forKey: .id)
|
|
|
|
let nestedContainer = try container.nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
|
|
try name = nestedContainer.decode(String.self, forKey: .name)
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(id, forKey: .id)
|
|
var nestedContainer = container.nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
|
|
try nestedContainer.encode(name, forKey: .name)
|
|
}
|
|
}
|
|
|
|
assertThat(Model(id: 12345, name: "ModelName"))
|
|
.roundTrips(to: [
|
|
"id": 12345,
|
|
"nested": ["name": "ModelName"],
|
|
])
|
|
}
|
|
|
|
class SuperModel: Codable, Equatable {
|
|
var superPower: Double? = 100.0
|
|
var superName: String? = "superName"
|
|
|
|
init(power: Double, name: String) {
|
|
superPower = power
|
|
superName = name
|
|
}
|
|
|
|
static func == (lhs: SuperModel, rhs: SuperModel) -> Bool {
|
|
return (lhs.superName == rhs.superName) && (lhs.superPower == rhs.superPower)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case superPower
|
|
case superName
|
|
}
|
|
|
|
required init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
superPower = try container.decode(Double.self, forKey: .superPower)
|
|
superName = try container.decode(String.self, forKey: .superName)
|
|
}
|
|
|
|
func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(superPower, forKey: .superPower)
|
|
try container.encode(superName, forKey: .superName)
|
|
}
|
|
}
|
|
|
|
class SubModel: SuperModel {
|
|
var timestamp: Timestamp? = Timestamp(seconds: 848_483_737, nanoseconds: 23423)
|
|
|
|
init(power: Double, name: String, seconds: Int64, nano: Int32) {
|
|
super.init(power: power, name: name)
|
|
timestamp = Timestamp(seconds: seconds, nanoseconds: nano)
|
|
}
|
|
|
|
static func == (lhs: SubModel, rhs: SubModel) -> Bool {
|
|
return ((lhs as SuperModel) == (rhs as SuperModel)) && (lhs.timestamp == rhs.timestamp)
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case timestamp
|
|
}
|
|
|
|
required init(from decoder: Decoder) throws {
|
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
|
timestamp = try container.decode(Timestamp.self, forKey: .timestamp)
|
|
try super.init(from: container.superDecoder())
|
|
}
|
|
|
|
override func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
try container.encode(timestamp, forKey: .timestamp)
|
|
try super.encode(to: container.superEncoder())
|
|
}
|
|
}
|
|
|
|
func testClassHierarchy() {
|
|
assertThat(SubModel(power: 100, name: "name", seconds: 123_456_789, nano: 654_321))
|
|
.roundTrips(to: [
|
|
"super": ["superPower": 100, "superName": "name"],
|
|
"timestamp": Timestamp(seconds: 123_456_789, nanoseconds: 654_321),
|
|
])
|
|
}
|
|
|
|
func testEncodingEncodableArrayNotSupported() {
|
|
struct Model: Codable, Equatable {
|
|
var name: String
|
|
}
|
|
assertThat([Model(name: "1")]).failsToEncode()
|
|
}
|
|
|
|
func testFieldValuePassthrough() throws {
|
|
struct Model: Encodable, Equatable {
|
|
var fieldValue: FieldValue
|
|
}
|
|
assertThat(Model(fieldValue: FieldValue.delete()))
|
|
.encodes(to: ["fieldValue": FieldValue.delete()])
|
|
}
|
|
|
|
func testEncodingFieldValueNotEmbeddedThrows() {
|
|
let ts = FieldValue.serverTimestamp()
|
|
assertThat(ts).failsEncodingAtTopLevel()
|
|
}
|
|
|
|
#if swift(>=5.1)
|
|
func testServerTimestamp() throws {
|
|
struct Model: Codable, Equatable {
|
|
@ServerTimestamp var timestamp: Timestamp? = nil
|
|
}
|
|
|
|
// Encoding a pending server timestamp
|
|
assertThat(Model())
|
|
.encodes(to: ["timestamp": FieldValue.serverTimestamp()])
|
|
|
|
// Encoding a resolved server timestamp yields a timestamp; decoding
|
|
// yields it back.
|
|
let timestamp = Timestamp(seconds: 123_456_789, nanoseconds: 4321)
|
|
assertThat(Model(timestamp: timestamp))
|
|
.roundTrips(to: ["timestamp": timestamp])
|
|
|
|
// Decoding a NSNull() leads to nil.
|
|
assertThat(["timestamp": NSNull()])
|
|
.decodes(to: Model(timestamp: nil))
|
|
}
|
|
|
|
func testServerTimestampUserType() throws {
|
|
struct Model: Codable, Equatable {
|
|
@ServerTimestamp var timestamp: String? = nil
|
|
}
|
|
|
|
// Encoding a pending server timestamp
|
|
assertThat(Model())
|
|
.encodes(to: ["timestamp": FieldValue.serverTimestamp()])
|
|
|
|
// Encoding a resolved server timestamp yields a timestamp; decoding
|
|
// yields it back.
|
|
let timestamp = Timestamp(seconds: 1_570_484_031, nanoseconds: 122_999_906)
|
|
assertThat(Model(timestamp: "2019-10-07T21:33:51.123Z"))
|
|
.roundTrips(to: ["timestamp": timestamp])
|
|
|
|
assertThat(Model(timestamp: "Invalid date"))
|
|
.failsToEncode()
|
|
}
|
|
#endif // swift(>=5.1)
|
|
|
|
@available(swift, deprecated: 5.1)
|
|
func testSwift4ServerTimestamp() throws {
|
|
struct Model: Codable, Equatable {
|
|
var timestamp: Swift4ServerTimestamp
|
|
}
|
|
|
|
// Encoding a pending server timestamp
|
|
assertThat(Model(timestamp: .pending))
|
|
.encodes(to: ["timestamp": FieldValue.serverTimestamp()])
|
|
|
|
// Encoding a resolved server timestamp yields a timestamp; decoding yields
|
|
// it back.
|
|
let timestamp = Timestamp(seconds: 123_456_789, nanoseconds: 4321)
|
|
assertThat(Model(timestamp: .resolved(timestamp)))
|
|
.roundTrips(to: ["timestamp": timestamp])
|
|
|
|
// Decoding a NSNull() leads to nil.
|
|
assertThat(["timestamp": NSNull()])
|
|
.decodes(to: Model(timestamp: .pending))
|
|
}
|
|
|
|
#if swift(>=5.1)
|
|
func testExplicitNull() throws {
|
|
struct Model: Codable, Equatable {
|
|
@ExplicitNull var name: String?
|
|
}
|
|
|
|
assertThat(Model(name: nil))
|
|
.roundTrips(to: ["name": NSNull()])
|
|
|
|
assertThat(Model(name: "good name"))
|
|
.roundTrips(to: ["name": "good name"])
|
|
}
|
|
#endif // swift(>=5.1)
|
|
|
|
@available(swift, deprecated: 5.1)
|
|
func testSwift4ExplicitNull() throws {
|
|
struct Model: Codable, Equatable {
|
|
var name: Swift4ExplicitNull<String>
|
|
}
|
|
|
|
assertThat(Model(name: .none))
|
|
.roundTrips(to: ["name": NSNull()])
|
|
|
|
assertThat(Model(name: .some("good name")))
|
|
.roundTrips(to: ["name": "good name"])
|
|
}
|
|
|
|
#if swift(>=5.1)
|
|
func testAutomaticallyPopulatesDocumentIDOnDocumentReference() throws {
|
|
struct Model: Codable, Equatable {
|
|
var name: String
|
|
@DocumentID var docId: DocumentReference?
|
|
}
|
|
assertThat(["name": "abc"], in: "abc/123")
|
|
.decodes(to: Model(name: "abc", docId: FSTTestDocRef("abc/123")))
|
|
}
|
|
|
|
func testAutomaticallyPopulatesDocumentIDOnString() throws {
|
|
struct Model: Codable, Equatable {
|
|
var name: String
|
|
@DocumentID var docId: String?
|
|
}
|
|
assertThat(["name": "abc"], in: "abc/123")
|
|
.decodes(to: Model(name: "abc", docId: "123"))
|
|
}
|
|
|
|
func testDocumentIDIgnoredInEncoding() throws {
|
|
struct Model: Codable, Equatable {
|
|
var name: String
|
|
@DocumentID var docId: DocumentReference?
|
|
}
|
|
assertThat(Model(name: "abc", docId: FSTTestDocRef("abc/123")))
|
|
.encodes(to: ["name": "abc"])
|
|
}
|
|
|
|
func testDocumentIDWithJsonEncoderThrows() {
|
|
assertThat(DocumentID(wrappedValue: FSTTestDocRef("abc/xyz")))
|
|
.failsEncodingWithJSONEncoder()
|
|
}
|
|
|
|
func testDecodingDocumentIDWithConfictingFieldsThrows() throws {
|
|
struct Model: Codable, Equatable {
|
|
var name: String
|
|
@DocumentID var docId: DocumentReference?
|
|
}
|
|
|
|
do {
|
|
_ = try Firestore.Decoder().decode(
|
|
Model.self,
|
|
from: ["name": "abc", "docId": "Causing conflict"],
|
|
in: FSTTestDocRef("abc/123")
|
|
)
|
|
XCTFail("Failed to throw")
|
|
} catch let FirestoreDecodingError.fieldNameConfict(msg) {
|
|
XCTAssertEqual(msg, "Field name [\"docId\"] was found from document \"abc/123\", "
|
|
+ "cannot assign the document reference to this field.")
|
|
return
|
|
} catch {
|
|
XCTFail("Unrecognized error: \(error)")
|
|
}
|
|
}
|
|
#endif // swift(>=5.1)
|
|
}
|
|
|
|
private func assertThat(_ dictionary: [String: Any], in document: String? = nil, file: StaticString = #file, line: UInt = #line) -> DictionarySubject {
|
|
return DictionarySubject(dictionary, in: document, file: file, line: line)
|
|
}
|
|
|
|
private func assertThat<X: Equatable & Codable>(_ model: X, file: StaticString = #file, line: UInt = #line) -> CodableSubject<X> {
|
|
return CodableSubject(model, file: file, line: line)
|
|
}
|
|
|
|
private func assertThat<X: Equatable & Encodable>(_ model: X, file: StaticString = #file, line: UInt = #line) -> EncodableSubject<X> {
|
|
return EncodableSubject(model, file: file, line: line)
|
|
}
|
|
|
|
private class EncodableSubject<X: Equatable & Encodable> {
|
|
var subject: X
|
|
var file: StaticString
|
|
var line: UInt
|
|
|
|
init(_ subject: X, file: StaticString, line: UInt) {
|
|
self.subject = subject
|
|
self.file = file
|
|
self.line = line
|
|
}
|
|
|
|
@discardableResult
|
|
func encodes(to expected: [String: Any]) -> DictionarySubject {
|
|
let encoded = assertEncodes(to: expected)
|
|
return DictionarySubject(encoded, file: file, line: line)
|
|
}
|
|
|
|
func failsToEncode() {
|
|
do {
|
|
_ = try Firestore.Encoder().encode(subject)
|
|
} catch {
|
|
return
|
|
}
|
|
XCTFail("Failed to throw")
|
|
}
|
|
|
|
func failsEncodingWithJSONEncoder() {
|
|
do {
|
|
_ = try JSONEncoder().encode(subject)
|
|
XCTFail("Failed to throw", file: file, line: line)
|
|
} catch FirestoreEncodingError.encodingIsNotSupported {
|
|
return
|
|
} catch {
|
|
XCTFail("Unrecognized error: \(error)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
func failsEncodingAtTopLevel() {
|
|
do {
|
|
_ = try Firestore.Encoder().encode(subject)
|
|
XCTFail("Failed to throw", file: file, line: line)
|
|
} catch EncodingError.invalidValue(_, _) {
|
|
return
|
|
} catch {
|
|
XCTFail("Unrecognized error: \(error)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
private func assertEncodes(to expected: [String: Any]) -> [String: Any] {
|
|
do {
|
|
let enc = try Firestore.Encoder().encode(subject)
|
|
XCTAssertEqual(enc as NSDictionary, expected as NSDictionary, file: file, line: line)
|
|
return enc
|
|
} catch {
|
|
XCTFail("Failed to encode \(X.self): error: \(error)")
|
|
return ["": -1]
|
|
}
|
|
}
|
|
}
|
|
|
|
private class CodableSubject<X: Equatable & Codable>: EncodableSubject<X> {
|
|
func roundTrips(to expected: [String: Any]) {
|
|
let reverseSubject = encodes(to: expected)
|
|
reverseSubject.decodes(to: subject)
|
|
}
|
|
}
|
|
|
|
private class DictionarySubject {
|
|
var subject: [String: Any]
|
|
var document: DocumentReference?
|
|
var file: StaticString
|
|
var line: UInt
|
|
|
|
init(_ subject: [String: Any], in documentName: String? = nil, file: StaticString, line: UInt) {
|
|
self.subject = subject
|
|
if let documentName = documentName {
|
|
document = FSTTestDocRef(documentName)
|
|
}
|
|
self.file = file
|
|
self.line = line
|
|
}
|
|
|
|
func decodes<X: Equatable & Codable>(to expected: X) -> Void {
|
|
do {
|
|
let decoded = try Firestore.Decoder().decode(X.self, from: subject, in: document)
|
|
XCTAssertEqual(decoded, expected)
|
|
} catch {
|
|
XCTFail("Failed to decode \(X.self): \(error)", file: file, line: line)
|
|
}
|
|
}
|
|
|
|
func failsDecoding<X: Equatable & Codable>(to _: X.Type) -> Void {
|
|
XCTAssertThrowsError(try Firestore.Decoder().decode(X.self, from: subject), file: file, line: line)
|
|
}
|
|
}
|
|
|
|
#if swift(>=5.1)
|
|
enum DateError: Error {
|
|
case invalidDate(String)
|
|
}
|
|
|
|
// Extends Strings to allow them to be wrapped with @ServerTimestamp. Resolved
|
|
// server timestamps will be stored in an ISO 8601 date format.
|
|
//
|
|
// This example exists outside the main implementation to show that users can
|
|
// extend @ServerTimestamp with arbitrary types.
|
|
extension String: ServerTimestampWrappable {
|
|
static let formatter: DateFormatter = {
|
|
let formatter = DateFormatter()
|
|
formatter.calendar = Calendar(identifier: .iso8601)
|
|
formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
formatter.timeZone = TimeZone(secondsFromGMT: 0)
|
|
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
|
|
return formatter
|
|
}()
|
|
|
|
public static func wrap(_ timestamp: Timestamp) throws -> Self {
|
|
return formatter.string(from: timestamp.dateValue())
|
|
}
|
|
|
|
public static func unwrap(_ value: Self) throws -> Timestamp {
|
|
let date = formatter.date(from: value)
|
|
if let date = date {
|
|
return Timestamp(date: date)
|
|
} else {
|
|
throw DateError.invalidDate(value)
|
|
}
|
|
}
|
|
}
|
|
#endif // swift(>=5.1)
|