Skip to content

Commit

Permalink
[feat] Add Expiration options to Files stores (#11)
Browse files Browse the repository at this point in the history
* [feat] Add Expiration options to Files stores

* [chore] Bump version number to 1.1
  • Loading branch information
omaralbeik authored Dec 28, 2018
1 parent d19e41e commit 74a7c46
Show file tree
Hide file tree
Showing 40 changed files with 1,737 additions and 90 deletions.
2 changes: 1 addition & 1 deletion PersistenceKit.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "PersistenceKit"
s.version = "1.0"
s.version = "1.1"
s.summary = "
Store and retrieve Codable objects to various persistence layers, in a couple lines of code! "
s.description = <<-DESC
Expand Down
12 changes: 8 additions & 4 deletions PersistenceKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
07572F4E217352490093BD3E /* KeychainError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07572F4D217352490093BD3E /* KeychainError.swift */; };
07572F50217352550093BD3E /* SingleKeychainStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07572F4F217352550093BD3E /* SingleKeychainStore.swift */; };
07572F53217354090093BD3E /* FilesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07572F52217354090093BD3E /* FilesStore.swift */; };
07572F55217354130093BD3E /* SingleFilesStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07572F54217354130093BD3E /* SingleFilesStore.swift */; };
07572F55217354130093BD3E /* SingleFileStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07572F54217354130093BD3E /* SingleFileStore.swift */; };
0773E54121D65BDD0095276F /* Expiration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0773E54021D65BDD0095276F /* Expiration.swift */; };
078D2A692147BA7500923390 /* SingleUserDefaultsStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078D2A682147BA7500923390 /* SingleUserDefaultsStore.swift */; };
078D2A6C2147BD4100923390 /* SingleUserDefaultsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 078D2A6A2147BD2E00923390 /* SingleUserDefaultsStoreTests.swift */; };
079A3F632179EA9000CB3339 /* SingleFilesStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 079A3F612179EA6E00CB3339 /* SingleFilesStoreTests.swift */; };
Expand Down Expand Up @@ -45,9 +46,10 @@
07572F4D217352490093BD3E /* KeychainError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainError.swift; sourceTree = "<group>"; };
07572F4F217352550093BD3E /* SingleKeychainStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleKeychainStore.swift; sourceTree = "<group>"; };
07572F52217354090093BD3E /* FilesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesStore.swift; sourceTree = "<group>"; };
07572F54217354130093BD3E /* SingleFilesStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFilesStore.swift; sourceTree = "<group>"; };
07572F54217354130093BD3E /* SingleFileStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFileStore.swift; sourceTree = "<group>"; };
07572F56217354A30093BD3E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
07572F57217354A30093BD3E /* LICENSE */ = {isa = PBXFileReference; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
0773E54021D65BDD0095276F /* Expiration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Expiration.swift; sourceTree = "<group>"; };
078D2A682147BA7500923390 /* SingleUserDefaultsStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleUserDefaultsStore.swift; sourceTree = "<group>"; };
078D2A6A2147BD2E00923390 /* SingleUserDefaultsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleUserDefaultsStoreTests.swift; sourceTree = "<group>"; };
079A3F612179EA6E00CB3339 /* SingleFilesStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleFilesStoreTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -120,7 +122,8 @@
isa = PBXGroup;
children = (
07572F52217354090093BD3E /* FilesStore.swift */,
07572F54217354130093BD3E /* SingleFilesStore.swift */,
07572F54217354130093BD3E /* SingleFileStore.swift */,
0773E54021D65BDD0095276F /* Expiration.swift */,
);
path = Files;
sourceTree = "<group>";
Expand Down Expand Up @@ -302,7 +305,8 @@
07563831210A3C4A005CDE89 /* UserDefaultsStore.swift in Sources */,
07572F53217354090093BD3E /* FilesStore.swift in Sources */,
07572F4E217352490093BD3E /* KeychainError.swift in Sources */,
07572F55217354130093BD3E /* SingleFilesStore.swift in Sources */,
07572F55217354130093BD3E /* SingleFileStore.swift in Sources */,
0773E54121D65BDD0095276F /* Expiration.swift in Sources */,
078D2A692147BA7500923390 /* SingleUserDefaultsStore.swift in Sources */,
07572F50217352550093BD3E /* SingleKeychainStore.swift in Sources */,
07563822210A396C005CDE89 /* Identifiable.swift in Sources */,
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ PersistenceKit offers 3 layers of persistence suitable for most use cases:
- Stores data to OS's keychain using the [`Security Framework`](https://developer.apple.com/documentation/security).
- Suitable for storing sensitive data, like access tokens.

