├── .gitignore ├── .swift-version ├── LICENSE ├── Package.swift ├── README.md ├── Sources └── Shallows │ ├── Async.swift │ ├── Composition.swift │ ├── DiskStorage │ ├── DiskExtensions.swift │ ├── DiskStorage.swift │ └── Filename.swift │ ├── MemoryStorage.swift │ ├── NSCacheStorage.swift │ ├── ReadOnlyStorage.swift │ ├── Shallows.swift │ ├── ShallowsResult.swift │ ├── Storage.swift │ ├── SyncStorage.swift │ ├── WriteOnlyStorage.swift │ └── Zip.swift └── Tests ├── LinuxMain.swift └── ShallowsTests ├── AdditionalSpec.swift ├── CompositionSpec.swift ├── DiskStorageSpec.swift ├── MemoryStorageSpec.swift ├── ResultSpec.swift ├── Spectre ├── Case.swift ├── Context.swift ├── Expectation.swift ├── Failure.swift ├── Global.swift ├── GlobalContext.swift ├── Reporter.swift └── Reporters.swift ├── TestExtensions.swift ├── XCTest.swift └── ZipSpec.swift /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | /.swiftpm 6 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 5.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 Oleg Dreyman 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Shallows", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Shallows", 12 | targets: ["Shallows"]), 13 | .library( 14 | name: "ShallowsStatic", 15 | type: .static, 16 | targets: ["Shallows"]), 17 | .library( 18 | name: "ShallowsDynamic", 19 | type: .dynamic, 20 | targets: ["Shallows"]), 21 | ], 22 | dependencies: [ 23 | // Dependencies declare other packages that this package depends on. 24 | // .package(url: /* package url */, from: "1.0.0"), 25 | ], 26 | targets: [ 27 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 28 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 29 | .target( 30 | name: "Shallows", 31 | dependencies: []), 32 | .testTarget( 33 | name: "ShallowsTests", 34 | dependencies: ["Shallows"]), 35 | ] 36 | ) 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Shallows 2 | 3 | [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdreymonde%2FShallows%2Fbadge%3Ftype%3Dswift-versions)](https://swiftpackageindex.com/dreymonde/Shallows) [![](https://img.shields.io/endpoint?url=https%3A%2F%2Fswiftpackageindex.com%2Fapi%2Fpackages%2Fdreymonde%2FShallows%2Fbadge%3Ftype%3Dplatforms)](https://swiftpackageindex.com/dreymonde/Shallows) 4 | 5 | **Shallows** is a generic abstraction layer over lightweight data storage and persistence. It provides a `Storage` type, instances of which can be easily transformed and composed with each other. It gives you an ability to create highly sophisticated, effective and reliable caching/persistence solutions. 6 | 7 | **Shallows** is deeply inspired by [Carlos][carlos-github-url] and [this amazing talk][composable-caches-in-swift-url] by [Brandon Kase][brandon-kase-twitter-url]. 8 | 9 | **Shallows** is a really small, component-based project, so if you need even more controllable solution – build one yourself! Our source code is there to help. 10 | 11 | ## Usage 12 | 13 | ```swift 14 | struct City : Codable { 15 | let name: String 16 | let foundationYear: Int 17 | } 18 | 19 | let diskStorage = DiskStorage.main.folder("cities", in: .cachesDirectory) 20 | .mapJSONObject(City.self) // Storage 21 | 22 | let kharkiv = City(name: "Kharkiv", foundationYear: 1654) 23 | diskStorage.set(kharkiv, forKey: "kharkiv") 24 | 25 | diskStorage.retrieve(forKey: "kharkiv") { (result) in 26 | if let city = try? result.get() { print(city) } 27 | } 28 | 29 | // or 30 | 31 | let city = try await diskStorage.retrieve(forKey: "kharkiv") 32 | 33 | ``` 34 | 35 | ## Guide 36 | 37 | A main type of **Shallows** is `Storage`. It's an abstract, type-erased structure which doesn't contain any logic -- it needs to be provided with one. The most basic one is `MemoryStorage`: 38 | 39 | ```swift 40 | let storage = MemoryStorage().asStorage() // Storage 41 | ``` 42 | 43 | Storage instances have `retrieve` and `set` methods, which are asynhronous and fallible: 44 | 45 | ```swift 46 | storage.retrieve(forKey: "some-key") { (result) in 47 | switch result { 48 | case .success(let value): 49 | print(value) 50 | case .failure(let error): 51 | print(error) 52 | } 53 | } 54 | storage.set(10, forKey: "some-key") { (result) in 55 | switch result { 56 | case .success: 57 | print("Value set!") 58 | case .failure(let error): 59 | print(error) 60 | } 61 | } 62 | ``` 63 | 64 | ### Transforms 65 | 66 | Keys and values can be mapped: 67 | 68 | ```swift 69 | let storage = DiskStorage.main.folder("images", in: .cachesDirectory) // Storage 70 | let images = storage 71 | .mapValues(to: UIImage.self, 72 | transformIn: { data in try UIImage.init(data: data).unwrap() }, 73 | transformOut: { image in try UIImagePNGRepresentation(image).unwrap() }) // Storage 74 | 75 | enum ImageKeys : String { 76 | case kitten, puppy, fish 77 | } 78 | 79 | let keyedImages = images 80 | .usingStringKeys() 81 | .mapKeys(toRawRepresentableType: ImageKeys.self) // Storage 82 | 83 | keyedImages.retrieve(forKey: .kitten, completion: { result in /* .. */ }) 84 | ``` 85 | 86 | **NOTE:** There are several convenience methods defined on `Storage` with value of `Data`: `.mapString(withEncoding:)`, `.mapJSON()`, `.mapJSONDictionary()`, `.mapJSONObject(_:)` `.mapPlist(format:)`, `.mapPlistDictionary(format:)`, `.mapPlistObject(_:)`. 87 | 88 | ### Storages composition 89 | 90 | Another core concept of **Shallows** is composition. Hitting a disk every time you request an image can be slow and inefficient. Instead, you can compose `MemoryStorage` and `FileSystemStorage`: 91 | 92 | ```swift 93 | let efficient = MemoryStorage().combined(with: imageStorage) 94 | ``` 95 | 96 | It does several things: 97 | 98 | 1. When trying to retrieve an image, the memory storage first will be checked first, and if it doesn't contain a value, the request will be made to disk storage. 99 | 2. If disk storage stores a value, it will be pulled to memory storage and returned to a user. 100 | 3. When setting an image, it will be set both to memory and disk storage. 101 | 102 | ### Read-only storage 103 | 104 | If you don't want to expose writing to your storage, you can make it a read-only storage: 105 | 106 | ```swift 107 | let readOnly = storage.asReadOnlyStorage() // ReadOnlyStorage 108 | ``` 109 | 110 | Read-only storages can also be mapped and composed: 111 | 112 | ```swift 113 | let immutableFileStorage = DiskStorage.main.folder("immutable", in: .applicationSupportDirectory) 114 | .mapString(withEncoding: .utf8) 115 | .asReadOnlyStorage() 116 | let storage = MemoryStorage() 117 | .backed(by: immutableFileStorage) 118 | .asReadOnlyStorage() // ReadOnlyStorage 119 | ``` 120 | 121 | ### Write-only storage 122 | 123 | In similar way, write-only storage is also available: 124 | 125 | ```swift 126 | let writeOnly = storage.asWriteOnlyStorage() // WriteOnlyStorage 127 | ``` 128 | 129 | ### Different ways of composition 130 | 131 | **Compositions available for `Storage`**: 132 | 133 | - `.combined(with:)` (see [Storages composition](#Storages-composition)) 134 | - `.backed(by:)` will work the same as `combined(with:)`, but it will not push the value to the back storage 135 | - `.pushing(to:)` will not retrieve the value from the back storage, but will push to it on `set` 136 | 137 | **Compositions available for `ReadOnlyStorage`**: 138 | 139 | - `.backed(by:)` 140 | 141 | **Compositions available for `WriteOnlyStorage`**: 142 | 143 | - `.pushing(to:)` 144 | 145 | ### Single element storage 146 | 147 | You can have a storage with keys `Void`. That means that you can store only one element there. **Shallows** provides a convenience `.singleKey` method to create it: 148 | 149 | ```swift 150 | let settings = DiskStorage.main.folder("settings", in: .applicationSupportDirectory) 151 | .mapJSONDictionary() 152 | .singleKey("settings") // Storage 153 | settings.retrieve { (result) in 154 | // ... 155 | } 156 | ``` 157 | 158 | ### Synchronous storage 159 | 160 | Storages in **Shallows** are asynchronous by design. However, in some situations (for example, when scripting or testing) it could be useful to have synchronous storages. You can make any storage synchronous by calling `.makeSyncStorage()` on it: 161 | 162 | ```swift 163 | let strings = DiskStorage.main.folder("strings", in: .cachesDirectory) 164 | .mapString(withEncoding: .utf8) 165 | .makeSyncStorage() // SyncStorage 166 | let existing = try strings.retrieve(forKey: "hello") 167 | try strings.set(existing.uppercased(), forKey: "hello") 168 | ``` 169 | 170 | ### Mutating value for key 171 | 172 | **Shallows** provides a convenient `.update` method on storages: 173 | 174 | ```swift 175 | let arrays = MemoryStorage() 176 | arrays.update(forKey: "some-key", { $0.append(10) }) 177 | ``` 178 | 179 | ### Zipping storages 180 | 181 | Zipping is a very powerful feature of **Shallows**. It allows you to compose your storages in a way that you get result only when both of them completes for your request. For example: 182 | 183 | ```swift 184 | let strings = MemoryStorage() 185 | let numbers = MemoryStorage() 186 | let zipped = zip(strings, numbers) // Storage 187 | zipped.retrieve(forKey: "some-key") { (result) in 188 | if let (string, number) = try? result.get() { 189 | print(string) 190 | print(number) 191 | } 192 | } 193 | zipped.set(("shallows", 3), forKey: "another-key") 194 | ``` 195 | 196 | Isn't it nice? 197 | 198 | ### Recovering from errors 199 | 200 | You can protect your storage instance from failures using `fallback(with:)` or `defaulting(to:)` methods: 201 | 202 | ```swift 203 | let storage = MemoryStorage() 204 | let protected = storage.fallback(with: { error in 205 | switch error { 206 | case MemoryStorageError.noValue: 207 | return 15 208 | default: 209 | return -1 210 | } 211 | }) 212 | ``` 213 | 214 | ```swift 215 | let storage = MemoryStorage() 216 | let defaulted = storage.defaulting(to: -1) 217 | ``` 218 | 219 | This is _especially_ useful when using `update` method: 220 | 221 | ```swift 222 | let storage = MemoryStorage() 223 | storage.defaulting(to: []).update(forKey: "first", { $0.append(10) }) 224 | ``` 225 | 226 | That means that in case of failure retrieving existing value, `update` will use default value of `[]` instead of just failing the whole update. 227 | 228 | ### Using `NSCacheStorage` 229 | 230 | `NSCache` is a tricky class: it supports only reference types, so you're forced to use, for example, `NSData` instead of `Data` and so on. To help you out, **Shallows** provides a set of convenience extensions for legacy Foundation types: 231 | 232 | ```swift 233 | let nscache = NSCacheStorage() 234 | .toNonObjCKeys() 235 | .toNonObjCValues() // Storage 236 | ``` 237 | 238 | ### Making your own storage 239 | 240 | To create your own caching layer, you should conform to `StorageProtocol`. That means that you should define these two methods: 241 | 242 | ```swift 243 | func retrieve(forKey key: Key, completion: @escaping (Result) -> ()) 244 | func set(_ value: Value, forKey key: Key, completion: @escaping (Result) -> ()) 245 | ``` 246 | 247 | Where `Key` and `Value` are associated types. 248 | 249 | **NOTE:** Please be aware that you are responsible for the thread-safety of your implementation. Very often `retrieve` and `set` will not be called from the main thread, so you should make sure that no race conditions will occur. 250 | 251 | To use it as `Storage` instance, simply call `.asStorage()` on it: 252 | 253 | ```swift 254 | let storage = MyStorage().asStorage() 255 | ``` 256 | 257 | You can also conform to a `ReadOnlyStorageProtocol` only. That way, you only need to define a `retrieve(forKey:completion:)` method. 258 | 259 | ## Installation 260 | 261 | #### Swift Package Manager 262 | 263 | Starting with Xcode 11, **Shallows** is officially available *only* via Swift Package Manager. 264 | 265 | In Xcode 11 or greater, in you project, select: `File > Swift Packages > Add Package Dependency` 266 | 267 | In the search bar type 268 | 269 | ``` 270 | https://github.com/dreymonde/Shallows 271 | ``` 272 | 273 | Then proceed with installation. 274 | 275 | > If you can't find anything in the panel of the Swift Packages you probably haven't added yet your github account. 276 | You can do that under the **Preferences** panel of your Xcode, in the **Accounts** section. 277 | 278 | For command-line based apps, you can just add this directly to your **Package.swift** file: 279 | 280 | ```swift 281 | dependencies: [ 282 | .package(url: "https://github.com/dreymonde/Shallows", from: "0.13.0"), 283 | ] 284 | ``` 285 | 286 | #### Manual 287 | 288 | Of course, you always have an option of just copying-and-pasting the code. 289 | 290 | #### Deprecated dependency managers 291 | 292 | Last **Shallows** version to support [Carthage][carthage-url] and [Cocoapods][cocoapods-url] is **0.10.0**. Carthage and Cocoapods will no longer be officially supported. 293 | 294 | Carthage: 295 | 296 | ```ruby 297 | github "dreymonde/Shallows" ~> 0.10.0 298 | ``` 299 | 300 | Cocoapods: 301 | 302 | ```ruby 303 | pod 'Shallows', '~> 0.10.0' 304 | ``` 305 | 306 | [carthage-url]: https://github.com/Carthage/Carthage 307 | [swift-badge]: https://img.shields.io/badge/Swift-5.1-orange.svg?style=flat 308 | [swift-url]: https://swift.org 309 | [platform-badge]: https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg 310 | [platform-url]: https://developer.apple.com/swift/ 311 | [carlos-github-url]: https://github.com/WeltN24/Carlos 312 | [composable-caches-in-swift-url]: https://www.youtube.com/watch?v=8uqXuEZLyUU 313 | [brandon-kase-twitter-url]: https://twitter.com/bkase_ 314 | [avenues-github-url]: https://github.com/dreymonde/Avenues 315 | [avenues-shallows-github-url]: https://github.com/dreymonde/Avenues-Shallows 316 | [cocoapods-url]: https://github.com/CocoaPods/CocoaPods 317 | -------------------------------------------------------------------------------- /Sources/Shallows/Async.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | #if swift(>=5.5) 4 | @available(iOS 13.0.0, *) 5 | @available(macOS 10.15.0, *) 6 | @available(watchOS 6.0, *) 7 | @available(tvOS 13.0.0, *) 8 | extension ReadableStorageProtocol { 9 | public func retrieve(forKey key: Key) async throws -> Value { 10 | return try await withCheckedThrowingContinuation({ (continuation) in 11 | self.retrieve(forKey: key) { result in 12 | continuation.resume(with: result) 13 | } 14 | }) 15 | } 16 | } 17 | 18 | @available(iOS 13.0.0, *) 19 | @available(macOS 10.15.0, *) 20 | @available(watchOS 6.0, *) 21 | @available(tvOS 13.0.0, *) 22 | extension ReadableStorageProtocol where Key == Void { 23 | public func retrieve() async throws -> Value { 24 | return try await withCheckedThrowingContinuation({ (continuation) in 25 | self.retrieve() { result in 26 | continuation.resume(with: result) 27 | } 28 | }) 29 | } 30 | } 31 | 32 | @available(iOS 13.0.0, *) 33 | @available(macOS 10.15.0, *) 34 | @available(watchOS 6.0, *) 35 | @available(tvOS 13.0.0, *) 36 | extension WritableStorageProtocol { 37 | public func set(_ value: Value, forKey key: Key) async throws -> Void { 38 | return try await withCheckedThrowingContinuation({ (continuation) in 39 | self.set(value, forKey: key) { result in 40 | continuation.resume(with: result) 41 | } 42 | }) 43 | } 44 | } 45 | 46 | @available(iOS 13.0.0, *) 47 | @available(macOS 10.15.0, *) 48 | @available(watchOS 6.0, *) 49 | @available(tvOS 13.0.0, *) 50 | extension WritableStorageProtocol where Key == Void { 51 | public func set(_ value: Value) async throws -> Void { 52 | return try await withCheckedThrowingContinuation({ (continuation) in 53 | self.set(value) { result in 54 | continuation.resume(with: result) 55 | } 56 | }) 57 | } 58 | } 59 | 60 | public struct ValueValidationError: Error { 61 | public init() { } 62 | } 63 | 64 | @available(iOS 13.0.0, *) 65 | @available(macOS 10.15.0, *) 66 | @available(watchOS 6.0, *) 67 | @available(tvOS 13.0.0, *) 68 | extension ReadOnlyStorageProtocol { 69 | public func asyncMapValues( 70 | to type: OtherValue.Type = OtherValue.self, 71 | _ transform: @escaping (Value) async throws -> OtherValue 72 | ) -> ReadOnlyStorage { 73 | return ReadOnlyStorage(storageName: storageName, retrieve: { (key, completion) in 74 | self.retrieve(forKey: key, completion: { (result) in 75 | switch result { 76 | case .success(let value): 77 | Task { 78 | do { 79 | let newValue = try await transform(value) 80 | completion(.success(newValue)) 81 | } catch { 82 | completion(.failure(error)) 83 | } 84 | } 85 | case .failure(let error): 86 | completion(.failure(error)) 87 | } 88 | }) 89 | }) 90 | } 91 | 92 | public func asyncMapKeys( 93 | to type: OtherKey.Type = OtherKey.self, 94 | _ transform: @escaping (OtherKey) async throws -> Key) 95 | -> ReadOnlyStorage { 96 | return ReadOnlyStorage(storageName: storageName, retrieve: { key, completion in 97 | Task { 98 | do { 99 | let newKey = try await transform(key) 100 | self.retrieve(forKey: newKey, completion: completion) 101 | } catch { 102 | completion(.failure(error)) 103 | } 104 | } 105 | }) 106 | } 107 | 108 | public func validate(_ isValid: @escaping (Value) async throws -> Bool) -> ReadOnlyStorage { 109 | return asyncMapValues(to: Value.self) { value in 110 | if try await isValid(value) { 111 | return value 112 | } else { 113 | throw ValueValidationError() 114 | } 115 | } 116 | } 117 | } 118 | 119 | @available(iOS 13.0.0, *) 120 | @available(macOS 10.15.0, *) 121 | @available(watchOS 6.0, *) 122 | @available(tvOS 13.0.0, *) 123 | extension WriteOnlyStorageProtocol { 124 | public func asyncMapValues( 125 | to type: OtherValue.Type = OtherValue.self, 126 | _ transform: @escaping (OtherValue) async throws -> Value 127 | ) -> WriteOnlyStorage { 128 | return WriteOnlyStorage(storageName: storageName, set: { (value, key, completion) in 129 | Task { 130 | do { 131 | let newValue = try await transform(value) 132 | self.set(newValue, forKey: key, completion: completion) 133 | } catch { 134 | completion(.failure(error)) 135 | } 136 | } 137 | }) 138 | } 139 | 140 | public func asyncMapKeys( 141 | to type: OtherKey.Type = OtherKey.self, 142 | _ transform: @escaping (OtherKey) async throws -> Key) 143 | -> WriteOnlyStorage { 144 | return WriteOnlyStorage(storageName: storageName, set: { value, key, completion in 145 | Task { 146 | do { 147 | let newKey = try await transform(key) 148 | self.set(value, forKey: newKey, completion: completion) 149 | } catch { 150 | completion(.failure(error)) 151 | } 152 | } 153 | }) 154 | } 155 | 156 | public func validate(_ isValid: @escaping (Value) async throws -> Bool) -> WriteOnlyStorage { 157 | return asyncMapValues(to: Value.self) { value in 158 | if try await isValid(value) { 159 | return value 160 | } else { 161 | throw ValueValidationError() 162 | } 163 | } 164 | } 165 | } 166 | 167 | @available(iOS 13.0.0, *) 168 | @available(macOS 10.15.0, *) 169 | @available(watchOS 6.0, *) 170 | @available(tvOS 13.0.0, *) 171 | extension StorageProtocol { 172 | public func asyncMapValues(to type: OtherValue.Type = OtherValue.self, 173 | transformIn: @escaping (Value) async throws -> OtherValue, 174 | transformOut: @escaping (OtherValue) async throws -> Value) -> Storage { 175 | return Storage(read: asReadOnlyStorage().asyncMapValues(transformIn), 176 | write: asWriteOnlyStorage().asyncMapValues(transformOut)) 177 | } 178 | 179 | public func asyncMapKeys(to type: OtherKey.Type = OtherKey.self, 180 | _ transform: @escaping (OtherKey) async throws -> Key) -> Storage { 181 | return Storage(read: asReadOnlyStorage().asyncMapKeys(transform), 182 | write: asWriteOnlyStorage().asyncMapKeys(transform)) 183 | } 184 | 185 | public func validate(_ isValid: @escaping (Value) async throws -> Bool) -> Storage { 186 | return asyncMapValues( 187 | to: Value.self, 188 | transformIn: { value in 189 | if try await isValid(value) { 190 | return value 191 | } else { 192 | throw ValueValidationError() 193 | } 194 | }, 195 | transformOut: { value in 196 | if try await isValid(value) { 197 | return value 198 | } else { 199 | throw ValueValidationError() 200 | } 201 | } 202 | ) 203 | } 204 | } 205 | #endif 206 | -------------------------------------------------------------------------------- /Sources/Shallows/Composition.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Composition.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 21.02.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | extension ReadOnlyStorageProtocol { 10 | 11 | public func backed(by storage: ReadableStorageType) -> ReadOnlyStorage where ReadableStorageType.Key == Key, ReadableStorageType.Value == Value { 12 | return ReadOnlyStorage(storageName: "\(self.storageName)-\(storage.storageName)", retrieve: { (key, completion) in 13 | self.retrieve(forKey: key, completion: { (firstResult) in 14 | if firstResult.isFailure { 15 | shallows_print("Storage (\(self.storageName)) miss for key: \(key). Attempting to retrieve from \(storage.storageName)") 16 | storage.retrieve(forKey: key, completion: completion) 17 | } else { 18 | completion(firstResult) 19 | } 20 | }) 21 | }) 22 | } 23 | 24 | } 25 | 26 | extension WritableStorageProtocol { 27 | 28 | public func pushing(to storage: WritableStorageType) -> WriteOnlyStorage where WritableStorageType.Key == Key, WritableStorageType.Value == Value { 29 | return WriteOnlyStorage(storageName: "\(self.storageName)-\(storage.storageName)", set: { (value, key, completion) in 30 | self.set(value, forKey: key, pushingTo: storage, completion: completion) 31 | }) 32 | } 33 | 34 | } 35 | 36 | extension StorageProtocol { 37 | 38 | public func combined(with backStorage: StorageType) -> Storage where StorageType.Key == Key, StorageType.Value == Value { 39 | let name = Shallows.storageName(left: self.storageName, right: backStorage.storageName, pullingFromBack: true, pushingToBack: true) 40 | return Storage(storageName: name, retrieve: { (key, completion) in 41 | self.retrieve(forKey: key, backedBy: backStorage, completion: completion) 42 | }, set: { (value, key, completion) in 43 | self.set(value, forKey: key, pushingTo: backStorage, completion: completion) 44 | }) 45 | } 46 | 47 | public func backed(by readableStorage: ReadableStorageType) -> Storage where ReadableStorageType.Key == Key, ReadableStorageType.Value == Value { 48 | let name = Shallows.storageName(left: storageName, right: readableStorage.storageName, pullingFromBack: true, pushingToBack: false) 49 | return Storage(storageName: name, retrieve: { (key, completion) in 50 | self.retrieve(forKey: key, backedBy: readableStorage, completion: completion) 51 | }, set: { (value, key, completion) in 52 | self.set(value, forKey: key, completion: completion) 53 | }) 54 | } 55 | 56 | } 57 | 58 | extension StorageProtocol { 59 | 60 | fileprivate func retrieve(forKey key: Key, 61 | backedBy storage: StorageType, 62 | completion: @escaping (ShallowsResult) -> ()) where StorageType.Key == Key, StorageType.Value == Value { 63 | self.retrieve(forKey: key, completion: { (firstResult) in 64 | if firstResult.isFailure { 65 | shallows_print("Storage (\(self.storageName)) miss for key: \(key). Attempting to retrieve from \(storage.storageName)") 66 | storage.retrieve(forKey: key, completion: { (secondResult) in 67 | if let value = secondResult.value { 68 | shallows_print("Success retrieving \(key) from \(storage.storageName). Setting value back to \(self.storageName)") 69 | self.set(value, forKey: key, completion: { _ in completion(secondResult) }) 70 | } else { 71 | shallows_print("Storage miss for final destination (\(storage.storageName)). Completing with failure result") 72 | completion(secondResult) 73 | } 74 | }) 75 | } else { 76 | completion(firstResult) 77 | } 78 | }) 79 | } 80 | 81 | } 82 | 83 | extension WritableStorageProtocol { 84 | 85 | fileprivate func set(_ value: Value, 86 | forKey key: Key, 87 | pushingTo storage: StorageType, 88 | completion: @escaping (ShallowsResult) -> ()) where StorageType.Key == Key, StorageType.Value == Value { 89 | self.set(value, forKey: key, completion: { (result) in 90 | if result.isFailure { 91 | shallows_print("Failed setting \(key) to \(self.storageName). Aborting") 92 | completion(result) 93 | } else { 94 | shallows_print("Succesfull set of \(key). Pushing to \(storage.storageName)") 95 | storage.set(value, forKey: key, completion: completion) 96 | } 97 | }) 98 | } 99 | 100 | } 101 | 102 | extension StorageProtocol { 103 | 104 | public var dev: Storage.Dev { 105 | return Storage.Dev(self.asStorage()) 106 | } 107 | 108 | } 109 | 110 | extension Storage { 111 | 112 | public struct Dev { 113 | 114 | fileprivate let frontStorage: Storage 115 | 116 | fileprivate init(_ storage: Storage) { 117 | self.frontStorage = storage 118 | } 119 | 120 | public func set(_ value: Value, 121 | forKey key: Key, 122 | pushingTo backStorage: StorageType, 123 | completion: @escaping (ShallowsResult) -> ()) where StorageType.Key == Key, StorageType.Value == Value { 124 | frontStorage.set(value, forKey: key, pushingTo: backStorage, completion: completion) 125 | } 126 | 127 | public func retrieve(forKey key: Key, 128 | backedBy backStorage: StorageType, 129 | completion: @escaping (ShallowsResult) -> ()) where StorageType.Key == Key, StorageType.Value == Value{ 130 | frontStorage.retrieve(forKey: key, backedBy: backStorage, completion: completion) 131 | } 132 | 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /Sources/Shallows/DiskStorage/DiskExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskExtensions.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 18.01.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | extension StorageProtocol where Value == Data { 12 | 13 | public func mapJSON(readingOptions: JSONSerialization.ReadingOptions = [], 14 | writingOptions: JSONSerialization.WritingOptions = []) -> Storage { 15 | return Storage(read: asReadOnlyStorage().mapJSON(options: readingOptions), 16 | write: asWriteOnlyStorage().mapJSON(options: writingOptions)) 17 | } 18 | 19 | public func mapJSONDictionary(readingOptions: JSONSerialization.ReadingOptions = [], 20 | writingOptions: JSONSerialization.WritingOptions = []) -> Storage { 21 | return Storage(read: asReadOnlyStorage().mapJSONDictionary(options: readingOptions), 22 | write: asWriteOnlyStorage().mapJSONDictionary(options: writingOptions)) 23 | } 24 | 25 | public func mapJSONObject(_ objectType: JSONObject.Type, 26 | decoder: JSONDecoder = JSONDecoder(), 27 | encoder: JSONEncoder = JSONEncoder()) -> Storage { 28 | return Storage(read: asReadOnlyStorage().mapJSONObject(objectType, decoder: decoder), 29 | write: asWriteOnlyStorage().mapJSONObject(objectType, encoder: encoder)) 30 | } 31 | 32 | public func mapPlist(format: PropertyListSerialization.PropertyListFormat = .xml, 33 | readOptions: PropertyListSerialization.ReadOptions = [], 34 | writeOptions: PropertyListSerialization.WriteOptions = 0) -> Storage { 35 | return Storage(read: asReadOnlyStorage().mapPlist(format: format, options: readOptions), 36 | write: asWriteOnlyStorage().mapPlist(format: format, options: writeOptions)) 37 | } 38 | 39 | public func mapPlistDictionary(format: PropertyListSerialization.PropertyListFormat = .xml, 40 | readOptions: PropertyListSerialization.ReadOptions = [], 41 | writeOptions: PropertyListSerialization.WriteOptions = 0) -> Storage { 42 | return Storage(read: asReadOnlyStorage().mapPlistDictionary(format: format, options: readOptions), 43 | write: asWriteOnlyStorage().mapPlistDictionary(format: format, options: writeOptions)) 44 | } 45 | 46 | public func mapPlistObject(_ objectType: PlistObject.Type, 47 | decoder: PropertyListDecoder = PropertyListDecoder(), 48 | encoder: PropertyListEncoder = PropertyListEncoder()) -> Storage { 49 | return Storage(read: asReadOnlyStorage().mapPlistObject(objectType, decoder: decoder), 50 | write: asWriteOnlyStorage().mapPlistObject(objectType, encoder: encoder)) 51 | } 52 | 53 | public func mapString(withEncoding encoding: String.Encoding = .utf8) -> Storage { 54 | return Storage(read: asReadOnlyStorage().mapString(withEncoding: encoding), 55 | write: asWriteOnlyStorage().mapString(withEncoding: encoding)) 56 | } 57 | 58 | } 59 | 60 | public struct DecodingError: Error { 61 | @StringPrinted public var originalData: Data 62 | public var rawError: Error 63 | } 64 | 65 | @propertyWrapper 66 | public struct StringPrinted: CustomStringConvertible { 67 | public var wrappedValue: Data 68 | 69 | public init(wrappedValue: Data) { 70 | self.wrappedValue = wrappedValue 71 | } 72 | 73 | public var description: String { 74 | if let string = String.init(data: wrappedValue, encoding: .utf8) { 75 | return string 76 | } else { 77 | return "__raw-unmappable-data__" 78 | } 79 | } 80 | } 81 | 82 | extension ReadOnlyStorageProtocol where Value == Data { 83 | 84 | public func mapJSON(options: JSONSerialization.ReadingOptions = []) -> ReadOnlyStorage { 85 | return mapValues({ data in 86 | do { 87 | return try JSONSerialization.jsonObject(with: data, options: options) 88 | } catch { 89 | throw DecodingError(originalData: data, rawError: error) 90 | } 91 | }) 92 | } 93 | 94 | public func mapJSONDictionary(options: JSONSerialization.ReadingOptions = []) -> ReadOnlyStorage { 95 | return mapJSON(options: options).mapValues(throwing({ $0 as? [String : Any] })) 96 | } 97 | 98 | public func mapJSONObject(_ objectType: JSONObject.Type, 99 | decoder: JSONDecoder = JSONDecoder()) -> ReadOnlyStorage { 100 | return mapValues({ data in 101 | do { 102 | return try decoder.decode(objectType, from: data) 103 | } catch { 104 | throw DecodingError(originalData: data, rawError: error) 105 | } 106 | }) 107 | } 108 | 109 | public func mapPlist(format: PropertyListSerialization.PropertyListFormat = .xml, 110 | options: PropertyListSerialization.ReadOptions = []) -> ReadOnlyStorage { 111 | return mapValues({ data in 112 | do { 113 | var formatRef = format 114 | return try PropertyListSerialization.propertyList(from: data, options: options, format: &formatRef) 115 | } catch { 116 | throw DecodingError(originalData: data, rawError: error) 117 | } 118 | }) 119 | } 120 | 121 | public func mapPlistDictionary(format: PropertyListSerialization.PropertyListFormat = .xml, 122 | options: PropertyListSerialization.ReadOptions = []) -> ReadOnlyStorage { 123 | return mapPlist(format: format, options: options).mapValues(throwing({ $0 as? [String : Any] })) 124 | } 125 | 126 | public func mapPlistObject(_ objectType: PlistObject.Type, 127 | decoder: PropertyListDecoder = PropertyListDecoder()) -> ReadOnlyStorage { 128 | return mapValues({ data in 129 | do { 130 | return try decoder.decode(objectType, from: data) 131 | } catch { 132 | throw DecodingError(originalData: data, rawError: error) 133 | } 134 | }) 135 | } 136 | 137 | public func mapString(withEncoding encoding: String.Encoding = .utf8) -> ReadOnlyStorage { 138 | return mapValues(throwing({ String(data: $0, encoding: encoding) })) 139 | } 140 | 141 | } 142 | 143 | extension WriteOnlyStorageProtocol where Value == Data { 144 | 145 | public func mapJSON(options: JSONSerialization.WritingOptions = []) -> WriteOnlyStorage { 146 | return mapValues({ try JSONSerialization.data(withJSONObject: $0, options: options) }) 147 | } 148 | 149 | public func mapJSONDictionary(options: JSONSerialization.WritingOptions = []) -> WriteOnlyStorage { 150 | return mapJSON(options: options).mapValues({ $0 as Any }) 151 | } 152 | 153 | public func mapJSONObject(_ objectType: JSONObject.Type, 154 | encoder: JSONEncoder = JSONEncoder()) -> WriteOnlyStorage { 155 | return mapValues({ try encoder.encode($0) }) 156 | } 157 | 158 | public func mapPlist(format: PropertyListSerialization.PropertyListFormat = .xml, 159 | options: PropertyListSerialization.WriteOptions = 0) -> WriteOnlyStorage { 160 | return mapValues({ try PropertyListSerialization.data(fromPropertyList: $0, format: format, options: options) }) 161 | } 162 | 163 | public func mapPlistDictionary(format: PropertyListSerialization.PropertyListFormat = .xml, 164 | options: PropertyListSerialization.WriteOptions = 0) -> WriteOnlyStorage { 165 | return mapPlist(format: format, options: options).mapValues({ $0 as Any }) 166 | } 167 | 168 | public func mapPlistObject(_ objectType: PlistObject.Type, 169 | encoder: PropertyListEncoder = PropertyListEncoder()) -> WriteOnlyStorage { 170 | return mapValues({ try encoder.encode($0) }) 171 | } 172 | 173 | public func mapString(withEncoding encoding: String.Encoding = .utf8) -> WriteOnlyStorage { 174 | return mapValues(throwing({ $0.data(using: encoding) })) 175 | } 176 | 177 | } 178 | 179 | extension StorageProtocol where Key == Filename { 180 | 181 | public func usingStringKeys() -> Storage { 182 | return mapKeys(Filename.init(rawValue:)) 183 | } 184 | 185 | } 186 | 187 | extension ReadOnlyStorageProtocol where Key == Filename { 188 | 189 | public func usingStringKeys() -> ReadOnlyStorage { 190 | return mapKeys(Filename.init(rawValue:)) 191 | } 192 | 193 | } 194 | 195 | extension WriteOnlyStorageProtocol where Key == Filename { 196 | 197 | public func usingStringKeys() -> WriteOnlyStorage { 198 | return mapKeys(Filename.init(rawValue:)) 199 | } 200 | 201 | } 202 | 203 | -------------------------------------------------------------------------------- /Sources/Shallows/DiskStorage/DiskStorage.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskStorage.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 18.01.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public final class DiskFolderStorage : StorageProtocol { 12 | 13 | public let storageName: String 14 | public let folderURL: URL 15 | 16 | private let diskStorage: Storage 17 | 18 | public let filenameEncoder: Filename.Encoder 19 | 20 | public var clearsOnDeinit = false 21 | 22 | public init(folderURL: URL, 23 | diskStorage: Storage = DiskStorage.main.asStorage(), 24 | filenameEncoder: Filename.Encoder = .base64) { 25 | self.diskStorage = diskStorage 26 | self.folderURL = folderURL 27 | self.filenameEncoder = filenameEncoder 28 | self.storageName = "disk-\(folderURL.lastPathComponent)" 29 | } 30 | 31 | deinit { 32 | if clearsOnDeinit { 33 | clear() 34 | } 35 | } 36 | 37 | public func clear() { 38 | do { 39 | try FileManager.default.removeItem(at: folderURL) 40 | } catch { } 41 | } 42 | 43 | public func fileURL(forFilename filename: Filename) -> URL { 44 | let finalForm = filenameEncoder.encodedString(representing: filename) 45 | return folderURL.appendingPathComponent(finalForm) 46 | } 47 | 48 | public func retrieve(forKey filename: Filename, completion: @escaping (ShallowsResult) -> ()) { 49 | let fileURL = self.fileURL(forFilename: filename) 50 | diskStorage.retrieve(forKey: fileURL, completion: completion) 51 | } 52 | 53 | public func set(_ data: Data, forKey filename: Filename, completion: @escaping (ShallowsResult) -> ()) { 54 | let fileURL = self.fileURL(forFilename: filename) 55 | diskStorage.set(data, forKey: fileURL, completion: completion) 56 | } 57 | 58 | } 59 | 60 | extension URL { 61 | 62 | fileprivate init(directory: FileManager.SearchPathDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) { 63 | let urls = FileManager.default.urls(for: directory, in: domainMask) 64 | self = urls[0] 65 | } 66 | 67 | } 68 | 69 | extension DiskFolderStorage { 70 | 71 | public static func url(forFolder folderName: String, in directory: FileManager.SearchPathDirectory, domainMask: FileManager.SearchPathDomainMask = .userDomainMask) -> URL { 72 | let folderURL = URL(directory: directory, domainMask: domainMask).appendingPathComponent(folderName, isDirectory: true) 73 | return folderURL 74 | } 75 | 76 | public static func inDirectory(_ directory: FileManager.SearchPathDirectory, 77 | folderName: String, 78 | domainMask: FileManager.SearchPathDomainMask = .userDomainMask, 79 | diskStorage: Storage, 80 | filenameEncoder: Filename.Encoder) -> DiskFolderStorage { 81 | let directoryURL = url(forFolder: folderName, in: directory, domainMask: domainMask) 82 | return DiskFolderStorage(folderURL: directoryURL, 83 | diskStorage: diskStorage, 84 | filenameEncoder: filenameEncoder) 85 | } 86 | 87 | } 88 | 89 | public final class DiskStorage : StorageProtocol { 90 | 91 | public var storageName: String { 92 | return "disk" 93 | } 94 | 95 | private let queue: DispatchQueue = DispatchQueue(label: "disk-storage-queue", qos: .userInitiated) 96 | private let fileManager = FileManager.default 97 | 98 | internal let creatingDirectories: Bool 99 | 100 | public init(creatingDirectories: Bool = true) { 101 | self.creatingDirectories = creatingDirectories 102 | } 103 | 104 | public func retrieve(forKey key: URL, completion: @escaping (ShallowsResult) -> ()) { 105 | queue.async { 106 | do { 107 | let data = try Data.init(contentsOf: key) 108 | completion(succeed(with: data)) 109 | } catch { 110 | completion(fail(with: error)) 111 | } 112 | } 113 | } 114 | 115 | public enum Error : Swift.Error { 116 | case cantCreateFile 117 | case cantCreateDirectory(Swift.Error) 118 | } 119 | 120 | public func set(_ value: Data, forKey key: URL, completion: @escaping (ShallowsResult) -> ()) { 121 | queue.async { 122 | do { 123 | try self.createDirectoryURLIfNotExisting(for: key) 124 | let path = key.path 125 | if self.fileManager.createFile(atPath: path, 126 | contents: value, 127 | attributes: nil) { 128 | completion(.success) 129 | } else { 130 | completion(fail(with: Error.cantCreateFile)) 131 | } 132 | } catch { 133 | completion(fail(with: error)) 134 | } 135 | } 136 | } 137 | 138 | public static func directoryURL(of fileURL: URL) -> URL { 139 | return fileURL.deletingLastPathComponent() 140 | } 141 | 142 | fileprivate func createDirectoryURLIfNotExisting(for fileURL: URL) throws { 143 | guard creatingDirectories else { 144 | return 145 | } 146 | let directoryURL = DiskStorage.directoryURL(of: fileURL) 147 | if !fileManager.fileExists(atPath: directoryURL.path) { 148 | do { 149 | try fileManager.createDirectory(at: directoryURL, 150 | withIntermediateDirectories: true, 151 | attributes: nil) 152 | } catch { 153 | throw Error.cantCreateDirectory(error) 154 | } 155 | } 156 | } 157 | 158 | } 159 | 160 | extension DiskStorage { 161 | 162 | public static let main = DiskStorage(creatingDirectories: true) 163 | 164 | public static func folder(_ folderName: String, 165 | in directory: FileManager.SearchPathDirectory, 166 | domainMask: FileManager.SearchPathDomainMask = .userDomainMask, 167 | filenameEncoder: Filename.Encoder = .base64) -> DiskFolderStorage { 168 | return DiskStorage.main.folder(folderName, in: directory, domainMask: domainMask, filenameEncoder: filenameEncoder) 169 | } 170 | 171 | public func folder(_ folderName: String, 172 | in directory: FileManager.SearchPathDirectory, 173 | domainMask: FileManager.SearchPathDomainMask = .userDomainMask, 174 | filenameEncoder: Filename.Encoder = .base64) -> DiskFolderStorage { 175 | return DiskFolderStorage.inDirectory( 176 | directory, 177 | folderName: folderName, 178 | diskStorage: self.asStorage(), 179 | filenameEncoder: filenameEncoder 180 | ) 181 | } 182 | 183 | } 184 | -------------------------------------------------------------------------------- /Sources/Shallows/DiskStorage/Filename.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Filename.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 13.02.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public struct Filename : RawRepresentable, Hashable, ExpressibleByStringLiteral { 12 | 13 | public var hashValue: Int { 14 | return rawValue.hashValue 15 | } 16 | 17 | public var rawValue: String 18 | 19 | public init(rawValue: String) { 20 | self.rawValue = rawValue 21 | } 22 | 23 | public init(stringLiteral value: String) { 24 | self.init(rawValue: value) 25 | } 26 | 27 | public init(unicodeScalarLiteral value: String) { 28 | self.init(rawValue: value) 29 | } 30 | 31 | public init(extendedGraphemeClusterLiteral value: String) { 32 | self.init(rawValue: value) 33 | } 34 | 35 | public func base64Encoded() -> String { 36 | guard let data = rawValue.data(using: .utf8) else { 37 | print("Something is very, very wrong: string \(rawValue) cannot be encoded with utf8") 38 | return rawValue 39 | } 40 | return data.base64EncodedString() 41 | } 42 | 43 | public func base64URLEncoded() -> String { 44 | return base64Encoded() 45 | .replacingOccurrences(of: "+", with: "-") 46 | .replacingOccurrences(of: "/", with: "_") 47 | } 48 | 49 | public struct Encoder { 50 | 51 | private let encode: (Filename) -> String 52 | 53 | private init(encode: @escaping (Filename) -> String) { 54 | self.encode = encode 55 | } 56 | 57 | @available(*, deprecated, message: "for any new storages, please use .base64URL") 58 | public static let base64: Encoder = Encoder(encode: { $0.base64Encoded() }) 59 | public static let base64URL: Encoder = Encoder(encode: { $0.base64URLEncoded() }) 60 | public static let noEncoding: Encoder = Encoder(encode: { $0.rawValue }) 61 | public static func custom(_ encode: @escaping (Filename) -> String) -> Encoder { 62 | return Encoder(encode: encode) 63 | } 64 | 65 | public func encodedString(representing filename: Filename) -> String { 66 | return encode(filename) 67 | } 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /Sources/Shallows/MemoryStorage.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | public final class MemoryStorage : StorageProtocol { 4 | 5 | public let storageName: String 6 | 7 | private var _storage: ThreadSafe<[Key : Value]> 8 | 9 | public var storage: [Key : Value] { 10 | get { 11 | return _storage.read() 12 | } 13 | set { 14 | _storage.write(newValue) 15 | } 16 | } 17 | 18 | public init(storage: [Key : Value] = [:]) { 19 | self._storage = ThreadSafe(storage) 20 | self.storageName = "memory-storage-\(Key.self):\(Value.self)" 21 | } 22 | 23 | public func set(_ value: Value, forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 24 | _storage.write(with: { $0[key] = value }) 25 | completion(.success) 26 | } 27 | 28 | public func retrieve(forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 29 | let result: ShallowsResult = { 30 | if let value = _storage.read()[key] { 31 | return .success(value) 32 | } else { 33 | return .failure(MemoryStorageError.noValue) 34 | } 35 | }() 36 | completion(result) 37 | } 38 | 39 | } 40 | 41 | enum MemoryStorageError : Error { 42 | case noValue 43 | } 44 | 45 | public struct ThreadSafe { 46 | 47 | private var value: Value 48 | private let queue = DispatchQueue(label: "thread-safety-queue", attributes: [.concurrent]) 49 | 50 | public init(_ value: Value) { 51 | self.value = value 52 | } 53 | 54 | public func read() -> Value { 55 | return queue.sync { value } 56 | } 57 | 58 | public mutating func write(with modify: (inout Value) -> ()) { 59 | queue.sync(flags: .barrier) { 60 | modify(&value) 61 | } 62 | } 63 | 64 | public mutating func write(_ newValue: Value) { 65 | queue.sync(flags: .barrier) { 66 | value = newValue 67 | } 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /Sources/Shallows/NSCacheStorage.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | 3 | public final class NSCacheStorage : StorageProtocol { 4 | 5 | public enum Error : Swift.Error { 6 | case noValue(Key) 7 | } 8 | 9 | public let cache: NSCache 10 | public let storageName: String = "nscache" 11 | 12 | public init(storage: NSCache = NSCache()) { 13 | self.cache = storage 14 | } 15 | 16 | public func set(_ value: Value, forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 17 | cache.setObject(value, forKey: key) 18 | completion(.success) 19 | } 20 | 21 | public func retrieve(forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 22 | if let object = cache.object(forKey: key) { 23 | completion(.success(object)) 24 | } else { 25 | completion(.failure(Error.noValue(key))) 26 | } 27 | } 28 | 29 | } 30 | 31 | extension StorageProtocol where Key == NSString { 32 | 33 | public func toNonObjCKeys() -> Storage { 34 | return mapKeys({ $0 as NSString }) 35 | } 36 | 37 | } 38 | 39 | extension StorageProtocol where Value == NSString { 40 | 41 | public func toNonObjCValues() -> Storage { 42 | return mapValues(transformIn: { $0 as String }, 43 | transformOut: { $0 as NSString }) 44 | } 45 | 46 | } 47 | 48 | extension ReadOnlyStorageProtocol where Key == NSString { 49 | 50 | public func toNonObjCKeys() -> ReadOnlyStorage { 51 | return mapKeys({ $0 as NSString }) 52 | } 53 | 54 | } 55 | 56 | extension ReadOnlyStorageProtocol where Value == NSString { 57 | 58 | public func toNonObjCValues() -> ReadOnlyStorage { 59 | return mapValues({ $0 as String }) 60 | } 61 | 62 | } 63 | 64 | extension WriteOnlyStorageProtocol where Key == NSString { 65 | 66 | public func toNonObjCKeys() -> WriteOnlyStorage { 67 | return mapKeys({ $0 as NSString }) 68 | } 69 | 70 | } 71 | 72 | extension WriteOnlyStorageProtocol where Value == NSString { 73 | 74 | public func toNonObjCValues() -> WriteOnlyStorage { 75 | return mapValues({ $0 as NSString }) 76 | } 77 | 78 | } 79 | 80 | // TODO: Other WriteOnlyStorage extensions 81 | 82 | extension StorageProtocol where Key == NSURL { 83 | 84 | public func toNonObjCKeys() -> Storage { 85 | return mapKeys({ $0 as NSURL }) 86 | } 87 | 88 | } 89 | 90 | extension StorageProtocol where Value == NSURL { 91 | 92 | public func toNonObjCValues() -> Storage { 93 | return mapValues(transformIn: { $0 as URL }, 94 | transformOut: { $0 as NSURL }) 95 | } 96 | 97 | } 98 | 99 | extension ReadOnlyStorageProtocol where Key == NSURL { 100 | 101 | public func toNonObjCKeys() -> ReadOnlyStorage { 102 | return mapKeys({ $0 as NSURL }) 103 | } 104 | 105 | } 106 | 107 | extension ReadOnlyStorageProtocol where Value == NSURL { 108 | 109 | public func toNonObjCValues() -> ReadOnlyStorage { 110 | return mapValues({ $0 as URL }) 111 | } 112 | 113 | } 114 | 115 | extension StorageProtocol where Key == NSIndexPath { 116 | 117 | public func toNonObjCKeys() -> Storage { 118 | return mapKeys({ $0 as NSIndexPath }) 119 | } 120 | 121 | } 122 | 123 | extension StorageProtocol where Value == NSIndexPath { 124 | 125 | public func toNonObjCValues() -> Storage { 126 | return mapValues(transformIn: { $0 as IndexPath }, 127 | transformOut: { $0 as NSIndexPath }) 128 | } 129 | 130 | } 131 | 132 | extension ReadOnlyStorageProtocol where Key == NSIndexPath { 133 | 134 | public func toNonObjCKeys() -> ReadOnlyStorage { 135 | return mapKeys({ $0 as NSIndexPath }) 136 | } 137 | 138 | } 139 | 140 | extension ReadOnlyStorageProtocol where Value == NSIndexPath { 141 | 142 | public func toNonObjCValues() -> ReadOnlyStorage { 143 | return mapValues({ $0 as IndexPath }) 144 | } 145 | 146 | } 147 | 148 | extension StorageProtocol where Value == NSData { 149 | 150 | public func toNonObjCValues() -> Storage { 151 | return mapValues(transformIn: { $0 as Data }, 152 | transformOut: { $0 as NSData }) 153 | } 154 | 155 | } 156 | 157 | extension ReadOnlyStorageProtocol where Value == NSData { 158 | 159 | public func toNonObjCValues() -> ReadOnlyStorage { 160 | return mapValues({ $0 as Data }) 161 | } 162 | 163 | } 164 | 165 | extension StorageProtocol where Value == NSDate { 166 | 167 | public func toNonObjCValues() -> Storage { 168 | return mapValues(transformIn: { $0 as Date }, 169 | transformOut: { $0 as NSDate }) 170 | } 171 | 172 | } 173 | 174 | extension ReadOnlyStorageProtocol where Value == NSDate { 175 | 176 | public func toNonObjCValues() -> ReadOnlyStorage { 177 | return mapValues({ $0 as Date }) 178 | } 179 | 180 | } 181 | -------------------------------------------------------------------------------- /Sources/Shallows/ReadOnlyStorage.swift: -------------------------------------------------------------------------------- 1 | public protocol ReadableStorageProtocol : StorageDesign { 2 | 3 | associatedtype Key 4 | associatedtype Value 5 | 6 | func retrieve(forKey key: Key, completion: @escaping (ShallowsResult) -> ()) 7 | func asReadOnlyStorage() -> ReadOnlyStorage 8 | 9 | } 10 | 11 | extension ReadableStorageProtocol { 12 | 13 | public func asReadOnlyStorage() -> ReadOnlyStorage { 14 | if let alreadyNormalized = self as? ReadOnlyStorage { 15 | return alreadyNormalized 16 | } 17 | return ReadOnlyStorage(self) 18 | } 19 | 20 | } 21 | 22 | public protocol ReadOnlyStorageProtocol : ReadableStorageProtocol { } 23 | 24 | public struct ReadOnlyStorage : ReadOnlyStorageProtocol { 25 | 26 | public let storageName: String 27 | 28 | private let _retrieve: (Key, @escaping (ShallowsResult) -> ()) -> () 29 | 30 | public init(storageName: String, retrieve: @escaping (Key, @escaping (ShallowsResult) -> ()) -> ()) { 31 | self._retrieve = retrieve 32 | self.storageName = storageName 33 | } 34 | 35 | public init(_ storage: StorageType) where StorageType.Key == Key, StorageType.Value == Value { 36 | self._retrieve = storage.retrieve 37 | self.storageName = storage.storageName 38 | } 39 | 40 | public func retrieve(forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 41 | _retrieve(key, completion) 42 | } 43 | 44 | } 45 | 46 | extension ReadOnlyStorageProtocol { 47 | 48 | public func mapKeys(to type: OtherKey.Type = OtherKey.self, 49 | _ transform: @escaping (OtherKey) throws -> Key) -> ReadOnlyStorage { 50 | return ReadOnlyStorage(storageName: storageName, retrieve: { key, completion in 51 | do { 52 | let newKey = try transform(key) 53 | self.retrieve(forKey: newKey, completion: completion) 54 | } catch { 55 | completion(.failure(error)) 56 | } 57 | }) 58 | } 59 | 60 | public func mapValues(to type: OtherValue.Type = OtherValue.self, 61 | _ transform: @escaping (Value) throws -> OtherValue) -> ReadOnlyStorage { 62 | return ReadOnlyStorage(storageName: storageName, retrieve: { (key, completion) in 63 | self.retrieve(forKey: key, completion: { (result) in 64 | switch result { 65 | case .success(let value): 66 | do { 67 | let newValue = try transform(value) 68 | completion(.success(newValue)) 69 | } catch { 70 | completion(.failure(error)) 71 | } 72 | case .failure(let error): 73 | completion(.failure(error)) 74 | } 75 | }) 76 | }) 77 | } 78 | 79 | } 80 | 81 | extension ReadOnlyStorageProtocol { 82 | 83 | public func mapValues(toRawRepresentableType type: OtherValue.Type) -> ReadOnlyStorage where OtherValue.RawValue == Value { 84 | return mapValues(throwing(OtherValue.init(rawValue:))) 85 | } 86 | 87 | public func mapKeys(toRawRepresentableType type: OtherKey.Type) -> ReadOnlyStorage where OtherKey.RawValue == Key { 88 | return mapKeys({ $0.rawValue }) 89 | } 90 | 91 | } 92 | 93 | extension ReadOnlyStorageProtocol { 94 | 95 | public func singleKey(_ key: Key) -> ReadOnlyStorage { 96 | return mapKeys({ key }) 97 | } 98 | 99 | } 100 | 101 | extension ReadOnlyStorageProtocol { 102 | 103 | public func fallback(with produceValue: @escaping (Error) throws -> Value) -> ReadOnlyStorage { 104 | return ReadOnlyStorage(storageName: self.storageName, retrieve: { (key, completion) in 105 | self.retrieve(forKey: key, completion: { (result) in 106 | switch result { 107 | case .failure(let error): 108 | do { 109 | let fallbackValue = try produceValue(error) 110 | completion(.success(fallbackValue)) 111 | } catch let fallbackError { 112 | completion(.failure(fallbackError)) 113 | } 114 | case .success(let value): 115 | completion(.success(value)) 116 | } 117 | }) 118 | }) 119 | } 120 | 121 | public func defaulting(to defaultValue: @autoclosure @escaping () -> Value) -> ReadOnlyStorage { 122 | return fallback(with: { _ in defaultValue() }) 123 | } 124 | 125 | } 126 | 127 | public enum UnsupportedTransformationStorageError : Error { 128 | case storageIsReadOnly 129 | case storageIsWriteOnly 130 | } 131 | 132 | extension ReadOnlyStorageProtocol { 133 | 134 | public func usingUnsupportedTransformation(_ transformation: (Storage) -> Storage) -> ReadOnlyStorage { 135 | let fullStorage = Storage(storageName: self.storageName, retrieve: self.retrieve) { (_, _, completion) in 136 | completion(fail(with: UnsupportedTransformationStorageError.storageIsReadOnly)) 137 | } 138 | return transformation(fullStorage).asReadOnlyStorage() 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /Sources/Shallows/Shallows.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Shallows.swift 3 | // Shallows 4 | // 5 | // Created by Oleg Dreyman on {TODAY}. 6 | // Copyright © 2017 Shallows. All rights reserved. 7 | // 8 | 9 | public enum EmptyCacheError : Error, Equatable { 10 | case cacheIsAlwaysEmpty 11 | } 12 | 13 | extension StorageProtocol { 14 | 15 | public func renaming(to newName: String) -> Storage { 16 | return Storage(storageName: newName, retrieve: self.retrieve, set: self.set) 17 | } 18 | 19 | } 20 | 21 | extension ReadOnlyStorageProtocol { 22 | 23 | public func renaming(to newName: String) -> ReadOnlyStorage { 24 | return ReadOnlyStorage(storageName: newName, retrieve: self.retrieve) 25 | } 26 | 27 | } 28 | 29 | extension WriteOnlyStorageProtocol { 30 | 31 | public func renaming(to newName: String) -> WriteOnlyStorage { 32 | return WriteOnlyStorage(storageName: newName, set: self.set) 33 | } 34 | 35 | } 36 | 37 | extension Storage { 38 | 39 | public static func empty() -> Storage { 40 | return Storage(storageName: "empty", retrieve: { (_, completion) in 41 | completion(.failure(EmptyCacheError.cacheIsAlwaysEmpty)) 42 | }, set: { (_, _, completion) in 43 | completion(.failure(EmptyCacheError.cacheIsAlwaysEmpty)) 44 | }) 45 | } 46 | 47 | } 48 | 49 | extension ReadOnlyStorage { 50 | 51 | public static func empty() -> ReadOnlyStorage { 52 | return ReadOnlyStorage(storageName: "empty", retrieve: { (_, completion) in 53 | completion(.failure(EmptyCacheError.cacheIsAlwaysEmpty)) 54 | }) 55 | } 56 | 57 | } 58 | 59 | extension WriteOnlyStorage { 60 | 61 | public static func empty() -> WriteOnlyStorage { 62 | return WriteOnlyStorage(storageName: "empty", set: { (_, _, completion) in 63 | completion(.failure(EmptyCacheError.cacheIsAlwaysEmpty)) 64 | }) 65 | } 66 | 67 | } 68 | 69 | public func throwing(_ block: @escaping (In) -> Out?) -> (In) throws -> Out { 70 | return { input in 71 | try block(input).unwrap() 72 | } 73 | } 74 | 75 | internal func shallows_print(_ item: Any) { 76 | if ShallowsLog.isEnabled { 77 | print(item) 78 | } 79 | } 80 | 81 | public enum ShallowsLog { 82 | 83 | public static var isEnabled = false 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Sources/Shallows/ShallowsResult.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Result.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 13.02.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | public typealias ShallowsResult = Swift.Result 12 | 13 | extension Swift.Result { 14 | 15 | internal var isFailure: Bool { 16 | if case .failure = self { 17 | return true 18 | } 19 | return false 20 | } 21 | 22 | internal var isSuccess: Bool { 23 | return !isFailure 24 | } 25 | 26 | internal var value: Success? { 27 | if case .success(let value) = self { 28 | return value 29 | } 30 | return nil 31 | } 32 | 33 | internal var error: Error? { 34 | if case .failure(let error) = self { 35 | return error 36 | } 37 | return nil 38 | } 39 | 40 | } 41 | 42 | public func fail(with error: Error) -> ShallowsResult { 43 | return .failure(error) 44 | } 45 | 46 | public func succeed(with value: Value) -> ShallowsResult { 47 | return .success(value) 48 | } 49 | 50 | extension Swift.Result where Success == Void { 51 | 52 | internal static var success: ShallowsResult { 53 | return .success(()) 54 | } 55 | 56 | } 57 | 58 | extension Optional { 59 | 60 | public struct UnwrapError : Error { 61 | public init() { } 62 | } 63 | 64 | public func unwrap() throws -> Wrapped { 65 | return try unwrap(orThrow: UnwrapError()) 66 | } 67 | 68 | public func unwrap(orThrow thr: @autoclosure () -> Error) throws -> Wrapped { 69 | if let wrapped = self { 70 | return wrapped 71 | } else { 72 | throw thr() 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Sources/Shallows/Storage.swift: -------------------------------------------------------------------------------- 1 | public protocol StorageDesign { 2 | 3 | var storageName: String { get } 4 | 5 | } 6 | 7 | extension StorageDesign { 8 | 9 | public var storageName: String { 10 | return String(describing: Self.self) 11 | } 12 | 13 | } 14 | 15 | public protocol StorageProtocol : ReadableStorageProtocol, WritableStorageProtocol { } 16 | 17 | public struct Storage : StorageProtocol { 18 | 19 | public let storageName: String 20 | 21 | private let _retrieve: ReadOnlyStorage 22 | private let _set: WriteOnlyStorage 23 | 24 | public init(storageName: String, 25 | read: ReadOnlyStorage, 26 | write: WriteOnlyStorage) { 27 | self.storageName = storageName 28 | self._retrieve = read.renaming(to: storageName) 29 | self._set = write.renaming(to: storageName) 30 | } 31 | 32 | public init(storageName: String, 33 | retrieve: @escaping (Key, @escaping (ShallowsResult) -> ()) -> (), 34 | set: @escaping (Value, Key, @escaping (ShallowsResult) -> ()) -> ()) { 35 | self.storageName = storageName 36 | self._retrieve = ReadOnlyStorage(storageName: storageName, retrieve: retrieve) 37 | self._set = WriteOnlyStorage(storageName: storageName, set: set) 38 | } 39 | 40 | public init(_ storage: StorageType) where StorageType.Key == Key, StorageType.Value == Value { 41 | self.storageName = storage.storageName 42 | self._retrieve = storage.asReadOnlyStorage() 43 | self._set = storage.asWriteOnlyStorage() 44 | } 45 | 46 | public init(read: ReadOnlyStorage, 47 | write: WriteOnlyStorage) { 48 | self.init(storageName: read.storageName, 49 | read: read, 50 | write: write) 51 | } 52 | 53 | public func retrieve(forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 54 | _retrieve.retrieve(forKey: key, completion: completion) 55 | } 56 | 57 | public func set(_ value: Value, forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 58 | _set.set(value, forKey: key, completion: completion) 59 | } 60 | 61 | @available(swift, deprecated: 5.5, message: "use async version or provide completion handler explicitly") 62 | public func set(_ value: Value, forKey key: Key) { 63 | _set.set(value, forKey: key, completion: { _ in }) 64 | } 65 | 66 | public func asReadOnlyStorage() -> ReadOnlyStorage { 67 | return _retrieve 68 | } 69 | 70 | public func asWriteOnlyStorage() -> WriteOnlyStorage { 71 | return _set 72 | } 73 | 74 | } 75 | 76 | internal func storageName(left: String, right: String, pullingFromBack: Bool, pushingToBack: Bool) -> String { 77 | switch (pullingFromBack, pushingToBack) { 78 | case (true, true): 79 | return left + "<->" + right 80 | case (true, false): 81 | return left + "<-" + right 82 | case (false, true): 83 | return left + "->" + right 84 | case (false, false): 85 | return left + "-" + right 86 | } 87 | } 88 | 89 | extension StorageProtocol { 90 | 91 | public func asStorage() -> Storage { 92 | if let alreadyNormalized = self as? Storage { 93 | return alreadyNormalized 94 | } 95 | return Storage(self) 96 | } 97 | 98 | @available(swift, deprecated: 5.5, message: "`update` is deprecated because it was creating potentially wrong assumptions regarding the serial nature of this function. `update` cannot guarantee that no concurrent calls to `retrieve` or `set` from other places will be made during the update") 99 | public func update(forKey key: Key, 100 | _ modify: @escaping (inout Value) -> (), 101 | completion: @escaping (ShallowsResult) -> () = { _ in }) { 102 | retrieve(forKey: key) { (result) in 103 | switch result { 104 | case .success(var value): 105 | modify(&value) 106 | self.set(value, forKey: key, completion: { (setResult) in 107 | switch setResult { 108 | case .success: 109 | completion(.success(value)) 110 | case .failure(let error): 111 | completion(.failure(error)) 112 | } 113 | }) 114 | case .failure(let error): 115 | completion(.failure(error)) 116 | } 117 | } 118 | } 119 | } 120 | 121 | extension StorageProtocol { 122 | 123 | public func mapKeys(to type: OtherKey.Type = OtherKey.self, 124 | _ transform: @escaping (OtherKey) throws -> Key) -> Storage { 125 | return Storage(read: asReadOnlyStorage().mapKeys(transform), 126 | write: asWriteOnlyStorage().mapKeys(transform)) 127 | } 128 | 129 | public func mapValues(to type: OtherValue.Type = OtherValue.self, 130 | transformIn: @escaping (Value) throws -> OtherValue, 131 | transformOut: @escaping (OtherValue) throws -> Value) -> Storage { 132 | return Storage(read: asReadOnlyStorage().mapValues(transformIn), 133 | write: asWriteOnlyStorage().mapValues(transformOut)) 134 | } 135 | 136 | } 137 | 138 | extension StorageProtocol { 139 | 140 | public func mapValues(toRawRepresentableType type: OtherValue.Type) -> Storage where OtherValue.RawValue == Value { 141 | return mapValues(transformIn: throwing(OtherValue.init(rawValue:)), 142 | transformOut: { $0.rawValue }) 143 | } 144 | 145 | public func mapKeys(toRawRepresentableType type: OtherKey.Type) -> Storage where OtherKey.RawValue == Key { 146 | return mapKeys({ $0.rawValue }) 147 | } 148 | 149 | } 150 | 151 | extension StorageProtocol { 152 | 153 | public func fallback(with produceValue: @escaping (Error) throws -> Value) -> Storage { 154 | let readOnly = asReadOnlyStorage().fallback(with: produceValue) 155 | return Storage(read: readOnly, write: asWriteOnlyStorage()) 156 | } 157 | 158 | public func defaulting(to defaultValue: @autoclosure @escaping () -> Value) -> Storage { 159 | return fallback(with: { _ in defaultValue() }) 160 | } 161 | 162 | } 163 | 164 | extension StorageProtocol { 165 | 166 | public func singleKey(_ key: Key) -> Storage { 167 | return mapKeys({ key }) 168 | } 169 | 170 | } 171 | 172 | extension ReadableStorageProtocol where Key == Void { 173 | 174 | public func retrieve(completion: @escaping (ShallowsResult) -> ()) { 175 | retrieve(forKey: (), completion: completion) 176 | } 177 | 178 | } 179 | 180 | extension WritableStorageProtocol where Key == Void { 181 | 182 | public func set(_ value: Value, completion: @escaping (ShallowsResult) -> ()) { 183 | set(value, forKey: (), completion: completion) 184 | } 185 | 186 | @available(swift, deprecated: 5.5, message: "use async version or provide completion handler explicitly") 187 | public func set(_ value: Value) { 188 | set(value, completion: { _ in }) 189 | } 190 | } 191 | 192 | extension StorageProtocol where Key == Void { 193 | 194 | @available(swift, deprecated: 5.5, message: "`update` is deprecated because it was creating potentially wrong assumptions regarding the serial nature of this function. `update` cannot guarantee that no concurrent calls to `retrieve` or `set` from other places will be made during the update") 195 | public func update(_ modify: @escaping (inout Value) -> (), completion: @escaping (ShallowsResult) -> () = {_ in }) { 196 | self.update(forKey: (), modify, completion: completion) 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /Sources/Shallows/SyncStorage.swift: -------------------------------------------------------------------------------- 1 | import Dispatch 2 | 3 | internal func dispatched(to queue: DispatchQueue, _ function: @escaping (In) -> ()) -> (In) -> () { 4 | return { input in 5 | queue.async(execute: { function(input) }) 6 | } 7 | } 8 | 9 | internal func dispatched(to queue: DispatchQueue, _ function: @escaping (In1, In2) -> ()) -> (In1, In2) -> () { 10 | return { in1, in2 in 11 | queue.async(execute: { function(in1, in2) }) 12 | } 13 | } 14 | 15 | internal func dispatched(to queue: DispatchQueue, _ function: @escaping (In1, In2, In3) -> ()) -> (In1, In2, In3) -> () { 16 | return { in1, in2, in3 in 17 | queue.async(execute: { function(in1, in2, in3) }) 18 | } 19 | } 20 | 21 | extension StorageProtocol { 22 | 23 | public func synchronizedCalls() -> Storage { 24 | let queue: DispatchQueue = DispatchQueue(label: "\(Self.self)-storage-thread-safety-queue") 25 | return Storage(storageName: self.storageName, 26 | retrieve: dispatched(to: queue, self.retrieve(forKey:completion:)), 27 | set: dispatched(to: queue, self.set(_:forKey:completion:))) 28 | } 29 | 30 | } 31 | 32 | public struct SyncStorage { 33 | 34 | public let storageName: String 35 | 36 | private let _retrieve: (Key) throws -> Value 37 | private let _set: (Value, Key) throws -> Void 38 | 39 | public init(storageName: String, retrieve: @escaping (Key) throws -> Value, set: @escaping (Value, Key) throws -> Void) { 40 | self.storageName = storageName 41 | self._retrieve = retrieve 42 | self._set = set 43 | } 44 | 45 | public func retrieve(forKey key: Key) throws -> Value { 46 | return try _retrieve(key) 47 | } 48 | 49 | public func set(_ value: Value, forKey key: Key) throws { 50 | try _set(value, key) 51 | } 52 | 53 | } 54 | 55 | public struct ReadOnlySyncStorage { 56 | 57 | public let storageName: String 58 | 59 | private let _retrieve: (Key) throws -> Value 60 | 61 | public init(storageName: String, retrieve: @escaping (Key) throws -> Value) { 62 | self.storageName = storageName 63 | self._retrieve = retrieve 64 | } 65 | 66 | public func retrieve(forKey key: Key) throws -> Value { 67 | return try _retrieve(key) 68 | } 69 | 70 | } 71 | 72 | public struct WriteOnlySyncStorage { 73 | 74 | public let storageName: String 75 | 76 | private let _set: (Value, Key) throws -> Void 77 | 78 | public init(storageName: String, set: @escaping (Value, Key) throws -> Void) { 79 | self.storageName = storageName 80 | self._set = set 81 | } 82 | 83 | public func set(_ value: Value, forKey key: Key) throws { 84 | try _set(value, key) 85 | } 86 | 87 | } 88 | 89 | extension ReadOnlyStorageProtocol { 90 | 91 | public func makeSyncStorage() -> ReadOnlySyncStorage { 92 | return ReadOnlySyncStorage(storageName: "\(self.storageName)-sync", retrieve: { (key) throws -> Value in 93 | let semaphore = DispatchSemaphore(value: 0) 94 | var r_result: ShallowsResult? 95 | self.retrieve(forKey: key, completion: { (result) in 96 | r_result = result 97 | semaphore.signal() 98 | }) 99 | semaphore.wait() 100 | return try r_result!.get() 101 | }) 102 | } 103 | 104 | } 105 | 106 | extension WriteOnlyStorageProtocol { 107 | 108 | public func makeSyncStorage() -> WriteOnlySyncStorage { 109 | return WriteOnlySyncStorage(storageName: self.storageName + "-sync", set: { (value, key) in 110 | let semaphore = DispatchSemaphore(value: 0) 111 | var r_result: ShallowsResult? 112 | self.set(value, forKey: key, completion: { (result) in 113 | r_result = result 114 | semaphore.signal() 115 | }) 116 | semaphore.wait() 117 | return try r_result!.get() 118 | }) 119 | } 120 | 121 | } 122 | 123 | extension StorageProtocol { 124 | 125 | public func makeSyncStorage() -> SyncStorage { 126 | let readOnly = asReadOnlyStorage().makeSyncStorage() 127 | let writeOnly = asWriteOnlyStorage().makeSyncStorage() 128 | return SyncStorage(storageName: readOnly.storageName, retrieve: readOnly.retrieve, set: writeOnly.set) 129 | } 130 | 131 | } 132 | 133 | extension SyncStorage where Key == Void { 134 | 135 | public func retrieve() throws -> Value { 136 | return try retrieve(forKey: ()) 137 | } 138 | 139 | public func set(_ value: Value) throws { 140 | try set(value, forKey: ()) 141 | } 142 | 143 | } 144 | 145 | extension ReadOnlySyncStorage where Key == Void { 146 | 147 | public func retrieve() throws -> Value { 148 | return try retrieve(forKey: ()) 149 | } 150 | 151 | } 152 | 153 | extension WriteOnlySyncStorage where Key == Void { 154 | 155 | public func set(_ value: Value) throws { 156 | try set(value, forKey: ()) 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /Sources/Shallows/WriteOnlyStorage.swift: -------------------------------------------------------------------------------- 1 | public protocol WritableStorageProtocol : StorageDesign { 2 | 3 | associatedtype Key 4 | associatedtype Value 5 | 6 | func set(_ value: Value, forKey key: Key, completion: @escaping (ShallowsResult) -> ()) 7 | func asWriteOnlyStorage() -> WriteOnlyStorage 8 | 9 | } 10 | 11 | extension WritableStorageProtocol { 12 | 13 | public func asWriteOnlyStorage() -> WriteOnlyStorage { 14 | if let alreadyNormalized = self as? WriteOnlyStorage { 15 | return alreadyNormalized 16 | } 17 | return WriteOnlyStorage(self) 18 | } 19 | 20 | } 21 | 22 | public protocol WriteOnlyStorageProtocol : WritableStorageProtocol { } 23 | 24 | public struct WriteOnlyStorage : WriteOnlyStorageProtocol { 25 | 26 | public let storageName: String 27 | 28 | private let _set: (Value, Key, @escaping (ShallowsResult) -> ()) -> () 29 | 30 | public init(storageName: String, set: @escaping (Value, Key, @escaping (ShallowsResult) -> ()) -> ()) { 31 | self._set = set 32 | self.storageName = storageName 33 | } 34 | 35 | public init(_ storage: StorageType) where StorageType.Key == Key, StorageType.Value == Value { 36 | self._set = storage.set 37 | self.storageName = storage.storageName 38 | } 39 | 40 | public func set(_ value: Value, forKey key: Key, completion: @escaping (ShallowsResult) -> ()) { 41 | self._set(value, key, completion) 42 | } 43 | 44 | } 45 | 46 | extension WriteOnlyStorageProtocol { 47 | 48 | public func mapKeys(to type: OtherKey.Type = OtherKey.self, 49 | _ transform: @escaping (OtherKey) throws -> Key) -> WriteOnlyStorage { 50 | return WriteOnlyStorage(storageName: storageName, set: { (value, key, completion) in 51 | do { 52 | let newKey = try transform(key) 53 | self.set(value, forKey: newKey, completion: completion) 54 | } catch { 55 | completion(.failure(error)) 56 | } 57 | }) 58 | } 59 | 60 | public func mapValues(to type: OtherValue.Type = OtherValue.self, 61 | _ transform: @escaping (OtherValue) throws -> Value) -> WriteOnlyStorage { 62 | return WriteOnlyStorage(storageName: storageName, set: { (value, key, completion) in 63 | do { 64 | let newValue = try transform(value) 65 | self.set(newValue, forKey: key, completion: completion) 66 | } catch { 67 | completion(.failure(error)) 68 | } 69 | }) 70 | } 71 | 72 | } 73 | 74 | extension WriteOnlyStorageProtocol { 75 | 76 | public func singleKey(_ key: Key) -> WriteOnlyStorage { 77 | return mapKeys({ key }) 78 | } 79 | 80 | } 81 | 82 | extension WriteOnlyStorageProtocol { 83 | 84 | public func mapValues(toRawRepresentableType type: OtherValue.Type) -> WriteOnlyStorage where OtherValue.RawValue == Value { 85 | return mapValues({ $0.rawValue }) 86 | } 87 | 88 | public func mapKeys(toRawRepresentableType type: OtherKey.Type) -> WriteOnlyStorage where OtherKey.RawValue == Key { 89 | return mapKeys({ $0.rawValue }) 90 | } 91 | 92 | } 93 | 94 | extension WriteOnlyStorageProtocol { 95 | 96 | public func usingUnsupportedTransformation(_ transformation: (Storage) -> Storage) -> WriteOnlyStorage { 97 | let fullStorage = Storage(storageName: self.storageName, retrieve: { (_, completion) in 98 | completion(fail(with: UnsupportedTransformationStorageError.storageIsWriteOnly)) 99 | }, set: self.set) 100 | return transformation(fullStorage).asWriteOnlyStorage() 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /Sources/Shallows/Zip.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Zip.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 07.05.17. 6 | // Copyright © 2017 Shallows. All rights reserved. 7 | // 8 | 9 | import Dispatch 10 | 11 | public func zip(_ lhs: ReadOnlyStorage, _ rhs: ReadOnlyStorage) -> ReadOnlyStorage { 12 | return ReadOnlyStorage(storageName: lhs.storageName + "+" + rhs.storageName, retrieve: { (key, completion) in 13 | let container = CompletionContainer, ShallowsResult>() { left, right in 14 | completion(zip(left, right)) 15 | } 16 | lhs.retrieve(forKey: key, completion: { container.completeLeft(with: $0) }) 17 | rhs.retrieve(forKey: key, completion: { container.completeRight(with: $0) }) 18 | }) 19 | } 20 | 21 | public func zip(_ lhs: WriteOnlyStorage, _ rhs: WriteOnlyStorage) -> WriteOnlyStorage { 22 | return WriteOnlyStorage(storageName: lhs.storageName + "+" + rhs.storageName, set: { (value, key, completion) in 23 | let container = CompletionContainer, ShallowsResult>(completion: { (left, right) in 24 | let zipped = zip(left, right) 25 | switch zipped { 26 | case .success: 27 | completion(.success) 28 | case .failure(let error): 29 | completion(.failure(error)) 30 | } 31 | }) 32 | lhs.set(value.0, forKey: key, completion: { container.completeLeft(with: $0) }) 33 | rhs.set(value.1, forKey: key, completion: { container.completeRight(with: $0) }) 34 | }) 35 | } 36 | 37 | public func zip(_ lhs: Storage1, _ rhs: Storage2) -> Storage where Storage1.Key == Storage2.Key { 38 | let readOnlyZipped = zip(lhs.asReadOnlyStorage(), rhs.asReadOnlyStorage()) 39 | let writeOnlyZipped = zip(lhs.asWriteOnlyStorage(), rhs.asWriteOnlyStorage()) 40 | return Storage(read: readOnlyZipped, write: writeOnlyZipped) 41 | } 42 | 43 | fileprivate final class CompletionContainer { 44 | 45 | private var left: Left? 46 | private var right: Right? 47 | private let queue = DispatchQueue(label: "container-completion") 48 | 49 | private let completion: (Left, Right) -> () 50 | 51 | fileprivate init(completion: @escaping (Left, Right) -> ()) { 52 | self.completion = completion 53 | } 54 | 55 | fileprivate func completeLeft(with left: Left) { 56 | queue.async { 57 | self.left = left 58 | self.check() 59 | } 60 | } 61 | 62 | fileprivate func completeRight(with right: Right) { 63 | queue.async { 64 | self.right = right 65 | self.check() 66 | } 67 | } 68 | 69 | private func check() { 70 | if let left = left, let right = right { 71 | completion(left, right) 72 | } 73 | } 74 | 75 | } 76 | 77 | public struct ZippedResultError : Error { 78 | 79 | public let left: Error? 80 | public let right: Error? 81 | 82 | public init(left: Error?, right: Error?) { 83 | self.left = left 84 | self.right = right 85 | } 86 | 87 | } 88 | 89 | fileprivate func zip(_ lhs: ShallowsResult, _ rhs: ShallowsResult) -> ShallowsResult<(Value1, Value2)> { 90 | switch (lhs, rhs) { 91 | case (.success(let left), .success(let right)): 92 | return Result.success((left, right)) 93 | default: 94 | return Result.failure(ZippedResultError(left: lhs.error, right: rhs.error)) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import ShallowsTests 3 | 4 | XCTMain([ 5 | testCase(ShallowsTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/AdditionalSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AdditionalSpec.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 21.02.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | @testable import Shallows 10 | import Dispatch 11 | 12 | func testAdditional() { 13 | 14 | describe("update") { 15 | $0.it("retrieves, updates and sets value") { 16 | let storage = MemoryStorage(storage: [1: 10]) 17 | let sema = DispatchSemaphore(value: 0) 18 | var val: Int? 19 | storage.update(forKey: 1, { $0 += 1 }, completion: { (result) in 20 | val = result.value 21 | sema.signal() 22 | }) 23 | sema.wait() 24 | try expect(val) == 11 25 | try expect(storage.storage[1]) == 11 26 | } 27 | $0.it("fails if retrieve fails") { 28 | let read = ReadOnlyStorage.alwaysFailing(with: "read fails") 29 | let write = MemoryStorage(storage: [1: 10]) 30 | let storage = Storage(read: read, write: write.asWriteOnlyStorage()) 31 | let sema = DispatchSemaphore(value: 0) 32 | var er: Error? 33 | storage.update(forKey: 1, { $0 += 1 }, completion: { (result) in 34 | er = result.error 35 | sema.signal() 36 | }) 37 | sema.wait() 38 | try expect(er as? String) == "read fails" 39 | try expect(write.storage[1]) == 10 40 | } 41 | $0.it("fails if set fails") { 42 | let read = ReadOnlyStorage.alwaysSucceeding(with: 10) 43 | let write = WriteOnlyStorage.alwaysFailing(with: "write fails") 44 | let storage = Storage(read: read, write: write) 45 | let sema = DispatchSemaphore(value: 0) 46 | var er: Error? 47 | storage.update(forKey: 1, { $0 += 1 }, completion: { (result) in 48 | er = result.error 49 | sema.signal() 50 | }) 51 | sema.wait() 52 | try expect(er as? String) == "write fails" 53 | } 54 | } 55 | 56 | describe("renaming") { 57 | $0.it("renames read-only storage") { 58 | let r1 = ReadOnlyStorage.empty() 59 | let r2 = r1.renaming(to: "r2") 60 | try expect(r2.storageName) == "r2" 61 | } 62 | $0.it("renames write-only storage") { 63 | let w1 = WriteOnlyStorage.empty() 64 | let w2 = w1.renaming(to: "w2") 65 | try expect(w2.storageName) == "w2" 66 | } 67 | $0.describe("storage") { 68 | let s1 = Storage.empty() 69 | let s2 = s1.renaming(to: "s2") 70 | $0.it("renames it") { 71 | try expect(s2.storageName) == "s2" 72 | } 73 | $0.it("renames asReadOnlyStorage") { 74 | try expect(s2.asReadOnlyStorage().storageName) == "s2" 75 | } 76 | $0.it("renames asWriteOnlyStorage") { 77 | try expect(s2.asWriteOnlyStorage().storageName) == "s2" 78 | } 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/CompositionSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CompositionSpec.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 13.02.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | @testable import Shallows 10 | import Foundation 11 | 12 | func testComposition() { 13 | 14 | describe("read-only storage composition") { 15 | $0.describe("backed") { 16 | $0.it("returns front if existing") { 17 | let front = MemoryStorage(storage: [1: 1]).asReadOnlyStorage() 18 | let back = MemoryStorage(storage: [1: 2]).asReadOnlyStorage() 19 | let backed = front.backed(by: back) 20 | let sema = DispatchSemaphore(value: 0) 21 | var val: Int? 22 | backed.retrieve(forKey: 1, completion: { (result) in 23 | val = result.value 24 | sema.signal() 25 | }) 26 | sema.wait() 27 | try expect(val) == 1 28 | } 29 | $0.it("returns back if front is missing") { 30 | let front = ReadOnlyStorage.empty() 31 | let back = MemoryStorage(storage: [1: 2]) 32 | let backed = front.backed(by: back) 33 | let sema = DispatchSemaphore(value: 0) 34 | var val: Int? 35 | backed.retrieve(forKey: 1, completion: { (result) in 36 | val = result.value 37 | sema.signal() 38 | }) 39 | sema.wait() 40 | try expect(val) == 2 41 | } 42 | $0.it("fails if both fail") { 43 | let front = ReadOnlyStorage.empty() 44 | let back = ReadOnlyStorage.alwaysFailing(with: "back fails") 45 | let backed = front.backed(by: back) 46 | let sema = DispatchSemaphore(value: 0) 47 | var er: Error? 48 | backed.retrieve(forKey: 1, completion: { (result) in 49 | er = result.error 50 | sema.signal() 51 | }) 52 | sema.wait() 53 | try expect(er as? String) == "back fails" 54 | } 55 | } 56 | } 57 | 58 | describe("writable storage composition") { 59 | $0.describe("pushingto") { 60 | $0.it("pushes to front and back") { 61 | let front = MemoryStorage() 62 | let back = MemoryStorage() 63 | let pushing = front.asWriteOnlyStorage().pushing(to: back.asWriteOnlyStorage()) 64 | let sema = DispatchSemaphore(value: 0) 65 | var er: Error? 66 | pushing.set(10, forKey: 1, completion: { (result) in 67 | er = result.error 68 | sema.signal() 69 | }) 70 | sema.wait() 71 | try expect(er).to.beNil() 72 | try expect(front.storage[1]) == 10 73 | try expect(back.storage[1]) == 10 74 | } 75 | $0.it("doesnt set to back if front fails") { 76 | let front = Storage.alwaysFailing(with: "front is failing") 77 | let back = MemoryStorage() 78 | let pushing = front.asWriteOnlyStorage().pushing(to: back.asWriteOnlyStorage()) 79 | let sema = DispatchSemaphore(value: 0) 80 | var er: Error? 81 | pushing.set(10, forKey: 1, completion: { (res) in 82 | er = res.error 83 | sema.signal() 84 | }) 85 | sema.wait() 86 | try expect(er as? String) == "front is failing" 87 | try expect(back.storage[1] == nil).to.beTrue() 88 | } 89 | $0.it("sets to front and fails if back fails") { 90 | let front = MemoryStorage() 91 | let back = Storage.alwaysFailing(with: "back fails") 92 | let pushing = front.asWriteOnlyStorage().pushing(to: back.asWriteOnlyStorage()) 93 | let sema = DispatchSemaphore(value: 0) 94 | var er: Error? 95 | pushing.set(10, forKey: 1, completion: { (res) in 96 | er = res.error 97 | sema.signal() 98 | }) 99 | sema.wait() 100 | try expect(er as? String) == "back fails" 101 | try expect(front.storage[1]) == 10 102 | } 103 | } 104 | } 105 | 106 | describe("storage composition") { 107 | 108 | $0.describe("backed") { 109 | $0.it("returns front if existing") { 110 | let front = MemoryStorage(storage: [1: 1]) 111 | let back = MemoryStorage(storage: [1: 2]).asReadOnlyStorage() 112 | let backed = front.backed(by: back) 113 | let sema = DispatchSemaphore(value: 0) 114 | var val: Int? = nil 115 | backed.retrieve(forKey: 1, completion: { (result) in 116 | val = result.value 117 | sema.signal() 118 | }) 119 | sema.wait() 120 | try expect(val) == 1 121 | } 122 | $0.it("returns back if not existing, setting value back to front") { 123 | let front = MemoryStorage() 124 | let back = MemoryStorage(storage: [1: 2]).asReadOnlyStorage() 125 | let combined = front.backed(by: back) 126 | let sema = DispatchSemaphore(value: 0) 127 | var val: Int? = nil 128 | combined.retrieve(forKey: 1, completion: { (result) in 129 | val = result.value 130 | sema.signal() 131 | }) 132 | sema.wait() 133 | try expect(val) == 2 134 | try expect(front.storage[1]) == 2 135 | } 136 | $0.it("fails if value is non existing in both storages") { 137 | let front = Storage.empty() 138 | let back = Storage.empty().asReadOnlyStorage() 139 | let backed = front.backed(by: back) 140 | let sema = DispatchSemaphore(value: 0) 141 | var er: Error? = nil 142 | backed.retrieve(forKey: 0, completion: { (result) in 143 | er = result.error 144 | sema.signal() 145 | }) 146 | sema.wait() 147 | try expect(er as? EmptyCacheError) == .cacheIsAlwaysEmpty 148 | } 149 | $0.it("sets to front") { 150 | let front = MemoryStorage() 151 | let back = MemoryStorage().asReadOnlyStorage() 152 | let backed = front.backed(by: back) 153 | let sema = DispatchSemaphore(value: 0) 154 | backed.set(10, forKey: 1, completion: { (_) in 155 | sema.signal() 156 | }) 157 | sema.wait() 158 | try expect(front.storage[1]) == 10 159 | } 160 | $0.it("fails if front set fails") { 161 | let front = Storage.alwaysFailing(with: "front fails") 162 | let back = MemoryStorage().asReadOnlyStorage() 163 | let backed = front.backed(by: back) 164 | var er: Error? 165 | let sema = DispatchSemaphore(value: 0) 166 | backed.set(10, forKey: 1, completion: { (result) in 167 | er = result.error 168 | sema.signal() 169 | }) 170 | sema.wait() 171 | try expect(er as? String) == "front fails" 172 | } 173 | } 174 | 175 | $0.describe("combined") { 176 | $0.describe("retrieve") { 177 | $0.it("returns front if existing") { 178 | let front = MemoryStorage(storage: [1: 1]) 179 | let back = MemoryStorage(storage: [1: 2]) 180 | let combined = front.combined(with: back) 181 | let sema = DispatchSemaphore(value: 0) 182 | var val: Int? = nil 183 | combined.retrieve(forKey: 1, completion: { (result) in 184 | val = result.value 185 | sema.signal() 186 | }) 187 | sema.wait() 188 | try expect(val) == 1 189 | } 190 | $0.it("returns back if not existing, setting value back to front") { 191 | let front = MemoryStorage() 192 | let back = MemoryStorage(storage: [1: 2]) 193 | let combined = front.combined(with: back) 194 | let sema = DispatchSemaphore(value: 0) 195 | var val: Int? = nil 196 | combined.retrieve(forKey: 1, completion: { (result) in 197 | val = result.value 198 | sema.signal() 199 | }) 200 | sema.wait() 201 | try expect(val) == 2 202 | try expect(front.storage[1]) == 2 203 | } 204 | $0.it("fails if value is non existing in both storages") { 205 | let front = Storage.empty() 206 | let back = Storage.empty() 207 | let combined = front.combined(with: back) 208 | let sema = DispatchSemaphore(value: 0) 209 | var er: Error? = nil 210 | combined.retrieve(forKey: 0, completion: { (result) in 211 | er = result.error 212 | sema.signal() 213 | }) 214 | sema.wait() 215 | try expect(er as? EmptyCacheError) == .cacheIsAlwaysEmpty 216 | } 217 | } 218 | $0.describe("set") { 219 | $0.it("sets to both storages") { 220 | let front = MemoryStorage() 221 | let back = MemoryStorage() 222 | let combined = front.combined(with: back) 223 | let sema = DispatchSemaphore(value: 0) 224 | combined.set(10, forKey: 1, completion: { (_) in 225 | sema.signal() 226 | }) 227 | sema.wait() 228 | try expect(front.storage[1]) == 10 229 | try expect(back.storage[1]) == 10 230 | } 231 | $0.it("doesnt set to back if front fails") { 232 | let front = Storage.alwaysFailing(with: "front is failing") 233 | let back = MemoryStorage() 234 | let combined = front.combined(with: back) 235 | let sema = DispatchSemaphore(value: 0) 236 | var er: Error? 237 | combined.set(10, forKey: 1, completion: { (res) in 238 | er = res.error 239 | sema.signal() 240 | }) 241 | sema.wait() 242 | try expect(er as? String) == "front is failing" 243 | try expect(back.storage[1] == nil).to.beTrue() 244 | } 245 | $0.it("sets to front and fails if back fails") { 246 | let front = MemoryStorage() 247 | let back = Storage.alwaysFailing(with: "back fails") 248 | let combined = front.combined(with: back) 249 | let sema = DispatchSemaphore(value: 0) 250 | var er: Error? 251 | combined.set(10, forKey: 1, completion: { (res) in 252 | er = res.error 253 | sema.signal() 254 | }) 255 | sema.wait() 256 | try expect(er as? String) == "back fails" 257 | try expect(front.storage[1]) == 10 258 | } 259 | } 260 | 261 | } 262 | 263 | } 264 | 265 | } 266 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/DiskStorageSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // DiskStorageTests.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 18.01.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import Shallows 11 | 12 | func testDiskStorage() { 13 | 14 | let currentDir = FileManager.default.currentDirectoryPath 15 | print("CURRENT DIR", currentDir) 16 | let currentDirURL = URL.init(fileURLWithPath: currentDir) 17 | 18 | describe("filename type") { 19 | let example = "spectre.cool" 20 | let filename = Filename(rawValue: example) 21 | $0.it("has a raw value") { 22 | try expect(filename.rawValue) == example 23 | } 24 | $0.it("is hashable") { 25 | let anotherValue = Filename(rawValue: example) 26 | try expect(anotherValue.hashValue) == filename.hashValue 27 | } 28 | $0.it("encodes to base64") { 29 | let data = example.data(using: .utf8)! 30 | let base64 = data.base64EncodedString() 31 | try expect(filename.base64Encoded()) == base64 32 | } 33 | $0.describe("encodes") { 34 | $0.it("to base64") { 35 | let data = example.data(using: .utf8)! 36 | let base64 = data.base64EncodedString() 37 | try expect(Filename.Encoder.base64.encodedString(representing: filename)) == base64 38 | } 39 | $0.it("to base64url") { 40 | let data = example.data(using: .utf8)! 41 | let base64 = data.base64EncodedString() 42 | .replacingOccurrences(of: "+", with: "-") 43 | .replacingOccurrences(of: "/", with: "_") 44 | try expect(Filename.Encoder.base64URL.encodedString(representing: filename)) == base64 45 | // try expect(base64.contains("+")) == false 46 | // try expect(base64.contains("/")) == false 47 | } 48 | $0.it("without encoding") { 49 | try expect(Filename.Encoder.noEncoding.encodedString(representing: filename)) == example 50 | } 51 | $0.it("with custom encoding") { 52 | let custom = Filename.Encoder.custom({ $0.rawValue.uppercased() }) 53 | try expect(custom.encodedString(representing: filename)) == example.uppercased() 54 | } 55 | } 56 | } 57 | 58 | describe("disk storage") { 59 | $0.describe("helper methods") { 60 | $0.it("retrieve directory URL from file URL") { 61 | let fileURL = currentDirURL.appendingPathComponent("cool.json") 62 | let directoryURL = DiskStorage.directoryURL(of: fileURL) 63 | try expect(directoryURL) == currentDirURL 64 | } 65 | } 66 | $0.describe("when !creatingDirectories") { 67 | let unknownDirectoryURL = currentDirURL.appendingPathComponent("_unknown", isDirectory: true) 68 | let clear = { deleteEverything(at: unknownDirectoryURL) } 69 | $0.before(clear) 70 | $0.it("fails if directory doesn't exist") { 71 | let diskStorage = DiskStorage(creatingDirectories: false).mapString().makeSyncStorage() 72 | let fileURL = unknownDirectoryURL.appendingPathComponent("nope.json") 73 | try expect(try diskStorage.set("none", forKey: fileURL)).toThrow() 74 | } 75 | $0.after(clear) 76 | } 77 | $0.describe("performs") { 78 | let filename = "_ronaldo.json" 79 | let dirURL = currentDirURL.appendingPathComponent("_tmp_test", isDirectory: true) 80 | let tempFileURL = dirURL.appendingPathComponent(filename) 81 | let disk = DiskStorage.main 82 | .mapString() 83 | .makeSyncStorage() 84 | let clear = { deleteEverything(at: dirURL) } 85 | $0.before(clear) 86 | $0.it("writes") { 87 | try disk.set("25:12", forKey: tempFileURL) 88 | } 89 | $0.it("reads") { 90 | let data = "25:12".data(using: .utf8) 91 | try FileManager.default.createDirectory(at: dirURL, withIntermediateDirectories: false, attributes: nil) 92 | FileManager.default.createFile(atPath: tempFileURL.path, contents: data, attributes: nil) 93 | let back = try disk.retrieve(forKey: tempFileURL) 94 | try expect(back) == "25:12" 95 | } 96 | $0.it("writes and reads") { 97 | let string = "penalty" 98 | try disk.set(string, forKey: tempFileURL) 99 | let back = try disk.retrieve(forKey: tempFileURL) 100 | try expect(back) == string 101 | } 102 | $0.it("fails when there is no file") { 103 | try expect(try disk.retrieve(forKey: tempFileURL)).toThrow() 104 | } 105 | $0.after(clear) 106 | } 107 | } 108 | 109 | describe("folder storage") { 110 | $0.describe("helper methods") { 111 | $0.it("creates URL for search path directory") { 112 | let directoryURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] 113 | let createdURL = DiskFolderStorage.url(forFolder: "Wigan", in: .cachesDirectory) 114 | try expect(createdURL) == directoryURL.appendingPathComponent("Wigan", isDirectory: true) 115 | } 116 | } 117 | $0.it("can be created in one of a FileManager.SearchPathDirectory") { 118 | let folderURL = DiskFolderStorage.url(forFolder: "ManUTD", in: .cachesDirectory) 119 | let folderStorage = DiskFolderStorage.inDirectory(.cachesDirectory, folderName: "ManUTD", diskStorage: .empty(), filenameEncoder: .noEncoding) 120 | try expect(folderStorage.folderURL) == folderURL 121 | } 122 | $0.it("can be created by disk storage") { 123 | let folderURL = DiskFolderStorage.url(forFolder: "Scholes", in: .cachesDirectory) 124 | let folder = DiskStorage.main.folder("Scholes", in: .cachesDirectory) 125 | try expect(folder.folderURL) == folderURL 126 | 127 | let folderURL2 = DiskFolderStorage.url(forFolder: "Rooney", in: .cachesDirectory) 128 | let folder2 = DiskStorage.folder("Rooney", in: .cachesDirectory) 129 | try expect(folder2.folderURL) == folderURL2 130 | } 131 | $0.it("encodes filename") { 132 | func storage(encoder: Filename.Encoder) -> DiskFolderStorage { 133 | return DiskFolderStorage.inDirectory(.cachesDirectory, folderName: "aaa", diskStorage: .empty(), filenameEncoder: encoder) 134 | } 135 | func checkStorage(for encoder: Filename.Encoder) throws { 136 | let example: Filename = "Nineties" 137 | let str = storage(encoder: encoder) 138 | try expect(str.fileURL(forFilename: example)) == str.folderURL.appendingPathComponent(encoder.encodedString(representing: example)) 139 | } 140 | try checkStorage(for: .base64) 141 | try checkStorage(for: .noEncoding) 142 | try checkStorage(for: .custom({ $0.rawValue.uppercased() })) 143 | } 144 | $0.it("really encodes filename") { 145 | var isFilenameCorrect = true 146 | let customStorage = MemoryStorage() 147 | .mapKeys({ (url: URL) -> URL in 148 | if url.lastPathComponent != "modified-filename" { 149 | isFilenameCorrect = false 150 | } 151 | return url 152 | }) 153 | let storage = DiskFolderStorage(folderURL: currentDirURL, diskStorage: customStorage, filenameEncoder: .custom({ _ in "modified-filename" })) 154 | .mapString() 155 | .makeSyncStorage() 156 | try storage.set("doesn't matter", forKey: "key-to-be-modified") 157 | _ = try storage.retrieve(forKey: "another-to-be-modified-key") 158 | try expect(isFilenameCorrect).to.beTrue() 159 | } 160 | $0.it("uses injected storage") { 161 | let folder = DiskFolderStorage(folderURL: currentDirURL, diskStorage: .alwaysSucceeding(with: "great".data(using: .utf8)!)) 162 | .mapString(withEncoding: .utf8) 163 | .makeSyncStorage() 164 | let value = try folder.retrieve(forKey: "any-key") 165 | try expect(value) == "great" 166 | } 167 | $0.it("can clear its folder") { 168 | let folder = DiskFolderStorage(folderURL: currentDirURL.appendingPathComponent("_should_be_cleared", isDirectory: true)) 169 | let folderURL = folder.folderURL 170 | try folder 171 | .mapString() 172 | .makeSyncStorage() 173 | .set("Scholes", forKey: "some-key") 174 | let exists: () -> Bool = { 175 | return FileManager.default.fileExists(atPath: folderURL.path) 176 | } 177 | try expect(exists()).to.beTrue() 178 | folder.clear() 179 | try expect(exists()).to.beFalse() 180 | } 181 | // $0.it("writes and reads") { 182 | // let folder = DiskFolderStorage(folderURL: currentDirURL, diskStorage: MemoryStorage().asStorage()) 183 | // .mapString() 184 | // .singleKey("Oh my god") 185 | // .makeSyncStorage() 186 | // try folder.set("Wigan-ManUTD") 187 | // let back = try folder.retrieve() 188 | // try expect(back) == "Wigan-ManUTD" 189 | // } 190 | } 191 | 192 | } 193 | 194 | fileprivate func deleteEverything(at url: URL) { 195 | do { 196 | try FileManager.default.removeItem(at: url) 197 | } catch { } 198 | } 199 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/MemoryStorageSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MemoryStorageSpec.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 18.01.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import Shallows 11 | 12 | func testMemoryStorage() { 13 | 14 | describe("ThreadSafe") { 15 | $0.it("can read the value") { 16 | let threadSafe = ThreadSafe(15) 17 | try expect(threadSafe.read()) == 15 18 | } 19 | $0.it("can write to the value") { 20 | var threadSafe = ThreadSafe(15) 21 | threadSafe.write(20) 22 | try expect(threadSafe.read()) == 20 23 | } 24 | $0.it("can modify the value") { 25 | var threadSafe = ThreadSafe(15) 26 | threadSafe.write(with: { $0 -= 5 }) 27 | try expect(threadSafe.read()) == 10 28 | } 29 | } 30 | 31 | describe("memory storage") { 32 | $0.it("always writes the value") { 33 | let memoryStorage = MemoryStorage().makeSyncStorage() 34 | try memoryStorage.set(10, forKey: 10) 35 | } 36 | $0.it("reads the value") { 37 | let memoryStorage = MemoryStorage(storage: [1 : 5]).makeSyncStorage() 38 | let value = try memoryStorage.retrieve(forKey: 1) 39 | try expect(value) == 5 40 | } 41 | $0.it("always writes and reads the value") { 42 | let mem = MemoryStorage().singleKey(0).makeSyncStorage() 43 | try mem.set(10) 44 | let value = try mem.retrieve() 45 | try expect(value) == 10 46 | } 47 | $0.it("fails if there is no value") { 48 | let mem = MemoryStorage().makeSyncStorage() 49 | try expect(try mem.retrieve(forKey: 10)).toThrow() 50 | } 51 | $0.it("can be modified directly") { 52 | let dict: [Int : Int] = [1: 5, 2: 6] 53 | let mem = MemoryStorage(storage: dict) 54 | try expect(mem.storage) == dict 55 | mem.storage[3] = 7 56 | try expect(try mem.makeSyncStorage().retrieve(forKey: 3)) == 7 57 | var copy = dict 58 | copy[3] = 7 59 | try expect(mem.storage) == copy 60 | } 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/ResultSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ResultSpec.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 13.02.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | @testable import Shallows 11 | 12 | public func testResult() { 13 | 14 | describe("result") { 15 | $0.it("can be a failure") { 16 | let failure = ShallowsResult.failure("Test") 17 | try expect(failure.isFailure) == true 18 | try expect(failure.isSuccess) == false 19 | } 20 | $0.it("can be a success") { 21 | let success = ShallowsResult.success(10) 22 | try expect(success.isSuccess) == true 23 | try expect(success.isFailure) == false 24 | } 25 | $0.it("contains a value if it's a success") { 26 | let success = succeed(with: 15) 27 | try expect(success.value) == 15 28 | try expect(success.error).to.beNil() 29 | } 30 | $0.it("contains an error if it's a failure") { 31 | let failure: ShallowsResult = fail(with: "Test") 32 | try expect(failure.error as? String) == "Test" 33 | try expect(failure.value).to.beNil() 34 | } 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/Case.swift: -------------------------------------------------------------------------------- 1 | protocol CaseType { 2 | /// Run a test case in the given reporter 3 | func run(reporter: ContextReporter) 4 | } 5 | 6 | class Case : CaseType { 7 | let name:String 8 | let disabled: Bool 9 | let closure:() throws -> () 10 | 11 | let function: String 12 | let file: String 13 | let line: Int 14 | 15 | init(name: String, disabled: Bool = false, closure: @escaping () throws -> (), function: String = #function, file: String = #file, line: Int = #line) { 16 | self.name = name 17 | self.disabled = disabled 18 | self.closure = closure 19 | 20 | self.function = function 21 | self.file = file 22 | self.line = line 23 | } 24 | 25 | func run(reporter: ContextReporter) { 26 | if disabled { 27 | reporter.addDisabled(name) 28 | return 29 | } 30 | 31 | do { 32 | try closure() 33 | reporter.addSuccess(name) 34 | } catch _ as Skip { 35 | reporter.addDisabled(name) 36 | } catch let error as FailureType { 37 | reporter.addFailure(name, failure: error) 38 | } catch { 39 | reporter.addFailure(name, failure: Failure(reason: "Unhandled error: \(error)", function: function, file: file, line: line)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/Context.swift: -------------------------------------------------------------------------------- 1 | public protocol ContextType { 2 | /// Creates a new sub-context 3 | func context(_ name: String, closure: (ContextType) -> Void) 4 | 5 | /// Creates a new sub-context 6 | func describe(_ name: String, closure: (ContextType) -> Void) 7 | 8 | /// Creates a new disabled sub-context 9 | func xcontext(_ name: String, closure: (ContextType) -> Void) 10 | 11 | /// Creates a new disabled sub-context 12 | func xdescribe(_ name: String, closure: (ContextType) -> Void) 13 | 14 | func before(_ closure: @escaping () -> Void) 15 | func after(_ closure: @escaping () -> Void) 16 | 17 | /// Adds a new test case 18 | func it(_ name: String, closure: @escaping () throws -> Void) 19 | 20 | /// Adds a disabled test case 21 | func xit(_ name: String, closure: @escaping () throws -> Void) 22 | } 23 | 24 | class Context : ContextType, CaseType { 25 | let name: String 26 | let disabled: Bool 27 | fileprivate weak var parent: Context? 28 | var cases = [CaseType]() 29 | 30 | typealias Before = (() -> Void) 31 | typealias After = (() -> Void) 32 | 33 | var befores = [Before]() 34 | var afters = [After]() 35 | 36 | init(name: String, disabled: Bool = false, parent: Context? = nil) { 37 | self.name = name 38 | self.disabled = disabled 39 | self.parent = parent 40 | } 41 | 42 | func context(_ name: String, closure: (ContextType) -> Void) { 43 | let context = Context(name: name, parent: self) 44 | closure(context) 45 | cases.append(context) 46 | } 47 | 48 | func describe(_ name: String, closure: (ContextType) -> Void) { 49 | let context = Context(name: name, parent: self) 50 | closure(context) 51 | cases.append(context) 52 | } 53 | 54 | func xcontext(_ name: String, closure: (ContextType) -> Void) { 55 | let context = Context(name: name, disabled: true, parent: self) 56 | closure(context) 57 | cases.append(context) 58 | } 59 | 60 | func xdescribe(_ name: String, closure: (ContextType) -> Void) { 61 | let context = Context(name: name, disabled: true, parent: self) 62 | closure(context) 63 | cases.append(context) 64 | } 65 | 66 | func before(_ closure: @escaping () -> Void) { 67 | befores.append(closure) 68 | } 69 | 70 | func after(_ closure: @escaping () -> Void) { 71 | afters.append(closure) 72 | } 73 | 74 | func it(_ name: String, closure: @escaping () throws -> Void) { 75 | cases.append(Case(name: name, closure: closure)) 76 | } 77 | 78 | func xit(_ name: String, closure: @escaping () throws -> Void) { 79 | cases.append(Case(name: name, disabled: true, closure: closure)) 80 | } 81 | 82 | func runBefores() { 83 | parent?.runBefores() 84 | befores.forEach { $0() } 85 | } 86 | 87 | func runAfters() { 88 | afters.forEach { $0() } 89 | parent?.runAfters() 90 | } 91 | 92 | func run(reporter: ContextReporter) { 93 | if disabled { 94 | reporter.addDisabled(name) 95 | return 96 | } 97 | 98 | reporter.report(name) { reporter in 99 | cases.forEach { 100 | runBefores() 101 | $0.run(reporter: reporter) 102 | runAfters() 103 | } 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/Expectation.swift: -------------------------------------------------------------------------------- 1 | public protocol ExpectationType { 2 | associatedtype ValueType 3 | var expression: () throws -> ValueType? { get } 4 | func failure(_ reason: String) -> FailureType 5 | } 6 | 7 | 8 | struct ExpectationFailure : FailureType { 9 | let file: String 10 | let line: Int 11 | let function: String 12 | 13 | let reason: String 14 | 15 | init(reason: String, file: String, line: Int, function: String) { 16 | self.reason = reason 17 | self.file = file 18 | self.line = line 19 | self.function = function 20 | } 21 | } 22 | 23 | open class Expectation : ExpectationType { 24 | public typealias ValueType = T 25 | public let expression: () throws -> ValueType? 26 | 27 | let file: String 28 | let line: Int 29 | let function: String 30 | 31 | open var to: Expectation { 32 | return self 33 | } 34 | 35 | init(file: String, line: Int, function: String, expression: @escaping () throws -> ValueType?) { 36 | self.file = file 37 | self.line = line 38 | self.function = function 39 | self.expression = expression 40 | } 41 | 42 | open func failure(_ reason: String) -> FailureType { 43 | return ExpectationFailure(reason: reason, file: file, line: line, function: function) 44 | } 45 | } 46 | 47 | public func expect( _ expression: @autoclosure @escaping () throws -> T?, file: String = #file, line: Int = #line, function: String = #function) -> Expectation { 48 | return Expectation(file: file, line: line, function: function, expression: expression) 49 | } 50 | 51 | public func expect(_ file: String = #file, line: Int = #line, function: String = #function, expression: @escaping () throws -> T?) -> Expectation { 52 | return Expectation(file: file, line: line, function: function, expression: expression) 53 | } 54 | 55 | // MARK: Equatability 56 | 57 | public func == (lhs: E, rhs: E.ValueType) throws where E.ValueType: Equatable { 58 | if let value = try lhs.expression() { 59 | if value != rhs { 60 | throw lhs.failure("\(String(describing: value)) is not equal to \(rhs)") 61 | } 62 | } else { 63 | throw lhs.failure("given value is nil") 64 | } 65 | } 66 | 67 | public func != (lhs: E, rhs: E.ValueType) throws where E.ValueType: Equatable { 68 | let value = try lhs.expression() 69 | if value == rhs { 70 | throw lhs.failure("\(String(describing: value)) is equal to \(rhs)") 71 | } 72 | } 73 | 74 | // MARK: Array Equatability 75 | 76 | public func == (lhs: Expectation<[Element]>, rhs: [Element]) throws { 77 | if let value = try lhs.expression() { 78 | if value != rhs { 79 | throw lhs.failure("\(String(describing: value)) is not equal to \(rhs)") 80 | } 81 | } else { 82 | throw lhs.failure("given value is nil") 83 | } 84 | } 85 | 86 | public func != (lhs: Expectation<[Element]>, rhs: [Element]) throws { 87 | if let value = try lhs.expression() { 88 | if value == rhs { 89 | throw lhs.failure("\(String(describing: value)) is equal to \(rhs)") 90 | } 91 | } else { 92 | throw lhs.failure("given value is nil") 93 | } 94 | } 95 | 96 | // MARK: Dictionary Equatability 97 | 98 | public func == (lhs: Expectation<[Key: Value]>, rhs: [Key: Value]) throws { 99 | if let value = try lhs.expression() { 100 | if value != rhs { 101 | throw lhs.failure("\(String(describing: value)) is not equal to \(rhs)") 102 | } 103 | } else { 104 | throw lhs.failure("given value is nil") 105 | } 106 | } 107 | 108 | public func != (lhs: Expectation<[Key: Value]>, rhs: [Key: Value]) throws { 109 | if let value = try lhs.expression() { 110 | if value == rhs { 111 | throw lhs.failure("\(String(describing: value)) is equal to \(rhs)") 112 | } 113 | } else { 114 | throw lhs.failure("given value is nil") 115 | } 116 | } 117 | 118 | // MARK: Nil 119 | 120 | extension ExpectationType { 121 | public func beNil() throws { 122 | let value = try expression() 123 | if value != nil { 124 | throw failure("value is not nil") 125 | } 126 | } 127 | } 128 | 129 | // MARK: Boolean 130 | 131 | extension ExpectationType where ValueType == Bool { 132 | public func beTrue() throws { 133 | let value = try expression() 134 | if value != true { 135 | throw failure("value is not true") 136 | } 137 | } 138 | 139 | public func beFalse() throws { 140 | let value = try expression() 141 | if value != false { 142 | throw failure("value is not false") 143 | } 144 | } 145 | } 146 | 147 | // Mark: Types 148 | 149 | extension ExpectationType { 150 | public func beOfType(_ expectedType: Any.Type) throws { 151 | guard let value = try expression() else { throw failure("cannot determine type: expression threw an error or value is nil") } 152 | let valueType = Mirror(reflecting: value).subjectType 153 | if valueType != expectedType { 154 | throw failure("'\(valueType)' is not the expected type '\(expectedType)'") 155 | } 156 | } 157 | } 158 | 159 | // MARK: Error Handling 160 | 161 | extension ExpectationType { 162 | public func toThrow() throws { 163 | var didThrow = false 164 | 165 | do { 166 | _ = try expression() 167 | } catch { 168 | didThrow = true 169 | } 170 | 171 | if !didThrow { 172 | throw failure("expression did not throw an error") 173 | } 174 | } 175 | 176 | public func toThrow(_ error: T) throws { 177 | var thrownError: Error? = nil 178 | 179 | do { 180 | _ = try expression() 181 | } catch { 182 | thrownError = error 183 | } 184 | 185 | if let thrownError = thrownError { 186 | if let thrownError = thrownError as? T { 187 | if error != thrownError { 188 | throw failure("\(thrownError) is not \(error)") 189 | } 190 | } else { 191 | throw failure("\(thrownError) is not \(error)") 192 | } 193 | } else { 194 | throw failure("expression did not throw an error") 195 | } 196 | } 197 | } 198 | 199 | // MARK: Comparable 200 | 201 | public func > (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 202 | let value = try lhs.expression() 203 | guard value! > rhs else { 204 | throw lhs.failure("\(String(describing: value)) is not more than \(rhs)") 205 | } 206 | } 207 | 208 | public func >= (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 209 | let value = try lhs.expression() 210 | guard value! >= rhs else { 211 | throw lhs.failure("\(String(describing: value)) is not more than or equal to \(rhs)") 212 | } 213 | } 214 | 215 | public func < (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 216 | let value = try lhs.expression() 217 | guard value! < rhs else { 218 | throw lhs.failure("\(String(describing: value)) is not less than \(rhs)") 219 | } 220 | } 221 | 222 | public func <= (lhs: E, rhs: E.ValueType) throws where E.ValueType: Comparable { 223 | let value = try lhs.expression() 224 | guard value! <= rhs else { 225 | throw lhs.failure("\(String(describing: value)) is not less than or equal to \(rhs)") 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/Failure.swift: -------------------------------------------------------------------------------- 1 | public protocol FailureType : Error { 2 | var function: String { get } 3 | var file: String { get } 4 | var line: Int { get } 5 | 6 | var reason: String { get } 7 | } 8 | 9 | struct Failure : FailureType { 10 | let reason: String 11 | 12 | let function: String 13 | let file: String 14 | let line: Int 15 | 16 | init(reason: String, function: String = #function, file: String = #file, line: Int = #line) { 17 | self.reason = reason 18 | self.function = function 19 | self.file = file 20 | self.line = line 21 | } 22 | } 23 | 24 | struct Skip: Error { 25 | let reason: String? 26 | } 27 | 28 | public func skip(_ reason: String? = nil) -> Error { 29 | return Skip(reason: reason) 30 | } 31 | 32 | 33 | public func failure(_ reason: String? = nil, function: String = #function, file: String = #file, line: Int = #line) -> FailureType { 34 | return Failure(reason: reason ?? "-", function: function, file: file, line: line) 35 | } 36 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/Global.swift: -------------------------------------------------------------------------------- 1 | #if os(Linux) 2 | import Glibc 3 | #else 4 | import Darwin 5 | #endif 6 | 7 | 8 | let globalContext: GlobalContext = { 9 | atexit { run() } 10 | return GlobalContext() 11 | }() 12 | 13 | public func describe(_ name: String, closure: (ContextType) -> Void) { 14 | globalContext.describe(name, closure: closure) 15 | } 16 | 17 | public func it(_ name: String, closure: @escaping () throws -> Void) { 18 | globalContext.it(name, closure: closure) 19 | } 20 | 21 | public func run() -> Never { 22 | let reporter: Reporter 23 | 24 | if CommandLine.arguments.contains("--tap") { 25 | reporter = TapReporter() 26 | } else if CommandLine.arguments.contains("-t") { 27 | reporter = DotReporter() 28 | } else { 29 | reporter = StandardReporter() 30 | } 31 | 32 | run(reporter: reporter) 33 | } 34 | 35 | public func run(reporter: Reporter) -> Never { 36 | if globalContext.run(reporter: reporter) { 37 | exit(0) 38 | } 39 | exit(1) 40 | } 41 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/GlobalContext.swift: -------------------------------------------------------------------------------- 1 | class GlobalContext { 2 | var cases = [CaseType]() 3 | 4 | func describe(_ name: String, closure: (ContextType) -> Void) { 5 | let context = Context(name: name) 6 | closure(context) 7 | cases.append(context) 8 | } 9 | 10 | func it(_ name: String, closure: @escaping () throws -> Void) { 11 | cases.append(Case(name: name, closure: closure)) 12 | } 13 | 14 | func run(reporter: Reporter) -> Bool { 15 | return reporter.report { reporter in 16 | for `case` in cases { 17 | `case`.run(reporter: reporter) 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/Reporter.swift: -------------------------------------------------------------------------------- 1 | public protocol Reporter { 2 | /// Create a new report 3 | func report(closure: (ContextReporter) -> Void) -> Bool 4 | } 5 | 6 | public protocol ContextReporter { 7 | func report(_ name: String, closure: (ContextReporter) -> Void) 8 | 9 | /// Add a passing test case 10 | func addSuccess(_ name: String) 11 | 12 | /// Add a disabled test case 13 | func addDisabled(_ name: String) 14 | 15 | /// Adds a failing test case 16 | func addFailure(_ name: String, failure: FailureType) 17 | } 18 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/Spectre/Reporters.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | #if os(Linux) 3 | import Glibc 4 | #else 5 | import Darwin.C 6 | #endif 7 | 8 | 9 | enum ANSI : String, CustomStringConvertible { 10 | case Red = "\u{001B}[0;31m" 11 | case Green = "\u{001B}[0;32m" 12 | case Yellow = "\u{001B}[0;33m" 13 | 14 | case Bold = "\u{001B}[0;1m" 15 | case Reset = "\u{001B}[0;0m" 16 | 17 | static var supportsANSI: Bool { 18 | guard isatty(STDOUT_FILENO) != 0 else { 19 | return false 20 | } 21 | 22 | guard let termType = getenv("TERM") else { 23 | return false 24 | } 25 | 26 | guard String(cString: termType).lowercased() != "dumb" else { 27 | return false 28 | } 29 | 30 | return true 31 | } 32 | 33 | var description: String { 34 | if ANSI.supportsANSI { 35 | return rawValue 36 | } 37 | 38 | return "" 39 | } 40 | } 41 | 42 | 43 | struct CaseFailure { 44 | let position: [String] 45 | let failure: FailureType 46 | 47 | init(position: [String], failure: FailureType) { 48 | self.position = position 49 | self.failure = failure 50 | } 51 | } 52 | 53 | 54 | fileprivate func stripCurrentDirectory(_ file: String) -> String { 55 | var currentPath = FileManager.`default`.currentDirectoryPath 56 | if !currentPath.hasSuffix("/") { 57 | currentPath += "/" 58 | } 59 | 60 | if file.hasPrefix(currentPath) { 61 | return String(file.suffix(from: currentPath.endIndex)) 62 | } 63 | 64 | return file 65 | } 66 | 67 | 68 | func printFailures(_ failures: [CaseFailure]) { 69 | for failure in failures { 70 | let name = failure.position.joined(separator: " ") 71 | Swift.print(ANSI.Red, name) 72 | 73 | let file = "\(stripCurrentDirectory(failure.failure.file)):\(failure.failure.line)" 74 | Swift.print(" \(ANSI.Bold)\(file)\(ANSI.Reset) \(ANSI.Yellow)\(failure.failure.reason)\(ANSI.Reset)\n") 75 | 76 | if let contents = try? String(contentsOfFile: failure.failure.file, encoding: String.Encoding.utf8) as String { 77 | let lines = contents.components(separatedBy: CharacterSet.newlines) 78 | let line = lines[failure.failure.line - 1] 79 | let trimmedLine = line.trimmingCharacters(in: CharacterSet.whitespaces) 80 | Swift.print(" ```") 81 | Swift.print(" \(trimmedLine)") 82 | Swift.print(" ```") 83 | } 84 | } 85 | } 86 | 87 | 88 | class CountReporter : Reporter, ContextReporter { 89 | var depth = 0 90 | var successes = 0 91 | var disabled = 0 92 | var position = [String]() 93 | var failures = [CaseFailure]() 94 | 95 | func printStatus() { 96 | printFailures(failures) 97 | 98 | let disabledMessage: String 99 | if disabled > 0 { 100 | disabledMessage = " \(disabled) skipped," 101 | } else { 102 | disabledMessage = "" 103 | } 104 | 105 | if failures.count == 1 { 106 | print("\(successes) passes\(disabledMessage) and \(failures.count) failure") 107 | } else { 108 | print("\(successes) passes\(disabledMessage) and \(failures.count) failures") 109 | } 110 | } 111 | 112 | #if swift(>=3.0) 113 | func report(closure: (ContextReporter) -> Void) -> Bool { 114 | closure(self) 115 | printStatus() 116 | return failures.isEmpty 117 | } 118 | #else 119 | func report(@noescape _ closure: (ContextReporter) -> Void) -> Bool { 120 | closure(self) 121 | printStatus() 122 | return failures.isEmpty 123 | } 124 | #endif 125 | 126 | #if swift(>=3.0) 127 | func report(_ name: String, closure: (ContextReporter) -> Void) { 128 | depth += 1 129 | position.append(name) 130 | closure(self) 131 | depth -= 1 132 | position.removeLast() 133 | } 134 | #else 135 | func report(_ name: String, @noescape closure: (ContextReporter) -> Void) { 136 | depth += 1 137 | position.append(name) 138 | closure(self) 139 | depth -= 1 140 | position.removeLast() 141 | } 142 | #endif 143 | 144 | func addSuccess(_ name: String) { 145 | successes += 1 146 | } 147 | 148 | func addDisabled(_ name: String) { 149 | disabled += 1 150 | } 151 | 152 | func addFailure(_ name: String, failure: FailureType) { 153 | failures.append(CaseFailure(position: position + [name], failure: failure)) 154 | } 155 | } 156 | 157 | 158 | /// Standard reporter 159 | class StandardReporter : CountReporter { 160 | override func report(_ name: String, closure: (ContextReporter) -> Void) { 161 | colour(.Bold, "-> \(name)") 162 | super.report(name, closure: closure) 163 | print("") 164 | } 165 | 166 | override func addSuccess(_ name: String) { 167 | super.addSuccess(name) 168 | colour(.Green, "-> \(name)") 169 | } 170 | 171 | override func addDisabled(_ name: String) { 172 | super.addDisabled(name) 173 | colour(.Yellow, "-> \(name)") 174 | } 175 | 176 | override func addFailure(_ name: String, failure: FailureType) { 177 | super.addFailure(name, failure: failure) 178 | colour(.Red, "-> \(name)") 179 | } 180 | 181 | func colour(_ colour: ANSI, _ message: String) { 182 | let indentation = String(repeating: " ", count: depth * 2) 183 | print("\(indentation)\(colour)\(message)\(ANSI.Reset)") 184 | } 185 | } 186 | 187 | 188 | /// Simple reporter that outputs minimal . F and S. 189 | class DotReporter : CountReporter { 190 | override func addSuccess(_ name: String) { 191 | super.addSuccess(name) 192 | print(ANSI.Green, ".", ANSI.Reset, separator: "", terminator: "") 193 | } 194 | 195 | override func addDisabled(_ name: String) { 196 | super.addDisabled(name) 197 | print(ANSI.Yellow, "S", ANSI.Reset, separator: "", terminator: "") 198 | } 199 | 200 | override func addFailure(_ name: String, failure: FailureType) { 201 | super.addFailure(name, failure: failure) 202 | print(ANSI.Red, "F", ANSI.Reset, separator: "", terminator: "") 203 | } 204 | 205 | override func printStatus() { 206 | print("\n") 207 | super.printStatus() 208 | } 209 | } 210 | 211 | 212 | /// Test Anything Protocol compatible reporter 213 | /// http://testanything.org 214 | class TapReporter : CountReporter { 215 | var count = 0 216 | 217 | override func addSuccess(_ name: String) { 218 | count += 1 219 | super.addSuccess(name) 220 | 221 | let message = (position + [name]).joined(separator: " ") 222 | print("ok \(count) - \(message)") 223 | } 224 | 225 | override func addDisabled(_ name: String) { 226 | count += 1 227 | super.addDisabled(name) 228 | 229 | let message = (position + [name]).joined(separator: " ") 230 | print("ok \(count) - # skip \(message)") 231 | } 232 | 233 | override func addFailure(_ name: String, failure: FailureType) { 234 | count += 1 235 | super.addFailure(name, failure: failure) 236 | 237 | let message = (position + [name]).joined(separator: " ") 238 | print("not ok \(count) - \(message)") 239 | print("# \(failure.reason) from \(stripCurrentDirectory(failure.file)):\(failure.line)") 240 | } 241 | 242 | override func printStatus() { 243 | print("\(min(1, count))..\(count)") 244 | } 245 | } 246 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/TestExtensions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ShallowsTests.swift 3 | // Shallows 4 | // 5 | // Created by Oleg Dreyman on {TODAY}. 6 | // Copyright © 2017 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import XCTest 11 | @testable import Shallows 12 | 13 | extension String : Error { } 14 | 15 | extension Storage { 16 | 17 | static func alwaysFailing(with error: Error) -> Storage { 18 | return Storage(read: .alwaysFailing(with: error), 19 | write: .alwaysFailing(with: error)) 20 | } 21 | 22 | static func alwaysSucceeding(with value: Value) -> Storage { 23 | return Storage(read: .alwaysSucceeding(with: value), 24 | write: .alwaysSucceeding()) 25 | } 26 | 27 | } 28 | 29 | extension ReadOnlyStorage { 30 | 31 | static func alwaysFailing(with error: Error) -> ReadOnlyStorage { 32 | return ReadOnlyStorage(storageName: "", retrieve: { _, completion in completion(.failure(error)) }) 33 | } 34 | 35 | static func alwaysSucceeding(with value: Value) -> ReadOnlyStorage { 36 | return ReadOnlyStorage(storageName: "", retrieve: { _, completion in completion(.success(value)) }) 37 | } 38 | 39 | } 40 | 41 | extension WriteOnlyStorage { 42 | 43 | static func alwaysFailing(with error: Error) -> WriteOnlyStorage { 44 | return WriteOnlyStorage.init(storageName: "", set: { _, _, completion in completion(fail(with: error)) }) 45 | } 46 | 47 | static func alwaysSucceeding() -> WriteOnlyStorage { 48 | return WriteOnlyStorage(storageName: "", set: { _, _, completion in completion(succeed(with: ())) }) 49 | } 50 | 51 | } 52 | 53 | class ShallowsTests: XCTestCase { 54 | 55 | override func setUp() { 56 | ShallowsLog.isEnabled = true 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/XCTest.swift: -------------------------------------------------------------------------------- 1 | // 2 | // XCTest.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 18.01.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import XCTest 10 | @testable import Shallows 11 | #if os(iOS) 12 | import UIKit 13 | #endif 14 | 15 | class ShallowsXCTests : XCTestCase { 16 | 17 | func testShallows() { 18 | testMemoryStorage() 19 | testDiskStorage() 20 | testResult() 21 | testZip() 22 | testComposition() 23 | testAdditional() 24 | } 25 | 26 | } 27 | 28 | 29 | 30 | func readme() { 31 | struct City : Codable { 32 | let name: String 33 | let foundationYear: Int 34 | } 35 | 36 | let diskStorage = DiskStorage.main.folder("cities", in: .cachesDirectory) 37 | .mapJSONObject(City.self) 38 | 39 | diskStorage.retrieve(forKey: "Beijing") { (result) in 40 | if let city = result.value { print(city) } 41 | } 42 | 43 | let kharkiv = City(name: "Kharkiv", foundationYear: 1654) 44 | diskStorage.set(kharkiv, forKey: "Kharkiv") 45 | } 46 | 47 | func readme2() { 48 | #if os(iOS) 49 | let storage = DiskStorage.main.folder("images", in: .cachesDirectory) 50 | let images = storage 51 | .mapValues(to: UIImage.self, 52 | transformIn: { data in try UIImage(data: data).unwrap() }, 53 | transformOut: { image in try image.pngData().unwrap() }) 54 | 55 | enum ImageKeys : String { 56 | case kitten, puppy, fish 57 | } 58 | 59 | let keyedImages = images 60 | .usingStringKeys() 61 | .mapKeys(toRawRepresentableType: ImageKeys.self) 62 | 63 | keyedImages.retrieve(forKey: .kitten, completion: { result in /* .. */ }) 64 | #endif 65 | } 66 | 67 | func readme3() { 68 | let immutableFileStorage = DiskStorage.main.folder("immutable", in: .applicationSupportDirectory) 69 | .mapString(withEncoding: .utf8) 70 | .asReadOnlyStorage() 71 | let storage = MemoryStorage() 72 | .backed(by: immutableFileStorage) 73 | .asReadOnlyStorage() // ReadOnlyStorage 74 | print(storage) 75 | } 76 | 77 | func readme4() { 78 | // let settingsStorage = FileSystemStorage.inDirectory(.documentDirectory, appending: "settings") 79 | // .mapJSONDictionary() 80 | // .singleKey("settings") // Storage 81 | // settingsStorage.retrieve { (result) in 82 | // // ... 83 | // } 84 | let settings = DiskStorage.main.folder("settings", in: .applicationSupportDirectory) 85 | .mapJSONDictionary() 86 | .singleKey("settings") // Storage 87 | settings.retrieve { (result) in 88 | // ... 89 | } 90 | } 91 | 92 | func readme5() throws { 93 | let strings = DiskStorage.main.folder("strings", in: .cachesDirectory) 94 | .mapString(withEncoding: .utf8) 95 | .makeSyncStorage() // SyncStorage 96 | let existing = try strings.retrieve(forKey: "hello") 97 | try strings.set(existing.uppercased(), forKey: "hello") 98 | } 99 | 100 | func readme6() { 101 | let arrays = MemoryStorage() 102 | arrays.update(forKey: "some-key", { $0.append(10) }) 103 | } 104 | 105 | func readme7() { 106 | let strings = MemoryStorage() 107 | let numbers = MemoryStorage() 108 | let zipped = zip(strings, numbers) // Storage 109 | zipped.retrieve(forKey: "some-key") { (result) in 110 | if let (string, number) = result.value { 111 | print(string) 112 | print(number) 113 | } 114 | } 115 | zipped.set(("shallows", 3), forKey: "another-key") 116 | } 117 | -------------------------------------------------------------------------------- /Tests/ShallowsTests/ZipSpec.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZipSpec.swift 3 | // Shallows 4 | // 5 | // Created by Олег on 13.02.2018. 6 | // Copyright © 2018 Shallows. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | import Shallows 11 | 12 | func testZip() { 13 | 14 | describe("read-only zipping") { 15 | let readOnly1: ReadOnlyStorage = ReadOnlyStorage.alwaysSucceeding(with: 10) 16 | let readOnly2: ReadOnlyStorage = ReadOnlyStorage.alwaysSucceeding(with: "hooray!") 17 | $0.it("succeeds with both values when present") { 18 | let zipped = zip(readOnly1, readOnly2).makeSyncStorage() 19 | let (int, str) = try zipped.retrieve() 20 | try expect(int) == 10 21 | try expect(str) == "hooray!" 22 | } 23 | $0.it("fails when one of the values is missing") { 24 | let failing: ReadOnlyStorage = .alwaysFailing(with: "test") 25 | let zipped = zip(readOnly1, failing).makeSyncStorage() 26 | try expect(zipped.retrieve()).toThrow() 27 | let zipped2 = zip(failing, readOnly1).makeSyncStorage() 28 | try expect(zipped2.retrieve()).toThrow() 29 | } 30 | $0.it("fails when both values are missing") { 31 | let failing1: ReadOnlyStorage = .alwaysFailing(with: "test-1") 32 | let failing2: ReadOnlyStorage = .alwaysFailing(with: "test-2") 33 | let zipped = zip(failing1, failing2).makeSyncStorage() 34 | try expect(zipped.retrieve()).toThrow() 35 | } 36 | } 37 | 38 | describe("write-only zipping") { 39 | var int = 0 40 | var str = "" 41 | let writeOnly1: WriteOnlyStorage = wos { val, _ in 42 | int = val 43 | } 44 | let writeOnly2: WriteOnlyStorage = wos { val, _ in 45 | str = val 46 | } 47 | $0.after { 48 | int = 0 49 | str = "" 50 | } 51 | $0.it("succeeds with both values when present") { 52 | let zipped = zip(writeOnly1, writeOnly2).makeSyncStorage() 53 | try zipped.set((5, "5")) 54 | try expect(int) == 5 55 | try expect(str) == "5" 56 | } 57 | $0.it("fails when one of the storages fails") { 58 | let failing: WriteOnlyStorage = .alwaysFailing(with: "test") 59 | let zipped = zip(writeOnly1, failing).makeSyncStorage() 60 | try expect(zipped.set((6, "1"))).toThrow() 61 | try expect(int) == 6 62 | let zipped2 = zip(failing, writeOnly1).makeSyncStorage() 63 | try expect(zipped2.set(("10", 10))).toThrow() 64 | } 65 | $0.it("fails when both storages are missing") { 66 | let failing1: WriteOnlyStorage = .alwaysFailing(with: "test-1") 67 | let failing2: WriteOnlyStorage = .alwaysFailing(with: "test-2") 68 | let zipped = zip(failing1, failing2).makeSyncStorage() 69 | try expect(zipped.set((7, 7))).toThrow() 70 | } 71 | } 72 | 73 | describe("zipping") { 74 | $0.it("works") { 75 | let storage1 = Storage.alwaysSucceeding(with: 10) 76 | let storage2 = Storage.alwaysSucceeding(with: "storage") 77 | let zipped = zip(storage1, storage2).makeSyncStorage() 78 | let (int, str) = try zipped.retrieve() 79 | try expect(int) == 10 80 | try expect(str) == "storage" 81 | try zipped.set((5, "100")) 82 | } 83 | } 84 | 85 | } 86 | 87 | internal func wos(_ set: @escaping (Value, Key) throws -> ()) -> WriteOnlyStorage { 88 | return WriteOnlyStorage(storageName: "wos", set: { (value, key, completion) in 89 | do { 90 | try set(value, key) 91 | completion(succeed(with: ())) 92 | } catch { 93 | completion(fail(with: error)) 94 | } 95 | }) 96 | } 97 | --------------------------------------------------------------------------------