{"id":1006,"url":"https://github.com/dreymonde/Shallows","last_synced_at":"2025-08-06T13:32:08.089Z","repository":{"id":62455285,"uuid":"89173939","full_name":"dreymonde/Shallows","owner":"dreymonde","description":"🛶 Your lightweight persistence toolbox","archived":false,"fork":false,"pushed_at":"2022-05-01T13:02:05.000Z","size":216,"stargazers_count":623,"open_issues_count":1,"forks_count":20,"subscribers_count":13,"default_branch":"master","last_synced_at":"2024-12-05T20:02:48.610Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"https://medium.com/anysuggestion","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dreymonde.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2017-04-23T21:34:15.000Z","updated_at":"2024-09-13T01:47:51.000Z","dependencies_parsed_at":"2022-11-02T00:01:06.773Z","dependency_job_id":null,"html_url":"https://github.com/dreymonde/Shallows","commit_stats":null,"previous_names":[],"tags_count":38,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreymonde%2FShallows","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreymonde%2FShallows/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreymonde%2FShallows/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dreymonde%2FShallows/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dreymonde","download_url":"https://codeload.github.com/dreymonde/Shallows/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":228905450,"owners_count":17989770,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-01-05T20:15:36.816Z","updated_at":"2024-12-09T14:30:46.784Z","avatar_url":"https://github.com/dreymonde.png","language":"Swift","funding_links":[],"categories":["Database","Libs","Swift","HarmonyOS","Data and Storage","AppPersistence","Data Management [🔝](#readme)"],"sub_categories":["Getting Started","Data Management","Other free courses","Linter","Windows Manager"],"readme":"# Shallows\n\n[![](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)\n\n**Shallows** is a generic abstraction layer over lightweight data storage and persistence. It provides a `Storage\u003cKey, Value\u003e` 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.\n\n**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].\n\n**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.\n\n## Usage\n\n```swift\nstruct City : Codable {\n    let name: String\n    let foundationYear: Int\n}\n\nlet diskStorage = DiskStorage.main.folder(\"cities\", in: .cachesDirectory)\n    .mapJSONObject(City.self) // Storage\u003cFilename, City\u003e\n\nlet kharkiv = City(name: \"Kharkiv\", foundationYear: 1654)\ndiskStorage.set(kharkiv, forKey: \"kharkiv\")\n\ndiskStorage.retrieve(forKey: \"kharkiv\") { (result) in\n    if let city = try? result.get() { print(city) }\n}\n\n// or\n\nlet city = try await diskStorage.retrieve(forKey: \"kharkiv\")\n\n```\n\n## Guide\n\nA main type of **Shallows** is `Storage\u003cKey, Value\u003e`. 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`:\n\n```swift\nlet storage = MemoryStorage\u003cString, Int\u003e().asStorage() // Storage\u003cString, Int\u003e\n```\n\nStorage instances have `retrieve` and `set` methods, which are asynhronous and fallible:\n\n```swift\nstorage.retrieve(forKey: \"some-key\") { (result) in\n    switch result {\n    case .success(let value):\n        print(value)\n    case .failure(let error):\n        print(error)\n    }\n}\nstorage.set(10, forKey: \"some-key\") { (result) in\n    switch result {\n    case .success:\n        print(\"Value set!\")\n    case .failure(let error):\n        print(error)\n    }\n}\n```\n\n### Transforms\n\nKeys and values can be mapped:\n\n```swift\nlet storage = DiskStorage.main.folder(\"images\", in: .cachesDirectory) // Storage\u003cFilename, Data\u003e\nlet images = storage\n    .mapValues(to: UIImage.self,\n               transformIn: { data in try UIImage.init(data: data).unwrap() },\n               transformOut: { image in try UIImagePNGRepresentation(image).unwrap() }) // Storage\u003cFilename, UIImage\u003e\n\nenum ImageKeys : String {\n    case kitten, puppy, fish\n}\n\nlet keyedImages = images\n    .usingStringKeys()\n    .mapKeys(toRawRepresentableType: ImageKeys.self) // Storage\u003cImageKeys, UIImage\u003e\n\nkeyedImages.retrieve(forKey: .kitten, completion: { result in /* .. */ })\n```\n\n**NOTE:** There are several convenience methods defined on `Storage` with value of `Data`: `.mapString(withEncoding:)`, `.mapJSON()`, `.mapJSONDictionary()`, `.mapJSONObject(_:)` `.mapPlist(format:)`, `.mapPlistDictionary(format:)`, `.mapPlistObject(_:)`.\n\n### Storages composition\n\nAnother 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`:\n\n```swift\nlet efficient = MemoryStorage\u003cFilename, UIImage\u003e().combined(with: imageStorage)\n```\n\nIt does several things:\n\n1. 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.\n2. If disk storage stores a value, it will be pulled to memory storage and returned to a user.\n3. When setting an image, it will be set both to memory and disk storage.\n\n### Read-only storage\n\nIf you don't want to expose writing to your storage, you can make it a read-only storage:\n\n```swift\nlet readOnly = storage.asReadOnlyStorage() // ReadOnlyStorage\u003cKey, Value\u003e\n```\n\nRead-only storages can also be mapped and composed:\n\n```swift\nlet immutableFileStorage = DiskStorage.main.folder(\"immutable\", in: .applicationSupportDirectory)\n    .mapString(withEncoding: .utf8)\n    .asReadOnlyStorage()\nlet storage = MemoryStorage\u003cFilename, String\u003e()\n    .backed(by: immutableFileStorage)\n    .asReadOnlyStorage() // ReadOnlyStorage\u003cFilename, String\u003e\n```\n\n### Write-only storage\n\nIn similar way, write-only storage is also available:\n\n```swift\nlet writeOnly = storage.asWriteOnlyStorage() // WriteOnlyStorage\u003cKey, Value\u003e\n```\n\n### Different ways of composition\n\n**Compositions available for `Storage`**:\n\n- `.combined(with:)` (see [Storages composition](#Storages-composition))\n- `.backed(by:)` will work the same as `combined(with:)`, but it will not push the value to the back storage\n- `.pushing(to:)` will not retrieve the value from the back storage, but will push to it on `set`\n\n**Compositions available for `ReadOnlyStorage`**:\n\n- `.backed(by:)`\n\n**Compositions available for `WriteOnlyStorage`**:\n\n- `.pushing(to:)`\n\n### Single element storage\n\nYou 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:\n\n```swift\nlet settings = DiskStorage.main.folder(\"settings\", in: .applicationSupportDirectory)\n    .mapJSONDictionary()\n    .singleKey(\"settings\") // Storage\u003cVoid, [String : Any]\u003e\nsettings.retrieve { (result) in\n    // ...\n}\n```\n\n### Synchronous storage\n\nStorages 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:\n\n```swift\nlet strings = DiskStorage.main.folder(\"strings\", in: .cachesDirectory)\n    .mapString(withEncoding: .utf8)\n    .makeSyncStorage() // SyncStorage\u003cFilename, String\u003e\nlet existing = try strings.retrieve(forKey: \"hello\")\ntry strings.set(existing.uppercased(), forKey: \"hello\")\n```\n\n### Mutating value for key\n\n**Shallows** provides a convenient `.update` method on storages:\n\n```swift\nlet arrays = MemoryStorage\u003cString, [Int]\u003e()\narrays.update(forKey: \"some-key\", { $0.append(10) })\n```\n\n### Zipping storages\n\nZipping 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:\n\n```swift\nlet strings = MemoryStorage\u003cString, String\u003e()\nlet numbers = MemoryStorage\u003cString, Int\u003e()\nlet zipped = zip(strings, numbers) // Storage\u003cString, (String, Int)\u003e\nzipped.retrieve(forKey: \"some-key\") { (result) in\n    if let (string, number) = try? result.get() {\n        print(string)\n        print(number)\n    }\n}\nzipped.set((\"shallows\", 3), forKey: \"another-key\")\n```\n\nIsn't it nice?\n\n### Recovering from errors\n\nYou can protect your storage instance from failures using `fallback(with:)` or `defaulting(to:)` methods:\n\n```swift\nlet storage = MemoryStorage\u003cString, Int\u003e()\nlet protected = storage.fallback(with: { error in\n    switch error {\n    case MemoryStorageError.noValue:\n        return 15\n    default:\n        return -1\n    }\n})\n```\n\n```swift\nlet storage = MemoryStorage\u003cString, Int\u003e()\nlet defaulted = storage.defaulting(to: -1)\n```\n\nThis is _especially_ useful when using `update` method:\n\n```swift\nlet storage = MemoryStorage\u003cString, [Int]\u003e()\nstorage.defaulting(to: []).update(forKey: \"first\", { $0.append(10) })\n```\n\nThat means that in case of failure retrieving existing value, `update` will use default value of `[]` instead of just failing the whole update.\n\n### Using `NSCacheStorage`\n\n`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:\n\n```swift\nlet nscache = NSCacheStorage\u003cNSURL, NSData\u003e()\n    .toNonObjCKeys()\n    .toNonObjCValues() // Storage\u003cURL, Data\u003e\n```\n\n### Making your own storage\n\nTo create your own caching layer, you should conform to `StorageProtocol`. That means that you should define these two methods:\n\n```swift\nfunc retrieve(forKey key: Key, completion: @escaping (Result\u003cValue, Error\u003e) -\u003e ())\nfunc set(_ value: Value, forKey key: Key, completion: @escaping (Result\u003cVoid, Error\u003e) -\u003e ())\n```\n\nWhere `Key` and `Value` are associated types.\n\n**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.\n\nTo use it as `Storage\u003cKey, Value\u003e` instance, simply call `.asStorage()` on it:\n\n```swift\nlet storage = MyStorage().asStorage()\n```\n\nYou can also conform to a `ReadOnlyStorageProtocol` only. That way, you only need to define a `retrieve(forKey:completion:)` method.\n\n## Installation\n\n#### Swift Package Manager\n\nStarting with Xcode 11, **Shallows** is officially available *only* via Swift Package Manager.\n\nIn Xcode 11 or greater, in you project, select: `File \u003e Swift Packages \u003e Add Package Dependency`\n\nIn the search bar type\n\n```\nhttps://github.com/dreymonde/Shallows\n``` \n\nThen proceed with installation.\n\n\u003e If you can't find anything in the panel of the Swift Packages you probably haven't added yet your github account.\nYou can do that under the **Preferences** panel of your Xcode, in the **Accounts** section.\n\nFor command-line based apps, you can just add this directly to your **Package.swift** file:\n\n```swift\ndependencies: [\n    .package(url: \"https://github.com/dreymonde/Shallows\", from: \"0.13.0\"),\n]\n```\n\n#### Manual\n\nOf course, you always have an option of just copying-and-pasting the code.\n\n#### Deprecated dependency managers\n\nLast **Shallows** version to support [Carthage][carthage-url] and [Cocoapods][cocoapods-url] is **0.10.0**. Carthage and Cocoapods will no longer be officially supported.\n\nCarthage:\n\n```ruby\ngithub \"dreymonde/Shallows\" ~\u003e 0.10.0\n```\n\nCocoapods:\n\n```ruby\npod 'Shallows', '~\u003e 0.10.0'\n```\n\n[carthage-url]: https://github.com/Carthage/Carthage\n[swift-badge]: https://img.shields.io/badge/Swift-5.1-orange.svg?style=flat\n[swift-url]: https://swift.org\n[platform-badge]: https://img.shields.io/badge/platform-iOS%20%7C%20macOS%20%7C%20watchOS%20%7C%20tvOS-lightgrey.svg\n[platform-url]: https://developer.apple.com/swift/\n[carlos-github-url]: https://github.com/WeltN24/Carlos\n[composable-caches-in-swift-url]: https://www.youtube.com/watch?v=8uqXuEZLyUU\n[brandon-kase-twitter-url]: https://twitter.com/bkase_\n[avenues-github-url]: https://github.com/dreymonde/Avenues\n[avenues-shallows-github-url]: https://github.com/dreymonde/Avenues-Shallows\n[cocoapods-url]: https://github.com/CocoaPods/CocoaPods\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdreymonde%2FShallows","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdreymonde%2FShallows","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdreymonde%2FShallows/lists"}