## What's new in v1.1

v1.1 Brings the ability to set expiration options to [`FilesStore`](https://github.com/Teknasyon-Teknoloji/PersistenceKit/blob/master/Sources/Files/FilesStore.swift) and [`SingleFilesStore`](https://github.com/Teknasyon-Teknoloji/PersistenceKit/blob/master/Sources/Files/SingleFilesStore.swift)

## Installation

Expand Down
62 changes: 62 additions & 0 deletions Sources/Files/Expiration.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
//
// PersistenceKit
//
// Copyright (c) 2018-Present Teknasyon Teknoloji.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

import Foundation

/// `Expiration` offers options about the validity of files stored in `FilesStore` and `SingleFileStore`.
public enum Expiration {

/// Object will not expire automatically.
case never

/// Object will expire after the specified amount of seconds.
case seconds(TimeInterval)

/// Object will expire on the specified date.
case date(Date)

}

// Source: https://github.com/hyperoslo/Cache/blob/master/Source/Shared/Library/Expiry.swift

extension Expiration {

/// Returns the appropriate date object
var date: Date {
switch self {
case .never:
// Ref: http://lists.apple.com/archives/cocoa-dev/2005/Apr/msg01833.html
return Date(timeIntervalSince1970: 60 * 60 * 24 * 365 * 68)
case .seconds(let seconds):
return Date().addingTimeInterval(seconds)
case .date(let date):
return date
}
}

/// Checks if cached object is expired according to expiration date
var isExpired: Bool {
return date < Date()
}

}
45 changes: 38 additions & 7 deletions Sources/Files/FilesStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ open class FilesStore<T: Codable & Identifiable> {
/// **Warning**: Never use the same identifier for two -or more- different stores.
public let uniqueIdentifier: String

/// Store `Expiration` option. _default is `.never`_
public let expiration: Expiration

/// JSON encoder. _default is `JSONEncoder()`_
open var encoder = JSONEncoder()

Expand All @@ -40,13 +43,16 @@ open class FilesStore<T: Codable & Identifiable> {
/// FileManager. _default is `FileManager.default`_
private var manager = FileManager.default

/// Initialize store with given identifier.
/// Initialize store with given identifiera and an optional expiry duration.
///
/// **Warning**: Never use the same identifier for two -or more- different stores.
///
/// - Parameter uniqueIdentifier: store's unique identifier.
required public init(uniqueIdentifier: String) {
/// - Parameters:
/// - uniqueIdentifier: store's unique identifier.
/// - expiryDuration: optional store's expiry duration _default is `.never`_.
required public init(uniqueIdentifier: String, expiration: Expiration = .never) {
self.uniqueIdentifier = uniqueIdentifier
self.expiration = expiration
}

/// Save object to store.
Expand All @@ -57,7 +63,16 @@ open class FilesStore<T: Codable & Identifiable> {
let url = try storeURL()
let data = try encoder.encode(object)
try manager.createDirectory(atPath: url.path, withIntermediateDirectories: true, attributes: nil)
manager.createFile(atPath: try fileURL(object: object).path, contents: data, attributes: nil)

let path = try fileURL(object: object).path
manager.createFile(atPath: path, contents: data, attributes: nil)

let attributes: [FileAttributeKey: Any] = [
.creationDate: Date(),
.modificationDate: expiration.date
]

try manager.setAttributes(attributes, ofItemAtPath: path)
}

/// Save optional object (if not nil) to store.
Expand Down Expand Up @@ -159,16 +174,32 @@ private extension FilesStore {
/// - Returns: optional object.
func object(withIdString idString: String) -> T? {
guard let path = try? fileURL(idString: idString).path else { return nil }
guard let attributes = try? manager.attributesOfItem(atPath: path) else { return nil }
guard let modificationDate = attributes[.modificationDate] as? Date else { return nil }
guard modificationDate >= Date() else {
if let url = try? fileURL(idString: idString), manager.fileExists(atPath: url.path) {
try? manager.removeItem(at: url)
}
return nil
}
guard let data = manager.contents(atPath: path) else { return nil }
return try? decoder.decode(T.self, from: data)
}

/// Documents URL.
/// Documents or Caches URL.
///
/// - Returns: Documents URL.
/// - Returns: Documents or Caches URL.
/// - Throws: `FileManager` error
func documentsURL() throws -> URL {
return try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let directory: FileManager.SearchPathDirectory
switch expiration {
case .never:
directory = .documentDirectory
default:
directory = .cachesDirectory
}

return try manager.url(for: directory, in: .userDomainMask, appropriateFor: nil, create: true)
}

/// FilesStore URL.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,17 @@

import Foundation

/// `SingleFilesStore` offers a convenient way to store a single `Codable` objects in the files system.
open class SingleFilesStore<T: Codable> {
/// `SingleFileStore` offers a convenient way to store a single `Codable` objects in the files system.
open class SingleFileStore<T: Codable> {

/// Store's unique identifier.
///
/// **Warning**: Never use the same identifier for two -or more- different stores.
public let uniqueIdentifier: String

/// Store `Expiration` option. _default is `.never`_
public let expiration: Expiration

/// JSON encoder. _default is `JSONEncoder()`_
open var encoder = JSONEncoder()

Expand All @@ -44,9 +47,12 @@ open class SingleFilesStore<T: Codable> {
///
/// **Warning**: Never use the same identifier for two -or more- different stores.
///
/// - Parameter uniqueIdentifier: store's unique identifier.
required public init(uniqueIdentifier: String) {
/// - Parameters:
/// - uniqueIdentifier: store's unique identifier.
/// - expiryDuration: optional store's expiry duration _default is `.never`_.
required public init(uniqueIdentifier: String, expiration: Expiration = .never) {
self.uniqueIdentifier = uniqueIdentifier
self.expiration = expiration
}

/// Save object to store.
Expand All @@ -58,6 +64,13 @@ open class SingleFilesStore<T: Codable> {
let url = try storeURL()
try manager.createDirectory(atPath: url.path, withIntermediateDirectories: true, attributes: nil)
manager.createFile(atPath: try fileURL().path, contents: data, attributes: nil)

let attributes: [FileAttributeKey: Any] = [
.creationDate: Date(),
.modificationDate: expiration.date
]

try manager.setAttributes(attributes, ofItemAtPath: fileURL().path)
}

/// Save optional object (if not nil) to store.
Expand All @@ -73,6 +86,14 @@ open class SingleFilesStore<T: Codable> {
public var object: T? {
guard let path = try? fileURL().path else { return nil }
guard let data = manager.contents(atPath: path) else { return nil }

guard let attributes = try? manager.attributesOfItem(atPath: path) else { return nil }
guard let modificationDate = attributes[.modificationDate] as? Date else { return nil }
guard modificationDate >= Date() else {
try? delete()
return nil
}

guard let dict = try? decoder.decode([String: T].self, from: data) else { return nil }
return extractObject(from: dict)
}
Expand All @@ -92,14 +113,22 @@ open class SingleFilesStore<T: Codable> {
}

// MARK: - Helpers
private extension SingleFilesStore {
private extension SingleFileStore {

/// Documents URL.
///
/// - Returns: Documents URL.
/// - Throws: `FileManager` error
func documentsURL() throws -> URL {
return try manager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
let directory: FileManager.SearchPathDirectory
switch expiration {
case .never:
directory = .documentDirectory
default:
directory = .cachesDirectory
}

return try manager.url(for: directory, in: .userDomainMask, appropriateFor: nil, create: true)
}

/// FilesStore URL.
Expand Down
2 changes: 1 addition & 1 deletion Sources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<string>1.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
Expand Down
6 changes: 3 additions & 3 deletions Tests/Files/SingleFilesStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ final class SingleFilesStoreTests: XCTestCase {
// MARK: - Helpers
private extension SingleFilesStoreTests {

func createFreshUsersStore() -> SingleFilesStore<TestUser> {
var store = SingleFilesStore<TestUser>(uniqueIdentifier: "single-user")
func createFreshUsersStore() -> SingleFileStore<TestUser> {
var store = SingleFileStore<TestUser>(uniqueIdentifier: "single-user")
XCTAssertNoThrow(try store.delete())
store = SingleFilesStore<TestUser>(uniqueIdentifier: "single-user")
store = SingleFileStore<TestUser>(uniqueIdentifier: "single-user")
return store
}

Expand Down
19 changes: 11 additions & 8 deletions docs/Classes.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
<a href="Classes/FilesStore.html">FilesStore</a>
</li>
<li class="nav-group-task">
<a href="Classes/SingleFilesStore.html">SingleFilesStore</a>
<a href="Classes/SingleFileStore.html">SingleFileStore</a>
</li>
<li class="nav-group-task">
<a href="Classes/SingleKeychainStore.html">SingleKeychainStore</a>
Expand All @@ -50,6 +50,9 @@
<li class="nav-group-name">
<a href="Enums.html">Enumerations</a>
<ul class="nav-group-tasks">
<li class="nav-group-task">
<a href="Enums/Expiration.html">Expiration</a>
</li>
<li class="nav-group-task">
<a href="Enums/KeychainAccessibilityOption.html">KeychainAccessibilityOption</a>
</li>
Expand Down Expand Up @@ -113,25 +116,25 @@ <h4>Declaration</h4>
<li class="item">
<div>
<code>
<a name="/s:14PersistenceKit16SingleFilesStoreC"></a>
<a name="//apple_ref/swift/Class/SingleFilesStore" class="dashAnchor"></a>
<a class="token" href="#/s:14PersistenceKit16SingleFilesStoreC">SingleFilesStore</a>
<a name="/s:14PersistenceKit15SingleFileStoreC"></a>
<a name="//apple_ref/swift/Class/SingleFileStore" class="dashAnchor"></a>
<a class="token" href="#/s:14PersistenceKit15SingleFileStoreC">SingleFileStore</a>
</code>
</div>
<div class="height-container">
<div class="pointer-container"></div>
<section class="section">
<div class="pointer"></div>
<div class="abstract">
<p><code>SingleFilesStore</code> offers a convenient way to store a single <code>Codable</code> objects in the files system.</p>
<p><code>SingleFileStore</code> offers a convenient way to store a single <code>Codable</code> objects in the files system.</p>

<a href="Classes/SingleFilesStore.html" class="slightly-smaller">See more</a>
<a href="Classes/SingleFileStore.html" class="slightly-smaller">See more</a>
</div>
<div class="declaration">
<h4>Declaration</h4>
<div class="language">
<p class="aside-title">Swift</p>
<pre class="highlight swift"><code><span class="kd">open</span> <span class="kd">class</span> <span class="kt">SingleFilesStore</span><span class="o">&lt;</span><span class="kt">T</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">T</span> <span class="p">:</span> <span class="kt">Decodable</span><span class="p">,</span> <span class="kt">T</span> <span class="p">:</span> <span class="kt">Encodable</span></code></pre>
<pre class="highlight swift"><code><span class="kd">open</span> <span class="kd">class</span> <span class="kt">SingleFileStore</span><span class="o">&lt;</span><span class="kt">T</span><span class="o">&gt;</span> <span class="k">where</span> <span class="kt">T</span> <span class="p">:</span> <span class="kt">Decodable</span><span class="p">,</span> <span class="kt">T</span> <span class="p">:</span> <span class="kt">Encodable</span></code></pre>

</div>
</div>
Expand Down Expand Up @@ -241,7 +244,7 @@ <h4>Declaration</h4>
</section>
</section>
<section id="footer">
<p>&copy; 2018 <a class="link" href="https://github.com/Teknasyon-Teknoloji/PersistenceKit" target="_blank" rel="external">Omar Albeik</a>. All rights reserved. (Last updated: 2018-11-28)</p>
<p>&copy; 2018 <a class="link" href="https://github.com/Teknasyon-Teknoloji/PersistenceKit" target="_blank" rel="external">Omar Albeik</a>. All rights reserved. (Last updated: 2018-12-28)</p>
<p>Generated by <a class="link" href="https://github.com/realm/jazzy" target="_blank" rel="external">jazzy ♪♫ v0.9.4</a>, a <a class="link" href="https://realm.io" target="_blank" rel="external">Realm</a> project.</p>
</section>
</article>
Expand Down
Loading

0 comments on commit 74a7c46

Please sign in to comment.