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:
parent
1a37206c12
commit
51faaf4937
@ -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:
|
||||
|
||||
99
README.md
99
README.md
@ -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 key’s 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
|
||||
),
|
||||
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`.
|
||||
@ -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)
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
// The Swift Programming Language
|
||||
// https://docs.swift.org/swift-book
|
||||
public enum StorageKeys {
|
||||
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user