From 5e44eca5725adf1eb8f661f17d5f77d87ba31a5f Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Fri, 16 Jan 2026 22:12:57 -0600 Subject: [PATCH] Update LocalData.swift and docs Summary: - Sources: update LocalData.swift - Docs: update docs for Proposal, README Stats: - 3 files changed, 61 insertions(+), 50 deletions(-) --- Proposal.md | 6 +- README.md | 101 +++++++++++++++++------------- Sources/LocalData/LocalData.swift | 4 +- 3 files changed, 61 insertions(+), 50 deletions(-) diff --git a/Proposal.md b/Proposal.md index 081ec70..a06e865 100644 --- a/Proposal.md +++ b/Proposal.md @@ -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: diff --git a/README.md b/README.md index 7b364d4..3de2b13 100644 --- a/README.md +++ b/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 - ) - let serializer: Serializer = .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) ] } ``` diff --git a/Sources/LocalData/LocalData.swift b/Sources/LocalData/LocalData.swift index 93173e5..28415ef 100644 --- a/Sources/LocalData/LocalData.swift +++ b/Sources/LocalData/LocalData.swift @@ -1,5 +1,3 @@ // The Swift Programming Language // https://docs.swift.org/swift-book -public enum StorageKeys { - -} +