diff --git a/Documentation/Design.md b/Documentation/Design.md index 1613ac9..5d7a4a3 100644 --- a/Documentation/Design.md +++ b/Documentation/Design.md @@ -9,7 +9,7 @@ The central `actor` that coordinates all storage operations. It acts as the primary API surface and handles routing, catalog validation, and migration. ### StorageKey -A protocol that defines the metadata for a single piece of persistent data. +A generic struct that defines the metadata for a single piece of persistent data. - **Value**: The type of the data (Codable). - **Domain**: Where the data is stored (UserDefaults, Keychain, FileSystem, etc.). - **Security**: How the data is secured (None, Keychain-native, or custom Encryption). diff --git a/Documentation/Migration.md b/Documentation/Migration.md index bbee2be..33a2dfa 100644 --- a/Documentation/Migration.md +++ b/Documentation/Migration.md @@ -1,7 +1,7 @@ # LocalData Migration Guide ## Overview -`LocalData` provides protocol-based migration support to move data from legacy storage locations to modern `StorageKey` definitions. +`LocalData` provides protocol-based migration support to move data from legacy storage locations to modern `StorageKey` values. ## Automatic Migration When calling `get(_:)` on a key, the `StorageRouter` automatically: @@ -25,16 +25,30 @@ This iterates through all keys in the catalog and calls `forceMigration(for:)` o ### Simple Legacy Migration For 1:1 migrations, attach a `SimpleLegacyMigration`: ```swift -struct MyNewKey: StorageKey { - // ... - var migration: AnyStorageMigration? { - AnyStorageMigration( - SimpleLegacyMigration( - destinationKey: self, - sourceKey: .key(LegacyKey(name: "old_key_name", domain: .userDefaults(suite: nil))) +extension StorageKey where Value == String { + static let legacyToken = StorageKey( + name: "old_key_name", + domain: .userDefaults(suite: nil), + security: .none, + serializer: .json, + owner: "MigrationDemo", + description: "Legacy token stored in UserDefaults." + ) + + static let modernToken = StorageKey( + name: "modern_token", + domain: .keychain(service: "com.myapp"), + owner: "MigrationDemo", + description: "Modern token stored in Keychain.", + migration: { key in + AnyStorageMigration( + SimpleLegacyMigration( + destinationKey: key, + sourceKey: .key(StorageKey.legacyToken) + ) ) - ) - } + } + ) } ``` @@ -42,9 +56,9 @@ struct MyNewKey: StorageKey { For complex scenarios, attach an explicit migration: ```swift struct MyMigration: StorageMigration { - typealias DestinationKey = MyNewKey + typealias Value = String - let destinationKey = MyNewKey() + let destinationKey = StorageKey.modernToken func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { try await router.exists(destinationKey) @@ -56,9 +70,13 @@ struct MyMigration: StorageMigration { } } -extension MyNewKey { - var migration: AnyStorageMigration? { - AnyStorageMigration(MyMigration()) - } +extension StorageKey where Value == String { + static let modernToken = StorageKey( + name: "modern_token", + domain: .keychain(service: "com.myapp"), + owner: "MigrationDemo", + description: "Modern token stored in Keychain.", + migration: { _ in AnyStorageMigration(MyMigration()) } + ) } ``` diff --git a/Documentation/Migration_Refactor_Plan_Clean.md b/Documentation/Migration_Refactor_Plan_Clean.md index 9e314a7..50566a1 100644 --- a/Documentation/Migration_Refactor_Plan_Clean.md +++ b/Documentation/Migration_Refactor_Plan_Clean.md @@ -61,10 +61,10 @@ Follow AGENTS.md Clean Architecture and File Organization Principles for all new ```swift /// Core migration protocol with high-level methods public protocol StorageMigration: Sendable { - associatedtype DestinationKey: StorageKey + associatedtype Value: Codable & Sendable /// The destination storage key where migrated data will be stored - var destinationKey: DestinationKey { get } + var destinationKey: StorageKey { get } /// Validate if migration should proceed (conditional logic) func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool @@ -179,27 +179,47 @@ public struct MigrationContext: Sendable { } ``` -### 1.2 Update StorageKey Protocol +### 1.2 Update StorageKey Type -#### Modify `Sources/LocalData/Protocols/StorageKey.swift` +#### Modify `Sources/LocalData/Models/StorageKey.swift` ```swift -public protocol StorageKey: Sendable, CustomStringConvertible { - associatedtype Value: Codable & Sendable +public struct StorageKey: Sendable, CustomStringConvertible { + public let name: String + public let domain: StorageDomain + public let security: SecurityPolicy + public let serializer: Serializer + public let owner: String + public let description: String + public let availability: PlatformAvailability + public let syncPolicy: SyncPolicy - var name: String { get } - var domain: StorageDomain { get } - var security: SecurityPolicy { get } - var serializer: Serializer { get } - var owner: String { get } - var availability: PlatformAvailability { get } - var syncPolicy: SyncPolicy { get } + private let migrationBuilder: (@Sendable (StorageKey) -> AnyStorageMigration?)? - /// Optional migration for simple or complex scenarios - var migration: AnyStorageMigration? { get } -} + public init( + name: String, + domain: StorageDomain, + security: SecurityPolicy = .recommended, + serializer: Serializer = .json, + owner: String, + description: String, + availability: PlatformAvailability = .all, + syncPolicy: SyncPolicy = .never, + migration: (@Sendable (StorageKey) -> AnyStorageMigration?)? = nil + ) { + self.name = name + self.domain = domain + self.security = security + self.serializer = serializer + self.owner = owner + self.description = description + self.availability = availability + self.syncPolicy = syncPolicy + self.migrationBuilder = migration + } -extension StorageKey { - public var migration: AnyStorageMigration? { nil } + public var migration: AnyStorageMigration? { + migrationBuilder?(self) + } } ``` @@ -212,7 +232,7 @@ public struct AnyStorageKey: Sendable { public internal(set) var migration: AnyStorageMigration? private let migrateAction: @Sendable (StorageRouter) async throws -> Void - public init(_ key: Key) { + public init(_ key: StorageKey) { self.descriptor = .from(key) self.migration = key.migration self.migrateAction = { router in @@ -231,13 +251,11 @@ public struct AnyStorageKey: Sendable { #### `Sources/LocalData/Migrations/SimpleLegacyMigration.swift` ```swift /// Simple 1:1 legacy migration -public struct SimpleLegacyMigration: StorageMigration { - public typealias DestinationKey = Destination - - public let destinationKey: Destination +public struct SimpleLegacyMigration: StorageMigration { + public let destinationKey: StorageKey public let sourceKey: AnyStorageKey - public init(destinationKey: Destination, sourceKey: AnyStorageKey) { + public init(destinationKey: StorageKey, sourceKey: AnyStorageKey) { self.destinationKey = destinationKey self.sourceKey = sourceKey } @@ -290,13 +308,12 @@ public struct SimpleLegacyMigration: StorageMigration { #### `Sources/LocalData/Migrations/AppVersionConditionalMigration.swift` ```swift /// Conditional migration for app version-based migration -public struct AppVersionConditionalMigration: ConditionalMigration { - public typealias DestinationKey = Destination - public let destinationKey: Destination +public struct AppVersionConditionalMigration: ConditionalMigration { + public let destinationKey: StorageKey public let minAppVersion: String public let fallbackMigration: AnyStorageMigration - public init(destinationKey: Destination, minAppVersion: String, fallbackMigration: AnyStorageMigration) { + public init(destinationKey: StorageKey, minAppVersion: String, fallbackMigration: AnyStorageMigration) { self.destinationKey = destinationKey self.minAppVersion = minAppVersion self.fallbackMigration = fallbackMigration @@ -319,24 +336,21 @@ public struct AppVersionConditionalMigration: Condition ```swift /// Migration protocol that supports data transformation during migration public protocol TransformingMigration: StorageMigration { - associatedtype SourceKey: StorageKey + associatedtype SourceValue: Codable & Sendable - var sourceKey: SourceKey { get } - func transform(_ source: SourceKey.Value) async throws -> DestinationKey.Value + var sourceKey: StorageKey { get } + func transform(_ source: SourceValue) async throws -> Value } -public struct DefaultTransformingMigration: TransformingMigration { - public typealias SourceKey = Source - public typealias DestinationKey = Destination - - public let destinationKey: Destination - public let sourceKey: Source - public let transform: (Source.Value) async throws -> Destination.Value +public struct DefaultTransformingMigration: TransformingMigration { + public let destinationKey: StorageKey + public let sourceKey: StorageKey + public let transform: (SourceValue) async throws -> DestinationValue public init( - destinationKey: Destination, - sourceKey: Source, - transform: @escaping (Source.Value) async throws -> Destination.Value + destinationKey: StorageKey, + sourceKey: StorageKey, + transform: @escaping (SourceValue) async throws -> DestinationValue ) { self.destinationKey = destinationKey self.sourceKey = sourceKey @@ -384,20 +398,18 @@ public struct DefaultTransformingMigration DestinationKey.Value + func aggregate(_ sources: [AnyCodable]) async throws -> Value } -public struct DefaultAggregatingMigration: AggregatingMigration { - public typealias DestinationKey = Destination - - public let destinationKey: Destination +public struct DefaultAggregatingMigration: AggregatingMigration { + public let destinationKey: StorageKey public let sourceKeys: [AnyStorageKey] - public let aggregate: ([AnyCodable]) async throws -> Destination.Value + public let aggregate: ([AnyCodable]) async throws -> Value public init( - destinationKey: Destination, + destinationKey: StorageKey, sourceKeys: [AnyStorageKey], - aggregate: @escaping ([AnyCodable]) async throws -> Destination.Value + aggregate: @escaping ([AnyCodable]) async throws -> Value ) { self.destinationKey = destinationKey self.sourceKeys = sourceKeys @@ -524,7 +536,7 @@ internal func recordMigration(for descriptor: StorageKeyDescriptor) { **Enhanced `get` method:** ```swift -public func get(_ key: Key) async throws -> Key.Value { +public func get(_ key: StorageKey) async throws -> Value { try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) @@ -570,12 +582,12 @@ private func migrateAllRegisteredKeys() async throws { **New Methods:** ```swift /// Get migration history for a key -public func migrationHistory(for key: Key) async -> Date? { +public func migrationHistory(for key: StorageKey) async -> Date? { migrationHistory[key.name] } /// Force migration for a specific key using its attached migration -public func forceMigration(for key: Key) async throws -> MigrationResult { +public func forceMigration(for key: StorageKey) async throws -> MigrationResult { guard let migration = key.migration else { throw MigrationError.sourceDataNotFound } @@ -871,39 +883,32 @@ struct UnifiedSettings: Codable { **`ConditionalMigrationKeys.swift`** ```swift -extension StorageKeys { - struct LegacyConditionalProfileKey: StorageKey { - typealias Value = UserProfile - - let name = "legacy_conditional_profile" - let domain: StorageDomain = .userDefaults(suite: nil) - let security: SecurityPolicy = .none - let serializer: Serializer = .json - let owner = "MigrationDemo" - let description = "Legacy profile for conditional migration demo." - let availability: PlatformAvailability = .all - let syncPolicy: SyncPolicy = .never - } - - struct ModernConditionalProfileKey: StorageKey { - typealias Value = UserProfile - - let name = "modern_conditional_profile" - let domain: StorageDomain = .keychain(service: "com.mbrucedogs.securestorage") - let security: SecurityPolicy = .keychain( +extension StorageKey where Value == UserProfile { + static let legacyConditionalProfile = StorageKey( + name: "legacy_conditional_profile", + domain: .userDefaults(suite: nil), + security: .none, + serializer: .json, + owner: "MigrationDemo", + description: "Legacy profile for conditional migration demo.", + availability: .all, + syncPolicy: .never + ) + + static let modernConditionalProfile = StorageKey( + name: "modern_conditional_profile", + domain: .keychain(service: "com.mbrucedogs.securestorage"), + security: .keychain( accessibility: .afterFirstUnlock, accessControl: .userPresence - ) - let serializer: Serializer = .json - let owner = "MigrationDemo" - let description = "Modern profile for conditional migration demo." - let availability: PlatformAvailability = .all - let syncPolicy: SyncPolicy = .never - - var migration: AnyStorageMigration? { - AnyStorageMigration(ConditionalProfileMigration()) - } - } + ), + serializer: .json, + owner: "MigrationDemo", + description: "Modern profile for conditional migration demo.", + availability: .all, + syncPolicy: .never, + migration: { _ in AnyStorageMigration(ConditionalProfileMigration()) } + ) } ``` @@ -949,9 +954,9 @@ The `StorageMigration` protocol provides the foundation for all migration types: ```swift public protocol StorageMigration: Sendable { - associatedtype DestinationKey: StorageKey + associatedtype Value: Codable & Sendable - var destinationKey: DestinationKey { get } + var destinationKey: StorageKey { get } func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool func migrate(using router: StorageRouter, context: MigrationContext) async throws -> MigrationResult } @@ -1197,27 +1202,25 @@ conditional behavior. ### 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 + ) } ``` @@ -1265,7 +1268,7 @@ extension UserTokenKey { ```swift // Save -let key = StorageKeys.UserTokenKey() +let key = StorageKey.userToken try await StorageRouter.shared.set("token123", for: key) // Retrieve (triggers automatic migration if needed) @@ -1279,7 +1282,7 @@ try await StorageRouter.shared.remove(key) ```swift // Force migration -let result = try await StorageRouter.shared.forceMigration(for: UserTokenKey()) +let result = try await StorageRouter.shared.forceMigration(for: StorageKey.userToken) ``` ``` @@ -1380,10 +1383,8 @@ struct MigrationIntegrationTests { #### `Tests/LocalDataTests/Mocks/MockMigration.swift` ```swift /// Mock migration for testing -struct MockMigration: StorageMigration { - typealias DestinationKey = Destination - - let destinationKey: Destination +struct MockMigration: StorageMigration { + let destinationKey: StorageKey let shouldSucceed: Bool let shouldMigrateResult: Bool let migrationDelay: TimeInterval @@ -1408,10 +1409,8 @@ struct MockMigration: StorageMigration { } /// Failing migration for error testing -struct FailingMigration: StorageMigration { - typealias DestinationKey = Destination - - let destinationKey: Destination +struct FailingMigration: StorageMigration { + let destinationKey: StorageKey let error: MigrationError func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { true } @@ -1547,7 +1546,7 @@ public enum MigrationUtils { ## Migration Checklist - [x] Create core protocol files -- [x] Update StorageKey protocol with migration +- [x] Update StorageKey type with migration - [x] Create concrete migration implementations - [x] Update StorageRouter integration with simple and advanced migration handling - [x] Add WatchOS/sync defaults to StorageMigration diff --git a/Sources/LocalData/LocalData.swift b/Sources/LocalData/LocalData.swift deleted file mode 100644 index 28415ef..0000000 --- a/Sources/LocalData/LocalData.swift +++ /dev/null @@ -1,3 +0,0 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book -