Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
5ed222e423
commit
046b0d007d
@ -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:
|
||||||
|
|||||||
101
README.md
101
README.md
@ -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 key’s storage metadata
|
- **StorageKeyDescriptor** - Audit snapshot of a key’s 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
|
||||||
)
|
),
|
||||||
let serializer: Serializer<String> = .json
|
serializer: .json,
|
||||||
let owner = "AuthService"
|
owner: "AuthService",
|
||||||
let description = "Stores the current user auth token."
|
description: "Stores the current user auth token.",
|
||||||
let availability: PlatformAvailability = .phoneOnly
|
availability: .phoneOnly,
|
||||||
let syncPolicy: SyncPolicy = .never
|
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)
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user