diff --git a/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift b/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift index d434c1d..e6e02ff 100644 --- a/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift +++ b/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift @@ -1,15 +1,13 @@ import Foundation /// 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, + destinationKey: StorageKey, minAppVersion: String, fallbackMigration: AnyStorageMigration ) { diff --git a/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift b/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift index 90812f3..27b34dc 100644 --- a/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift +++ b/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift @@ -1,23 +1,21 @@ import Foundation -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 aggregateAction: ([AnyCodable]) async throws -> Destination.Value + public let aggregateAction: ([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 self.aggregateAction = aggregate } - public func aggregate(_ sources: [AnyCodable]) async throws -> Destination.Value { + public func aggregate(_ sources: [AnyCodable]) async throws -> Value { try await aggregateAction(sources) } diff --git a/Sources/LocalData/Migrations/DefaultTransformingMigration.swift b/Sources/LocalData/Migrations/DefaultTransformingMigration.swift index b6c02d2..3ad8f44 100644 --- a/Sources/LocalData/Migrations/DefaultTransformingMigration.swift +++ b/Sources/LocalData/Migrations/DefaultTransformingMigration.swift @@ -1,24 +1,21 @@ import Foundation -public struct DefaultTransformingMigration: TransformingMigration { - public typealias SourceKey = Source - public typealias DestinationKey = Destination - - public let destinationKey: Destination - public let sourceKey: Source - public let transformAction: (Source.Value) async throws -> Destination.Value +public struct DefaultTransformingMigration: TransformingMigration { + public let destinationKey: StorageKey + public let sourceKey: StorageKey + public let transformAction: (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 self.transformAction = transform } - public func transform(_ source: Source.Value) async throws -> Destination.Value { + public func transform(_ source: SourceValue) async throws -> DestinationValue { try await transformAction(source) } diff --git a/Sources/LocalData/Migrations/SimpleLegacyMigration.swift b/Sources/LocalData/Migrations/SimpleLegacyMigration.swift index 332b8da..b48195a 100644 --- a/Sources/LocalData/Migrations/SimpleLegacyMigration.swift +++ b/Sources/LocalData/Migrations/SimpleLegacyMigration.swift @@ -1,13 +1,11 @@ import Foundation /// 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 } diff --git a/Sources/LocalData/Models/AnyStorageKey.swift b/Sources/LocalData/Models/AnyStorageKey.swift index ee5904d..eed05ab 100644 --- a/Sources/LocalData/Models/AnyStorageKey.swift +++ b/Sources/LocalData/Models/AnyStorageKey.swift @@ -3,7 +3,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 @@ -21,7 +21,7 @@ public struct AnyStorageKey: Sendable { self.migrateAction = migrateAction } - public static func key(_ key: Key) -> AnyStorageKey { + public static func key(_ key: StorageKey) -> AnyStorageKey { AnyStorageKey(key) } diff --git a/Sources/LocalData/Models/StorageKey.swift b/Sources/LocalData/Models/StorageKey.swift new file mode 100644 index 0000000..718e5cc --- /dev/null +++ b/Sources/LocalData/Models/StorageKey.swift @@ -0,0 +1,40 @@ +import Foundation + +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 + + private let migrationBuilder: (@Sendable (StorageKey) -> AnyStorageMigration?)? + + 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 + } + + public var migration: AnyStorageMigration? { + migrationBuilder?(self) + } +} diff --git a/Sources/LocalData/Models/StorageKeyDescriptor.swift b/Sources/LocalData/Models/StorageKeyDescriptor.swift index 719899b..d84cf15 100644 --- a/Sources/LocalData/Models/StorageKeyDescriptor.swift +++ b/Sources/LocalData/Models/StorageKeyDescriptor.swift @@ -36,8 +36,8 @@ public struct StorageKeyDescriptor: Sendable, CustomStringConvertible { self.catalog = catalog } - public static func from( - _ key: Key, + public static func from( + _ key: StorageKey, catalog: String? = nil ) -> StorageKeyDescriptor { StorageKeyDescriptor( @@ -45,7 +45,7 @@ public struct StorageKeyDescriptor: Sendable, CustomStringConvertible { domain: key.domain, security: key.security, serializer: key.serializer.name, - valueType: String(describing: Key.Value.self), + valueType: String(describing: Value.self), owner: key.owner, availability: key.availability, syncPolicy: key.syncPolicy, diff --git a/Sources/LocalData/Protocols/AggregatingMigration.swift b/Sources/LocalData/Protocols/AggregatingMigration.swift index 704b8d4..83cb5c0 100644 --- a/Sources/LocalData/Protocols/AggregatingMigration.swift +++ b/Sources/LocalData/Protocols/AggregatingMigration.swift @@ -3,5 +3,5 @@ import Foundation /// Migration protocol that combines multiple sources into a 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 } diff --git a/Sources/LocalData/Protocols/StorageKey+Defaults.swift b/Sources/LocalData/Protocols/StorageKey+Defaults.swift deleted file mode 100644 index e4073c2..0000000 --- a/Sources/LocalData/Protocols/StorageKey+Defaults.swift +++ /dev/null @@ -1,3 +0,0 @@ -public extension StorageKey { - var security: SecurityPolicy { .recommended } -} diff --git a/Sources/LocalData/Protocols/StorageKey.swift b/Sources/LocalData/Protocols/StorageKey.swift deleted file mode 100644 index 9002be8..0000000 --- a/Sources/LocalData/Protocols/StorageKey.swift +++ /dev/null @@ -1,20 +0,0 @@ -import Foundation - -public protocol StorageKey: Sendable, CustomStringConvertible { - associatedtype Value: Codable & Sendable - - 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 } - - var migration: AnyStorageMigration? { get } -} - -extension StorageKey { - public var migration: AnyStorageMigration? { nil } -} diff --git a/Sources/LocalData/Protocols/StorageMigration.swift b/Sources/LocalData/Protocols/StorageMigration.swift index 4995a52..64655c9 100644 --- a/Sources/LocalData/Protocols/StorageMigration.swift +++ b/Sources/LocalData/Protocols/StorageMigration.swift @@ -2,10 +2,10 @@ import Foundation /// 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 diff --git a/Sources/LocalData/Protocols/StorageProviding.swift b/Sources/LocalData/Protocols/StorageProviding.swift index 5cdffac..1034038 100644 --- a/Sources/LocalData/Protocols/StorageProviding.swift +++ b/Sources/LocalData/Protocols/StorageProviding.swift @@ -1,7 +1,7 @@ import Foundation public protocol StorageProviding: Sendable { - func set(_ value: Key.Value, for key: Key) async throws - func get(_ key: Key) async throws -> Key.Value - func remove(_ key: Key) async throws + func set(_ value: Value, for key: StorageKey) async throws + func get(_ key: StorageKey) async throws -> Value + func remove(_ key: StorageKey) async throws } diff --git a/Sources/LocalData/Protocols/TransformingMigration.swift b/Sources/LocalData/Protocols/TransformingMigration.swift index df5db95..b063ba3 100644 --- a/Sources/LocalData/Protocols/TransformingMigration.swift +++ b/Sources/LocalData/Protocols/TransformingMigration.swift @@ -2,8 +2,8 @@ import Foundation /// 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 } diff --git a/Sources/LocalData/Services/StorageRouter.swift b/Sources/LocalData/Services/StorageRouter.swift index d455746..741b5c2 100644 --- a/Sources/LocalData/Services/StorageRouter.swift +++ b/Sources/LocalData/Services/StorageRouter.swift @@ -127,7 +127,7 @@ public actor StorageRouter: StorageProviding { } /// Returns the last migration date for a specific key, if available. - public func migrationHistory(for key: Key) -> Date? { + public func migrationHistory(for key: StorageKey) -> Date? { migrationHistory[key.name] } @@ -138,7 +138,7 @@ public actor StorageRouter: StorageProviding { /// - value: The value to store. /// - key: The storage key defining where and how to store. /// - Throws: Various errors depending on the storage domain and security policy. - public func set(_ value: Key.Value, for key: Key) async throws { + public func set(_ value: Value, for key: StorageKey) async throws { Logger.debug(">>> [STORAGE] SET: \(key.name) [Domain: \(key.domain)]") try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) @@ -155,7 +155,7 @@ public actor StorageRouter: StorageProviding { /// - Parameter key: The storage key to retrieve. /// - Returns: The stored value. /// - Throws: `StorageError.notFound` if no value exists, plus domain-specific errors. - public func get(_ key: Key) async throws -> Key.Value { + public func get(_ key: StorageKey) async throws -> Value { Logger.debug(">>> [STORAGE] GET: \(key.name)") try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) @@ -182,7 +182,7 @@ public actor StorageRouter: StorageProviding { /// - Parameter key: The storage key to migrate. /// - Returns: The migration result. /// - Throws: Migration or storage errors. - public func forceMigration(for key: Key) async throws -> MigrationResult { + public func forceMigration(for key: StorageKey) async throws -> MigrationResult { Logger.debug(">>> [STORAGE] MANUAL MIGRATION: \(key.name)") try validateCatalogRegistration(for: key) @@ -207,7 +207,7 @@ public actor StorageRouter: StorageProviding { /// Removes the value for the given key. /// - Parameter key: The storage key to remove. /// - Throws: Domain-specific errors if removal fails. - public func remove(_ key: Key) async throws { + public func remove(_ key: StorageKey) async throws { try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) try await delete(for: .from(key)) @@ -216,7 +216,7 @@ public actor StorageRouter: StorageProviding { /// Checks if a value exists for the given key. /// - Parameter key: The storage key to check. /// - Returns: True if a value exists. - public func exists(_ key: Key) async throws -> Bool { + public func exists(_ key: StorageKey) async throws -> Bool { try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) @@ -265,7 +265,7 @@ public actor StorageRouter: StorageProviding { // MARK: - Platform Validation - nonisolated private func validatePlatformAvailability(for key: Key) throws { + nonisolated private func validatePlatformAvailability(for key: StorageKey) throws { #if os(watchOS) if key.availability == .phoneOnly { throw StorageError.phoneOnlyKeyAccessedOnWatch(key.name) @@ -277,7 +277,7 @@ public actor StorageRouter: StorageProviding { #endif } - private func validateCatalogRegistration(for key: Key) throws { + private func validateCatalogRegistration(for key: StorageKey) throws { guard !registeredKeys.isEmpty else { return } guard registeredKeys[key.name] != nil else { let errorMessage = "UNREGISTERED STORAGE KEY: '\(key.name)' (Owner: \(key.owner)) accessed but not found in any registered catalog. Did you forget to call registerCatalog?" @@ -331,7 +331,7 @@ public actor StorageRouter: StorageProviding { // MARK: - Migration - private func attemptMigration(for key: Key) async throws -> Key.Value? { + private func attemptMigration(for key: StorageKey) async throws -> Value? { guard let migration = resolveMigration(for: key) else { return nil } let context = buildMigrationContext() @@ -354,7 +354,7 @@ public actor StorageRouter: StorageProviding { return nil } - private func resolveMigration(for key: Key) -> AnyStorageMigration? { + private func resolveMigration(for key: StorageKey) -> AnyStorageMigration? { key.migration } @@ -366,8 +366,8 @@ public actor StorageRouter: StorageProviding { migrationHistory[descriptor.name] = Date() } - internal func shouldAllowMigration( - for key: Key, + internal func shouldAllowMigration( + for key: StorageKey, context: MigrationContext ) async throws -> Bool { guard key.availability.isAvailable(on: context.deviceInfo.platform) else { @@ -440,7 +440,7 @@ public actor StorageRouter: StorageProviding { // MARK: - Storage Operations - private func store(_ data: Data, for key: any StorageKey) async throws { + private func store(_ data: Data, for key: StorageKey) async throws { try await store(data, for: .from(key)) } @@ -544,7 +544,7 @@ public actor StorageRouter: StorageProviding { // MARK: - Sync - private func handleSync(_ key: any StorageKey, data: Data) async throws { + private func handleSync(_ key: StorageKey, data: Data) async throws { try await sync.syncIfNeeded( data: data, keyName: key.name,