diff --git a/Sources/LocalData/Audit/StorageAuditReport.swift b/Sources/LocalData/Audit/StorageAuditReport.swift index 0326f58..dac8443 100644 --- a/Sources/LocalData/Audit/StorageAuditReport.swift +++ b/Sources/LocalData/Audit/StorageAuditReport.swift @@ -1,23 +1,29 @@ import Foundation +/// Renders audit reports for storage key catalogs and registries. public struct StorageAuditReport: Sendable { + /// Returns descriptors for all keys in a catalog. public static func items(for catalog: some StorageKeyCatalog) -> [StorageKeyDescriptor] { catalog.allKeys.map { $0.descriptor.withCatalog(catalog.name) } } + /// Renders a text report for a catalog. public static func renderText(_ catalog: some StorageKeyCatalog) -> String { renderText(items(for: catalog)) } + /// Renders a text report for a list of type-erased keys. public static func renderText(_ entries: [AnyStorageKey]) -> String { renderText(entries.map(\.descriptor)) } + /// Renders a text report for the global registry on the shared router. public static func renderGlobalRegistry() async -> String { let entries = await StorageRouter.shared.allRegisteredEntries() return renderText(entries) } + /// Renders a text report for the global registry grouped by catalog. public static func renderGlobalRegistryGrouped() async -> String { let catalogs = await StorageRouter.shared.allRegisteredCatalogs() var reportLines: [String] = [] @@ -34,6 +40,7 @@ public struct StorageAuditReport: Sendable { return reportLines.joined(separator: "\n") } + /// Renders a text report from storage key descriptors. public static func renderText(_ items: [StorageKeyDescriptor]) -> String { let lines = items.map { item in var parts: [String] = [] diff --git a/Sources/LocalData/Configuration/EncryptionConfiguration.swift b/Sources/LocalData/Configuration/EncryptionConfiguration.swift index 851340a..e00f036 100644 --- a/Sources/LocalData/Configuration/EncryptionConfiguration.swift +++ b/Sources/LocalData/Configuration/EncryptionConfiguration.swift @@ -1,13 +1,19 @@ import Foundation -/// Configuration for the EncryptionHelper. +/// Configuration for the encryption system. public struct EncryptionConfiguration: Sendable { + /// Keychain service for the master key. public let masterKeyService: String + /// Keychain account for the master key. public let masterKeyAccount: String + /// Master key length in bytes. public let masterKeyLength: Int + /// Default HKDF info string. public let defaultHKDFInfo: String + /// PBKDF2 iteration count. public let pbkdf2Iterations: Int + /// Creates an encryption configuration. public init( masterKeyService: String = "LocalData", masterKeyAccount: String = "MasterKey", @@ -22,5 +28,6 @@ public struct EncryptionConfiguration: Sendable { self.pbkdf2Iterations = pbkdf2Iterations } + /// Default encryption configuration. public static let `default` = EncryptionConfiguration() } diff --git a/Sources/LocalData/Configuration/FileStorageConfiguration.swift b/Sources/LocalData/Configuration/FileStorageConfiguration.swift index 3568bb4..8351212 100644 --- a/Sources/LocalData/Configuration/FileStorageConfiguration.swift +++ b/Sources/LocalData/Configuration/FileStorageConfiguration.swift @@ -10,10 +10,12 @@ public struct FileStorageConfiguration: Sendable { /// Primarily used for testing isolation. public let baseURL: URL? + /// Creates a file storage configuration. public init(subDirectory: String? = nil, baseURL: URL? = nil) { self.subDirectory = subDirectory self.baseURL = baseURL } + /// Default file storage configuration. public static let `default` = FileStorageConfiguration() } diff --git a/Sources/LocalData/Configuration/StorageConfiguration.swift b/Sources/LocalData/Configuration/StorageConfiguration.swift index 8cb1d24..8c92cfa 100644 --- a/Sources/LocalData/Configuration/StorageConfiguration.swift +++ b/Sources/LocalData/Configuration/StorageConfiguration.swift @@ -9,6 +9,7 @@ public struct StorageConfiguration: Sendable { /// The default App Group identifier to use if none is specified in a StorageKey. public let defaultAppGroupIdentifier: String? + /// Creates a configuration with optional defaults. public init( defaultKeychainService: String? = nil, defaultAppGroupIdentifier: String? = nil @@ -17,5 +18,6 @@ public struct StorageConfiguration: Sendable { self.defaultAppGroupIdentifier = defaultAppGroupIdentifier } + /// Default configuration with no predefined identifiers. public static let `default` = StorageConfiguration() } diff --git a/Sources/LocalData/Configuration/SyncConfiguration.swift b/Sources/LocalData/Configuration/SyncConfiguration.swift index 0350d72..7f3a1b1 100644 --- a/Sources/LocalData/Configuration/SyncConfiguration.swift +++ b/Sources/LocalData/Configuration/SyncConfiguration.swift @@ -5,9 +5,11 @@ public struct SyncConfiguration: Sendable { /// Maximum data size for automatic sync in bytes. public let maxAutoSyncSize: Int + /// Creates a sync configuration. public init(maxAutoSyncSize: Int = 100_000) { self.maxAutoSyncSize = maxAutoSyncSize } + /// Default sync configuration. public static let `default` = SyncConfiguration() } diff --git a/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift b/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift index e6e02ff..43e5f88 100644 --- a/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift +++ b/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift @@ -1,11 +1,15 @@ import Foundation -/// Conditional migration for app version-based migration. +/// Conditional migration that runs only when the app version is below a threshold. public struct AppVersionConditionalMigration: ConditionalMigration { + /// Destination key for the migration. public let destinationKey: StorageKey + /// Minimum app version required to skip this migration. public let minAppVersion: String + /// Migration to run when the version condition is met. public let fallbackMigration: AnyStorageMigration + /// Creates a version-gated migration. public init( destinationKey: StorageKey, minAppVersion: String, diff --git a/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift b/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift index b83371b..3abf3b7 100644 --- a/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift +++ b/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift @@ -1,10 +1,15 @@ import Foundation +/// Default migration that aggregates multiple source values into one destination value. public struct DefaultAggregatingMigration: AggregatingMigration { + /// Destination key for aggregated data. public let destinationKey: StorageKey + /// Source keys providing legacy values. public let sourceKeys: [AnyStorageKey] + /// Async aggregation closure for source values. public let aggregateAction: @Sendable ([AnyCodable]) async throws -> Value + /// Creates an aggregating migration with a custom aggregation closure. public init( destinationKey: StorageKey, sourceKeys: [AnyStorageKey], diff --git a/Sources/LocalData/Migrations/DefaultTransformingMigration.swift b/Sources/LocalData/Migrations/DefaultTransformingMigration.swift index dcdcbea..d3feb41 100644 --- a/Sources/LocalData/Migrations/DefaultTransformingMigration.swift +++ b/Sources/LocalData/Migrations/DefaultTransformingMigration.swift @@ -1,10 +1,15 @@ import Foundation +/// Default migration that transforms a single source value into a destination value. public struct DefaultTransformingMigration: TransformingMigration { + /// Destination key for the transformed value. public let destinationKey: StorageKey + /// Source key providing the legacy value. public let sourceKey: StorageKey + /// Async transform from source to destination. public let transformAction: @Sendable (SourceValue) async throws -> DestinationValue + /// Creates a transforming migration with a custom transform closure. public init( destinationKey: StorageKey, sourceKey: StorageKey, diff --git a/Sources/LocalData/Migrations/SimpleLegacyMigration.swift b/Sources/LocalData/Migrations/SimpleLegacyMigration.swift index b48195a..e7f2cac 100644 --- a/Sources/LocalData/Migrations/SimpleLegacyMigration.swift +++ b/Sources/LocalData/Migrations/SimpleLegacyMigration.swift @@ -1,10 +1,13 @@ import Foundation -/// Simple 1:1 legacy migration. +/// Simple 1:1 legacy migration from a single source key. public struct SimpleLegacyMigration: StorageMigration { + /// Destination key for migrated data. public let destinationKey: StorageKey + /// Source key providing legacy data. public let sourceKey: AnyStorageKey + /// Creates a migration from a legacy key to a destination key. public init(destinationKey: StorageKey, sourceKey: AnyStorageKey) { self.destinationKey = destinationKey self.sourceKey = sourceKey diff --git a/Sources/LocalData/Models/AnyCodable.swift b/Sources/LocalData/Models/AnyCodable.swift index a1642e8..3024bc7 100644 --- a/Sources/LocalData/Models/AnyCodable.swift +++ b/Sources/LocalData/Models/AnyCodable.swift @@ -1,8 +1,14 @@ import Foundation +/// Type-erased `Codable` wrapper for mixed-type payloads. +/// +/// - Important: `AnyCodable` is `@unchecked Sendable` because `Any` cannot be verified by the +/// compiler. Callers should only store values that are safe to pass across concurrency domains. public struct AnyCodable: Codable, @unchecked Sendable { + /// Underlying value (Bool, Int, Double, String, arrays, or dictionaries). public let value: Any + /// Wraps a value for encoding or decoding. public init(_ value: Any) { self.value = value } diff --git a/Sources/LocalData/Models/AnyStorageKey.swift b/Sources/LocalData/Models/AnyStorageKey.swift index eed05ab..4ec2327 100644 --- a/Sources/LocalData/Models/AnyStorageKey.swift +++ b/Sources/LocalData/Models/AnyStorageKey.swift @@ -1,8 +1,12 @@ +/// Type-erased wrapper around ``StorageKey`` for catalogs and audits. public struct AnyStorageKey: Sendable { + /// Snapshot of key metadata for auditing and storage operations. public internal(set) var descriptor: StorageKeyDescriptor + /// Optional migration associated with the key. public internal(set) var migration: AnyStorageMigration? private let migrateAction: @Sendable (StorageRouter) async throws -> Void + /// Creates a type-erased key from a typed ``StorageKey``. public init(_ key: StorageKey) { self.descriptor = .from(key) self.migration = key.migration @@ -21,6 +25,7 @@ public struct AnyStorageKey: Sendable { self.migrateAction = migrateAction } + /// Convenience factory for creating a type-erased key. public static func key(_ key: StorageKey) -> AnyStorageKey { AnyStorageKey(key) } diff --git a/Sources/LocalData/Models/AnyStorageMigration.swift b/Sources/LocalData/Models/AnyStorageMigration.swift index 839d9a6..fd727ad 100644 --- a/Sources/LocalData/Models/AnyStorageMigration.swift +++ b/Sources/LocalData/Models/AnyStorageMigration.swift @@ -1,12 +1,14 @@ import Foundation -/// Type-erased wrapper for StorageMigration to match AnyStorageKey patterns. +/// Type-erased wrapper for ``StorageMigration`` for use in catalogs and registrations. public struct AnyStorageMigration: Sendable { + /// Descriptor for the migration destination key. public let destinationDescriptor: StorageKeyDescriptor private let shouldMigrateAction: @Sendable (StorageRouter, MigrationContext) async throws -> Bool private let migrateAction: @Sendable (StorageRouter, MigrationContext) async throws -> MigrationResult + /// Creates a type-erased migration from a concrete migration. public init(_ migration: M) { self.destinationDescriptor = .from(migration.destinationKey) self.shouldMigrateAction = { @Sendable router, context in @@ -17,10 +19,12 @@ public struct AnyStorageMigration: Sendable { } } + /// Evaluates whether the migration should run for the given context. public func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { try await shouldMigrateAction(router, context) } + /// Executes the migration and returns its result. public func migrate(using router: StorageRouter, context: MigrationContext) async throws -> MigrationResult { try await migrateAction(router, context) } diff --git a/Sources/LocalData/Models/FileDirectory.swift b/Sources/LocalData/Models/FileDirectory.swift index ed04e15..01a089a 100644 --- a/Sources/LocalData/Models/FileDirectory.swift +++ b/Sources/LocalData/Models/FileDirectory.swift @@ -1,7 +1,15 @@ import Foundation +/// File system directory for file-based storage. public enum FileDirectory: Sendable, Hashable { - case documents, caches, custom(URL) + /// App documents directory. + case documents + /// App caches directory. + case caches + /// Custom directory URL. + case custom(URL) + + /// Resolves the directory to a concrete URL. public func url() -> URL { switch self { case .documents: diff --git a/Sources/LocalData/Models/KeyMaterialSource.swift b/Sources/LocalData/Models/KeyMaterialSource.swift index 3ba6a2b..bf822cd 100644 --- a/Sources/LocalData/Models/KeyMaterialSource.swift +++ b/Sources/LocalData/Models/KeyMaterialSource.swift @@ -1,8 +1,11 @@ import Foundation +/// Identifier for external key material providers. public struct KeyMaterialSource: Hashable, Sendable { + /// Stable identifier for the provider or key source. public let id: String + /// Creates a new key material source identifier. public init(id: String) { self.id = id } diff --git a/Sources/LocalData/Models/MigrationContext.swift b/Sources/LocalData/Models/MigrationContext.swift index 78e775b..8e87ede 100644 --- a/Sources/LocalData/Models/MigrationContext.swift +++ b/Sources/LocalData/Models/MigrationContext.swift @@ -2,12 +2,18 @@ import Foundation /// Context information available for conditional migrations. public struct MigrationContext: Sendable { + /// Current app version string. public let appVersion: String + /// Device metadata for platform checks. public let deviceInfo: DeviceInfo + /// Previously recorded migration timestamps keyed by storage key name. public let migrationHistory: [String: Date] + /// Caller-provided preferences that may influence migration behavior. public let userPreferences: [String: AnyCodable] + /// System information for conditional checks. public let systemInfo: SystemInfo + /// Creates a migration context with optional overrides. public init( appVersion: String = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown", deviceInfo: DeviceInfo = .current, diff --git a/Sources/LocalData/Models/MigrationError.swift b/Sources/LocalData/Models/MigrationError.swift index d44b46e..6638d38 100644 --- a/Sources/LocalData/Models/MigrationError.swift +++ b/Sources/LocalData/Models/MigrationError.swift @@ -2,13 +2,21 @@ import Foundation /// Migration-specific error types. public enum MigrationError: Error, Sendable, Equatable { + /// Validation failed before migration could run. case validationFailed(String) + /// Transformation failed while converting source to destination. case transformationFailed(String) + /// Underlying storage error occurred. case storageFailed(StorageError) + /// Conditional migration criteria were not met. case conditionalMigrationFailed + /// A migration is already in progress for the key. case migrationInProgress + /// No source data was found to migrate. case sourceDataNotFound + /// Source and destination types are incompatible. case incompatibleTypes(String) + /// Aggregation failed while combining multiple sources. case aggregationFailed(String) } diff --git a/Sources/LocalData/Models/MigrationResult.swift b/Sources/LocalData/Models/MigrationResult.swift index c98c39f..3308aef 100644 --- a/Sources/LocalData/Models/MigrationResult.swift +++ b/Sources/LocalData/Models/MigrationResult.swift @@ -2,12 +2,18 @@ import Foundation /// Result of a migration operation with detailed information. public struct MigrationResult: Sendable { + /// Whether the migration completed successfully. public let success: Bool + /// Number of values migrated. public let migratedCount: Int + /// Errors captured during migration. public let errors: [MigrationError] + /// Additional metadata provided by the migration. public let metadata: [String: AnyCodable] + /// Duration of the migration in seconds. public let duration: TimeInterval + /// Creates a migration result with optional details. public init( success: Bool, migratedCount: Int = 0, diff --git a/Sources/LocalData/Models/PlatformAvailability.swift b/Sources/LocalData/Models/PlatformAvailability.swift index ac198fa..72354e2 100644 --- a/Sources/LocalData/Models/PlatformAvailability.swift +++ b/Sources/LocalData/Models/PlatformAvailability.swift @@ -1,13 +1,19 @@ import Foundation +/// Specifies which platforms a storage key is allowed to run on. public enum PlatformAvailability: Sendable { - case all // iPhone + Watch (small only!) - case phoneOnly // iPhone only (large/sensitive) - case watchOnly // Watch local only - case phoneWithWatchSync // Small data for explicit sync + /// Available on iOS and watchOS (small data only on watch). + case all + /// Available only on iOS (large or sensitive data). + case phoneOnly + /// Available only on watchOS. + case watchOnly + /// Available on iOS and watchOS with explicit sync behavior. + case phoneWithWatchSync } public extension PlatformAvailability { + /// Returns `true` if the key should be available on the given platform. func isAvailable(on platform: Platform) -> Bool { switch self { case .all: diff --git a/Sources/LocalData/Models/SecurityPolicy.swift b/Sources/LocalData/Models/SecurityPolicy.swift index dc75a06..f2db56b 100644 --- a/Sources/LocalData/Models/SecurityPolicy.swift +++ b/Sources/LocalData/Models/SecurityPolicy.swift @@ -2,26 +2,40 @@ import Foundation import CryptoKit import Security +/// Security policy for a ``StorageKey``. public enum SecurityPolicy: Equatable, Sendable { + /// Stores data without additional security. case none + /// Encrypts data before storage using the specified policy. case encrypted(EncryptionPolicy) + /// Stores data directly in the Keychain with accessibility and access control options. case keychain(accessibility: KeychainAccessibility, accessControl: KeychainAccessControl?) + /// Recommended security policy for most sensitive data. public static let recommended: SecurityPolicy = .encrypted(.recommended) + /// Encryption algorithm and key derivation settings. public enum EncryptionPolicy: Equatable, Sendable { + /// AES-256-GCM encryption. case aes256(keyDerivation: KeyDerivation) + /// ChaCha20-Poly1305 encryption. case chacha20Poly1305(keyDerivation: KeyDerivation) + /// External key material with key derivation. case external(source: KeyMaterialSource, keyDerivation: KeyDerivation) + /// Recommended encryption policy for most cases. public static let recommended: EncryptionPolicy = .chacha20Poly1305(keyDerivation: .hkdf()) + /// Convenience for external key material with default HKDF. public static func external(source: KeyMaterialSource) -> EncryptionPolicy { .external(source: source, keyDerivation: .hkdf()) } } + /// Key derivation algorithms for encryption keys. public enum KeyDerivation: Equatable, Sendable { + /// PBKDF2 with optional iterations and salt. case pbkdf2(iterations: Int? = nil, salt: Data? = nil) + /// HKDF with optional salt and info. case hkdf(salt: Data? = nil, info: Data? = nil) } } diff --git a/Sources/LocalData/Models/Serializer.swift b/Sources/LocalData/Models/Serializer.swift index 8ae3ee9..9a317ad 100644 --- a/Sources/LocalData/Models/Serializer.swift +++ b/Sources/LocalData/Models/Serializer.swift @@ -1,10 +1,20 @@ import Foundation +/// Encodes and decodes values for storage. public struct Serializer: Sendable, CustomStringConvertible { + /// Encodes a value into `Data`. public let encode: @Sendable (Value) throws -> Data + /// Decodes a value from `Data`. public let decode: @Sendable (Data) throws -> Value + /// Human-readable serializer name used in audit reports. public let name: String + /// Creates a custom serializer. + /// + /// - Parameters: + /// - encode: Encoder for values. + /// - decode: Decoder for values. + /// - name: Display name for audit and logging. public init( encode: @escaping @Sendable (Value) throws -> Data, decode: @escaping @Sendable (Data) throws -> Value, @@ -15,8 +25,10 @@ public struct Serializer: Sendable, CustomStringConve self.name = name } + /// Description used by `CustomStringConvertible`. public var description: String { name } + /// JSON serializer using `JSONEncoder` and `JSONDecoder`. public static var json: Serializer { Serializer( encode: { try JSONEncoder().encode($0) }, @@ -25,6 +37,7 @@ public struct Serializer: Sendable, CustomStringConve ) } + /// Property list serializer using `PropertyListEncoder` and `PropertyListDecoder`. public static var plist: Serializer { Serializer( encode: { try PropertyListEncoder().encode($0) }, @@ -33,6 +46,7 @@ public struct Serializer: Sendable, CustomStringConve ) } + /// Convenience for custom serializers. public static func custom( encode: @escaping @Sendable (Value) throws -> Data, decode: @escaping @Sendable (Data) throws -> Value, @@ -43,6 +57,7 @@ public struct Serializer: Sendable, CustomStringConve } public extension Serializer where Value == Data { + /// Serializer that passes through raw `Data`. static var data: Serializer { Serializer(encode: { $0 }, decode: { $0 }, name: "data") } diff --git a/Sources/LocalData/Models/StorageDomain.swift b/Sources/LocalData/Models/StorageDomain.swift index 010439f..8eb011b 100644 --- a/Sources/LocalData/Models/StorageDomain.swift +++ b/Sources/LocalData/Models/StorageDomain.swift @@ -1,10 +1,17 @@ import Foundation +/// Storage location for a ``StorageKey``. public enum StorageDomain: Sendable, Equatable { + /// Standard `UserDefaults` using the provided suite name. case userDefaults(suite: String?) + /// App group `UserDefaults` using the provided group identifier. case appGroupUserDefaults(identifier: String?) + /// Keychain storage using the provided service identifier. case keychain(service: String?) + /// File system storage in the specified directory. case fileSystem(directory: FileDirectory) + /// Encrypted file system storage in the specified directory. case encryptedFileSystem(directory: FileDirectory) + /// App group file storage using the group identifier and directory. case appGroupFileSystem(identifier: String?, directory: FileDirectory) } diff --git a/Sources/LocalData/Models/StorageError.swift b/Sources/LocalData/Models/StorageError.swift index 953fecd..d05bee0 100644 --- a/Sources/LocalData/Models/StorageError.swift +++ b/Sources/LocalData/Models/StorageError.swift @@ -1,18 +1,34 @@ import Foundation +/// Errors thrown by storage operations and migrations. public enum StorageError: Error, Equatable { - case serializationFailed, deserializationFailed + /// Failed to encode a value. + case serializationFailed + /// Failed to decode stored data. + case deserializationFailed + /// Failed to apply or remove security for stored data. case securityApplicationFailed + /// Underlying Keychain error. case keychainError(OSStatus) + /// File system error description. case fileError(String) // Changed from Error to String for easier Equatable conformance + /// A phone-only key was accessed on watchOS. case phoneOnlyKeyAccessedOnWatch(String) + /// A watch-only key was accessed on iOS. case watchOnlyKeyAccessedOnPhone(String) + /// Invalid UserDefaults suite name. case invalidUserDefaultsSuite(String) + /// Invalid App Group identifier. case invalidAppGroupIdentifier(String) + /// Sync payload exceeded the configured maximum size. case dataTooLargeForSync + /// No value exists for the requested key. case notFound + /// The key is not registered in any catalog. case unregisteredKey(String) + /// Duplicate key names detected during registration. case duplicateRegisteredKeys([String]) + /// Missing or empty key description. case missingDescription(String) public static func == (lhs: StorageError, rhs: StorageError) -> Bool { diff --git a/Sources/LocalData/Models/StorageKey.swift b/Sources/LocalData/Models/StorageKey.swift index 718e5cc..b6cb0e3 100644 --- a/Sources/LocalData/Models/StorageKey.swift +++ b/Sources/LocalData/Models/StorageKey.swift @@ -1,17 +1,47 @@ import Foundation +/// Typed descriptor for a single piece of persisted data. +/// +/// Use `StorageKey` to define where a value is stored, how it is secured, how it is serialized, +/// and how it participates in sync and migration behaviors. +/// +/// - Important: `name` should be unique within its storage domain to avoid collisions. +/// - Note: `Value` must conform to `Codable` and `Sendable`. public struct StorageKey: Sendable, CustomStringConvertible { + /// Unique identifier for the stored value within its domain. public let name: String + /// Storage location for the value (UserDefaults, Keychain, file system, etc.). public let domain: StorageDomain + /// Security policy applied to stored bytes. public let security: SecurityPolicy + /// Serializer used to convert between `Value` and `Data`. public let serializer: Serializer + /// Owning feature or module for auditability. public let owner: String + /// Human-readable description for audit reports. public let description: String + /// Platform availability constraints for reads/writes and migrations. public let availability: PlatformAvailability + /// WatchConnectivity sync behavior for this key. public let syncPolicy: SyncPolicy + /// Lazily builds a migration using the fully initialized key. + /// + /// This avoids capturing `self` during initialization and keeps the destination key consistent. private let migrationBuilder: (@Sendable (StorageKey) -> AnyStorageMigration?)? + /// Creates a storage key with optional security, serializer, availability, sync, and migration. + /// + /// - Parameters: + /// - name: Unique identifier for the stored value. + /// - domain: Storage location for the value. + /// - security: Security policy applied to stored bytes. Defaults to `.recommended`. + /// - serializer: Serializer used to encode/decode values. Defaults to `.json`. + /// - owner: Owning feature or module for auditability. + /// - description: Human-readable description for audit reports. + /// - availability: Platform availability constraints. Defaults to `.all`. + /// - syncPolicy: WatchConnectivity sync behavior. Defaults to `.never`. + /// - migration: Optional builder that creates a migration using this key as destination. public init( name: String, domain: StorageDomain, @@ -34,6 +64,9 @@ public struct StorageKey: Sendable, CustomStringConve self.migrationBuilder = migration } + /// Construct a migration on demand using this key as the destination. + /// + /// - Returns: The migration for this key, or `nil` if none is configured. public var migration: AnyStorageMigration? { migrationBuilder?(self) } diff --git a/Sources/LocalData/Models/StorageKeyDescriptor.swift b/Sources/LocalData/Models/StorageKeyDescriptor.swift index d84cf15..be4c8c7 100644 --- a/Sources/LocalData/Models/StorageKeyDescriptor.swift +++ b/Sources/LocalData/Models/StorageKeyDescriptor.swift @@ -1,15 +1,26 @@ import Foundation +/// Snapshot of a ``StorageKey`` used for audit and registration. public struct StorageKeyDescriptor: Sendable, CustomStringConvertible { + /// Key name within its domain. public let name: String + /// Storage domain for the key. public let domain: StorageDomain + /// Security policy applied to the key. public let security: SecurityPolicy + /// Serializer name used for encoding/decoding. public let serializer: String + /// String representation of the value type. public let valueType: String + /// Owning module or feature name. public let owner: String + /// Platform availability for the key. public let availability: PlatformAvailability + /// Sync policy for WatchConnectivity. public let syncPolicy: SyncPolicy + /// Human-readable description for audit reports. public let description: String + /// Optional catalog name the key belongs to. public let catalog: String? init( @@ -36,6 +47,7 @@ public struct StorageKeyDescriptor: Sendable, CustomStringConvertible { self.catalog = catalog } + /// Builds a descriptor from a ``StorageKey``. public static func from( _ key: StorageKey, catalog: String? = nil diff --git a/Sources/LocalData/Models/SyncPolicy.swift b/Sources/LocalData/Models/SyncPolicy.swift index 355ebfc..2430c69 100644 --- a/Sources/LocalData/Models/SyncPolicy.swift +++ b/Sources/LocalData/Models/SyncPolicy.swift @@ -1,7 +1,11 @@ import Foundation +/// Defines how a key participates in WatchConnectivity sync. public enum SyncPolicy: Sendable { - case never // Default for most - case manual // Manual WCSession send - case automaticSmall // Auto-sync if small + /// No sync behavior. + case never + /// Sync only when the app explicitly requests it. + case manual + /// Automatically sync when data size is below the configured threshold. + case automaticSmall } diff --git a/Sources/LocalData/Protocols/AggregatingMigration.swift b/Sources/LocalData/Protocols/AggregatingMigration.swift index 83cb5c0..4d6eec1 100644 --- a/Sources/LocalData/Protocols/AggregatingMigration.swift +++ b/Sources/LocalData/Protocols/AggregatingMigration.swift @@ -2,6 +2,8 @@ import Foundation /// Migration protocol that combines multiple sources into a single destination. public protocol AggregatingMigration: StorageMigration { + /// The set of source keys used to build the destination value. var sourceKeys: [AnyStorageKey] { get } + /// Aggregates decoded source values into the destination value type. func aggregate(_ sources: [AnyCodable]) async throws -> Value } diff --git a/Sources/LocalData/Protocols/KeyMaterialProviding.swift b/Sources/LocalData/Protocols/KeyMaterialProviding.swift index ba364b7..c328f50 100644 --- a/Sources/LocalData/Protocols/KeyMaterialProviding.swift +++ b/Sources/LocalData/Protocols/KeyMaterialProviding.swift @@ -1,5 +1,7 @@ import Foundation +/// Supplies external key material for encryption policies. public protocol KeyMaterialProviding: Sendable { + /// Returns key material associated with the given key name. func keyMaterial(for keyName: String) async throws -> Data } diff --git a/Sources/LocalData/Protocols/KeychainStoring.swift b/Sources/LocalData/Protocols/KeychainStoring.swift index e22ad3f..2c10f37 100644 --- a/Sources/LocalData/Protocols/KeychainStoring.swift +++ b/Sources/LocalData/Protocols/KeychainStoring.swift @@ -1,8 +1,10 @@ import Foundation /// Protocol defining the interface for Keychain operations. -/// Allows for dependency injection and mocking in tests. +/// +/// Conformers enable dependency injection and mocking in tests. public protocol KeychainStoring: Sendable { + /// Stores data for a keychain entry. func set( _ data: Data, service: String, @@ -11,11 +13,15 @@ public protocol KeychainStoring: Sendable { accessControl: KeychainAccessControl? ) async throws + /// Retrieves data for a keychain entry. func get(service: String, key: String) async throws -> Data? + /// Deletes a keychain entry. func delete(service: String, key: String) async throws + /// Checks if a keychain entry exists. func exists(service: String, key: String) async throws -> Bool + /// Deletes all keychain entries for a service. func deleteAll(service: String) async throws } diff --git a/Sources/LocalData/Protocols/StorageKeyCatalog.swift b/Sources/LocalData/Protocols/StorageKeyCatalog.swift index 38e25a5..60c365d 100644 --- a/Sources/LocalData/Protocols/StorageKeyCatalog.swift +++ b/Sources/LocalData/Protocols/StorageKeyCatalog.swift @@ -1,9 +1,13 @@ +/// Collection of storage keys used for registration and auditing. public protocol StorageKeyCatalog: Sendable { + /// Human-readable catalog name used in audit reports. var name: String { get } + /// All keys owned by this catalog. var allKeys: [AnyStorageKey] { get } } extension StorageKeyCatalog { + /// Default catalog name derived from the type name. public var name: String { let fullName = String(describing: type(of: self)) // Simple cleanup for generic or nested names if needed, diff --git a/Sources/LocalData/Protocols/StorageMigration.swift b/Sources/LocalData/Protocols/StorageMigration.swift index 64655c9..ff222ec 100644 --- a/Sources/LocalData/Protocols/StorageMigration.swift +++ b/Sources/LocalData/Protocols/StorageMigration.swift @@ -1,20 +1,22 @@ import Foundation -/// Core migration protocol with high-level methods. +/// Core migration protocol for moving data into a destination ``StorageKey``. public protocol StorageMigration: Sendable { + /// The value type produced by the migration. associatedtype Value: Codable & Sendable /// The destination storage key where migrated data will be stored. var destinationKey: StorageKey { get } - /// Validate if migration should proceed (conditional logic). + /// Determines whether the migration should run for the given context. func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool - /// Execute the migration process. + /// Executes the migration and returns a result describing success or failure. func migrate(using router: StorageRouter, context: MigrationContext) async throws -> MigrationResult } public extension StorageMigration { + /// Default conditional behavior that checks platform availability and existing data. func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { try await router.shouldAllowMigration(for: destinationKey, context: context) } diff --git a/Sources/LocalData/Protocols/StorageProviding.swift b/Sources/LocalData/Protocols/StorageProviding.swift index 1034038..4ab45ab 100644 --- a/Sources/LocalData/Protocols/StorageProviding.swift +++ b/Sources/LocalData/Protocols/StorageProviding.swift @@ -1,7 +1,13 @@ import Foundation +/// Abstraction for basic storage operations. +/// +/// Conforming types persist and retrieve values described by a ``StorageKey``. public protocol StorageProviding: Sendable { + /// Stores a value for the given key. func set(_ value: Value, for key: StorageKey) async throws + /// Retrieves a value for the given key. func get(_ key: StorageKey) async throws -> Value + /// Removes a value for the given key. func remove(_ key: StorageKey) async throws } diff --git a/Sources/LocalData/Protocols/TransformingMigration.swift b/Sources/LocalData/Protocols/TransformingMigration.swift index b063ba3..5ad6913 100644 --- a/Sources/LocalData/Protocols/TransformingMigration.swift +++ b/Sources/LocalData/Protocols/TransformingMigration.swift @@ -1,9 +1,12 @@ import Foundation -/// Migration protocol that supports data transformation during migration. +/// Migration protocol that transforms a source value into a destination value. public protocol TransformingMigration: StorageMigration { + /// The value type stored at the source key. associatedtype SourceValue: Codable & Sendable + /// The source key to read from during migration. var sourceKey: StorageKey { get } + /// Transforms a source value into the destination value type. func transform(_ source: SourceValue) async throws -> Value } diff --git a/Sources/LocalData/Services/StorageRouter.swift b/Sources/LocalData/Services/StorageRouter.swift index 741b5c2..1df9bca 100644 --- a/Sources/LocalData/Services/StorageRouter.swift +++ b/Sources/LocalData/Services/StorageRouter.swift @@ -4,6 +4,7 @@ import Foundation /// Uses specialized helper actors for each storage domain. public actor StorageRouter: StorageProviding { + /// Shared router instance for app-wide storage access. public static let shared = StorageRouter() private var catalogRegistries: [String: [AnyStorageKey]] = [:] @@ -16,9 +17,10 @@ public actor StorageRouter: StorageProviding { private let defaults: UserDefaultsHelper private let sync: SyncHelper - /// Initialize a new StorageRouter. - /// Internal for testing isolation via @testable import. - /// Consumers should use the `shared` singleton. + /// Initializes a new router with injected helpers. + /// + /// - Important: Internal for testing isolation via `@testable import`. + /// Production code should use ``StorageRouter/shared``. internal init( keychain: KeychainStoring = KeychainHelper.shared, encryption: EncryptionHelper = .shared, @@ -111,7 +113,7 @@ public actor StorageRouter: StorageProviding { catalogRegistries } - /// Returns all currently registered storage keys. + /// Returns all currently registered storage keys as a flat list. public func allRegisteredEntries() -> [AnyStorageKey] { Array(registeredKeys.values) } diff --git a/Sources/LocalData/Utilities/DeviceInfo.swift b/Sources/LocalData/Utilities/DeviceInfo.swift index 886637a..af594b3 100644 --- a/Sources/LocalData/Utilities/DeviceInfo.swift +++ b/Sources/LocalData/Utilities/DeviceInfo.swift @@ -9,11 +9,16 @@ import WatchKit /// Device information for migration context. public struct DeviceInfo: Sendable { + /// Current platform (iOS, watchOS, unknown). public let platform: Platform + /// OS version string. public let systemVersion: String + /// Device model identifier or marketing name. public let model: String + /// Whether the device is a simulator. public let isSimulator: Bool + /// Current device info derived from the running environment. public static let current = DeviceInfo() private init() { @@ -34,6 +39,7 @@ public struct DeviceInfo: Sendable { self.isSimulator = ProcessInfo.processInfo.environment["SIMULATOR_DEVICE_NAME"] != nil } + /// Creates a custom device info instance (useful for tests). public init(platform: Platform, systemVersion: String, model: String, isSimulator: Bool) { self.platform = platform self.systemVersion = systemVersion diff --git a/Sources/LocalData/Utilities/MigrationUtils.swift b/Sources/LocalData/Utilities/MigrationUtils.swift index e7c312d..e8588a4 100644 --- a/Sources/LocalData/Utilities/MigrationUtils.swift +++ b/Sources/LocalData/Utilities/MigrationUtils.swift @@ -2,15 +2,18 @@ import Foundation /// Utilities for common migration operations. public enum MigrationUtils { + /// Returns `true` if a value can be transformed between the given types. public static func canTransform(from: T.Type, to: U.Type) -> Bool { if T.self is U.Type { return true } return T.self == String.self || U.self == String.self } + /// Estimates the size of a data payload in bytes. public static func estimatedSize(for data: Data) -> UInt64 { UInt64(data.count) } + /// Validates that source and destination descriptors are compatible. public static func validateCompatibility( source: StorageKeyDescriptor, destination: StorageKeyDescriptor diff --git a/Sources/LocalData/Utilities/Platform.swift b/Sources/LocalData/Utilities/Platform.swift index cc3dfc4..dbee813 100644 --- a/Sources/LocalData/Utilities/Platform.swift +++ b/Sources/LocalData/Utilities/Platform.swift @@ -1,7 +1,11 @@ import Foundation +/// Supported runtime platforms for storage availability checks. public enum Platform: String, CaseIterable, Sendable { + /// iOS platform. case iOS = "iOS" + /// watchOS platform. case watchOS = "watchOS" + /// Unknown or unsupported platform. case unknown = "unknown" } diff --git a/Sources/LocalData/Utilities/SystemInfo.swift b/Sources/LocalData/Utilities/SystemInfo.swift index 83afece..7e5314c 100644 --- a/Sources/LocalData/Utilities/SystemInfo.swift +++ b/Sources/LocalData/Utilities/SystemInfo.swift @@ -2,10 +2,14 @@ import Foundation /// System information for migration context. public struct SystemInfo: Sendable { + /// Free disk space in bytes. public let availableDiskSpace: UInt64 + /// Physical memory in bytes. public let availableMemory: UInt64 + /// Whether Low Power Mode is enabled. public let isLowPowerModeEnabled: Bool + /// Current system info derived from the running environment. public static let current = SystemInfo() private init() {