Remove LocalData.swift and docs
Summary: - Sources: remove LocalData.swift - Docs: update docs for Design, Migration, Migration_Refactor_Plan_Clean Stats: - 4 files changed, 146 insertions(+), 132 deletions(-)
This commit is contained in:
parent
5e44eca572
commit
779cfb375e
@ -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).
|
||||
|
||||
@ -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()) }
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
@ -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<Value> { 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<Value: Codable & Sendable>: Sendable, CustomStringConvertible {
|
||||
public let name: String
|
||||
public let domain: StorageDomain
|
||||
public let security: SecurityPolicy
|
||||
public let serializer: Serializer<Value>
|
||||
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<Value> { get }
|
||||
var owner: String { get }
|
||||
var availability: PlatformAvailability { get }
|
||||
var syncPolicy: SyncPolicy { get }
|
||||
private let migrationBuilder: (@Sendable (StorageKey<Value>) -> AnyStorageMigration?)?
|
||||
|
||||
/// Optional migration for simple or complex scenarios
|
||||
var migration: AnyStorageMigration? { get }
|
||||
}
|
||||
public init(
|
||||
name: String,
|
||||
domain: StorageDomain,
|
||||
security: SecurityPolicy = .recommended,
|
||||
serializer: Serializer<Value> = .json,
|
||||
owner: String,
|
||||
description: String,
|
||||
availability: PlatformAvailability = .all,
|
||||
syncPolicy: SyncPolicy = .never,
|
||||
migration: (@Sendable (StorageKey<Value>) -> 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: StorageKey>(_ key: Key) {
|
||||
public init<Value>(_ key: StorageKey<Value>) {
|
||||
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<Destination: StorageKey>: StorageMigration {
|
||||
public typealias DestinationKey = Destination
|
||||
|
||||
public let destinationKey: Destination
|
||||
public struct SimpleLegacyMigration<Value: Codable & Sendable>: StorageMigration {
|
||||
public let destinationKey: StorageKey<Value>
|
||||
public let sourceKey: AnyStorageKey
|
||||
|
||||
public init(destinationKey: Destination, sourceKey: AnyStorageKey) {
|
||||
public init(destinationKey: StorageKey<Value>, sourceKey: AnyStorageKey) {
|
||||
self.destinationKey = destinationKey
|
||||
self.sourceKey = sourceKey
|
||||
}
|
||||
@ -290,13 +308,12 @@ public struct SimpleLegacyMigration<Destination: StorageKey>: StorageMigration {
|
||||
#### `Sources/LocalData/Migrations/AppVersionConditionalMigration.swift`
|
||||
```swift
|
||||
/// Conditional migration for app version-based migration
|
||||
public struct AppVersionConditionalMigration<Destination: StorageKey>: ConditionalMigration {
|
||||
public typealias DestinationKey = Destination
|
||||
public let destinationKey: Destination
|
||||
public struct AppVersionConditionalMigration<Value: Codable & Sendable>: ConditionalMigration {
|
||||
public let destinationKey: StorageKey<Value>
|
||||
public let minAppVersion: String
|
||||
public let fallbackMigration: AnyStorageMigration
|
||||
|
||||
public init(destinationKey: Destination, minAppVersion: String, fallbackMigration: AnyStorageMigration) {
|
||||
public init(destinationKey: StorageKey<Value>, minAppVersion: String, fallbackMigration: AnyStorageMigration) {
|
||||
self.destinationKey = destinationKey
|
||||
self.minAppVersion = minAppVersion
|
||||
self.fallbackMigration = fallbackMigration
|
||||
@ -319,24 +336,21 @@ public struct AppVersionConditionalMigration<Destination: StorageKey>: 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<SourceValue> { get }
|
||||
func transform(_ source: SourceValue) async throws -> Value
|
||||
}
|
||||
|
||||
public struct DefaultTransformingMigration<Source: StorageKey, Destination: StorageKey>: 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<SourceValue: Codable & Sendable, DestinationValue: Codable & Sendable>: TransformingMigration {
|
||||
public let destinationKey: StorageKey<DestinationValue>
|
||||
public let sourceKey: StorageKey<SourceValue>
|
||||
public let transform: (SourceValue) async throws -> DestinationValue
|
||||
|
||||
public init(
|
||||
destinationKey: Destination,
|
||||
sourceKey: Source,
|
||||
transform: @escaping (Source.Value) async throws -> Destination.Value
|
||||
destinationKey: StorageKey<DestinationValue>,
|
||||
sourceKey: StorageKey<SourceValue>,
|
||||
transform: @escaping (SourceValue) async throws -> DestinationValue
|
||||
) {
|
||||
self.destinationKey = destinationKey
|
||||
self.sourceKey = sourceKey
|
||||
@ -384,20 +398,18 @@ public struct DefaultTransformingMigration<Source: StorageKey, Destination: Stor
|
||||
/// Migration protocol that combines multiple sources into single destination
|
||||
public protocol AggregatingMigration: StorageMigration {
|
||||
var sourceKeys: [AnyStorageKey] { get }
|
||||
func aggregate(_ sources: [AnyCodable]) async throws -> DestinationKey.Value
|
||||
func aggregate(_ sources: [AnyCodable]) async throws -> Value
|
||||
}
|
||||
|
||||
public struct DefaultAggregatingMigration<Destination: StorageKey>: AggregatingMigration {
|
||||
public typealias DestinationKey = Destination
|
||||
|
||||
public let destinationKey: Destination
|
||||
public struct DefaultAggregatingMigration<Value: Codable & Sendable>: AggregatingMigration {
|
||||
public let destinationKey: StorageKey<Value>
|
||||
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<Value>,
|
||||
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: StorageKey>(_ key: Key) async throws -> Key.Value {
|
||||
public func get<Value>(_ key: StorageKey<Value>) 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<Key: StorageKey>(for key: Key) async -> Date? {
|
||||
public func migrationHistory<Value>(for key: StorageKey<Value>) async -> Date? {
|
||||
migrationHistory[key.name]
|
||||
}
|
||||
|
||||
/// Force migration for a specific key using its attached migration
|
||||
public func forceMigration<Key: StorageKey>(for key: Key) async throws -> MigrationResult {
|
||||
public func forceMigration<Value>(for key: StorageKey<Value>) 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<UserProfile> = .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<UserProfile> = .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<Value> { 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<String> = .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<Destination: StorageKey>: StorageMigration {
|
||||
typealias DestinationKey = Destination
|
||||
|
||||
let destinationKey: Destination
|
||||
struct MockMigration<Value: Codable & Sendable>: StorageMigration {
|
||||
let destinationKey: StorageKey<Value>
|
||||
let shouldSucceed: Bool
|
||||
let shouldMigrateResult: Bool
|
||||
let migrationDelay: TimeInterval
|
||||
@ -1408,10 +1409,8 @@ struct MockMigration<Destination: StorageKey>: StorageMigration {
|
||||
}
|
||||
|
||||
/// Failing migration for error testing
|
||||
struct FailingMigration<Destination: StorageKey>: StorageMigration {
|
||||
typealias DestinationKey = Destination
|
||||
|
||||
let destinationKey: Destination
|
||||
struct FailingMigration<Value: Codable & Sendable>: StorageMigration {
|
||||
let destinationKey: StorageKey<Value>
|
||||
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
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
// The Swift Programming Language
|
||||
// https://docs.swift.org/swift-book
|
||||
|
||||
Loading…
Reference in New Issue
Block a user