51 |
52 | Store your awsome `Codable` objects and retrieve them with ease. `CodablePersist` gives you a convenient way to store your objects in a disk, UserDefaluts, and memory and manage them easily.
53 |
54 |
55 |
56 |
57 |
58 | ## Features
59 |
60 |
61 |
62 | - ✅ Easily save and retrieve any `Codable` type.
63 | - 📆 Set expiration date to the objects.
64 | - 🔍Time-based storage filtring.
65 | - 🗃Retrieve, delete, save objects using subscript
66 |
67 | ## Example
68 |
69 |
70 |
71 | To quickly show you how `CodablePersist` can be useful, consider the following use case:
72 |
73 | ```swift
74 | class PostFetcher {
75 | typealias Handler = (Result) -> Void
76 | private let cache = DiskStorage(storeName: "postStorage")
77 |
78 | func fetchPost(withID id: Post.ID, then handler: @escaping Handler) {
79 | // check if the post is cached or not
80 | if let cached = cache[id] { return handler(.success(cached)) }
81 | // if not cached fetch it from the backend
82 | performFetching { [weak self] result in
83 | // load post and cache it
84 | let post = try? result.get() post.map { self?.cache[id] = $0 }
85 | //then return the result
86 | handler(result)
87 | }
88 | }
89 | }
90 | ```
91 |
92 | ## Installation
93 |
94 |
95 |
96 | ### CocoaPods
97 |
98 |
99 |
100 | CodablePersist is available through [CocoaPods](http://cocoapods.org). To install
101 |
102 | it, simply add the following line to your Podfile:
103 |
104 |
105 |
106 | ```bash
107 |
108 | pod 'CodablePersist'
109 |
110 | ```
111 |
112 |
113 |
114 | ### Carthage
115 |
116 |
117 |
118 | [Carthage](https://github.com/Carthage/Carthage) is a decentralized dependency manager that builds your dependencies and provides you with binary frameworks.
119 |
120 |
121 |
122 | To integrate CodablePersist into your Xcode project using Carthage, specify it in your `Cartfile`:
123 |
124 |
125 |
126 | ```ogdl
127 |
128 | github "engali94/CodablePersist"
129 |
130 | ```
131 |
132 |
133 |
134 | Run `carthage update` to build the framework and drag the built `CodablePersist.framework` into your Xcode project.
135 |
136 |
137 |
138 | On your application targets’ “Build Phases” settings tab, click the “+” icon and choose “New Run Script Phase” and add the Framework path as mentioned in [Carthage Getting started Step 4, 5 and 6](https://github.com/Carthage/Carthage/blob/master/README.md#if-youre-building-for-ios-tvos-or-watchos)
139 |
140 |
141 |
142 | ### Swift Package Manager
143 |
144 |
145 |
146 | To integrate using Apple's [Swift Package Manager](https://swift.org/package-manager/), add the following as a dependency to your `Package.swift`:
147 |
148 |
149 |
150 | ```swift
151 |
152 | dependencies: [
153 |
154 | .package(url: "https://github.com/engali94/CodablePersist.git", from: "0.1")
155 |
156 | ]
157 |
158 | ```
159 | Next, add `CodablePersist` to your targets as follows:
160 | ```swift
161 | .target(
162 | name: "YOUR_TARGET_NAME",
163 | dependencies: [
164 | "CodablePersist",
165 | ]
166 | ),
167 | ```
168 | Then run `swift package update` to install the package.
169 |
170 |
171 | Alternatively navigate to your Xcode project, select `Swift Packages` and click the `+` icon to search for `CodablePersist`.
172 |
173 |
174 |
175 | ### Manually
176 |
177 |
178 |
179 | If you prefer not to use any of the aforementioned dependency managers, you can integrate CodablePersist into your project manually. Simply drag the `Sources` Folder into your Xcode project.
180 |
181 |
182 |
183 | ## Usage
184 |
185 | ### 1. Prepare your data
186 | Make sure your type conforms to `Identifiable` protocol, and assign a unique `idKey` property like follows:
187 | ``` swift
188 | struct Post: Codable, Identifiable {
189 | // Identifiable conformance
190 | static var idKey = \Post.id
191 |
192 | var title: String
193 | var id: String // should be unique per post
194 | }
195 | ```
196 |
197 |
198 | ### 2. Initialize Storage
199 |
200 | - `DiskStorage`: We can init a Disk Storage by passing a `storeName` and `expiryDate`
201 | ``` swift
202 | let storage = try? DiskStorage(storeName: storageName, expiryDate: .minutes(interval: 10))
203 | ```
204 |
205 | - `UserDefalutsStorage`: Also we can init UserDefaults Storage by passing a `storeName` and `expiryDate`
206 | ``` swift
207 | let storage = UserDefaultsStorage(storeName: storageName, expiryDate: .minutes(interval: 10))!
208 | ```
209 |
210 | - `MemoryStorage`: This should be used with precaution, if any memory load happens the system will delete some or all objects to free memory. **Consider using `DiskStorage` or `UserDefalutsStorage` for long term persistence.**
211 | we can init ``
212 | ```swift
213 | let storage = MemoryStorage(expiryDate: .minutes(interval: 10))
214 | ```
215 | ### 3. Storage!!
216 | Now you are good to go... You can persist, retrieve and delete your `Codable` objects 😎
217 | #### Saving
218 | ```swift
219 | let singlePost = Post(title: "I'm persistable", id: 1)
220 | let posts: [Post] = [Post(title: "I'm persistable2", id: 2), Post(title: "I'm persistable3", id: 3)]
221 |
222 | //Save a single object
223 | try? storage.save(singlePost)
224 |
225 | // Save single object by subscript
226 | storage[1] = singlePost
227 |
228 | //Save mutliple objects
229 | try? storage.save(posts)
230 |
231 |
232 | ```
233 | #### Retrieval
234 | ```swift
235 | // fetch single object
236 | let post1 = try? storage.fetchObject(for: 1)
237 |
238 | // fetch by subscript
239 | let post2 = storage[2]
240 |
241 | // fetch multiple objects
242 | let multiPosts = try? storage.fetchObjects(for: [3,2])
243 |
244 | // fetch all objects in the store sorted by date ascendingly
245 | let allObjets = try? storage.fetchAllObjects(descending: false)
246 |
247 | // fetch only objects saved in las ten minutes
248 | // ------(10)++++++(now) -> will only returns ++++++
249 | let obejetsAfterTenMin = try? storage.fetchObjectsStored(inLast: .minutes(interval: 10)
250 |
251 | // fetch object stored before the last 10 minutes
252 | // ++++++(10)------(now) -> will only returns ++++++
253 | let obejetsBeforeTemMin = try? storage.fetchObjectsStored(before: .minutes(interval: 10))
254 |
255 | // check if an object exists in the storage
256 | storage.contains(2)
257 |
258 | // check the number of objects in the storage
259 | storage.objectsCount
260 |
261 | ```
262 | #### Deletion
263 | ```swift
264 | // delete a single object
265 | try? storage.deleteObject(forKey: singlePost.id)
266 |
267 | // delete a single object by subscript
268 | storage[singlePost.id] = nil
269 |
270 | // delete multiple posts
271 | try? storage.deleteObjects(forKeys: [2,3])
272 |
273 | // delete all objects
274 | try? storage.deleteAll()
275 |
276 | // Delete expired objects
277 | storage.deleteExpired()
278 | ```
279 | ## Requirements
280 |
281 | - iOS 8.0+ / macOS 10.10+ / tvOS 9.0+ / watchOS 2.0+
282 | - Xcode 10.0+
283 | - Swift 4.2+
284 |
285 | ## Contributing
286 |
287 | Contributions are warmly welcomed 🙌
288 |
289 |
290 |
291 | ## License
292 |
293 | CodablePesist is released under the MIT license. See [LICENSE](https://github.com/engali94/CodablePersist/blob/master/LICENSE) for more information.
294 |
--------------------------------------------------------------------------------
/Sources/Core/CacheStorable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | /// A protocol that conform to `StorageReadable`, `StorageWritable`and `StorageRemovable`
25 | public protocol CacheStorable: StorageReadable, StorageWritable, StorageRemovable { }
26 |
--------------------------------------------------------------------------------
/Sources/Core/Identifiable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | /// A protocol conformed by objects that can be uniuqely identifed.
25 | public protocol Identifiable {
26 |
27 | associatedtype ID: Hashable & Codable
28 |
29 | /// a `KeyPath` to the uniuqe id.
30 | static var idKey: WritableKeyPath { get }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/Sources/Core/ObjectDescriptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | import Foundation
25 |
26 | /// Object Descriptor
27 | ///
28 | /// Describe an object that need to be stored and retrieved.
29 | /// - seealso: `MemoryStorage.Entry`
30 | public struct ObjectDescriptor {
31 |
32 | /// The original object needs to be stored or fetched.
33 | let object: T
34 |
35 | /// Adds expiry date to the object
36 | let expiryDate: Date
37 |
38 | /// Adds creation date to the object.
39 | let creationDate: Date
40 |
41 | /// Object's uniuqe ID.
42 | let key: T.ID
43 |
44 | /// Checks wheather an object has expired or not.
45 | var isExpired: Bool {
46 | return Date() > expiryDate
47 | }
48 |
49 | init(object: T, expiryDate: Date, creationDate: Date ) {
50 | self.object = object
51 | self.creationDate = creationDate
52 | self.key = object[keyPath: T.idKey]
53 | self.expiryDate = expiryDate
54 | }
55 | }
56 |
57 | // MARK: Codable Conformation
58 | extension ObjectDescriptor: Codable { }
59 |
--------------------------------------------------------------------------------
/Sources/Core/StorageReadable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | /// A protocol conformed by objects that can be read from the storage.
25 | public protocol StorageReadable {
26 | associatedtype T: Identifiable
27 |
28 | /// Retrives single object from the storage.
29 | ///
30 | /// - Parameters:
31 | /// - key: Key that uniqly identifies that object.
32 | /// - Returns: The object fetched from the specified key.
33 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied).
34 | func fetchObject(for key: T.ID) throws -> T?
35 |
36 | /// Retrives multiple objects from the storage.
37 | ///
38 | /// - Parameters:
39 | /// - keys: An array of keys that uniqly identifies the stored objects.
40 | /// - Returns: Array of objects fetched from the specified keys.
41 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied).
42 | func fetchObjects(for keys: [T.ID]) throws -> [T]?
43 |
44 | /// Retrives all valid (not expired) objects from the storage.
45 | ///
46 | /// - Parameters:
47 | /// - descending: The order of the fetched results, deafults to `true`.
48 | /// - Returns: Array of all valid (not expired) objects fetched from the storage.
49 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied).
50 | func fetchAllObjects(descending: Bool) throws -> [T]?
51 |
52 | /// Retrives a chunk of objects stored _after_ a specific date. i.e in the last ten minutes.
53 | ///
54 | ///- Parameters:
55 | /// - interval: An interval of time.
56 | /// - Returns: Array of objects fetched from the storage stored after the specified period.
57 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied).
58 | func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T]
59 |
60 | /// Retrives a chunk of objects stored _before_ a specific date. i.e before the last ten minutes.
61 | ///
62 | /// - Parameters:
63 | /// - interval: An interval of time.
64 | /// - Returns: Array of objects fetched from the storage stored after the specified period.
65 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied).
66 | func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T]
67 |
68 | /// Checks if the storage contains an object with the specifed key.
69 | ///
70 | /// - Parameters:
71 | /// - key: object's uniuqe identifer.
72 | /// - Returns: Boolean value.
73 | /// - Throws: Error encountered during the reading process (e.g. Missing object, or Decoding falied).
74 | func contains(_ key: T.ID) -> Bool
75 |
76 | /// Checks object associated with that key has expired or not.
77 | ///
78 | /// - Parameters:
79 | /// - key: object's uniuqe identifer.
80 | /// - Returns: Boolean value.
81 | func isExpired(for key: T.ID) -> Bool
82 |
83 | /// Gives the count of all the objects in that store.
84 | ///
85 | /// - Returns: Objects count in the store.
86 | var objectsCount: Int { get }
87 |
88 | /// Retrives a single value from th store throw subscript ([])
89 | ///
90 | /// - Returns: optinal single object.
91 | subscript(key: T.ID) -> T? { get set }
92 | }
93 |
--------------------------------------------------------------------------------
/Sources/Core/StorageRemovable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | /// A protocol conformed by objects that can be removed from the storage.
25 | ///
26 | /// **Assumptions**:
27 | /// - `CodablePersist` creates objects with the associated keys.
28 | /// - The objects are stored in unique paths
29 | /// - Each kay represents only one object on that path.
30 | /// - Each object can be removed if its unique identifer is known or its life cycle ended (i.e. expired).
31 | public protocol StorageRemovable {
32 |
33 | associatedtype T: Identifiable
34 |
35 | /// Delete an object related to the specified key.
36 | ///
37 | /// - Parameters:
38 | /// - key: The unique key for that object.
39 | /// - Throws: Error encountered during the deleting process (e.g. Missing object).
40 | func deleteObject(forKey key: T.ID) throws
41 |
42 | /// Delete objects related to the specified keys.
43 | ///
44 | /// - Parameters:
45 | /// - keys: An array of unique keys for objects wanted to be deleted.
46 | /// - Throws: Error encountered during the deleting process (e.g. Missing object(s)).
47 | func deleteObjects(forKeys keys: [T.ID]) throws
48 |
49 | /// Delete all objects stored in the storage.
50 | ///
51 | /// - Throws: Error encountered during the deleting process (e.g. Missing object(s)).
52 | func deleteAll() throws
53 |
54 | /// Delete all objects that has expired.
55 | func deleteExpired()
56 | }
57 |
--------------------------------------------------------------------------------
/Sources/Core/StorageWritable.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | /// A protocol conformed by objects that can be stored in the storage.
25 | ///
26 | /// **Assumptions**:
27 | /// - `CodablePersist` creates objects with the associated keys.
28 | /// - The objects are stored in unique paths
29 | /// - Each key represents only one object on that path.
30 | public protocol StorageWritable {
31 | associatedtype T: Identifiable
32 |
33 | /// Saves an objectto the storage, each type conforming to `Identifiable`, will have a uniuqe ID
34 | /// this will be extracted from that object to to uniuqely identify it. If `DiskStorage` is usedit will
35 | /// use that key to generate a uniqe path to the object in the storage, also in the other storages will
36 | /// be used as amunique identifier
37 | ///
38 | /// - Parameters:
39 | /// - object: The object wanted to be stored..
40 | /// - Throws: Error encountered during the saving process (e.g. Path incorrect, or Encoding falied).
41 | func save(_ object: T) throws
42 |
43 | /// Saves multiple object in the storage one time.
44 | ///
45 | /// - Parameters:
46 | /// - objects: An array of objects wanted to be stored..
47 | /// - Throws: Error encountered during the saving process (e.g. Path incorrect, or Encoding falied).
48 | func save(_ objects: [T]) throws
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/Sources/Helpers/SorageDateDescriptor.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | import Foundation
25 |
26 | /// Sorage Date Descriptor
27 | ///
28 | /// Describes object's expiry date as well as a time snapshot used in fetching
29 | /// objects based on a specific time interval
30 | public enum SorageDateDescriptor {
31 |
32 | /// Fetch the object stored in the last `interval` seconds,
33 | /// or set the object to be expired after `interval` seconds.
34 | case seconds(interval: Double)
35 |
36 | /// Fetch the object stored in the last `interval` minutes,
37 | /// or set the object to be expired after `interval` minutes.
38 | case minutes(interval: Double)
39 |
40 | /// Fetch the object stored in the last `interval` houres,
41 | /// or set the object to be expired after `interval` houres.
42 | case houres(interval: Double)
43 |
44 | /// Fetch the object stored in the last `interval` days,
45 | /// or set the object to be expired after `interval` days.
46 | case days(interval: Double)
47 |
48 | /// Fetch the object stored in the last `interval` months,
49 | /// or set the object to be expired after `interval` months.
50 | case months(interval: Double)
51 |
52 | /// **Warning**: Don't use this case with object retival. Use it only to set object's expiry.
53 | case never
54 |
55 | }
56 |
57 | // MARK: - Helpers
58 |
59 | extension SorageDateDescriptor {
60 |
61 | /// Converts time interval to `Date` object.
62 | /// The date will be in the past.
63 | var toDate: Date {
64 | switch self {
65 | case .seconds(let interval):
66 | return Date(timeIntervalSinceNow: -interval)
67 |
68 | case .minutes(let interval):
69 | return Date(timeIntervalSinceNow: -60 * interval)
70 |
71 | case .houres(let interval):
72 | return Date(timeIntervalSinceNow: -60 * 60 * interval)
73 |
74 | case .days(let interval):
75 | return Date(timeIntervalSinceNow: -60 * 60 * 24 * interval)
76 |
77 | case .months(let interval):
78 | return Date(timeIntervalSinceNow: -60 * 60 * 24 * 30 * interval)
79 |
80 | case .never:
81 | return Date()
82 | }
83 | }
84 |
85 | /// Converts time interval to `Date` object.
86 | /// The date will be in the future,
87 | var expireDate: Date {
88 | switch self {
89 | case .seconds(let interval):
90 | return Date().addingTimeInterval(interval)
91 |
92 | case .minutes(let interval):
93 | return Date().addingTimeInterval(60 * interval)
94 |
95 | case .houres(let interval):
96 | return Date().addingTimeInterval(60 * 60 * interval)
97 |
98 | case .days(let interval):
99 | return Date().addingTimeInterval(60 * 60 * 24 * interval)
100 |
101 | case .months(let interval):
102 | return Date().addingTimeInterval(60 * 60 * 24 * 30 * interval)
103 |
104 | case .never:
105 | return Date(timeIntervalSince1970: 60 * 60 * 24 * 365 * 68)
106 | }
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/Sources/Helpers/StorageError.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | import Foundation
25 |
26 | /// Describes various storage errors.
27 | enum StorageError: Swift.Error {
28 |
29 | /// The specified path could not be found
30 | case pathNotFound
31 |
32 | /// Obectt can't be stored or encodded.
33 | case objectUnreadable
34 |
35 | /// Object can't be fetched or encodded.
36 | case objectUnwritable
37 |
38 | /// Can't encode object
39 | case objectEncodingFailed(underlying: Swift.Error)
40 |
41 | /// Can't decode object
42 | case objectDecodingFailed(underlying: Swift.Error)
43 | }
44 |
45 | extension StorageError: LocalizedError {
46 |
47 | public var errorDescription: String? {
48 | switch self {
49 | case .pathNotFound:
50 | return "The object couldn't be found at the specified path."
51 | case .objectUnwritable:
52 | return "Obectt can't be stored or encodded."
53 | case .objectUnreadable:
54 | return "Object can't be fetched or encodded."
55 | case .objectEncodingFailed(let error):
56 | return "Can't encode object due to \(error.localizedDescription)"
57 | case .objectDecodingFailed(let error):
58 | return "Can't decode object due to \(error.localizedDescription)"
59 | }
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/Sources/Storage/DiskStorage.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Disk Storage utilizes `FileManager` to cache and retrive data to and from the disk
4 | /// it gives a convenient and easy to use way to cache any `Codable` and `Identifiable` object.
5 | public class DiskStorage {
6 |
7 | private let fileManager: FileManager
8 |
9 | /// Storage's name.
10 | ///
11 | /// **Warning**: This name should be unique per storage if you want to make two or more different storages.
12 | public let storeName: String
13 |
14 | /// A `URL` refers to preferted storage dorectory.
15 | public let directoryUrl: URL
16 |
17 | /// A `String `path refers to preferted storage dorectory.
18 | public let path: String
19 |
20 | /// Expiry date of the stored objects.
21 | public let expiryDate: SorageDateDescriptor
22 |
23 | /// JSON encoder to be used for encoding objects to be stored.
24 | open var encoder: JSONEncoder
25 |
26 | /// JSON decoder to be used to decode stored objects.
27 | open var decoder: JSONDecoder
28 |
29 | /// injectable `Date` object.
30 | private let dateProvider: () -> Date
31 |
32 | /// Initialize disk storage with the given name and expiry date.
33 | ///
34 | /// - Parameters:
35 | /// - storeName: Disk storage unique identifier.
36 | /// - directoryUrl: A `URL` refers to preferted storage dorectory. _if not given will use `cachesDirectory`
37 | /// as storage directory.
38 | /// - expiryDate: The expiry date of each object being stored.
39 | /// - fileManager: system's default file manager.
40 | /// - encoder: JSON encoder to be used for encoding objects to be stored.
41 | /// - decoder: JSON decoder to be used to decode stored objects.
42 |
43 | required public init(
44 | dateProvider: @escaping () -> Date = Date.init,
45 | storeName: String,
46 | directoryUrl: URL? = nil,
47 | expiryDate: SorageDateDescriptor = .never,
48 | fileManager: FileManager = FileManager.default,
49 | encoder: JSONEncoder = .init(),
50 | decoder: JSONDecoder = .init()
51 |
52 | ) throws {
53 | self.storeName = storeName
54 | self.fileManager = fileManager
55 | self.encoder = encoder
56 | self.decoder = decoder
57 | self.expiryDate = expiryDate
58 | self.dateProvider = dateProvider
59 |
60 | if let url = directoryUrl {
61 | self.directoryUrl = url
62 | } else {
63 | self.directoryUrl = try fileManager.url(
64 | for: .cachesDirectory,
65 | in: .userDomainMask,
66 | appropriateFor: nil,
67 | create: true)
68 | }
69 |
70 | let url = self.directoryUrl.appendingPathComponent(storeName, isDirectory: true)
71 | self.path = url.path
72 | try createFolder(at: url)
73 |
74 | }
75 |
76 | }
77 | // MARK: - CacheStorable
78 |
79 | extension DiskStorage: CacheStorable {
80 |
81 | public func save(_ object: T) throws {
82 |
83 | let entry = ObjectDescriptor(object: object, expiryDate: expiryDate.expireDate, creationDate: dateProvider())
84 | let data = try encoder.encode(entry)
85 | let filePath = makePath(for: object)
86 |
87 | fileManager.createFile(atPath: filePath, contents: data, attributes: [FileAttributeKey.creationDate: Date()])
88 | }
89 |
90 | public func save(_ objects: [T]) throws {
91 |
92 | for object in objects {
93 | try save(object)
94 | }
95 | }
96 |
97 | public func fetchObject(for key: T.ID) throws -> T? {
98 |
99 | let filePath = makePath(for: key)
100 | guard let data = fileManager.contents(atPath: filePath) else { return nil }
101 |
102 | let entry = try decoder.decode(ObjectDescriptor.self, from: data)
103 | if !entry.isExpired {
104 | return entry.object
105 | }
106 | return nil
107 | }
108 |
109 | public func fetchObjects(for keys: [T.ID]) throws -> [T]? {
110 |
111 | return try keys.compactMap { try fetchObject(for: $0) }
112 | }
113 |
114 | public func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T] {
115 |
116 | return try listAllEntries().filter { $0.creationDate > interval.toDate }.map { $0.object }
117 | }
118 |
119 | public func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T] {
120 |
121 | return try listAllEntries().filter { $0.creationDate < interval.toDate }.map { $0.object }
122 | }
123 |
124 | public func fetchAllObjects(descending: Bool = true) throws -> [T]? {
125 |
126 | return try listAllEntries().sorted { (en1, en2) -> Bool in
127 | if descending {
128 | return en1.creationDate.compare(en2.creationDate) == .orderedDescending
129 | } else {
130 | return en1.creationDate.compare(en2.creationDate) == .orderedAscending
131 | }
132 | }.map { $0.object }
133 | }
134 |
135 | public func contains(_ key: T.ID) -> Bool {
136 | do {
137 | return try fetchObject(for: key) != nil
138 | } catch {
139 | return false
140 | }
141 |
142 | }
143 |
144 | public subscript(key: T.ID) -> T? {
145 | get {
146 | return try? fetchObject(for: key)
147 | }
148 | set {
149 | guard let value = newValue else {
150 | // If nil was assigned using subscript,
151 | // then we remove any value for that key:
152 | try? deleteObject(forKey: key)
153 | return
154 | }
155 | try? save(value)
156 | }
157 | }
158 |
159 | public func isExpired(for key: T.ID) -> Bool {
160 | return !contains(key)
161 | }
162 |
163 | public var objectsCount: Int {
164 | do {
165 | let objects = try fileManager.contentsOfDirectory(atPath: path)
166 | return objects.count
167 | } catch {
168 | return 0
169 | }
170 | }
171 |
172 | public func deleteObject(forKey key: T.ID) throws {
173 |
174 | let objectPath = makePath(for: key)
175 | try fileManager.removeItem(atPath: objectPath)
176 | }
177 |
178 | public func deleteObjects(forKeys keys: [T.ID]) throws {
179 |
180 | for key in keys {
181 | try deleteObject(forKey: key)
182 | }
183 | }
184 |
185 | public func deleteAll() throws {
186 |
187 | let objects = try fileManager.contentsOfDirectory(atPath: path)
188 |
189 | for object in objects {
190 | let objectPath = pathFromFileName(object)
191 | try fileManager.removeItem(atPath: objectPath)
192 | }
193 | }
194 |
195 | public func deleteExpired() {
196 | _ = try? listAllEntries()
197 | }
198 | }
199 |
200 | // MARK: - Helpers
201 |
202 | private extension DiskStorage {
203 |
204 | /// Create folder at the specifed url
205 | ///
206 | /// - Parameters:
207 | /// - url: URL to crate the folder at.
208 | /// - Throws: FilerManager error.
209 | func createFolder(at url: URL) throws {
210 |
211 | if !fileManager.fileExists(atPath: url.path) {
212 | try fileManager.createDirectory(
213 | at: url,
214 | withIntermediateDirectories: true,
215 | attributes: nil
216 | )
217 | }
218 | }
219 |
220 | /// List all objects in the store and filter the expired ones
221 | ///
222 | /// - Throws: JSON decoding error.
223 | /// - Returns: array of `StorableObject`
224 | @discardableResult
225 | private func listAllEntries() throws -> [ObjectDescriptor] {
226 |
227 | let files = try fileManager.contentsOfDirectory(atPath: path)
228 | return try files.compactMap { file -> ObjectDescriptor? in
229 | guard let data = fileManager.contents(atPath: pathFromFileName(file)) else { return nil }
230 | let entry = try decoder.decode(ObjectDescriptor.self, from: data)
231 | if !entry.isExpired {
232 | return entry
233 | } else {
234 | // If the object has aleady expired
235 | // delete it and return nil
236 | try deleteObject(forKey: entry.key)
237 | return nil
238 | }
239 | }
240 | }
241 |
242 | /// Extract the key from object.
243 | ///
244 | /// - Parameters:
245 | /// - object: `T` object extract they key from.
246 | /// - Returns: unique path component of the passed object.
247 | func extractKey(from object: T) -> String {
248 | return "\(storeName)-\(object[keyPath: T.idKey])"
249 | }
250 |
251 | /// Reconstructs object's name from the passed key.
252 | ///
253 | /// - Parameters:
254 | /// - id: `T.ID` key to reconstruct path from it
255 | /// - Returns: unique path componen from the passed key.
256 | func reverseKey(for id: T.ID) -> String {
257 | return "\(storeName)-\(id)"
258 | }
259 |
260 | /// Construct a path to the passed object.
261 | ///
262 | /// - Parameters:
263 | /// - object: `T` object extract they key from.
264 | /// - Returns: a complete path of the passed object.
265 | func makePath(for object: T) -> String {
266 | return "\(path)/\(extractKey(from: object))"
267 | }
268 |
269 | /// Construct a path from the passed key.
270 | ///
271 | /// - Parameters:
272 | /// - key: a `T.ID` key to reconstruct path from it
273 | /// - Returns: a complete path of the passed key.
274 | func makePath(for id: T.ID) -> String {
275 | return "\(path)/\(reverseKey(for: id))"
276 | }
277 |
278 | /// Construct a path from the passed file name.
279 | ///
280 | /// - Parameters:
281 | /// - name: a `String` file name to reconstruct path from it
282 | /// - Returns: a complete path of the passed file name.
283 | func pathFromFileName(_ name: String) -> String {
284 | return path + "/" + name
285 | }
286 | }
287 |
--------------------------------------------------------------------------------
/Sources/Storage/MemoryStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | import Foundation
25 |
26 | /// Memory Storage utilizes `NSCache` to cache and retrive data to and from memory it gives a
27 | /// convenient and easy to use way to cache any `Codable` and `Identifiable` object.
28 | /// **WARNING**: **Don't use it for heavy objects as there will be potential lose of date if any
29 | /// memory pressure happens**.
30 | /// Consider using `DiskStorage` for heavy and long term persistence.
31 | final public class MemoryStorage {
32 |
33 | private let wrapped = NSCache()
34 | private let expiryDate: SorageDateDescriptor
35 | private let keyTracker = KeyTracker()
36 | private let dateProvider: () -> Date
37 |
38 | required public init( dateProvider: @escaping () -> Date = Date.init,
39 | expiryDate: SorageDateDescriptor,
40 | maxObjectsCount: Int = 50) {
41 |
42 | self.expiryDate = expiryDate
43 | wrapped.delegate = keyTracker
44 | wrapped.countLimit = maxObjectsCount
45 | self.dateProvider = dateProvider
46 |
47 | deleteExpired()
48 | }
49 |
50 | }
51 |
52 | // MARK: - CacheStorable
53 |
54 | extension MemoryStorage: CacheStorable {
55 |
56 | public func save(_ object: T) {
57 |
58 | let entry = Entry(object: object,
59 | creationDate: dateProvider(),
60 | expirationDate: expiryDate.expireDate)
61 |
62 | let key = object[keyPath: T.idKey]
63 | wrapped.setObject(entry, forKey: WrappedKey(key))
64 | keyTracker.keys.insert(key)
65 | }
66 |
67 | public func save(_ objects: [T]) {
68 |
69 | for object in objects {
70 | save(object)
71 | }
72 | }
73 |
74 | public func fetchObject(for key: T.ID) throws -> T? {
75 | guard let entry = wrapped.object(forKey: WrappedKey(key)), entry.isExpired == false else {
76 | throw StorageError.objectUnreadable
77 | }
78 | return entry.object
79 | }
80 |
81 | //Use compact map here
82 | public func fetchObjects(for keys: [T.ID]) throws -> [T]? {
83 | var objects: [T] = []
84 | for key in keys {
85 | guard let object = try fetchObject(for: key) else { continue }
86 | objects.append(object)
87 | }
88 | return objects
89 | }
90 |
91 | public func fetchAllObjects(descending: Bool = true) throws -> [T]? {
92 |
93 | let entries = listEntries()
94 | return entries.sorted { (en1, en2) -> Bool in
95 |
96 | if descending {
97 | return en1.creationDate.compare(en2.creationDate) == .orderedDescending
98 | } else {
99 | return en1.creationDate.compare(en2.creationDate) == .orderedAscending
100 | }
101 | }.map { $0.object }
102 | }
103 |
104 | public func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T] {
105 | let entries = listEntries()
106 | return entries.filter { $0.creationDate >= interval.toDate }.map { $0.object }
107 | }
108 |
109 | public func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T] {
110 | let entries = listEntries()
111 | return entries.filter { $0.creationDate <= interval.toDate }.map { $0.object }
112 | }
113 |
114 | private func listEntries() -> [Entry] {
115 | var entires: [Entry] = []
116 | for key in keyTracker.keys {
117 | if let entry = wrapped.object(forKey: WrappedKey(key)), entry.isExpired == false {
118 | entires.append(entry)
119 | }
120 | }
121 | return entires
122 | }
123 |
124 | public subscript(key: T.ID) -> T? {
125 |
126 | get { return try? fetchObject(for: key) }
127 | set {
128 | guard let value = newValue else {
129 | // If nil was assigned using our subscript,
130 | // then we remove any value for that key:
131 | try? deleteObject(forKey: key)
132 | return
133 | }
134 | save(value)
135 | }
136 | }
137 |
138 | public func contains(_ key: T.ID) -> Bool {
139 | do {
140 | return try fetchObject(for: key) != nil
141 | } catch {
142 | return false
143 | }
144 | }
145 |
146 | public func isExpired(for key: T.ID) -> Bool {
147 | return !contains(key)
148 | }
149 |
150 | public var objectsCount: Int {
151 | deleteExpired()
152 | return keyTracker.keys.count
153 | }
154 |
155 | public func deleteObject(forKey key: T.ID) throws {
156 | wrapped.removeObject(forKey: WrappedKey(key))
157 | }
158 |
159 | public func deleteObjects(forKeys keys: [T.ID]) throws {
160 | for key in keys {
161 | try deleteObject(forKey: key)
162 | }
163 | }
164 |
165 | public func deleteAll() throws {
166 | wrapped.removeAllObjects()
167 | }
168 |
169 | public func deleteExpired() {
170 | let keys = keyTracker.keys
171 | for key in keys {
172 | if let entry = wrapped.object(forKey: WrappedKey(key)), entry.isExpired {
173 | try? deleteObject(forKey: key)
174 | }
175 | }
176 | }
177 | }
178 |
179 | // MARK: - Key Wrapper
180 |
181 | private extension MemoryStorage {
182 |
183 | /// Key wrapper helps make `T.ID` compatblw with `NSCache` key
184 | /// Should sublcalss`NSObject` to be compatible with `NSCache` key
185 | final class WrappedKey: NSObject {
186 |
187 | /// Objects's key
188 | let key: T.ID
189 |
190 | init(_ key: T.ID) { self.key = key }
191 |
192 | override var hash: Int { return key.hashValue }
193 |
194 | override func isEqual(_ object: Any?) -> Bool {
195 | guard let value = object as? WrappedKey else {
196 | return false
197 | }
198 |
199 | return value.key == key
200 | }
201 | }
202 | }
203 |
204 | // MARK: - KeyTracker
205 |
206 | private extension MemoryStorage {
207 |
208 | /// Helper class to keep tracking of the objects in the memory
209 | final class KeyTracker: NSObject, NSCacheDelegate {
210 |
211 | /// A set of the stored keys,
212 | var keys = Set()
213 |
214 | /// `NSCache` delegate method get called each time the object is being evicted from memory.
215 | func cache(_ cache: NSCache,
216 | willEvictObject object: Any) {
217 | guard let entry = object as? Entry else {
218 | return
219 | }
220 |
221 | keys.remove(entry.key)
222 | }
223 | }
224 | }
225 |
226 | // MARK: - Entry Setup
227 |
228 | private extension MemoryStorage {
229 |
230 | /// An Entry descibing the stored objects
231 | /// - seeAlso: ObjectDescriptor
232 | final class Entry {
233 |
234 | // The original object needs to be stored or fetched.
235 | let object: T
236 |
237 | /// Adds expiry date to the object
238 | let expirationDate: Date
239 |
240 | /// Adds creation date to the object.
241 | let creationDate: Date
242 |
243 | /// Object's uniuqe ID.
244 | let key: T.ID
245 |
246 | /// Checks wheather an object has expired or not.
247 | var isExpired: Bool {
248 | return Date() > expirationDate
249 | }
250 |
251 | init(object: T, creationDate: Date, expirationDate: Date) {
252 | self.object = object
253 | self.expirationDate = expirationDate
254 | self.creationDate = creationDate
255 | self.key = object[keyPath: T.idKey]
256 | }
257 | }
258 | }
259 |
--------------------------------------------------------------------------------
/Sources/Storage/UserDefaultsStorage.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | import Foundation
25 |
26 | /// User Defaults Storage utilizes `UserDefaults` to cache and retrive data to and from
27 | /// UserDefaluts it gives a convenient and easy to use way to cache any `Codable` and `Identifiable`
28 | /// object. **WARNING**: **Don't use it for heavy and large number of objects**
29 | /// Consider using `DiskStorage` for heavy and long term persistence.
30 | public class UserDefaultsStorage {
31 |
32 | /// Default Store.
33 | private var defaultsStore: UserDefaults
34 |
35 | /// Object's expiry date.
36 | public let expiryDate: SorageDateDescriptor
37 |
38 | /// Uniuqe store name.
39 | public var storeName: String
40 |
41 | /// JSON encoder to be used for encoding objects to be stored.
42 | public let encoder: JSONEncoder
43 |
44 | /// JSON decoder to be used to decode stored objects.
45 | public let decoder: JSONDecoder
46 |
47 | /// Key tracker used to keep track ob uniqe keys of objects stored in the store
48 | private var keyTracker: KeyTracker
49 |
50 | /// injectable `Date` object.
51 | private let dateProvider: () -> Date
52 |
53 | /// Initialize DefalutStorage with the given name and expiry date.
54 | ///
55 | /// - Parameters:
56 | /// - storeName: Defalut storage unique identifier.
57 | /// - dateProvider: injectable date object
58 | /// - encoder: JSON encoder to be used for encoding objects to be stored.
59 | /// - decoder: SON decoder to be used to decode stored objects.
60 | /// - expiryDate: The expiry date of each object being stored.
61 | required public init?(
62 | storeName: String,
63 | dateProvider: @escaping () -> Date = Date.init,
64 | encoder: JSONEncoder = .init(),
65 | decoder: JSONDecoder = .init(),
66 | expiryDate: SorageDateDescriptor = .never) {
67 |
68 | self.storeName = storeName
69 | self.dateProvider = dateProvider
70 | self.encoder = encoder
71 | self.decoder = decoder
72 | self.expiryDate = expiryDate
73 | self.keyTracker = KeyTracker(keysStoreName: storeName + "-keys")
74 |
75 | keyTracker.listAllKeys()
76 | guard let defaultsStore = UserDefaults(suiteName: storeName) else { return nil }
77 | self.defaultsStore = defaultsStore
78 | }
79 |
80 | }
81 |
82 | // MARK: - CacheStorable
83 |
84 | extension UserDefaultsStorage: CacheStorable {
85 |
86 | public func save(_ object: T) throws {
87 | let entry = ObjectDescriptor(object: object, expiryDate: expiryDate.expireDate, creationDate: dateProvider())
88 | let data = try encoder.encode(entry)
89 | let key = extractKey(from: object)
90 | defaultsStore.set(data, forKey: key )
91 | keyTracker.keys.insert(key)
92 | keyTracker.updateKeysStore()
93 | }
94 |
95 | public func save(_ objects: [T]) throws {
96 |
97 | for object in objects {
98 | try save(object)
99 | }
100 | }
101 |
102 | public func fetchObject(for key: T.ID) throws -> T? {
103 |
104 | guard let data = defaultsStore.data(forKey: reverseKey(from: key)) else { return nil }
105 | let entry = try decoder.decode(ObjectDescriptor.self, from: data)
106 |
107 | if !entry.isExpired {
108 | return entry.object
109 | }
110 | return nil
111 | }
112 |
113 | public func fetchObjects(for keys: [T.ID]) throws -> [T]? {
114 |
115 | return try keys.compactMap { try fetchObject(for: $0) }
116 | }
117 |
118 | public func fetchAllObjects(descending: Bool = true) throws -> [T]? {
119 |
120 | return try listAllObjects().sorted { (en1, en2) -> Bool in
121 | if descending {
122 | return en1.creationDate.compare(en2.creationDate) == .orderedDescending
123 | } else {
124 | return en1.creationDate.compare(en2.creationDate) == .orderedAscending
125 | }
126 | }.map { $0.object }
127 | }
128 |
129 | public func fetchObjectsStored(inLast interval: SorageDateDescriptor) throws -> [T] {
130 |
131 | return try listAllObjects().filter { $0.creationDate >= interval.toDate }.map { $0.object }
132 | }
133 |
134 | public func fetchObjectsStored(before interval: SorageDateDescriptor) throws -> [T] {
135 |
136 | return try listAllObjects().filter { $0.creationDate <= interval.toDate }.map { $0.object }
137 | }
138 |
139 | public func contains(_ key: T.ID) -> Bool {
140 | do {
141 | return try fetchObject(for: key) != nil
142 | } catch {
143 | return false
144 | }
145 | }
146 |
147 | public func isExpired(for key: T.ID) -> Bool {
148 | return !contains(key)
149 | }
150 |
151 | public var objectsCount: Int {
152 | // Remove expired objects first
153 | // then calculate the objects countn from keys.
154 | deleteExpired()
155 | return keyTracker.keys.count
156 | }
157 |
158 | public subscript(key: T.ID) -> T? {
159 | get {
160 | return try? fetchObject(for: key)
161 | }
162 | set {
163 | guard let value = newValue else {
164 | // If nil was assigned using subscript,
165 | // then we remove any value for that key:
166 | try? deleteObject(forKey: key)
167 | return
168 | }
169 | try? save(value)
170 | }
171 | }
172 |
173 | public func deleteObject(forKey key: T.ID) throws {
174 | let objectKey = reverseKey(from: key)
175 | defaultsStore.removeObject(forKey: objectKey)
176 | keyTracker.keys.remove(objectKey)
177 | keyTracker.updateKeysStore()
178 | }
179 |
180 | public func deleteObjects(forKeys keys: [T.ID]) throws {
181 | for key in keys {
182 | try deleteObject(forKey: key)
183 | }
184 | }
185 |
186 | public func deleteAll() throws {
187 |
188 | for key in keyTracker.keys {
189 | defaultsStore.removeObject(forKey: key)
190 | }
191 |
192 | keyTracker.keys.removeAll()
193 | keyTracker.updateKeysStore()
194 | }
195 |
196 | public func deleteExpired() {
197 | _ = try? listAllObjects()
198 | }
199 |
200 | }
201 |
202 | // MARK: - Helpers
203 |
204 | private extension UserDefaultsStorage {
205 |
206 | /// List all objects in the store and filter the expired ones
207 | ///
208 | /// - Throws: JSON decoding error.
209 | /// - Returns: array of `StorableObject`.
210 | @discardableResult
211 | func listAllObjects() throws-> [ObjectDescriptor] {
212 |
213 | return try keyTracker.keys.compactMap { key in
214 | guard let data = defaultsStore.data(forKey: key) else { return nil }
215 | let entry = try decoder.decode(ObjectDescriptor.self, from: data)
216 | if !entry.isExpired {
217 | return entry
218 | } else {
219 | // If the object has aleady expired
220 | // delete it and return nil
221 | try deleteObject(forKey: entry.key)
222 | return nil
223 | }
224 | }
225 | }
226 |
227 | /// ectract and convert the key asscoated with the object to string.
228 | ///
229 | /// - Parameters:
230 | /// - object: object.
231 | /// - Returns: unique key represented by a string.
232 | func extractKey(from object: T) -> String {
233 | return "\(storeName)-\(object[keyPath: T.idKey])"
234 | }
235 |
236 | /// ectract and convert the key asscoated with the object to string.
237 | ///
238 | /// - Parameters:
239 | /// - id: object's unique id.
240 | /// - Returns: unique key represented by a string.
241 | func reverseKey(from id: T.ID) -> String {
242 | return "\(storeName)-\(id)"
243 | }
244 | }
245 |
246 | // MARK: - KeyTracker
247 | private extension UserDefaultsStorage {
248 |
249 | /// Helper struct to keep trackk of the keys in the store
250 | struct KeyTracker {
251 | /// store keys
252 | var keys = Set()
253 |
254 | /// A seperate store to hold the keys values.
255 | let keysStore: UserDefaults?
256 |
257 | /// Unique name for keys store
258 | let keysStoreName: String
259 |
260 | init(keysStoreName: String) {
261 | self.keysStoreName = keysStoreName
262 | self.keysStore = UserDefaults(suiteName: keysStoreName)
263 | }
264 |
265 | /// list all keys in the store
266 | mutating func listAllKeys() {
267 | let keys = keysStore?.array(forKey: keysPath) as? [String] ?? []
268 | self.keys = Set(keys)
269 | }
270 |
271 | /// update store with the new keys after each addition or deletion happens
272 | func updateKeysStore() {
273 | let keysArray = Array(keys)
274 | keysStore?.set(keysArray, forKey: keysPath)
275 | }
276 |
277 | /// Path to store the keys in the keysStore.
278 | var keysPath: String {
279 | return keysStoreName + "-keys"
280 | }
281 | }
282 | }
283 |
--------------------------------------------------------------------------------
/Tests/DiskStorageTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 |
25 | import XCTest
26 | @testable import CodablePersist
27 |
28 | class DiskStorageTest: XCTestCase {
29 |
30 | var storage: DiskStorage!
31 | let storageName = "storageTest"
32 |
33 | override func setUp() {
34 | super.setUp()
35 | storage = try! initStorageWith(expiry: .minutes(interval: 3))
36 |
37 | }
38 |
39 | override func tearDown() {
40 |
41 | try? storage.deleteAll()
42 | storage = nil
43 | super.tearDown()
44 | }
45 |
46 | func testInitStorage() {
47 |
48 | XCTAssertNoThrow(try initStorageWith(expiry: .minutes(interval: 10)))
49 | }
50 |
51 | func testSaveSingleObject() {
52 |
53 | XCTAssertNoThrow(try storage.save(Post.singlePost))
54 | XCTAssertEqual(storage.objectsCount, 1)
55 |
56 | }
57 |
58 | func testSaveMultiObjects() {
59 |
60 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
61 | XCTAssertEqual(10, storage.objectsCount)
62 | }
63 |
64 | func testFetchSingleObject() {
65 |
66 | XCTAssertNoThrow(try storage.save(Post.singlePost))
67 | XCTAssertNotNil(try? storage.fetchObject(for: 0) )
68 | XCTAssertEqual(0, try? storage.fetchObject(for: 0)?.id)
69 | }
70 |
71 | func testFetchMultiObjects() {
72 |
73 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
74 | XCTAssertNotNil(try? storage.fetchObjects(for: [3,2]) )
75 | XCTAssertEqual("Swift Article3", try? storage.fetchObject(for: 3)?.title)
76 | }
77 |
78 | func testFetchAllObjects() {
79 |
80 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
81 | XCTAssertEqual(10, try storage.fetchAllObjects()?.count)
82 | }
83 |
84 | func testObjectExpiry() {
85 |
86 | storage = try! initStorageWith(expiry: .seconds(interval: -10))
87 |
88 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
89 | XCTAssertTrue(storage.isExpired(for: 1))
90 | XCTAssertEqual(0, try storage.fetchAllObjects()?.count)
91 |
92 | }
93 |
94 | func testFetchStoredInLast() {
95 |
96 | storage = try! initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4))
97 |
98 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
99 | XCTAssertNotNil(try storage.fetchObjectsStored(inLast: .minutes(interval: 3)))
100 | XCTAssertEqual(0 , try storage.fetchObjectsStored(before: .minutes(interval: 4)).count)
101 | }
102 |
103 | func testfetchStoredBefore() {
104 |
105 | storage = try! initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 6))
106 |
107 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
108 | XCTAssertNotNil(try storage.fetchObjectsStored(before: .minutes(interval: 3)))
109 | XCTAssertEqual(0, try storage.fetchObjectsStored(inLast: .minutes(interval: 3)).count)
110 | }
111 |
112 | func testSubscript() {
113 |
114 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
115 | XCTAssertTrue(storage[1]?.id == 1 )
116 | XCTAssertFalse(storage[3]?.title == "Swift Article1")
117 | storage[1] = nil
118 | XCTAssertNil(storage[1])
119 | }
120 |
121 | func testDeleteSingleObject() {
122 |
123 | XCTAssertNoThrow(try storage.save(Post.singlePost))
124 | XCTAssertEqual(1, storage.objectsCount)
125 | XCTAssertNoThrow(try storage.deleteObject(forKey: Post.singlePost.id))
126 | XCTAssertNil(storage[0])
127 | }
128 |
129 | func testDeleteMultiObjects() {
130 |
131 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
132 | XCTAssertNoThrow(try storage.deleteObjects(forKeys: [1,2,3]))
133 | XCTAssertEqual(7, storage.objectsCount)
134 | XCTAssertNil(storage[3])
135 | }
136 |
137 | func testDeleteAllObjects() {
138 |
139 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
140 | XCTAssertEqual(10, storage.objectsCount)
141 | XCTAssertNoThrow(try storage.deleteAll())
142 | XCTAssertEqual(0, storage.objectsCount)
143 | }
144 |
145 | func testStorageContains() {
146 |
147 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
148 | XCTAssert(storage.contains(2))
149 | XCTAssertFalse(storage.contains(12))
150 | }
151 |
152 | func testDeleteExpired() {
153 | storage = try! initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 6))
154 |
155 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
156 | storage.deleteExpired()
157 | XCTAssertEqual(0, storage.objectsCount)
158 | }
159 |
160 | private func initStorageWith(injectedDate: Date) throws -> DiskStorage{
161 | return try DiskStorage(dateProvider: { () -> Date in
162 | return injectedDate
163 | }, storeName: storageName, expiryDate: .minutes(interval: -3))
164 | }
165 |
166 | private func initStorageWith(expiry: SorageDateDescriptor) throws -> DiskStorage {
167 | return try DiskStorage(storeName: storageName, expiryDate: expiry)
168 | }
169 | }
170 |
171 |
172 |
--------------------------------------------------------------------------------
/Tests/MemoryStorageTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | import XCTest
25 | @testable import CodablePersist
26 |
27 | class MemoryStorageTest: XCTestCase {
28 |
29 | var storage: MemoryStorage!
30 |
31 | override func setUp() {
32 | super.setUp()
33 | storage = MemoryStorage(expiryDate: .minutes(interval: 3))
34 | }
35 |
36 | override func tearDown() {
37 |
38 | try? storage.deleteAll()
39 | storage = nil
40 | super.tearDown()
41 | }
42 |
43 | func testInitStorage() {
44 | storage = MemoryStorage(expiryDate: .minutes(interval: 3))
45 | XCTAssertNotNil(storage)
46 | }
47 |
48 |
49 | func testSaveSingleObject() {
50 | storage.save(Post.singlePost)
51 | XCTAssertEqual(storage.objectsCount, 1)
52 | XCTAssertTrue(storage.contains(0))
53 | }
54 |
55 | func testSaveMultiObjects() {
56 | storage.save(Post.dummyPosts())
57 | XCTAssertEqual(10, storage.objectsCount)
58 | XCTAssertTrue(storage.contains(4))
59 | XCTAssertEqual([1,2,3],[ storage[1]?.id, storage[2]?.id, storage[3]?.id])
60 | }
61 |
62 | func testFetchSingleObject() {
63 | storage.save(Post.singlePost)
64 | XCTAssertNotNil(try storage.fetchObject(for: 0) )
65 | XCTAssertEqual(0, try storage.fetchObject(for: 0)?.id)
66 | XCTAssertEqual("Single post test", try storage.fetchObject(for: 0)?.title)
67 | }
68 |
69 |
70 | func testFetchMultiObjects() {
71 |
72 | storage.save(Post.dummyPosts())
73 | XCTAssertNotNil(try? storage.fetchObjects(for: [3,2]) )
74 | XCTAssertEqual("Swift Article3", try? storage.fetchObject(for: 3)?.title)
75 | }
76 |
77 | func testFetchAllObjects() {
78 |
79 | storage.save(Post.dummyPosts())
80 | XCTAssertEqual(10, try storage.fetchAllObjects()?.count)
81 | XCTAssertEqual(1, try storage.fetchAllObjects(descending: false)?[0].id)
82 | }
83 |
84 | func testObjectExpiry() {
85 | storage = initStorageWith(injectedDate: Date(), expiryDate: .seconds(interval: -10))
86 |
87 | storage.save(Post.dummyPosts())
88 | XCTAssertTrue(storage.isExpired(for: 1))
89 | XCTAssertEqual(0, try storage.fetchAllObjects()?.count)
90 | }
91 |
92 | func testFetchStoredInLast() {
93 |
94 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4))
95 | storage.save(Post.dummyPosts())
96 |
97 | XCTAssertEqual(10, try storage.fetchObjectsStored(inLast: .minutes(interval: 5)).count)
98 | XCTAssertEqual(0, try storage.fetchObjectsStored(before: .minutes(interval: 5)).count)
99 | XCTAssertNotNil(try storage.fetchObjectsStored(inLast: .minutes(interval: 5))[0])
100 | }
101 |
102 | func testfetchStoredBefore() {
103 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4))
104 | storage.save(Post.dummyPosts())
105 |
106 | XCTAssertEqual(0, try storage.fetchObjectsStored(inLast: .minutes(interval: 3)).count)
107 | XCTAssertEqual(10, try storage.fetchObjectsStored(before: .minutes(interval: 3)).count)
108 | XCTAssertNotNil(try storage.fetchObjectsStored(before: .minutes(interval: 3))[0])
109 | }
110 |
111 | func testSubscript() {
112 | storage.save(Post.dummyPosts())
113 | XCTAssertTrue(storage[1]?.id == 1 )
114 | XCTAssertFalse(storage[3]?.title == "Swift Article1")
115 | storage[1] = nil
116 | XCTAssertNil(storage[1])
117 | }
118 |
119 | func testDeleteSingleObject() {
120 | storage.save(Post.singlePost)
121 |
122 | XCTAssertEqual(1, storage.objectsCount)
123 | XCTAssertNoThrow(try storage.deleteObject(forKey: Post.singlePost.id))
124 | XCTAssertNil(storage[0])
125 | }
126 |
127 | func testDeleteMultiObjects() {
128 | storage.save(Post.dummyPosts())
129 | XCTAssertNoThrow(try storage.deleteObjects(forKeys: [1,2,3]))
130 | XCTAssertEqual(7, storage.objectsCount)
131 | XCTAssertNil(storage[3])
132 | }
133 |
134 | func testDeleteAllObjects() {
135 | storage.save(Post.dummyPosts())
136 | XCTAssertEqual(10, storage.objectsCount)
137 | XCTAssertNoThrow(try storage.deleteAll())
138 | XCTAssertEqual(0, storage.objectsCount)
139 | }
140 |
141 | func testStorageContains() {
142 |
143 | storage.save(Post.dummyPosts())
144 | XCTAssert(storage.contains(2))
145 | XCTAssertFalse(storage.contains(12))
146 | }
147 |
148 | func testDeleteExpired() {
149 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 10), expiryDate: .minutes(interval: -4))
150 | storage.save(Post.dummyPosts())
151 |
152 | storage.deleteExpired()
153 | XCTAssertEqual(0, storage.objectsCount)
154 | }
155 |
156 | func testStorageCapacity() {
157 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4), maxObjectsCount: 5)
158 | storage.save(Post.dummyPosts())
159 | XCTAssertEqual(5, storage.objectsCount)
160 | }
161 |
162 | private func initStorageWith(injectedDate: Date,
163 | expiryDate: SorageDateDescriptor = .minutes(interval: 4),
164 | maxObjectsCount: Int = 10) -> MemoryStorage{
165 |
166 | return MemoryStorage(dateProvider: {return injectedDate},
167 | expiryDate: expiryDate,
168 | maxObjectsCount: maxObjectsCount)
169 | }
170 |
171 |
172 | }
173 |
174 |
--------------------------------------------------------------------------------
/Tests/Mock/PostMock.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 | @testable import CodablePersist
25 | import Foundation
26 |
27 | struct Post: Identifiable, Codable {
28 | static var idKey = \Post.id
29 | var title: String
30 | var id: Int
31 |
32 | static func dummyPosts() -> [Post] {
33 | var reqs: [Post] = []
34 | for i in 1...10 {
35 | let re = Post(title: "Swift Article\(i)", id: i)
36 | reqs.append(re)
37 | }
38 | return reqs
39 |
40 | }
41 |
42 | static func dummyPosts2() -> [Post] {
43 | var reqs: [Post] = []
44 | for i in 1...10 {
45 | let re = Post(title: "Swift Article1\(i)", id: 10 + i)
46 | reqs.append(re)
47 | }
48 | return reqs
49 |
50 | }
51 |
52 | static let singlePost = Post(title: "Single post test", id: 0)
53 | }
54 |
--------------------------------------------------------------------------------
/Tests/UserDefaultsStorageTest.swift:
--------------------------------------------------------------------------------
1 | //
2 | // CodablePersist
3 | //
4 | // Copyright (c) 2020 Ali A. Hilal - https://github.com/engali94
5 | //
6 | // Permission is hereby granted, free of charge, to any person obtaining a copy
7 | // of this software and associated documentation files (the "Software"), to deal
8 | // in the Software without restriction, including without limitation the rights
9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | // copies of the Software, and to permit persons to whom the Software is
11 | // furnished to do so, subject to the following conditions:
12 | //
13 | // The above copyright notice and this permission notice shall be included in
14 | // all copies or substantial portions of the Software.
15 | //
16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22 | // THE SOFTWARE.
23 |
24 |
25 | import XCTest
26 | @testable import CodablePersist
27 |
28 | class UserDefaultsStorageTest: XCTestCase {
29 |
30 | var storage: UserDefaultsStorage!
31 | let storageName = "userdefaultsStorage"
32 |
33 | override func setUp() {
34 | storage = UserDefaultsStorage(storeName: storageName)
35 | super.setUp()
36 | }
37 |
38 | override func tearDown() {
39 | try? storage.deleteAll()
40 | storage = nil
41 | super.tearDown()
42 | }
43 |
44 | func testInitStorage() {
45 | storage = UserDefaultsStorage(storeName: storageName)
46 | XCTAssertNotNil(storage)
47 | XCTAssertEqual(storage.storeName, storageName)
48 | }
49 |
50 | func testSaveSingleObject() {
51 |
52 | XCTAssertNoThrow( try storage.save(Post.singlePost))
53 | XCTAssertEqual(storage.objectsCount, 1)
54 | XCTAssertTrue(storage.contains(0))
55 | }
56 |
57 | func testSaveMultiObjects() {
58 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
59 | XCTAssertEqual(10, storage.objectsCount)
60 | XCTAssertTrue(storage.contains(4))
61 | XCTAssertEqual([1,2,3],[ storage[1]?.id, storage[2]?.id, storage[3]?.id])
62 | }
63 |
64 | func testFetchSingleObject() {
65 | XCTAssertNoThrow(try storage.save(Post.singlePost))
66 | XCTAssertNotNil(try storage.fetchObject(for: 0) )
67 | XCTAssertEqual(0, try storage.fetchObject(for: 0)?.id)
68 | XCTAssertEqual("Single post test", try storage.fetchObject(for: 0)?.title)
69 | }
70 |
71 |
72 | func testFetchMultiObjects() {
73 |
74 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
75 | XCTAssertNotNil(try? storage.fetchObjects(for: [3,2]) )
76 | XCTAssertEqual("Swift Article3", try? storage.fetchObject(for: 3)?.title)
77 | }
78 |
79 | func testFetchAllObjects() {
80 |
81 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
82 | XCTAssertEqual(10, try storage.fetchAllObjects()?.count)
83 | XCTAssertEqual(1, try storage.fetchAllObjects(descending: false)?[0].id)
84 | }
85 |
86 | func testObjectExpiry() {
87 | storage = initStorageWith(injectedDate: Date(), expiryDate: .seconds(interval: -10))
88 |
89 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
90 | XCTAssertTrue(storage.isExpired(for: 1))
91 | XCTAssertEqual(0, try storage.fetchAllObjects()?.count)
92 | }
93 |
94 | func testFetchStoredInLast() {
95 |
96 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4))
97 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
98 |
99 |
100 | XCTAssertEqual(10, try storage.fetchObjectsStored(inLast: .minutes(interval: 5)).count)
101 | XCTAssertEqual(0, try storage.fetchObjectsStored(before: .minutes(interval: 5)).count)
102 | XCTAssertNotNil(try storage.fetchObjectsStored(inLast: .minutes(interval: 5))[0])
103 | }
104 |
105 | func testfetchStoredBefore() {
106 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 4))
107 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
108 |
109 | XCTAssertEqual(0, try storage.fetchObjectsStored(inLast: .minutes(interval: 3)).count)
110 | XCTAssertEqual(10, try storage.fetchObjectsStored(before: .minutes(interval: 3)).count)
111 | XCTAssertNotNil(try storage.fetchObjectsStored(before: .minutes(interval: 3))[0])
112 | }
113 |
114 | func testSubscript() {
115 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
116 | XCTAssertTrue(storage[1]?.id == 1 )
117 | XCTAssertFalse(storage[3]?.title == "Swift Article1")
118 | storage[1] = nil
119 | XCTAssertNil(storage[1])
120 | }
121 |
122 | func testDeleteSingleObject() {
123 | XCTAssertNoThrow(try storage.save(Post.singlePost))
124 |
125 | XCTAssertEqual(1, storage.objectsCount)
126 | XCTAssertNoThrow(try storage.deleteObject(forKey: Post.singlePost.id))
127 | XCTAssertNil(storage[0])
128 | }
129 |
130 | func testDeleteMultiObjects() {
131 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
132 | XCTAssertNoThrow(try storage.deleteObjects(forKeys: [1,2,3]))
133 | XCTAssertEqual(7, storage.objectsCount)
134 | XCTAssertNil(storage[3])
135 | }
136 |
137 | func testDeleteAllObjects() {
138 |
139 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
140 | XCTAssertEqual(10, storage.objectsCount)
141 | XCTAssertNoThrow(try storage.deleteAll())
142 | XCTAssertEqual(0, storage.objectsCount)
143 | }
144 |
145 | func testStorageContains() {
146 |
147 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
148 | XCTAssert(storage.contains(2))
149 | XCTAssertFalse(storage.contains(12))
150 | }
151 |
152 | func testDeleteExpired() {
153 |
154 | storage = initStorageWith(injectedDate: Date(timeIntervalSinceNow: -60 * 10), expiryDate: .minutes(interval: -4))
155 | XCTAssertNoThrow(try storage.save(Post.dummyPosts()))
156 |
157 | storage.deleteExpired()
158 | XCTAssertEqual(0, storage.objectsCount)
159 | }
160 |
161 | private func initStorageWith(injectedDate: Date,
162 | expiryDate: SorageDateDescriptor = .minutes(interval: 6)) -> UserDefaultsStorage{
163 |
164 | return UserDefaultsStorage(storeName: storageName, dateProvider: {return injectedDate},expiryDate: expiryDate)!
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/docs/Classes.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Classes Reference
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
Disk Storage utilizes FileManager to cache and retrive data to and from the disk
106 | it gives a convenient and easy to use way to cache any Codable and Identifiable object.
Memory Storage utilizes NSCache to cache and retrive data to and from memory it gives a
136 | convenient and easy to use way to cache any Codable and Identifiable object.
137 | WARNING: Don’t use it for heavy objects as there will be potential lose of date if any
138 | memory pressure happens.
139 | Consider using DiskStorage for heavy and long term persistence.
User Defaults Storage utilizes UserDefaults to cache and retrive data to and from
169 | UserDefaluts it gives a convenient and easy to use way to cache any Codable and Identifiable
170 | object. WARNING: Don’t use it for heavy and large number of objects
171 | Consider using DiskStorage for heavy and long term persistence.
Disk Storage utilizes FileManager to cache and retrive data to and from the disk
106 | it gives a convenient and easy to use way to cache any Codable and Identifiable object.
Memory Storage utilizes NSCache to cache and retrive data to and from memory it gives a
136 | convenient and easy to use way to cache any Codable and Identifiable object.
137 | WARNING: Don’t use it for heavy objects as there will be potential lose of date if any
138 | memory pressure happens.
139 | Consider using DiskStorage for heavy and long term persistence.
User Defaults Storage utilizes UserDefaults to cache and retrive data to and from
169 | UserDefaluts it gives a convenient and easy to use way to cache any Codable and Identifiable
170 | object. WARNING: Don’t use it for heavy and large number of objects
171 | Consider using DiskStorage for heavy and long term persistence.