FirestoreEncoderTests.swift 21 KB

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