FirestoreEncoderTests.swift 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. /*
  2. * Copyright 2019 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. import FirebaseFirestore
  17. import Foundation
  18. import XCTest
  19. class FirestoreEncoderTests: XCTestCase {
  20. func testInt() {
  21. struct Model: Codable, Equatable {
  22. let x: Int
  23. }
  24. let model = Model(x: 42)
  25. let dict = ["x": 42]
  26. assertThat(model).roundTrips(to: dict)
  27. }
  28. func testEmpty() {
  29. struct Model: Codable, Equatable {}
  30. assertThat(Model()).roundTrips(to: [String: Any]())
  31. }
  32. func testString() throws {
  33. struct Model: Codable, Equatable {
  34. let s: String
  35. }
  36. assertThat(Model(s: "abc")).roundTrips(to: ["s": "abc"])
  37. }
  38. func testOptional() {
  39. struct Model: Codable, Equatable {
  40. let x: Int
  41. let opt: Int?
  42. }
  43. assertThat(Model(x: 42, opt: nil)).roundTrips(to: ["x": 42])
  44. assertThat(Model(x: 42, opt: 7)).roundTrips(to: ["x": 42, "opt": 7])
  45. assertThat(["x": 42, "opt": 5]).decodes(to: Model(x: 42, opt: 5))
  46. assertThat(["x": 42, "opt": true]).failsDecoding(to: Model.self)
  47. assertThat(["x": 42, "opt": "abc"]).failsDecoding(to: Model.self)
  48. assertThat(["x": 45.55, "opt": 5]).failsDecoding(to: Model.self)
  49. assertThat(["opt": 5]).failsDecoding(to: Model.self)
  50. // TODO: - handle encoding keys with nil values
  51. // See https://stackoverflow.com/questions/47266862/encode-nil-value-as-null-with-jsonencoder
  52. // and https://bugs.swift.org/browse/SR-9232
  53. // XCTAssertTrue(encodedDict.keys.contains("x"))
  54. }
  55. func testEnum() {
  56. enum MyEnum: Codable, Equatable {
  57. case num(number: Int)
  58. case text(String)
  59. case timestamp(Timestamp)
  60. private enum CodingKeys: String, CodingKey {
  61. case num
  62. case text
  63. case timestamp
  64. }
  65. private enum DecodingError: Error {
  66. case decoding(String)
  67. }
  68. init(from decoder: Decoder) throws {
  69. let values = try decoder.container(keyedBy: CodingKeys.self)
  70. if let value = try? values.decode(Int.self, forKey: .num) {
  71. self = .num(number: value)
  72. return
  73. }
  74. if let value = try? values.decode(String.self, forKey: .text) {
  75. self = .text(value)
  76. return
  77. }
  78. if let value = try? values.decode(Timestamp.self, forKey: .timestamp) {
  79. self = .timestamp(value)
  80. return
  81. }
  82. throw DecodingError.decoding("Decoding error: \(dump(values))")
  83. }
  84. func encode(to encoder: Encoder) throws {
  85. var container = encoder.container(keyedBy: CodingKeys.self)
  86. switch self {
  87. case let .num(number):
  88. try container.encode(number, forKey: .num)
  89. case let .text(value):
  90. try container.encode(value, forKey: .text)
  91. case let .timestamp(stamp):
  92. try container.encode(stamp, forKey: .timestamp)
  93. }
  94. }
  95. }
  96. struct Model: Codable, Equatable {
  97. let x: Int
  98. let e: MyEnum
  99. }
  100. assertThat(Model(x: 42, e: MyEnum.num(number: 4)))
  101. .roundTrips(to: ["x": 42, "e": ["num": 4]])
  102. assertThat(Model(x: 43, e: MyEnum.text("abc")))
  103. .roundTrips(to: ["x": 43, "e": ["text": "abc"]])
  104. let timestamp = Timestamp(date: Date())
  105. assertThat(Model(x: 43, e: MyEnum.timestamp(timestamp)))
  106. .roundTrips(to: ["x": 43, "e": ["timestamp": timestamp]])
  107. }
  108. func testGeoPoint() {
  109. struct Model: Codable, Equatable {
  110. let p: GeoPoint
  111. }
  112. let geopoint = GeoPoint(latitude: 1, longitude: -2)
  113. assertThat(Model(p: geopoint)).roundTrips(to: ["p": geopoint])
  114. }
  115. func testDate() {
  116. struct Model: Codable, Equatable {
  117. let date: Date
  118. }
  119. let date = Date(timeIntervalSinceReferenceDate: 0)
  120. assertThat(Model(date: date)).roundTrips(to: ["date": Timestamp(date: date)])
  121. }
  122. func testTimestampCanDecodeAsDate() {
  123. struct EncodingModel: Codable, Equatable {
  124. let date: Timestamp
  125. }
  126. struct DecodingModel: Codable, Equatable {
  127. let date: Date
  128. }
  129. let date = Date(timeIntervalSinceReferenceDate: 0)
  130. let timestamp = Timestamp(date: date)
  131. assertThat(EncodingModel(date: timestamp))
  132. .encodes(to: ["date": timestamp])
  133. .decodes(to: DecodingModel(date: date))
  134. }
  135. func testDocumentReference() {
  136. struct Model: Codable, Equatable {
  137. let doc: DocumentReference
  138. }
  139. let d = FSTTestDocRef("abc/xyz")
  140. assertThat(Model(doc: d)).roundTrips(to: ["doc": d])
  141. }
  142. func testEncodingDocumentReferenceThrowsWithJSONEncoder() {
  143. assertThat(FSTTestDocRef("abc/xyz")).failsEncodingWithJSONEncoder()
  144. }
  145. func testEncodingDocumentReferenceNotEmbeddedThrows() {
  146. assertThat(FSTTestDocRef("abc/xyz")).failsEncodingAtTopLevel()
  147. }
  148. func testTimestamp() {
  149. struct Model: Codable, Equatable {
  150. let timestamp: Timestamp
  151. }
  152. let t = Timestamp(date: Date())
  153. assertThat(Model(timestamp: t)).roundTrips(to: ["timestamp": t])
  154. }
  155. func testBadValue() {
  156. struct Model: Codable, Equatable {
  157. let x: Int
  158. }
  159. assertThat(["x": "abc"]).failsDecoding(to: Model.self) // Wrong type
  160. }
  161. func testValueTooBig() {
  162. struct Model: Codable, Equatable {
  163. let x: CChar
  164. }
  165. assertThat(Model(x: 42)).roundTrips(to: ["x": 42])
  166. assertThat(["x": 12345]).failsDecoding(to: Model.self) // Overflow
  167. }
  168. // Inspired by https://github.com/firebase/firebase-android-sdk/blob/master/firebase-firestore/src/test/java/com/google/firebase/firestore/util/MapperTest.java
  169. func testBeans() {
  170. struct Model: Codable, Equatable {
  171. let s: String
  172. let d: Double
  173. let f: Float
  174. let l: CLongLong
  175. let i: Int
  176. let b: Bool
  177. let sh: CShort
  178. let byte: CChar
  179. let uchar: CUnsignedChar
  180. let ai: [Int]
  181. let si: [String]
  182. let caseSensitive: String
  183. let casESensitive: String
  184. let casESensitivE: String
  185. }
  186. let model = Model(
  187. s: "abc",
  188. d: 123,
  189. f: -4,
  190. l: 1_234_567_890_123,
  191. i: -4444,
  192. b: false,
  193. sh: 123,
  194. byte: 45,
  195. uchar: 44,
  196. ai: [1, 2, 3, 4],
  197. si: ["abc", "def"],
  198. caseSensitive: "aaa",
  199. casESensitive: "bbb",
  200. casESensitivE: "ccc"
  201. )
  202. let dict = [
  203. "s": "abc",
  204. "d": 123,
  205. "f": -4,
  206. "l": Int64(1_234_567_890_123),
  207. "i": -4444,
  208. "b": false,
  209. "sh": 123,
  210. "byte": 45,
  211. "uchar": 44,
  212. "ai": [1, 2, 3, 4],
  213. "si": ["abc", "def"],
  214. "caseSensitive": "aaa",
  215. "casESensitive": "bbb",
  216. "casESensitivE": "ccc",
  217. ] as [String: Any]
  218. assertThat(model).roundTrips(to: dict)
  219. }
  220. func testCodingKeysCanCustomizeEncodingAndDecoding() throws {
  221. struct Model: Codable, Equatable {
  222. var s: String
  223. var ms: String = "filler"
  224. var d: Double
  225. var md: Double = 42.42
  226. // Use CodingKeys to only encode part of the struct.
  227. enum CodingKeys: String, CodingKey {
  228. case s
  229. case d
  230. }
  231. }
  232. assertThat(Model(s: "abc", ms: "dummy", d: 123.3, md: 0))
  233. .encodes(to: ["s": "abc", "d": 123.3])
  234. .decodes(to: Model(s: "abc", ms: "filler", d: 123.3, md: 42.42))
  235. }
  236. func testNestedObjects() {
  237. struct SecondLevelNestedModel: Codable, Equatable {
  238. var age: Int8
  239. var weight: Double
  240. }
  241. struct NestedModel: Codable, Equatable {
  242. var group: String
  243. var groupList: [SecondLevelNestedModel]
  244. var groupMap: [String: SecondLevelNestedModel]
  245. var point: GeoPoint
  246. }
  247. struct Model: Codable, Equatable {
  248. var id: Int64
  249. var group: NestedModel
  250. }
  251. let model = Model(
  252. id: 123,
  253. group: NestedModel(
  254. group: "g1",
  255. groupList: [
  256. SecondLevelNestedModel(age: 20, weight: 80.1),
  257. SecondLevelNestedModel(age: 25, weight: 85.1),
  258. ],
  259. groupMap: [
  260. "name1": SecondLevelNestedModel(age: 30, weight: 64.2),
  261. "name2": SecondLevelNestedModel(age: 35, weight: 79.2),
  262. ],
  263. point: GeoPoint(latitude: 12.0, longitude: 9.1)
  264. )
  265. )
  266. let dict = [
  267. "group": [
  268. "group": "g1",
  269. "point": GeoPoint(latitude: 12.0, longitude: 9.1),
  270. "groupList": [
  271. [
  272. "age": 20,
  273. "weight": 80.1,
  274. ],
  275. [
  276. "age": 25,
  277. "weight": 85.1,
  278. ],
  279. ],
  280. "groupMap": [
  281. "name1": [
  282. "age": 30,
  283. "weight": 64.2,
  284. ],
  285. "name2": [
  286. "age": 35,
  287. "weight": 79.2,
  288. ],
  289. ],
  290. ],
  291. "id": 123,
  292. ] as [String: Any]
  293. assertThat(model).roundTrips(to: dict)
  294. }
  295. func testCollapsingNestedObjects() {
  296. // The model is flat but the document has a nested Map.
  297. struct Model: Codable, Equatable {
  298. var id: Int64
  299. var name: String
  300. init(id: Int64, name: String) {
  301. self.id = id
  302. self.name = name
  303. }
  304. private enum CodingKeys: String, CodingKey {
  305. case id
  306. case nested
  307. }
  308. private enum NestedCodingKeys: String, CodingKey {
  309. case name
  310. }
  311. init(from decoder: Decoder) throws {
  312. let container = try decoder.container(keyedBy: CodingKeys.self)
  313. try id = container.decode(Int64.self, forKey: .id)
  314. let nestedContainer = try container
  315. .nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
  316. try name = nestedContainer.decode(String.self, forKey: .name)
  317. }
  318. func encode(to encoder: Encoder) throws {
  319. var container = encoder.container(keyedBy: CodingKeys.self)
  320. try container.encode(id, forKey: .id)
  321. var nestedContainer = container
  322. .nestedContainer(keyedBy: NestedCodingKeys.self, forKey: .nested)
  323. try nestedContainer.encode(name, forKey: .name)
  324. }
  325. }
  326. assertThat(Model(id: 12345, name: "ModelName"))
  327. .roundTrips(to: [
  328. "id": 12345,
  329. "nested": ["name": "ModelName"],
  330. ])
  331. }
  332. class SuperModel: Codable, Equatable {
  333. var superPower: Double? = 100.0
  334. var superName: String? = "superName"
  335. init(power: Double, name: String) {
  336. superPower = power
  337. superName = name
  338. }
  339. static func == (lhs: SuperModel, rhs: SuperModel) -> Bool {
  340. return (lhs.superName == rhs.superName) && (lhs.superPower == rhs.superPower)
  341. }
  342. private enum CodingKeys: String, CodingKey {
  343. case superPower
  344. case superName
  345. }
  346. required init(from decoder: Decoder) throws {
  347. let container = try decoder.container(keyedBy: CodingKeys.self)
  348. superPower = try container.decode(Double.self, forKey: .superPower)
  349. superName = try container.decode(String.self, forKey: .superName)
  350. }
  351. func encode(to encoder: Encoder) throws {
  352. var container = encoder.container(keyedBy: CodingKeys.self)
  353. try container.encode(superPower, forKey: .superPower)
  354. try container.encode(superName, forKey: .superName)
  355. }
  356. }
  357. class SubModel: SuperModel {
  358. var timestamp: Timestamp? = Timestamp(seconds: 848_483_737, nanoseconds: 23423)
  359. init(power: Double, name: String, seconds: Int64, nano: Int32) {
  360. super.init(power: power, name: name)
  361. timestamp = Timestamp(seconds: seconds, nanoseconds: nano)
  362. }
  363. static func == (lhs: SubModel, rhs: SubModel) -> Bool {
  364. return ((lhs as SuperModel) == (rhs as SuperModel)) && (lhs.timestamp == rhs.timestamp)
  365. }
  366. private enum CodingKeys: String, CodingKey {
  367. case timestamp
  368. }
  369. required init(from decoder: Decoder) throws {
  370. let container = try decoder.container(keyedBy: CodingKeys.self)
  371. timestamp = try container.decode(Timestamp.self, forKey: .timestamp)
  372. try super.init(from: container.superDecoder())
  373. }
  374. override func encode(to encoder: Encoder) throws {
  375. var container = encoder.container(keyedBy: CodingKeys.self)
  376. try container.encode(timestamp, forKey: .timestamp)
  377. try super.encode(to: container.superEncoder())
  378. }
  379. }
  380. func testClassHierarchy() {
  381. assertThat(SubModel(power: 100, name: "name", seconds: 123_456_789, nano: 654_321))
  382. .roundTrips(to: [
  383. "super": ["superPower": 100, "superName": "name"],
  384. "timestamp": Timestamp(seconds: 123_456_789, nanoseconds: 654_321),
  385. ])
  386. }
  387. func testEncodingEncodableArrayNotSupported() {
  388. struct Model: Codable, Equatable {
  389. var name: String
  390. }
  391. assertThat([Model(name: "1")]).failsToEncode()
  392. }
  393. func testFieldValuePassthrough() throws {
  394. struct Model: Encodable, Equatable {
  395. var fieldValue: FieldValue
  396. }
  397. assertThat(Model(fieldValue: FieldValue.delete()))
  398. .encodes(to: ["fieldValue": FieldValue.delete()])
  399. }
  400. func testEncodingFieldValueNotEmbeddedThrows() {
  401. let ts = FieldValue.serverTimestamp()
  402. assertThat(ts).failsEncodingAtTopLevel()
  403. }
  404. func testServerTimestamp() throws {
  405. struct Model: Codable, Equatable {
  406. @ServerTimestamp var timestamp: Timestamp? = nil
  407. }
  408. // Encoding a pending server timestamp
  409. assertThat(Model())
  410. .encodes(to: ["timestamp": FieldValue.serverTimestamp()])
  411. // Encoding a resolved server timestamp yields a timestamp; decoding
  412. // yields it back.
  413. let timestamp = Timestamp(seconds: 123_456_789, nanoseconds: 4321)
  414. assertThat(Model(timestamp: timestamp))
  415. .roundTrips(to: ["timestamp": timestamp])
  416. // Decoding a NSNull() leads to nil.
  417. assertThat(["timestamp": NSNull()])
  418. .decodes(to: Model(timestamp: nil))
  419. }
  420. func testServerTimestampOfDate() throws {
  421. struct Model: Codable, Equatable {
  422. @ServerTimestamp var date: Date? = nil
  423. }
  424. // Encoding a pending server timestamp
  425. assertThat(Model())
  426. .encodes(to: ["date": FieldValue.serverTimestamp()])
  427. // Encoding a resolved server timestamp yields a timestamp; decoding
  428. // yields it back.
  429. let timestamp = Timestamp(seconds: 123_456_789, nanoseconds: 0)
  430. let date: Date = timestamp.dateValue()
  431. assertThat(Model(date: date))
  432. .roundTrips(to: ["date": timestamp])
  433. // Decoding a NSNull() leads to nil.
  434. assertThat(["date": NSNull()])
  435. .decodes(to: Model(date: nil))
  436. }
  437. func testServerTimestampUserType() throws {
  438. struct Model: Codable, Equatable {
  439. @ServerTimestamp var timestamp: String? = nil
  440. }
  441. // Encoding a pending server timestamp
  442. assertThat(Model())
  443. .encodes(to: ["timestamp": FieldValue.serverTimestamp()])
  444. // Encoding a resolved server timestamp yields a timestamp; decoding
  445. // yields it back.
  446. let timestamp = Timestamp(seconds: 1_570_484_031, nanoseconds: 122_999_906)
  447. assertThat(Model(timestamp: "2019-10-07T21:33:51.123Z"))
  448. .roundTrips(to: ["timestamp": timestamp])
  449. assertThat(Model(timestamp: "Invalid date"))
  450. .failsToEncode()
  451. }
  452. func testExplicitNull() throws {
  453. struct Model: Codable, Equatable {
  454. @ExplicitNull var name: String?
  455. }
  456. assertThat(Model(name: nil))
  457. .roundTrips(to: ["name": NSNull()])
  458. assertThat(Model(name: "good name"))
  459. .roundTrips(to: ["name": "good name"])
  460. }
  461. func testAutomaticallyPopulatesDocumentIDOnDocumentReference() throws {
  462. struct Model: Codable, Equatable {
  463. var name: String
  464. @DocumentID var docId: DocumentReference?
  465. }
  466. assertThat(["name": "abc"], in: "abc/123")
  467. .decodes(to: Model(name: "abc", docId: FSTTestDocRef("abc/123")))
  468. }
  469. func testAutomaticallyPopulatesDocumentIDOnString() throws {
  470. struct Model: Codable, Equatable {
  471. var name: String
  472. @DocumentID var docId: String?
  473. }
  474. assertThat(["name": "abc"], in: "abc/123")
  475. .decodes(to: Model(name: "abc", docId: "123"))
  476. }
  477. func testDocumentIDIgnoredInEncoding() throws {
  478. struct Model: Codable, Equatable {
  479. var name: String
  480. @DocumentID var docId: DocumentReference?
  481. }
  482. assertThat(Model(name: "abc", docId: FSTTestDocRef("abc/123")))
  483. .encodes(to: ["name": "abc"])
  484. }
  485. func testDocumentIDWithJsonEncoderThrows() {
  486. assertThat(DocumentID(wrappedValue: FSTTestDocRef("abc/xyz")))
  487. .failsEncodingWithJSONEncoder()
  488. }
  489. func testDecodingDocumentIDWithConfictingFieldsDoesNotThrow() throws {
  490. struct Model: Codable, Equatable {
  491. var name: String
  492. @DocumentID var docId: DocumentReference?
  493. }
  494. _ = try Firestore.Decoder().decode(
  495. Model.self,
  496. from: ["name": "abc", "docId": "Does not cause conflict"],
  497. in: FSTTestDocRef("abc/123")
  498. )
  499. }
  500. }
  501. private func assertThat(_ dictionary: [String: Any],
  502. in document: String? = nil,
  503. file: StaticString = #file,
  504. line: UInt = #line) -> DictionarySubject {
  505. return DictionarySubject(dictionary, in: document, file: file, line: line)
  506. }
  507. private func assertThat<X: Equatable & Codable>(_ model: X, file: StaticString = #file,
  508. line: UInt = #line) -> CodableSubject<X> {
  509. return CodableSubject(model, file: file, line: line)
  510. }
  511. private func assertThat<X: Equatable & Encodable>(_ model: X, file: StaticString = #file,
  512. line: UInt = #line) -> EncodableSubject<X> {
  513. return EncodableSubject(model, file: file, line: line)
  514. }
  515. private class EncodableSubject<X: Equatable & Encodable> {
  516. var subject: X
  517. var file: StaticString
  518. var line: UInt
  519. init(_ subject: X, file: StaticString, line: UInt) {
  520. self.subject = subject
  521. self.file = file
  522. self.line = line
  523. }
  524. @discardableResult
  525. func encodes(to expected: [String: Any]) -> DictionarySubject {
  526. let encoded = assertEncodes(to: expected)
  527. return DictionarySubject(encoded, file: file, line: line)
  528. }
  529. func failsToEncode() {
  530. do {
  531. _ = try Firestore.Encoder().encode(subject)
  532. } catch {
  533. return
  534. }
  535. XCTFail("Failed to throw")
  536. }
  537. func failsEncodingWithJSONEncoder() {
  538. do {
  539. _ = try JSONEncoder().encode(subject)
  540. XCTFail("Failed to throw", file: file, line: line)
  541. } catch FirestoreEncodingError.encodingIsNotSupported {
  542. return
  543. } catch {
  544. XCTFail("Unrecognized error: \(error)", file: file, line: line)
  545. }
  546. }
  547. func failsEncodingAtTopLevel() {
  548. do {
  549. _ = try Firestore.Encoder().encode(subject)
  550. XCTFail("Failed to throw", file: file, line: line)
  551. } catch EncodingError.invalidValue(_, _) {
  552. return
  553. } catch {
  554. XCTFail("Unrecognized error: \(error)", file: file, line: line)
  555. }
  556. }
  557. private func assertEncodes(to expected: [String: Any]) -> [String: Any] {
  558. do {
  559. let enc = try Firestore.Encoder().encode(subject)
  560. XCTAssertEqual(enc as NSDictionary, expected as NSDictionary, file: file, line: line)
  561. return enc
  562. } catch {
  563. XCTFail("Failed to encode \(X.self): error: \(error)")
  564. return ["": -1]
  565. }
  566. }
  567. }
  568. private class CodableSubject<X: Equatable & Codable>: EncodableSubject<X> {
  569. func roundTrips(to expected: [String: Any]) {
  570. let reverseSubject = encodes(to: expected)
  571. reverseSubject.decodes(to: subject)
  572. }
  573. }
  574. private class DictionarySubject {
  575. var subject: [String: Any]
  576. var document: DocumentReference?
  577. var file: StaticString
  578. var line: UInt
  579. init(_ subject: [String: Any], in documentName: String? = nil, file: StaticString, line: UInt) {
  580. self.subject = subject
  581. if let documentName {
  582. document = FSTTestDocRef(documentName)
  583. }
  584. self.file = file
  585. self.line = line
  586. }
  587. func decodes<X: Equatable & Codable>(to expected: X) -> Void {
  588. do {
  589. let decoded = try Firestore.Decoder().decode(X.self, from: subject, in: document)
  590. XCTAssertEqual(decoded, expected)
  591. } catch {
  592. XCTFail("Failed to decode \(X.self): \(error)", file: file, line: line)
  593. }
  594. }
  595. func failsDecoding<X: Equatable & Codable>(to _: X.Type) -> Void {
  596. XCTAssertThrowsError(try Firestore.Decoder().decode(X.self, from: subject), file: file,
  597. line: line)
  598. }
  599. }
  600. enum DateError: Error {
  601. case invalidDate(String)
  602. }
  603. // Extends Strings to allow them to be wrapped with @ServerTimestamp. Resolved
  604. // server timestamps will be stored in an ISO 8601 date format.
  605. //
  606. // This example exists outside the main implementation to show that users can
  607. // extend @ServerTimestamp with arbitrary types.
  608. extension String: ServerTimestampWrappable {
  609. static let formatter: DateFormatter = {
  610. let formatter = DateFormatter()
  611. formatter.calendar = Calendar(identifier: .iso8601)
  612. formatter.locale = Locale(identifier: "en_US_POSIX")
  613. formatter.timeZone = TimeZone(secondsFromGMT: 0)
  614. formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSXXXXX"
  615. return formatter
  616. }()
  617. public static func wrap(_ timestamp: Timestamp) throws -> Self {
  618. return formatter.string(from: timestamp.dateValue())
  619. }
  620. public static func unwrap(_ value: Self) throws -> Timestamp {
  621. let date = formatter.date(from: value)
  622. if let date {
  623. return Timestamp(date: date)
  624. } else {
  625. throw DateError.invalidDate(value)
  626. }
  627. }
  628. }