Update LocalData.swift + docs

Summary:
- Sources: LocalData.swift
- Docs: Proposal, README
- Added symbols: extension StorageKey, typealias Value
- Removed symbols: extension StorageKeys, struct UserTokenKey, typealias Value, struct ProfileKey, struct ModernKey, typealias DestinationKey (+1 more)

Stats:
- 3 files changed, 61 insertions(+), 50 deletions(-)
This commit is contained in:
Matt Bruce 2026-01-16 22:12:57 -06:00
parent 1a37206c12
commit 51faaf4937
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 ## Architecture
### Core Components ### Core Components
- **StorageKey** protocol - Defines storage configuration for each data type - **StorageKey** (generic struct) - Defines storage configuration for each data type
- **StorageKey.migration** - Optional protocol-based migration attached to a key - **StorageKey.migration** - Optional migration attached to a key
- **StorageRouter** actor - Main entry point coordinating all storage operations - **StorageRouter** actor - Main entry point coordinating all storage operations
- **StorageProviding** protocol - Abstraction for storage operations - **StorageProviding** protocol - Abstraction for storage operations
- **StorageKeyCatalog** protocol - Catalog of keys for auditing/validation - **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 - **DeviceInfo / SystemInfo** - Device and system metrics used by migrations
## Usage Pattern ## 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 ## 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: Apps can register multiple `StorageKeyCatalog`s (e.g., one per module) to generate audit reports and enforce key registration. Registration is additive and validates:

View File

@ -72,7 +72,6 @@ flowchart TD
## What Ships in the Package ## What Ships in the Package
### Protocols ### Protocols
- **StorageKey** - Define storage configuration for each data type
- **StorageProviding** - Abstraction for storage operations - **StorageProviding** - Abstraction for storage operations
- **KeyMaterialProviding** - Supplies external key material for encryption - **KeyMaterialProviding** - Supplies external key material for encryption
- **StorageMigration** - Protocol-based migration workflows - **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.) - **KeychainAccessControl** - All 6 access control options (biometry, passcode, etc.)
- **FileDirectory** - documents, caches, custom URL - **FileDirectory** - documents, caches, custom URL
- **StorageError** - Comprehensive error types - **StorageError** - Comprehensive error types
- **StorageKey** - Typed storage configuration (generic over Value)
- **StorageKeyDescriptor** - Audit snapshot of a keys storage metadata - **StorageKeyDescriptor** - Audit snapshot of a keys storage metadata
- **AnyStorageKey** - Type-erased storage key for catalogs - **AnyStorageKey** - Type-erased storage key for catalogs
- **AnyCodable** - Type-erased Codable for mixed-type payloads - **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 ## Usage
### 1. Define Keys ### 1. Define Keys
Extend `StorageKeys` with your own key types: Extend `StorageKey` with typed static keys:
```swift ```swift
import LocalData import LocalData
extension StorageKeys { extension StorageKey where Value == String {
struct UserTokenKey: StorageKey { static let userToken = StorageKey(
typealias Value = String name: "user_token",
domain: .keychain(service: "com.myapp"),
let name = "user_token" security: .keychain(
let domain: StorageDomain = .keychain(service: "com.myapp")
let security: SecurityPolicy = .keychain(
accessibility: .afterFirstUnlock, accessibility: .afterFirstUnlock,
accessControl: .biometryAny accessControl: .biometryAny
),
serializer: .json,
owner: "AuthService",
description: "Stores the current user auth token.",
availability: .phoneOnly,
syncPolicy: .never
) )
let serializer: Serializer<String> = .json
let owner = "AuthService"
let description = "Stores the current user auth token."
let availability: PlatformAvailability = .phoneOnly
let syncPolicy: SyncPolicy = .never
}
} }
``` ```
If you omit `security`, it defaults to `SecurityPolicy.recommended`. 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 ### 2. Use StorageRouter
```swift ```swift
// Save // Save
let key = StorageKeys.UserTokenKey() let key = StorageKey.userToken
try await StorageRouter.shared.set("token123", for: key) try await StorageRouter.shared.set("token123", for: key)
// Retrieve // Retrieve
@ -174,13 +172,13 @@ struct UserProfile: Codable {
let settings: [String: String] let settings: [String: String]
} }
extension StorageKeys { extension StorageKey where Value == UserProfile {
struct ProfileKey: StorageKey { static let profile = StorageKey(
typealias Value = UserProfile // Library handles serialization name: "user_profile",
let name = "user_profile" domain: .fileSystem(directory: .documents),
let domain: StorageDomain = .fileSystem(directory: .documents) owner: "ProfileService",
// ... other properties description: "Stores the current user profile."
} )
} }
// ... other properties // ... 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. - It is returned to the caller.
```swift ```swift
extension StorageKeys { extension StorageKey where Value == String {
struct ModernKey: StorageKey { static let legacyToken = StorageKey(
typealias Value = String name: "legacy_token",
// ... other properties domain: .userDefaults(suite: nil),
var migration: AnyStorageMigration? { 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( AnyStorageMigration(
SimpleLegacyMigration( SimpleLegacyMigration(
destinationKey: self, destinationKey: key,
sourceKey: .key(LegacyKey()) sourceKey: .key(StorageKey.legacyToken)
) )
) )
} }
} )
} }
``` ```
@ -220,10 +229,10 @@ For complex migrations, implement `StorageMigration` and attach it to the key.
```swift ```swift
struct TokenMigration: StorageMigration { struct TokenMigration: StorageMigration {
typealias DestinationKey = StorageKeys.UserTokenKey typealias Value = String
let destinationKey = StorageKeys.UserTokenKey() let destinationKey = StorageKey.userToken
let legacyKey = StorageKeys.LegacyTokenKey() let legacyKey = StorageKey.legacyToken
func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool {
try await router.exists(legacyKey) try await router.exists(legacyKey)
@ -237,10 +246,14 @@ struct TokenMigration: StorageMigration {
} }
} }
extension StorageKeys.UserTokenKey { extension StorageKey where Value == String {
var migration: AnyStorageMigration? { static let userToken = StorageKey(
AnyStorageMigration(TokenMigration()) 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 #### Manual Call
```swift ```swift
try await StorageRouter.shared.forceMigration(for: StorageKeys.ModernKey()) try await StorageRouter.shared.forceMigration(for: StorageKey.modernToken)
``` ```
#### Automated Startup Sweep #### Automated Startup Sweep
@ -268,7 +281,7 @@ try await StorageRouter.shared.registerCatalog(ProfileCatalog())
## Storage Design Philosophy ## 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? ### Why types instead of strings?
1. **Safety**: The compiler prevents typos. You can't accidentally load from `"user_name"` and save to `"username"`. 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`? ### 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: 1) Define a catalog in your app that lists all keys:
```swift ```swift
struct AppStorageCatalog: StorageKeyCatalog { struct AppStorageCatalog: StorageKeyCatalog {
let allKeys: [AnyStorageKey] = [ let allKeys: [AnyStorageKey] = [
.key(StorageKeys.AppVersionKey()), .key(StorageKey.appVersion),
.key(StorageKeys.UserPreferencesKey()) .key(StorageKey.userPreferences)
] ]
} }
``` ```

View File

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