Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-16 22:12:57 -06:00
parent 5ed222e423
commit 046b0d007d
3 changed files with 61 additions and 50 deletions

View File

@ -17,8 +17,8 @@ Create a single, typed, discoverable namespace for persisted app data with consi
## Architecture
### Core Components
- **StorageKey** protocol - Defines storage configuration for each data type
- **StorageKey.migration** - Optional protocol-based migration attached to a key
- **StorageKey** (generic struct) - Defines storage configuration for each data type
- **StorageKey.migration** - Optional migration attached to a key
- **StorageRouter** actor - Main entry point coordinating all storage operations
- **StorageProviding** protocol - Abstraction for storage operations
- **StorageKeyCatalog** protocol - Catalog of keys for auditing/validation
@ -54,7 +54,7 @@ Each helper is a dedicated actor providing thread-safe access to a specific stor
- **DeviceInfo / SystemInfo** - Device and system metrics used by migrations
## Usage Pattern
Apps extend StorageKeys with their own key types and use StorageRouter.shared. This follows the Notification.Name pattern for discoverable keys.
Apps extend `StorageKey` with typed static keys (e.g., `extension StorageKey where Value == String`) and use `StorageRouter.shared`. This follows the Notification.Name pattern for discoverable keys while preserving value-type inference.
## Audit & Validation
Apps can register multiple `StorageKeyCatalog`s (e.g., one per module) to generate audit reports and enforce key registration. Registration is additive and validates:

101
README.md
View File

