Skip to content

Commit

Permalink
Merge pull request #190 from hyperoslo/update/readme
Browse files Browse the repository at this point in the history
Update README
  • Loading branch information
onmyway133 authored Jun 13, 2018
2 parents 1432835 + 64af552 commit fdf7ef1
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 49 deletions.
4 changes: 4 additions & 0 deletions Cache.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@
D27014AF20D12D84003B45C7 /* AsyncStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D292DB031F6AA0730060F614 /* AsyncStorageTests.swift */; };
D27014B020D12E37003B45C7 /* StorageSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98881F695F9400CE8F68 /* StorageSupportTests.swift */; };
D27014B120D12E38003B45C7 /* StorageSupportTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D2CF98881F695F9400CE8F68 /* StorageSupportTests.swift */; };
D27014B320D13E2C003B45C7 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236F3191F6BEF73004EE01F /* StorageTests.swift */; };
D27014B420D13E2C003B45C7 /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D236F3191F6BEF73004EE01F /* StorageTests.swift */; };
D28897051F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */; };
D28897061F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */; };
D28897071F8B79B300C61DEE /* JSONDecoder+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D28897041F8B79B300C61DEE /* JSONDecoder+Extensions.swift */; };
Expand Down Expand Up @@ -828,6 +830,7 @@
files = (
D2CF98201F69427C00CE8F68 /* TestCase+Extensions.swift in Sources */,
D2CF98231F69427C00CE8F68 /* TestHelper.swift in Sources */,
D27014B420D13E2C003B45C7 /* StorageTests.swift in Sources */,
D27014A320D129A3003B45C7 /* DiskStorageTests.swift in Sources */,
D27014A020D12870003B45C7 /* MemoryStorageTests.swift in Sources */,
D2CF98261F69427C00CE8F68 /* User.swift in Sources */,
Expand All @@ -852,6 +855,7 @@
D2CF987F1F69513800CE8F68 /* ImageWrapperTests.swift in Sources */,
D2D4CC1A1FA3166900E4A2D5 /* MD5Tests.swift in Sources */,
D2D4CC281FA342CA00E4A2D5 /* JSONWrapperTests.swift in Sources */,
D27014B320D13E2C003B45C7 /* StorageTests.swift in Sources */,
D28C9BAF1F67EF8300C180C1 /* UIImage+ExtensionsTests.swift in Sources */,
D2CF987D1F69513800CE8F68 /* MemoryCapsuleTests.swift in Sources */,
D27014A120D129A2003B45C7 /* DiskStorageTests.swift in Sources */,
Expand Down
77 changes: 51 additions & 26 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@
* [Sync APIs](#sync-apis)
* [Async APIS](#async-apis)
* [Expiry date](#expiry-date)
* [What about images?](#what-about-images)
* [Handling JSON response](#handling-json-response)
* [What about images?](#what-about-images)
* [Installation](#installation)
* [Author](#author)
* [Contributing](#contributing)
Expand All @@ -36,33 +36,65 @@ with out-of-box implementations and great customization possibilities. `Cache` u
## Key features

- [x] Work with Swift 4 `Codable`. Anything conforming to `Codable` will be saved and loaded easily by `Storage`.
- [X] Disk storage by default. Optionally using `memory storage` to enable hybrid.
- [x] Hybrid with memory and disk storage.
- [X] Many options via `DiskConfig` and `MemoryConfig`.
- [x] Support `expiry` and clean up of expired objects.
- [x] Thread safe. Operations can be accessed from any queue.
- [x] Sync by default. Also support Async APIs.
- [X] Store images via `ImageWrapper`.
- [x] Extensive unit test coverage and great documentation.
- [x] iOS, tvOS and macOS support.

## Usage

### Storage

`Cache` is built based on [Chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern), in which there are many processing objects, each knows how to do 1 task and delegates to the next one. But that's just implementation detail. All you need to know is `Storage`, it saves and loads `Codable` objects.
`Cache` is built based on [Chain-of-responsibility pattern](https://en.wikipedia.org/wiki/Chain-of-responsibility_pattern), in which there are many processing objects, each knows how to do 1 task and delegates to the next one, so can you compose Storages the way you like.

For now the following Storage are supported

- `MemoryStorage`: save object to memory.
- `DiskStorage`: save object to disk.
- `HybridStorage`: save object to memory and disk, so you get persistented object on disk, while fast access with in memory objects.
- `SyncStorage`: blocking APIs, all read and write operations are scheduled in a serial queue, all sync manner.
- `AsyncStorage`: non-blocking APIs, operations are scheduled in an internal queue for serial processing. No read and write should happen at the same time.

`Storage` has disk storage and an optional memory storage. Memory storage should be less time and memory consuming, while disk storage is used for content that outlives the application life-cycle, see it more like a convenient way to store user information that should persist across application launches.
Although you can use those Storage at your discretion, you don't have to. Because we also provide a convenient `Storage` which uses `HybridStorage` under the hood, while exposes sync and async APIs through `SyncStorage` and `AsyncStorage`.

`DiskConfig` is required to set up disk storage. You can optionally pass `MemoryConfig` to use memory as front storage.
All you need to do is to specify the configuration you want with `DiskConfig` and `MemoryConfig`. The default configurations are good to go, but you can customise a lot.


```swift
let diskConfig = DiskConfig(name: "Floppy")
let memoryConfig = MemoryConfig(expiry: .never, countLimit: 10, totalCostLimit: 10)

let storage = try? Storage(diskConfig: diskConfig, memoryConfig: memoryConfig)
let storage = try? Storage(
diskConfig: diskConfig,
memoryConfig: memoryConfig,
transformer: TransformerFactory.forCodable(ofType: User.self) // Storage<User>
)
```

### Generic, Type safety and Transformer

All `Storage` now are generic by default, so you can get a type safety experience. Once you create a Storage, it has a type constraint that you don't need to specify type for each operation afterwards.

If you want to change the type, `Cache` offers `transform` functions, look for `Transformer` and `TransformerFactory` for built-in transformers.

```swift
let storage: Storage<User> = ...
storage.setObject(superman, forKey: "user")

let imageStorage = storage.transformImage() // Storage<UIImage>
imageStorage.setObject(image, forKey: "image")

let stringStorage = storage.transformCodable(ofType: String.self) // Storage<String>
stringStorage.setObject("hello world", forKey: "string")
```

Each transformation allows you to work with a specific type, however the underlying caching mechanism remains the same, you're working with the same `Storage`, just with different type annotation. You can also create different `Storage` for each type if you want.

`Transformer` is necessary because the need of serialising and deserialising objects to and from `Data` for disk persistency. `Cache` provides default `Transformer ` for `Data`, `Codable` and `UIImage/NSImage`

#### Codable types

`Storage` supports any objects that conform to [Codable](https://developer.apple.com/documentation/swift/codable) protocol. You can [make your own things conform to Codable](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types) so that can be saved and loaded from `Storage`.
Expand Down Expand Up @@ -95,6 +127,8 @@ public enum StorageError: Error {
case encodingFailed
/// The storage has been deallocated
case deallocated
/// Fail to perform transformation to or from Data
case transformerFail
}
```

Expand Down Expand Up @@ -156,11 +190,11 @@ try? storage.setObject(data, forKey: "a bunch of bytes")
try? storage.setObject(authorizeURL, forKey: "authorization URL")

// Load from storage
let score = try? storage.object(ofType: Int.self, forKey: "score")
let favoriteCharacter = try? storage.object(ofType: String.self, forKey: "my favorite city")
let score = try? storage.object(forKey: "score")
let favoriteCharacter = try? storage.object(forKey: "my favorite city")

// Check if an object exists
let hasFavoriteCharacter = try? storage.existsObject(ofType: String.self, forKey: "my favorite city")
let hasFavoriteCharacter = try? storage.existsObject(forKey: "my favorite city")

// Remove an object in storage
try? storage.removeObject(forKey: "my favorite city")
Expand All @@ -177,7 +211,7 @@ try? storage.removeExpiredObjects()
There is time you want to get object together with its expiry information and meta data. You can use `Entry`

```swift
let entry = try? storage.entry(ofType: String.self, forKey: "my favorite city")
let entry = try? storage.entry(forKey: "my favorite city")
print(entry?.object)
print(entry?.expiry)
print(entry?.meta)
Expand Down Expand Up @@ -215,7 +249,7 @@ storage.async.setObject("Oslo", forKey: "my favorite city") { result in
}
}

storage.async.object(ofType: String.self, forKey: "my favorite city") { result in
storage.async.object(forKey: "my favorite city") { result in
switch result {
case .value(let city):
print("my favorite city is \(city)")
Expand All @@ -224,7 +258,7 @@ storage.async.object(ofType: String.self, forKey: "my favorite city") { result i
}
}

storage.async.existsObject(ofType: String.self, forKey: "my favorite city") { result in
storage.async.existsObject(forKey: "my favorite city") { result in
if case .value(let exists) = result, exists {
print("I have a favorite city")
}
Expand Down Expand Up @@ -268,19 +302,6 @@ try? storage.setObject(
storage.removeExpiredObjects()
```

## What about images?

As you may know, `NSImage` and `UIImage` don't conform to `Codable` by default. To make it play well with `Codable`, we introduce `ImageWrapper`, so you can save and load images like

```swift
let wrapper = ImageWrapper(image: starIconImage)
try? storage.setObject(wrapper, forKey: "star")

let icon = try? storage.object(ofType: ImageWrapper.self, forKey: "star").image
```

If you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https://github.com/hyperoslo/Imaginary) and uses `Cache` under the hood to make you life easier when it comes to working with remote images.

## Handling JSON response

Most of the time, our use case is to fetch some json from backend, display it while saving the json to storage for future uses. If you're using libraries like [Alamofire](https://github.com/Alamofire/Alamofire) or [Malibu](https://github.com/hyperoslo/Malibu), you mostly get json in the form of dictionary, string, or data.
Expand Down Expand Up @@ -308,6 +329,10 @@ Alamofire.request("https://gameofthrones.org/mostFavoriteCharacter").responseStr
}
```

## What about images

If you want to load image into `UIImageView` or `NSImageView`, then we also have a nice gift for you. It's called [Imaginary](https://github.com/hyperoslo/Imaginary) and uses `Cache` under the hood to make you life easier when it comes to working with remote images.

## Installation

### Cocoapods
Expand Down
23 changes: 14 additions & 9 deletions Source/Shared/Storage/DiskStorage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,23 @@ final public class DiskStorage<T> {

// MARK: - Initialization

public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
self.config = config
self.fileManager = fileManager
self.transformer = transformer

public convenience init(config: DiskConfig, fileManager: FileManager = FileManager.default, transformer: Transformer<T>) throws {
let url: URL
if let directory = config.directory {
url = directory
} else {
url = try fileManager.url(
for: .documentDirectory,
for: .cachesDirectory,
in: .userDomainMask,
appropriateFor: nil,
create: true
)
}

// path
self.path = url.appendingPathComponent(config.name, isDirectory: true).path
let path = url.appendingPathComponent(config.name, isDirectory: true).path

self.init(config: config, fileManager: fileManager, path: path, transformer: transformer)

try createDirectory()

Expand All @@ -48,6 +46,13 @@ final public class DiskStorage<T> {
}
#endif
}

public required init(config: DiskConfig, fileManager: FileManager = FileManager.default, path: String, transformer: Transformer<T>) {
self.config = config
self.fileManager = fileManager
self.path = path
self.transformer = transformer
}
}

extension DiskStorage: StorageAware {
Expand Down Expand Up @@ -239,10 +244,10 @@ extension DiskStorage {

public extension DiskStorage {
func transform<U>(transformer: Transformer<U>) -> DiskStorage<U> {
// swiftlint:disable force_try
let storage = try! DiskStorage<U>(
let storage = DiskStorage<U>(
config: config,
fileManager: fileManager,
path: path,
transformer: transformer
)

Expand Down
2 changes: 1 addition & 1 deletion Tests/iOS/Tests/Storage/DiskStorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ final class DiskStorageTests: XCTestCase {
/// Test that it returns the correct path
func testDefaultPath() {
let paths = NSSearchPathForDirectoriesInDomains(
.documentDirectory, FileManager.SearchPathDomainMask.userDomainMask, true
.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true
)
let path = "\(paths.first!)/\(config.name.capitalized)"
XCTAssertEqual(storage.path, path)
Expand Down
34 changes: 21 additions & 13 deletions Tests/iOS/Tests/Storage/StorageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import XCTest
import Cache

final class StorageTests: XCTestCase {
private var storage: Storage!
private var storage: Storage<User>!
let user = User(firstName: "John", lastName: "Snow")

override func setUp() {
super.setUp()

storage = try! Storage(diskConfig: DiskConfig(name: "Thor"), memoryConfig: MemoryConfig())
storage = try! Storage<User>(
diskConfig: DiskConfig(name: "Thor"),
memoryConfig: MemoryConfig(),
transformer: TransformerFactory.forCodable(ofType: User.self)
)
}

override func tearDown() {
Expand All @@ -18,7 +22,7 @@ final class StorageTests: XCTestCase {

func testSync() throws {
try storage.setObject(user, forKey: "user")
let cachedObject = try storage.object(ofType: User.self, forKey: "user")
let cachedObject = try storage.object(forKey: "user")

XCTAssertEqual(cachedObject, user)
}
Expand All @@ -27,7 +31,7 @@ final class StorageTests: XCTestCase {
let expectation = self.expectation(description: #function)
storage.async.setObject(user, forKey: "user", expiry: nil, completion: { _ in })

storage.async.object(ofType: User.self, forKey: "user", completion: { result in
storage.async.object(forKey: "user", completion: { result in
switch result {
case .value(let cachedUser):
XCTAssertEqual(cachedUser, self.user)
Expand All @@ -50,20 +54,23 @@ final class StorageTests: XCTestCase {
let lastName: String
}

let person1Storage = storage.transformCodable(ofType: Person1.self)
let person2Storage = storage.transformCodable(ofType: Person2.self)

// Firstly, save object of type Person1
let person = Person1(fullName: "John Snow")

try! storage.setObject(person, forKey: "person")
XCTAssertNil(try? storage.object(ofType: Person2.self, forKey: "person"))
try! person1Storage.setObject(person, forKey: "person")
XCTAssertNil(try? person2Storage.object(forKey: "person"))

// Later, convert to Person2, do the migration, then overwrite
let tempPerson = try! storage.object(ofType: Person1.self, forKey: "person")
let tempPerson = try! person1Storage.object(forKey: "person")
let parts = tempPerson.fullName.split(separator: " ")
let migratedPerson = Person2(firstName: String(parts[0]), lastName: String(parts[1]))
try! storage.setObject(migratedPerson, forKey: "person")
try! person2Storage.setObject(migratedPerson, forKey: "person")

XCTAssertEqual(
try! storage.object(ofType: Person2.self, forKey: "person").firstName,
try! person2Storage.object(forKey: "person").firstName,
"John"
)
}
Expand All @@ -79,13 +86,14 @@ final class StorageTests: XCTestCase {
let lastName: String
}

let personStorage = storage.transformCodable(ofType: Person.self)
let alienStorage = storage.transformCodable(ofType: Alien.self)

let person = Person(firstName: "John", lastName: "Snow")
try! storage.setObject(person, forKey: "person")
try! personStorage.setObject(person, forKey: "person")

// As long as it has same properties, it works too
let cachedObject = try! storage.object(ofType: Alien.self, forKey: "person")
let cachedObject = try! alienStorage.object(forKey: "person")
XCTAssertEqual(cachedObject.firstName, "John")
}
}


0 comments on commit fdf7ef1

Please sign in to comment.