Disk+Codable.swift 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import Foundation
  2. public extension Disk {
  3. /// Save encodable struct to disk as JSON data
  4. ///
  5. /// - Parameters:
  6. /// - value: the Encodable struct to store
  7. /// - directory: user directory to store the file in
  8. /// - path: file location to store the data (i.e. "Folder/file.json")
  9. /// - encoder: custom JSONEncoder to encode value
  10. /// - Throws: Error if there were any issues encoding the struct or writing it to disk
  11. static func save<T: Encodable>(
  12. _ value: T,
  13. to directory: Directory,
  14. as path: String,
  15. encoder: JSONEncoder = JSONEncoder()
  16. ) throws {
  17. if path.hasSuffix("/") {
  18. throw createInvalidFileNameForStructsError()
  19. }
  20. do {
  21. let url = try createURL(for: path, in: directory)
  22. let data = try encoder.encode(value)
  23. try createSubfoldersBeforeCreatingFile(at: url)
  24. try data.write(to: url, options: .atomic)
  25. } catch {
  26. throw error
  27. }
  28. }
  29. /// Append Codable struct JSON data to a file's data
  30. ///
  31. /// - Parameters:
  32. /// - value: the struct to store to disk
  33. /// - path: file location to store the data (i.e. "Folder/file.json")
  34. /// - directory: user directory to store the file in
  35. /// - decoder: custom JSONDecoder to decode existing values
  36. /// - encoder: custom JSONEncoder to encode new value
  37. /// - Throws: Error if there were any issues with encoding/decoding or writing the encoded struct to disk
  38. static func append<T: Codable>(
  39. _ value: T,
  40. to path: String,
  41. in directory: Directory,
  42. decoder: JSONDecoder = JSONDecoder(),
  43. encoder: JSONEncoder = JSONEncoder()
  44. ) throws {
  45. if path.hasSuffix("/") {
  46. throw createInvalidFileNameForStructsError()
  47. }
  48. do {
  49. if let url = try? getExistingFileURL(for: path, in: directory) {
  50. let oldData = try Data(contentsOf: url)
  51. if !(!oldData.isEmpty) {
  52. try save([value], to: directory, as: path, encoder: encoder)
  53. } else {
  54. let new: [T]
  55. if let old = try? decoder.decode(T.self, from: oldData) {
  56. new = [old, value]
  57. } else if var old = try? decoder.decode([T].self, from: oldData) {
  58. old.append(value)
  59. new = old
  60. } else {
  61. throw createDeserializationErrorForAppendingStructToInvalidType(url: url, type: value)
  62. }
  63. let newData = try encoder.encode(new)
  64. try newData.write(to: url, options: .atomic)
  65. }
  66. } else {
  67. try save([value], to: directory, as: path, encoder: encoder)
  68. }
  69. } catch {
  70. throw error
  71. }
  72. }
  73. /// Append Codable struct array JSON data to a file's data
  74. ///
  75. /// - Parameters:
  76. /// - value: the Codable struct array to store
  77. /// - path: file location to store the data (i.e. "Folder/file.json")
  78. /// - directory: user directory to store the file in
  79. /// - decoder: custom JSONDecoder to decode existing values
  80. /// - encoder: custom JSONEncoder to encode new value
  81. /// - Throws: Error if there were any issues writing the encoded struct array to disk
  82. static func append<T: Codable>(
  83. _ value: [T],
  84. to path: String,
  85. in directory: Directory,
  86. decoder: JSONDecoder = JSONDecoder(),
  87. encoder: JSONEncoder = JSONEncoder()
  88. ) throws {
  89. if path.hasSuffix("/") {
  90. throw createInvalidFileNameForStructsError()
  91. }
  92. do {
  93. if let url = try? getExistingFileURL(for: path, in: directory) {
  94. let oldData = try Data(contentsOf: url)
  95. if !(!oldData.isEmpty) {
  96. try save(value, to: directory, as: path, encoder: encoder)
  97. } else {
  98. let new: [T]
  99. if let old = try? decoder.decode(T.self, from: oldData) {
  100. new = [old] + value
  101. } else if var old = try? decoder.decode([T].self, from: oldData) {
  102. old.append(contentsOf: value)
  103. new = old
  104. } else {
  105. throw createDeserializationErrorForAppendingStructToInvalidType(url: url, type: value)
  106. }
  107. let newData = try encoder.encode(new)
  108. try newData.write(to: url, options: .atomic)
  109. }
  110. } else {
  111. try save(value, to: directory, as: path, encoder: encoder)
  112. }
  113. } catch {
  114. throw error
  115. }
  116. }
  117. /// Retrieve and decode a struct from a file on disk
  118. ///
  119. /// - Parameters:
  120. /// - path: path of the file holding desired data
  121. /// - directory: user directory to retrieve the file from
  122. /// - type: struct type (i.e. Message.self or [Message].self)
  123. /// - decoder: custom JSONDecoder to decode existing values
  124. /// - Returns: decoded structs of data
  125. /// - Throws: Error if there were any issues retrieving the data or decoding it to the specified type
  126. static func retrieve<T: Decodable>(
  127. _ path: String,
  128. from directory: Directory,
  129. as type: T.Type,
  130. decoder: JSONDecoder = JSONDecoder()
  131. ) throws -> T {
  132. if path.hasSuffix("/") {
  133. throw createInvalidFileNameForStructsError()
  134. }
  135. do {
  136. let url = try getExistingFileURL(for: path, in: directory)
  137. let data = try Data(contentsOf: url)
  138. let value = try decoder.decode(type, from: data)
  139. return value
  140. } catch {
  141. throw error
  142. }
  143. }
  144. }
  145. private extension Disk {
  146. /// Helper method to create deserialization error for append(:path:directory:) functions
  147. static func createDeserializationErrorForAppendingStructToInvalidType<T>(url: URL, type _: T) -> Error {
  148. Disk.createError(
  149. .deserialization,
  150. description: "Could not deserialize the existing data at \(url.path) to a valid type to append to.",
  151. failureReason: "JSONDecoder could not decode type \(T.self) from the data existing at the file location.",
  152. recoverySuggestion: "Ensure that you only append data structure(s) with the same type as the data existing at the file location."
  153. )
  154. }
  155. /// Helper method to create error for when trying to saving Codable structs as multiple files to a folder
  156. static func createInvalidFileNameForStructsError() -> Error {
  157. Disk.createError(
  158. .invalidFileName,
  159. description: "Cannot save/retrieve the Codable struct without a valid file name. Unlike how arrays of UIImages or Data are stored, Codable structs are not saved as multiple files in a folder, but rather as one JSON file. If you already successfully saved Codable struct(s) to your folder name, try retrieving it as a file named 'Folder' instead of as a folder 'Folder/'",
  160. failureReason: "Disk does not save structs or arrays of structs as multiple files to a folder like it does UIImages or Data.",
  161. recoverySuggestion: "Save your struct or array of structs as one file that encapsulates all the data (i.e. \"multiple-messages.json\")"
  162. )
  163. }
  164. }