LocalData/Sources/LocalData/Migrations/DefaultTransformingMigration.swift
Matt Bruce 2a42e3dba0 Update Audit, Helpers, Migrations (+2 more)
Summary:
- Sources: update Audit, Helpers, Migrations (+2 more)

Stats:
- 29 files changed, 471 insertions(+), 25 deletions(-)
2026-01-18 13:43:12 -06:00

94 lines
3.8 KiB
Swift

import Foundation
/// Default migration that transforms a single source value into a destination value.
///
/// Use this migration when the destination value type differs from the legacy
/// type or when you need to normalize/clean legacy data before storage.
public struct DefaultTransformingMigration<SourceValue: Codable & Sendable, DestinationValue: Codable & Sendable>: TransformingMigration {
/// Destination key for the transformed value.
public let destinationKey: StorageKey<DestinationValue>
/// Source key providing the legacy value.
public let sourceKey: StorageKey<SourceValue>
/// Async transform from source to destination.
public let transformAction: @Sendable (SourceValue) async throws -> DestinationValue
/// Creates a transforming migration with a custom transform closure.
///
/// - Parameters:
/// - destinationKey: The key that receives the transformed value.
/// - sourceKey: The legacy key providing the source value.
/// - transform: Closure that converts the source value into the destination type.
public init(
destinationKey: StorageKey<DestinationValue>,
sourceKey: StorageKey<SourceValue>,
transform: @escaping @Sendable (SourceValue) async throws -> DestinationValue
) {
self.destinationKey = destinationKey
self.sourceKey = sourceKey
self.transformAction = transform
}
/// Transforms a source value into the destination type.
///
/// - Parameter source: The value read from ``sourceKey``.
/// - Returns: The transformed destination value.
public func transform(_ source: SourceValue) async throws -> DestinationValue {
try await transformAction(source)
}
/// Determines whether the migration should run.
///
/// The migration runs when:
/// - the destination is allowed on the current platform
/// - the destination does not already exist
/// - the source key contains data
///
/// - Parameters:
/// - router: The storage router used to query state.
/// - context: Migration context for conditional checks.
/// - Returns: `true` when migration should proceed.
public func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool {
guard try await router.shouldAllowMigration(for: destinationKey, context: context) else {
return false
}
if try await router.exists(destinationKey) {
return false
}
return try await router.exists(sourceKey)
}
/// Executes the migration and returns a result.
///
/// The migration:
/// 1. Reads the source value.
/// 2. Transforms it into the destination type.
/// 3. Writes the destination value.
/// 4. Deletes the source key.
///
/// - Parameters:
/// - router: The storage router used to read and write values.
/// - context: Migration context for conditional checks.
/// - Returns: A ``MigrationResult`` describing success or failure.
public func migrate(using router: StorageRouter, context: MigrationContext) async throws -> MigrationResult {
let startTime = Date()
do {
let sourceData = try await router.get(sourceKey)
let transformedData = try await transform(sourceData)
try await router.set(transformedData, for: destinationKey)
try await router.remove(sourceKey)
let duration = Date().timeIntervalSince(startTime)
return MigrationResult(success: true, migratedCount: 1, duration: duration)
} catch {
let duration = Date().timeIntervalSince(startTime)
return MigrationResult(
success: false,
errors: [.transformationFailed(error.localizedDescription)],
duration: duration
)
}
}
}