@ -72,7 +72,6 @@ flowchart TD
## What Ships in the Package
### Protocols
- **StorageKey** - Define storage configuration for each data type
- **StorageProviding** - Abstraction for storage operations
- **KeyMaterialProviding** - Supplies external key material for encryption
- **StorageMigration** - Protocol-based migration workflows
@ -110,6 +109,7 @@ These are used at app lifecycle start to tune library engine behaviors:
- **KeychainAccessControl** - All 6 access control options (biometry, passcode, etc.)
- **FileDirectory** - documents, caches, custom URL
- **StorageError** - Comprehensive error types
- **StorageKey** - Typed storage configuration (generic over Value)
- **StorageKeyDescriptor** - Audit snapshot of a keys storage metadata
- **AnyStorageKey** - Type-erased storage key for catalogs
- **AnyCodable** - Type-erased Codable for mixed-type payloads
@ -126,27 +126,25 @@ These are used at app lifecycle start to tune library engine behaviors:
## Usage
### 1. Define Keys
Extend `StorageKeys` with your own key types:
Extend `StorageKey` with typed static keys:
```swift
import LocalData
extension StorageKeys {
struct UserTokenKey: StorageKey {
typealias Value = String
let name = "user_token"
let domain: StorageDomain = .keychain(service: "com.myapp")
let security: SecurityPolicy = .keychain(
extension StorageKey where Value == String {
static let userToken = StorageKey(
name: "user_token",
domain: .keychain(service: "com.myapp"),
security: .keychain(
accessibility: .afterFirstUnlock,
accessControl: .biometryAny
)
let serializer: Serializer<String> = .json
let owner = "AuthService"
let description = "Stores the current user auth token."
let availability: PlatformAvailability = .phoneOnly
let syncPolicy: SyncPolicy = .never
}
),
serializer: .json,
owner: "AuthService",
description: "Stores the current user auth token.",
availability: .phoneOnly,
syncPolicy: .never
)
}
```
If you omit `security`, it defaults to `SecurityPolicy.recommended`.
@ -154,7 +152,7 @@ If you omit `security`, it defaults to `SecurityPolicy.recommended`.
### 2. Use StorageRouter
```swift
// Save
let key = StorageKeys.UserTokenKey()
let key = StorageKey.userToken
try await StorageRouter.shared.set("token123", for: key)
// Retrieve
@ -174,13 +172,13 @@ struct UserProfile: Codable {
let settings: [String: String]
}
extension StorageKeys {
struct ProfileKey: StorageKey {
typealias Value = UserProfile // Library handles serialization
let name = "user_profile"
let domain: StorageDomain = .fileSystem(directory: .documents)
// ... other properties
}
extension StorageKey where Value == UserProfile {
static let profile = StorageKey(
name: "user_profile",
domain: .fileSystem(directory: .documents),
owner: "ProfileService",
description: "Stores the current user profile."
)
}
// ... other properties
@ -199,19 +197,30 @@ When you define a `migration` on a key, `StorageRouter.get(key)` will automatica
- It is returned to the caller.
```swift
extension StorageKeys {
struct ModernKey: StorageKey {
typealias Value = String
// ... other properties
var migration: AnyStorageMigration? {
extension StorageKey where Value == String {
static let legacyToken = StorageKey(
name: "legacy_token",
domain: .userDefaults(suite: nil),
security: .none,
serializer: .json,
owner: "AuthService",
description: "Legacy token stored in UserDefaults."
)
static let modernToken = StorageKey(
name: "modern_token",
domain: .keychain(service: "com.myapp"),
owner: "AuthService",
description: "Stores the current user auth token.",
migration: { key in
AnyStorageMigration(
SimpleLegacyMigration(
destinationKey: self,
sourceKey: .key(LegacyKey())
destinationKey: key,
sourceKey: .key(StorageKey.legacyToken)
)
)
}
}
)
}
```
@ -220,10 +229,10 @@ For complex migrations, implement `StorageMigration` and attach it to the key.
```swift
struct TokenMigration: StorageMigration {
typealias DestinationKey = StorageKeys.UserTokenKey
typealias Value = String
let destinationKey = StorageKeys.UserTokenKey()
let legacyKey = StorageKeys.LegacyTokenKey()
let destinationKey = StorageKey.userToken
let legacyKey = StorageKey.legacyToken
func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool {
try await router.exists(legacyKey)
@ -237,10 +246,14 @@ struct TokenMigration: StorageMigration {
}
}
extension StorageKeys.UserTokenKey {
var migration: AnyStorageMigration? {
AnyStorageMigration(TokenMigration())
}
extension StorageKey where Value == String {
static let userToken = StorageKey(
name: "user_token",
domain: .keychain(service: "com.myapp"),
owner: "AuthService",
description: "Stores the current user auth token.",
migration: { _ in AnyStorageMigration(TokenMigration()) }
)
}
```
@ -249,7 +262,7 @@ To ensure no "ghost data" remains in legacy keys (e.g., if a bug causes old code
#### Manual Call
```swift
try await StorageRouter.shared.forceMigration(for: StorageKeys.ModernKey())
try await StorageRouter.shared.forceMigration(for: StorageKey.modernToken)
```
#### Automated Startup Sweep
@ -268,7 +281,7 @@ try await StorageRouter.shared.registerCatalog(ProfileCatalog())
## Storage Design Philosophy
This app intentionally uses a **Type-Safe Storage Design**. Unlike standard iOS development which uses string keys (e.g., `UserDefaults.standard.string(forKey: "user_name")`), this library requires you to define a `StorageKey` type.
This app intentionally uses a **Type-Safe Storage Design**. Unlike standard iOS development which uses string keys (e.g., `UserDefaults.standard.string(forKey: "user_name")`), this library requires you to define `StorageKey` values.
### Why types instead of strings?
1. **Safety**: The compiler prevents typos. You can't accidentally load from `"user_name"` and save to `"username"`.
@ -453,15 +466,15 @@ LocalData can generate a catalog of all configured storage keys, even if no data
### Why `AnyStorageKey`?
`StorageKey` has an associated type (`Value`), which means you cannot store different keys in a single array using `[StorageKey]` or `some StorageKey`. Swift requires type erasure for heterogeneous protocol values, so the catalog uses `[AnyStorageKey]` and builds descriptors behind the scenes.
`StorageKey` is generic over `Value`, which means you cannot store different key value types in a single array using `[StorageKey]`. Swift requires type erasure for heterogeneous key values, so the catalog uses `[AnyStorageKey]` and builds descriptors behind the scenes.
1) Define a catalog in your app that lists all keys:
```swift
struct AppStorageCatalog: StorageKeyCatalog {
let allKeys: [AnyStorageKey] = [
.key(StorageKeys.AppVersionKey()),
.key(StorageKeys.UserPreferencesKey())
.key(StorageKey.appVersion),
.key(StorageKey.userPreferences)
]
}
```

View File

@ -1,5 +1,3 @@
// The Swift Programming Language
// https://docs.swift.org/swift-book
public enum StorageKeys {
}