FirestoreEncoderTests.swift 21 KB

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