From aa33326198564154d90a3b708e1d5afe96be19cc Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Sat, 17 Jan 2026 10:33:36 -0600 Subject: [PATCH] Update Audit, Helpers, Migrations (+4 more) Summary: - Sources: update Audit, Helpers, Migrations (+4 more) Stats: - 27 files changed, 303 insertions(+), 12 deletions(-) --- .../LocalData/Audit/StorageAuditReport.swift | 16 ++++++ .../LocalData/Helpers/EncryptionHelper.swift | 17 ++++++- .../LocalData/Helpers/FileStorageHelper.swift | 50 ++++++++++++++++++- .../LocalData/Helpers/KeychainHelper.swift | 33 +++++++++++- Sources/LocalData/Helpers/SyncHelper.swift | 16 +++++- .../Helpers/UserDefaultsHelper.swift | 7 ++- .../AppVersionConditionalMigration.swift | 12 +++++ .../DefaultAggregatingMigration.swift | 16 ++++++ .../DefaultTransformingMigration.swift | 16 ++++++ .../Migrations/SimpleLegacyMigration.swift | 12 +++++ Sources/LocalData/Models/AnyCodable.swift | 2 + Sources/LocalData/Models/AnyStorageKey.swift | 4 ++ .../Models/AnyStorageMigration.swift | 12 +++++ .../Models/KeychainAccessControl.swift | 5 +- .../Models/KeychainAccessibility.swift | 4 +- Sources/LocalData/Models/MigrationError.swift | 1 + .../Models/PlatformAvailability.swift | 1 + Sources/LocalData/Models/Serializer.swift | 13 +++++ Sources/LocalData/Models/StorageError.swift | 1 + .../Models/StorageKeyDescriptor.swift | 8 +++ .../Protocols/AggregatingMigration.swift | 3 ++ .../Protocols/StorageKeyCatalog.swift | 2 + .../Protocols/StorageMigration.swift | 11 ++++ .../Protocols/StorageProviding.swift | 9 ++++ .../Protocols/TransformingMigration.swift | 3 ++ .../LocalData/Services/StorageRouter.swift | 33 ++++++++++-- Sources/LocalData/Utilities/Logger.swift | 8 +++ 27 files changed, 303 insertions(+), 12 deletions(-) diff --git a/Sources/LocalData/Audit/StorageAuditReport.swift b/Sources/LocalData/Audit/StorageAuditReport.swift index dac8443..99aa7ac 100644 --- a/Sources/LocalData/Audit/StorageAuditReport.swift +++ b/Sources/LocalData/Audit/StorageAuditReport.swift @@ -3,27 +3,40 @@ import Foundation /// Renders audit reports for storage key catalogs and registries. public struct StorageAuditReport: Sendable { /// Returns descriptors for all keys in a catalog. + /// + /// - Parameter catalog: Catalog containing keys to describe. + /// - Returns: An array of ``StorageKeyDescriptor`` values. public static func items(for catalog: some StorageKeyCatalog) -> [StorageKeyDescriptor] { catalog.allKeys.map { $0.descriptor.withCatalog(catalog.name) } } /// Renders a text report for a catalog. + /// + /// - Parameter catalog: Catalog containing keys to render. + /// - Returns: A newline-delimited report string. public static func renderText(_ catalog: some StorageKeyCatalog) -> String { renderText(items(for: catalog)) } /// Renders a text report for a list of type-erased keys. + /// + /// - Parameter entries: The keys to render. + /// - Returns: A newline-delimited report string. public static func renderText(_ entries: [AnyStorageKey]) -> String { renderText(entries.map(\.descriptor)) } /// Renders a text report for the global registry on the shared router. + /// + /// - Returns: A newline-delimited report string. 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. + /// + /// - Returns: A report string grouped by catalog name. public static func renderGlobalRegistryGrouped() async -> String { let catalogs = await StorageRouter.shared.allRegisteredCatalogs() var reportLines: [String] = [] @@ -41,6 +54,9 @@ public struct StorageAuditReport: Sendable { } /// Renders a text report from storage key descriptors. + /// + /// - Parameter items: The descriptors to render. + /// - Returns: A newline-delimited report string. public static func renderText(_ items: [StorageKeyDescriptor]) -> String { let lines = items.map { item in var parts: [String] = [] diff --git a/Sources/LocalData/Helpers/EncryptionHelper.swift b/Sources/LocalData/Helpers/EncryptionHelper.swift index b060600..9e0de5b 100644 --- a/Sources/LocalData/Helpers/EncryptionHelper.swift +++ b/Sources/LocalData/Helpers/EncryptionHelper.swift @@ -2,15 +2,22 @@ import Foundation import CryptoKit /// Actor that handles all encryption and decryption operations. +/// /// Uses AES-GCM or ChaChaPoly for symmetric encryption with derived keys. actor EncryptionHelper { + /// Shared encryption helper instance. public static let shared = EncryptionHelper() private var configuration: EncryptionConfiguration private var keychain: KeychainStoring private var keyMaterialProviders: [KeyMaterialSource: any KeyMaterialProviding] = [:] + /// Creates an encryption helper with a configuration and keychain provider. + /// + /// - Parameters: + /// - configuration: Encryption configuration to apply. + /// - keychain: Keychain provider for master key storage. internal init( configuration: EncryptionConfiguration = .default, keychain: KeychainStoring = KeychainHelper.shared @@ -22,12 +29,16 @@ actor EncryptionHelper { // MARK: - Configuration /// Updates the configuration for the actor. + /// + /// - Parameter configuration: New encryption configuration. public func updateConfiguration(_ configuration: EncryptionConfiguration) { self.configuration = configuration } /// Updates the keychain helper used for master key storage. - /// Internal for testing isolation. + /// + /// - Parameter keychain: Keychain provider to use. + /// - Note: Internal for testing isolation. public func updateKeychainHelper(_ keychain: KeychainStoring) { self.keychain = keychain } @@ -35,6 +46,10 @@ actor EncryptionHelper { // MARK: - Public Interface /// Registers a key material provider for external encryption policies. + /// + /// - Parameters: + /// - provider: The provider that supplies key material. + /// - source: Identifier used to look up the provider. public func registerKeyMaterialProvider( _ provider: any KeyMaterialProviding, for source: KeyMaterialSource diff --git a/Sources/LocalData/Helpers/FileStorageHelper.swift b/Sources/LocalData/Helpers/FileStorageHelper.swift index 279f804..1460316 100644 --- a/Sources/LocalData/Helpers/FileStorageHelper.swift +++ b/Sources/LocalData/Helpers/FileStorageHelper.swift @@ -1,18 +1,26 @@ import Foundation /// Actor that handles all file system operations. -/// Provides thread-safe file reading, writing, and deletion. +/// +/// Provides thread-safe file reading, writing, deletion, and listing for +/// app sandbox and App Group containers. actor FileStorageHelper { + /// Shared file storage helper instance. public static let shared = FileStorageHelper() private var configuration: FileStorageConfiguration + /// Creates a helper with a specific configuration. + /// + /// - Parameter configuration: File storage configuration to apply. internal init(configuration: FileStorageConfiguration = .default) { self.configuration = configuration } /// Updates the file storage configuration. + /// + /// - Parameter configuration: New configuration to apply. public func updateConfiguration(_ configuration: FileStorageConfiguration) { self.configuration = configuration } @@ -20,6 +28,14 @@ actor FileStorageHelper { // MARK: - Public Interface /// Writes data to an App Group container. + /// + /// - Parameters: + /// - data: The data to write. + /// - directory: The base directory. + /// - fileName: The file name within the directory. + /// - appGroupIdentifier: App Group identifier. + /// - useCompleteFileProtection: Whether to use iOS complete file protection. + /// - Throws: ``StorageError/fileError(_:)`` or ``StorageError/invalidAppGroupIdentifier(_:)``. public func write( _ data: Data, to directory: FileDirectory, @@ -76,6 +92,13 @@ actor FileStorageHelper { } /// Reads data from an App Group container. + /// + /// - Parameters: + /// - directory: The base directory. + /// - fileName: The file name within the directory. + /// - appGroupIdentifier: App Group identifier. + /// - Returns: The file contents, or `nil` if the file doesn't exist. + /// - Throws: ``StorageError/fileError(_:)`` or ``StorageError/invalidAppGroupIdentifier(_:)``. public func read( from directory: FileDirectory, fileName: String, @@ -103,6 +126,12 @@ actor FileStorageHelper { } /// Deletes a file from an App Group container. + /// + /// - Parameters: + /// - directory: The base directory. + /// - fileName: The file name within the directory. + /// - appGroupIdentifier: App Group identifier. + /// - Throws: ``StorageError/fileError(_:)`` or ``StorageError/invalidAppGroupIdentifier(_:)``. public func delete( from directory: FileDirectory, fileName: String, @@ -133,6 +162,12 @@ actor FileStorageHelper { } /// Checks if a file exists in an App Group container. + /// + /// - Parameters: + /// - directory: The base directory. + /// - fileName: The file name within the directory. + /// - appGroupIdentifier: App Group identifier. + /// - Returns: `true` if the file exists. public func exists( in directory: FileDirectory, fileName: String, @@ -158,6 +193,12 @@ actor FileStorageHelper { } /// Lists all files in an App Group container directory. + /// + /// - Parameters: + /// - directory: The directory to list. + /// - appGroupIdentifier: App Group identifier. + /// - Returns: An array of file names. + /// - Throws: ``StorageError/fileError(_:)`` or ``StorageError/invalidAppGroupIdentifier(_:)``. public func list(in directory: FileDirectory, appGroupIdentifier: String) throws -> [String] { let baseURL = try appGroupContainerURL(identifier: appGroupIdentifier) let directoryURL = try resolveDirectoryURL(baseURL: baseURL, directory: directory) @@ -180,6 +221,13 @@ actor FileStorageHelper { } /// Gets the size of a file in an App Group container. + /// + /// - Parameters: + /// - directory: The base directory. + /// - fileName: The file name within the directory. + /// - appGroupIdentifier: App Group identifier. + /// - Returns: The file size in bytes, or `nil` if the file doesn't exist. + /// - Throws: ``StorageError/fileError(_:)`` or ``StorageError/invalidAppGroupIdentifier(_:)``. public func size( of directory: FileDirectory, fileName: String, diff --git a/Sources/LocalData/Helpers/KeychainHelper.swift b/Sources/LocalData/Helpers/KeychainHelper.swift index 33494cc..a74a5d9 100644 --- a/Sources/LocalData/Helpers/KeychainHelper.swift +++ b/Sources/LocalData/Helpers/KeychainHelper.swift @@ -2,9 +2,12 @@ import Foundation import Security /// Actor that handles all Keychain operations in isolation. -/// Provides thread-safe access to the iOS/watchOS Keychain. +/// +/// Provides thread-safe access to the iOS/watchOS Keychain and conforms to +/// ``KeychainStoring`` for dependency injection and testing. actor KeychainHelper: KeychainStoring { + /// Shared keychain helper instance. public static let shared = KeychainHelper() private init() {} @@ -12,6 +15,14 @@ actor KeychainHelper: KeychainStoring { // MARK: - KeychainStoring Implementation /// Stores data in the keychain. + /// + /// - Parameters: + /// - data: The data to store. + /// - service: Keychain service identifier. + /// - key: Keychain account name. + /// - accessibility: Keychain accessibility level. + /// - accessControl: Optional access control policy. + /// - Throws: ``StorageError/keychainError(_:)`` or ``StorageError/securityApplicationFailed``. public func set( _ data: Data, service: String, @@ -58,6 +69,12 @@ actor KeychainHelper: KeychainStoring { } /// Retrieves data from the keychain. + /// + /// - Parameters: + /// - service: Keychain service identifier. + /// - key: Keychain account name. + /// - Returns: Stored data if present, otherwise `nil`. + /// - Throws: ``StorageError/keychainError(_:)``. public func get(service: String, key: String) throws -> Data? { var query = baseQuery(service: service, key: key) query[kSecReturnData as String] = true @@ -81,6 +98,11 @@ actor KeychainHelper: KeychainStoring { } /// Deletes data from the keychain. + /// + /// - Parameters: + /// - service: Keychain service identifier. + /// - key: Keychain account name. + /// - Throws: ``StorageError/keychainError(_:)``. public func delete(service: String, key: String) throws { let query = baseQuery(service: service, key: key) let status = SecItemDelete(query as CFDictionary) @@ -96,6 +118,12 @@ actor KeychainHelper: KeychainStoring { } /// Checks if an item exists in the keychain. + /// + /// - Parameters: + /// - service: Keychain service identifier. + /// - key: Keychain account name. + /// - Returns: `true` if the item exists. + /// - Throws: ``StorageError/keychainError(_:)``. public func exists(service: String, key: String) throws -> Bool { var query = baseQuery(service: service, key: key) query[kSecReturnData as String] = false @@ -117,6 +145,9 @@ actor KeychainHelper: KeychainStoring { } /// Deletes all items for a given service. + /// + /// - Parameter service: Keychain service identifier. + /// - Throws: ``StorageError/keychainError(_:)``. public func deleteAll(service: String) throws { let query: [String: Any] = [ kSecClass as String: kSecClassGenericPassword, diff --git a/Sources/LocalData/Helpers/SyncHelper.swift b/Sources/LocalData/Helpers/SyncHelper.swift index daa0758..32ce2de 100644 --- a/Sources/LocalData/Helpers/SyncHelper.swift +++ b/Sources/LocalData/Helpers/SyncHelper.swift @@ -2,23 +2,32 @@ import Foundation import WatchConnectivity /// Actor that handles WatchConnectivity sync operations. +/// /// Manages data synchronization between iPhone and Apple Watch. actor SyncHelper { + /// Shared sync helper instance. public static let shared = SyncHelper() private var configuration: SyncConfiguration + /// Creates a helper with a specific configuration. + /// + /// - Parameter configuration: Sync configuration to apply. internal init(configuration: SyncConfiguration = .default) { self.configuration = configuration } /// Updates the sync configuration. + /// + /// - Parameter configuration: New sync configuration. public func updateConfiguration(_ configuration: SyncConfiguration) { self.configuration = configuration } /// Exposes the current max auto-sync size for filtering outbound payloads. + /// + /// - Returns: The maximum size in bytes for automatic sync. public func maxAutoSyncSize() -> Int { configuration.maxAutoSyncSize } @@ -68,7 +77,8 @@ actor SyncHelper { } /// Checks if sync is available. - /// - Returns: True if WatchConnectivity is supported and active. + /// + /// - Returns: `true` if WatchConnectivity is supported and active. public func isSyncAvailable() -> Bool { guard WCSession.isSupported() else { return false } @@ -88,6 +98,7 @@ actor SyncHelper { } /// Gets the current application context. + /// /// - Returns: The current application context dictionary. public func currentContext() -> [String: Any] { guard WCSession.isSupported() else { return [:] } @@ -141,8 +152,9 @@ actor SyncHelper { } } -/// An internal proxy class to handle WCSessionDelegate callbacks and route them to the SyncHelper actor. +/// Internal proxy class that routes WCSessionDelegate callbacks to ``SyncHelper``. internal final class SessionDelegateProxy: NSObject, WCSessionDelegate { + /// Shared delegate proxy instance. static let shared = SessionDelegateProxy() func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) { diff --git a/Sources/LocalData/Helpers/UserDefaultsHelper.swift b/Sources/LocalData/Helpers/UserDefaultsHelper.swift index c517c7a..1a47154 100644 --- a/Sources/LocalData/Helpers/UserDefaultsHelper.swift +++ b/Sources/LocalData/Helpers/UserDefaultsHelper.swift @@ -1,13 +1,18 @@ import Foundation /// Actor that handles all UserDefaults operations. -/// Provides thread-safe access to UserDefaults with suite support. +/// +/// Provides thread-safe access to UserDefaults with suite and App Group support. actor UserDefaultsHelper { + /// Shared helper instance. public static let shared = UserDefaultsHelper() private let defaults: UserDefaults + /// Creates a helper with a specific `UserDefaults` instance. + /// + /// - Parameter defaults: The defaults instance to use (standard by default). internal init(defaults: UserDefaults = .standard) { self.defaults = defaults } diff --git a/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift b/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift index 43e5f88..e2de2ea 100644 --- a/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift +++ b/Sources/LocalData/Migrations/AppVersionConditionalMigration.swift @@ -20,12 +20,24 @@ public struct AppVersionConditionalMigration: Conditi self.fallbackMigration = fallbackMigration } + /// Determines whether the migration should run based on the app version. + /// + /// - Parameters: + /// - router: The storage router used to query state. + /// - context: Migration context containing the app version. + /// - Returns: `true` when migration should proceed. public func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool { let isEligible = context.appVersion.compare(minAppVersion, options: .numeric) == .orderedAscending guard isEligible else { return false } return try await router.shouldAllowMigration(for: destinationKey, context: context) } + /// Executes the fallback migration when the version condition is met. + /// + /// - 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 { try await fallbackMigration.migrate(using: router, context: context) } diff --git a/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift b/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift index 3abf3b7..b8a393e 100644 --- a/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift +++ b/Sources/LocalData/Migrations/DefaultAggregatingMigration.swift @@ -20,10 +20,20 @@ public struct DefaultAggregatingMigration: Aggregatin self.aggregateAction = aggregate } + /// Aggregates decoded source values into the destination type. + /// + /// - Parameter sources: Values fetched from ``sourceKeys``. + /// - Returns: The aggregated destination value. public func aggregate(_ sources: [AnyCodable]) async throws -> Value { try await aggregateAction(sources) } + /// Determines whether the migration should run. + /// + /// - 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 @@ -41,6 +51,12 @@ public struct DefaultAggregatingMigration: Aggregatin return false } + /// Executes the migration and returns a result. + /// + /// - 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() var sourceData: [AnyCodable] = [] diff --git a/Sources/LocalData/Migrations/DefaultTransformingMigration.swift b/Sources/LocalData/Migrations/DefaultTransformingMigration.swift index d3feb41..eed3612 100644 --- a/Sources/LocalData/Migrations/DefaultTransformingMigration.swift +++ b/Sources/LocalData/Migrations/DefaultTransformingMigration.swift @@ -20,10 +20,20 @@ public struct DefaultTransformingMigration DestinationValue { try await transformAction(source) } + /// Determines whether the migration should run. + /// + /// - 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 @@ -35,6 +45,12 @@ public struct DefaultTransformingMigration MigrationResult { let startTime = Date() diff --git a/Sources/LocalData/Migrations/SimpleLegacyMigration.swift b/Sources/LocalData/Migrations/SimpleLegacyMigration.swift index e7f2cac..6ac8185 100644 --- a/Sources/LocalData/Migrations/SimpleLegacyMigration.swift +++ b/Sources/LocalData/Migrations/SimpleLegacyMigration.swift @@ -13,6 +13,12 @@ public struct SimpleLegacyMigration: StorageMigration self.sourceKey = sourceKey } + /// Determines whether the migration should run. + /// + /// - 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 @@ -24,6 +30,12 @@ public struct SimpleLegacyMigration: StorageMigration return try await router.exists(descriptor: sourceKey.descriptor) } + /// Executes the migration and returns a result. + /// + /// - 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() var errors: [MigrationError] = [] diff --git a/Sources/LocalData/Models/AnyCodable.swift b/Sources/LocalData/Models/AnyCodable.swift index 3024bc7..9bbb9f8 100644 --- a/Sources/LocalData/Models/AnyCodable.swift +++ b/Sources/LocalData/Models/AnyCodable.swift @@ -13,6 +13,7 @@ public struct AnyCodable: Codable, @unchecked Sendable { self.value = value } + /// Decodes a value from a single-value container. public init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() @@ -33,6 +34,7 @@ public struct AnyCodable: Codable, @unchecked Sendable { } } + /// Encodes the wrapped value into a single-value container. public func encode(to encoder: Encoder) throws { var container = encoder.singleValueContainer() diff --git a/Sources/LocalData/Models/AnyStorageKey.swift b/Sources/LocalData/Models/AnyStorageKey.swift index 4ec2327..747d534 100644 --- a/Sources/LocalData/Models/AnyStorageKey.swift +++ b/Sources/LocalData/Models/AnyStorageKey.swift @@ -7,6 +7,8 @@ public struct AnyStorageKey: Sendable { private let migrateAction: @Sendable (StorageRouter) async throws -> Void /// Creates a type-erased key from a typed ``StorageKey``. + /// + /// - Parameter key: The concrete key to erase. public init(_ key: StorageKey) { self.descriptor = .from(key) self.migration = key.migration @@ -26,6 +28,8 @@ public struct AnyStorageKey: Sendable { } /// Convenience factory for creating a type-erased key. + /// + /// - Parameter key: The concrete key to erase. public static func key(_ key: StorageKey) -> AnyStorageKey { AnyStorageKey(key) } diff --git a/Sources/LocalData/Models/AnyStorageMigration.swift b/Sources/LocalData/Models/AnyStorageMigration.swift index fd727ad..f7415b6 100644 --- a/Sources/LocalData/Models/AnyStorageMigration.swift +++ b/Sources/LocalData/Models/AnyStorageMigration.swift @@ -9,6 +9,8 @@ public struct AnyStorageMigration: Sendable { private let migrateAction: @Sendable (StorageRouter, MigrationContext) async throws -> MigrationResult /// Creates a type-erased migration from a concrete migration. + /// + /// - Parameter migration: The concrete migration to wrap. public init(_ migration: M) { self.destinationDescriptor = .from(migration.destinationKey) self.shouldMigrateAction = { @Sendable router, context in @@ -20,11 +22,21 @@ public struct AnyStorageMigration: Sendable { } /// Evaluates whether the migration should run for the given context. + /// + /// - Parameters: + /// - router: 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 { try await shouldMigrateAction(router, context) } /// Executes the migration and returns its result. + /// + /// - Parameters: + /// - router: 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 { try await migrateAction(router, context) } diff --git a/Sources/LocalData/Models/KeychainAccessControl.swift b/Sources/LocalData/Models/KeychainAccessControl.swift index 7f975f7..1a257ae 100644 --- a/Sources/LocalData/Models/KeychainAccessControl.swift +++ b/Sources/LocalData/Models/KeychainAccessControl.swift @@ -2,6 +2,7 @@ import Foundation import Security /// Defines additional access control requirements for keychain items. +/// /// These flags can require user authentication before accessing the item. public enum KeychainAccessControl: Equatable, Sendable, CaseIterable { /// Requires any form of user presence (biometric or passcode). @@ -26,9 +27,9 @@ public enum KeychainAccessControl: Equatable, Sendable, CaseIterable { /// If biometric changes, still accessible via passcode. case biometryCurrentSetOrDevicePasscode - /// Creates a SecAccessControl object with the specified accessibility. + /// Creates a `SecAccessControl` object with the specified accessibility. /// - Parameter accessibility: The base accessibility level. - /// - Returns: A configured SecAccessControl, or nil if creation fails. + /// - Returns: A configured `SecAccessControl`, or `nil` if creation fails. func accessControl(accessibility: KeychainAccessibility) -> SecAccessControl? { let accessibilityValue = accessibility.cfString diff --git a/Sources/LocalData/Models/KeychainAccessibility.swift b/Sources/LocalData/Models/KeychainAccessibility.swift index 4e8b506..789cc70 100644 --- a/Sources/LocalData/Models/KeychainAccessibility.swift +++ b/Sources/LocalData/Models/KeychainAccessibility.swift @@ -2,7 +2,8 @@ import Foundation import Security /// Defines when a keychain item can be accessed. -/// Maps directly to Security framework's kSecAttrAccessible constants. +/// +/// Maps directly to Security framework's `kSecAttrAccessible` constants. public enum KeychainAccessibility: Equatable, Sendable, CaseIterable { /// Item is only accessible while the device is unlocked. /// This is the most restrictive option for general use. @@ -56,6 +57,7 @@ public enum KeychainAccessibility: Equatable, Sendable, CaseIterable { } } + /// All supported accessibility cases. public static var allCases: [KeychainAccessibility] { [ .whenUnlocked, diff --git a/Sources/LocalData/Models/MigrationError.swift b/Sources/LocalData/Models/MigrationError.swift index 6638d38..af0bbdd 100644 --- a/Sources/LocalData/Models/MigrationError.swift +++ b/Sources/LocalData/Models/MigrationError.swift @@ -21,6 +21,7 @@ public enum MigrationError: Error, Sendable, Equatable { } extension MigrationError: LocalizedError { + /// Localized description for display or logging. public var errorDescription: String? { switch self { case .validationFailed(let message): diff --git a/Sources/LocalData/Models/PlatformAvailability.swift b/Sources/LocalData/Models/PlatformAvailability.swift index 72354e2..a3e10f3 100644 --- a/Sources/LocalData/Models/PlatformAvailability.swift +++ b/Sources/LocalData/Models/PlatformAvailability.swift @@ -12,6 +12,7 @@ public enum PlatformAvailability: Sendable { case phoneWithWatchSync } +/// Convenience helpers for platform checks. public extension PlatformAvailability { /// Returns `true` if the key should be available on the given platform. func isAvailable(on platform: Platform) -> Bool { diff --git a/Sources/LocalData/Models/Serializer.swift b/Sources/LocalData/Models/Serializer.swift index 9a317ad..330dd6b 100644 --- a/Sources/LocalData/Models/Serializer.swift +++ b/Sources/LocalData/Models/Serializer.swift @@ -29,6 +29,8 @@ public struct Serializer: Sendable, CustomStringConve public var description: String { name } /// JSON serializer using `JSONEncoder` and `JSONDecoder`. + /// + /// - Returns: A serializer that encodes and decodes JSON. public static var json: Serializer { Serializer( encode: { try JSONEncoder().encode($0) }, @@ -38,6 +40,8 @@ public struct Serializer: Sendable, CustomStringConve } /// Property list serializer using `PropertyListEncoder` and `PropertyListDecoder`. + /// + /// - Returns: A serializer that encodes and decodes property lists. public static var plist: Serializer { Serializer( encode: { try PropertyListEncoder().encode($0) }, @@ -47,6 +51,12 @@ public struct Serializer: Sendable, CustomStringConve } /// Convenience for custom serializers. + /// + /// - Parameters: + /// - encode: Encoder for values. + /// - decode: Decoder for values. + /// - name: Display name for audit and logging. + /// - Returns: A serializer built from the provided closures. public static func custom( encode: @escaping @Sendable (Value) throws -> Data, decode: @escaping @Sendable (Data) throws -> Value, @@ -56,8 +66,11 @@ public struct Serializer: Sendable, CustomStringConve } } +/// Convenience serializers for raw data. public extension Serializer where Value == Data { /// Serializer that passes through raw `Data`. + /// + /// - Returns: A serializer that returns `Data` unchanged. static var data: Serializer { Serializer(encode: { $0 }, decode: { $0 }, name: "data") } diff --git a/Sources/LocalData/Models/StorageError.swift b/Sources/LocalData/Models/StorageError.swift index d05bee0..ffbd2dd 100644 --- a/Sources/LocalData/Models/StorageError.swift +++ b/Sources/LocalData/Models/StorageError.swift @@ -31,6 +31,7 @@ public enum StorageError: Error, Equatable { /// Missing or empty key description. case missingDescription(String) + /// Compares two storage errors for equality. public static func == (lhs: StorageError, rhs: StorageError) -> Bool { switch (lhs, rhs) { case (.serializationFailed, .serializationFailed), diff --git a/Sources/LocalData/Models/StorageKeyDescriptor.swift b/Sources/LocalData/Models/StorageKeyDescriptor.swift index be4c8c7..b90d967 100644 --- a/Sources/LocalData/Models/StorageKeyDescriptor.swift +++ b/Sources/LocalData/Models/StorageKeyDescriptor.swift @@ -48,6 +48,11 @@ public struct StorageKeyDescriptor: Sendable, CustomStringConvertible { } /// Builds a descriptor from a ``StorageKey``. + /// + /// - Parameters: + /// - key: The key to describe. + /// - catalog: Optional catalog name for audit context. + /// - Returns: A populated ``StorageKeyDescriptor``. public static func from( _ key: StorageKey, catalog: String? = nil @@ -67,6 +72,9 @@ public struct StorageKeyDescriptor: Sendable, CustomStringConvertible { } /// Returns a new descriptor with the catalog name set. + /// + /// - Parameter catalogName: Catalog name to assign. + /// - Returns: A new descriptor with the catalog field set. public func withCatalog(_ catalogName: String) -> StorageKeyDescriptor { StorageKeyDescriptor( name: self.name, diff --git a/Sources/LocalData/Protocols/AggregatingMigration.swift b/Sources/LocalData/Protocols/AggregatingMigration.swift index 4d6eec1..676577d 100644 --- a/Sources/LocalData/Protocols/AggregatingMigration.swift +++ b/Sources/LocalData/Protocols/AggregatingMigration.swift @@ -5,5 +5,8 @@ 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. + /// + /// - Parameter sources: The values fetched from ``sourceKeys``. + /// - Returns: The aggregated destination value. func aggregate(_ sources: [AnyCodable]) async throws -> Value } diff --git a/Sources/LocalData/Protocols/StorageKeyCatalog.swift b/Sources/LocalData/Protocols/StorageKeyCatalog.swift index 60c365d..3fa5d91 100644 --- a/Sources/LocalData/Protocols/StorageKeyCatalog.swift +++ b/Sources/LocalData/Protocols/StorageKeyCatalog.swift @@ -3,6 +3,8 @@ public protocol StorageKeyCatalog: Sendable { /// Human-readable catalog name used in audit reports. var name: String { get } /// All keys owned by this catalog. + /// + /// Use type-erased ``AnyStorageKey`` to allow heterogeneous value types. var allKeys: [AnyStorageKey] { get } } diff --git a/Sources/LocalData/Protocols/StorageMigration.swift b/Sources/LocalData/Protocols/StorageMigration.swift index ff222ec..96a4d3d 100644 --- a/Sources/LocalData/Protocols/StorageMigration.swift +++ b/Sources/LocalData/Protocols/StorageMigration.swift @@ -9,12 +9,23 @@ public protocol StorageMigration: Sendable { var destinationKey: StorageKey { get } /// Determines whether the migration should run for the given context. + /// + /// - Parameters: + /// - router: Storage router used to query state. + /// - context: Migration context for conditional checks. + /// - Returns: `true` when migration should proceed. func shouldMigrate(using router: StorageRouter, context: MigrationContext) async throws -> Bool /// Executes the migration and returns a result describing success or failure. + /// + /// - Parameters: + /// - router: Storage router used to read/write values. + /// - context: Migration context for conditional checks. + /// - Returns: A ``MigrationResult`` describing the outcome. func migrate(using router: StorageRouter, context: MigrationContext) async throws -> MigrationResult } +/// Default behavior for storage migrations. public extension StorageMigration { /// Default conditional behavior that checks platform availability and existing data. 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 4ab45ab..ade3dac 100644 --- a/Sources/LocalData/Protocols/StorageProviding.swift +++ b/Sources/LocalData/Protocols/StorageProviding.swift @@ -5,9 +5,18 @@ import Foundation /// Conforming types persist and retrieve values described by a ``StorageKey``. public protocol StorageProviding: Sendable { /// Stores a value for the given key. + /// + /// - Parameters: + /// - value: The value to store. + /// - key: The storage key describing where and how to store the value. func set(_ value: Value, for key: StorageKey) async throws /// Retrieves a value for the given key. + /// + /// - Parameter key: The storage key to read. + /// - Returns: The stored value. func get(_ key: StorageKey) async throws -> Value /// Removes a value for the given key. + /// + /// - Parameter key: The storage key to remove. func remove(_ key: StorageKey) async throws } diff --git a/Sources/LocalData/Protocols/TransformingMigration.swift b/Sources/LocalData/Protocols/TransformingMigration.swift index 5ad6913..92a4cdd 100644 --- a/Sources/LocalData/Protocols/TransformingMigration.swift +++ b/Sources/LocalData/Protocols/TransformingMigration.swift @@ -8,5 +8,8 @@ public protocol TransformingMigration: StorageMigration { /// The source key to read from during migration. var sourceKey: StorageKey { get } /// Transforms a source value into the destination value type. + /// + /// - Parameter source: The value read from ``sourceKey``. + /// - Returns: The transformed value for the destination key. func transform(_ source: SourceValue) async throws -> Value } diff --git a/Sources/LocalData/Services/StorageRouter.swift b/Sources/LocalData/Services/StorageRouter.swift index 1df9bca..04817de 100644 --- a/Sources/LocalData/Services/StorageRouter.swift +++ b/Sources/LocalData/Services/StorageRouter.swift @@ -2,6 +2,12 @@ import Foundation /// The main storage router that coordinates all storage operations. /// Uses specialized helper actors for each storage domain. +/// Central coordinator for all LocalData storage operations. +/// +/// `StorageRouter` orchestrates serialization, security, storage domain routing, +/// catalog validation, migrations, and WatchConnectivity sync. Use the shared +/// instance for app-wide access and register catalogs at launch to enable +/// auditability and duplicate key detection. public actor StorageRouter: StorageProviding { /// Shared router instance for app-wide storage access. @@ -38,25 +44,31 @@ public actor StorageRouter: StorageProviding { // MARK: - Configuration /// Updates the encryption configuration. - /// > [!WARNING] - /// > Changing these constants in an existing app will cause the app to look for the master key - /// > under a new name. Previously encrypted data will be lost. + /// + /// - Warning: Changing these constants in an existing app causes the app to look for + /// the master key under a new name. Previously encrypted data may become inaccessible. public func updateEncryptionConfiguration(_ configuration: EncryptionConfiguration) async { await encryption.updateConfiguration(configuration) await encryption.updateKeychainHelper(keychain) } /// Updates the sync configuration. + /// + /// - Parameter configuration: New sync settings, including maximum payload size. public func updateSyncConfiguration(_ configuration: SyncConfiguration) async { await sync.updateConfiguration(configuration) } /// Updates the file storage configuration. + /// + /// - Parameter configuration: New file storage settings, including subdirectory scope. public func updateFileStorageConfiguration(_ configuration: FileStorageConfiguration) async { await file.updateConfiguration(configuration) } /// Updates the global storage configuration (defaults). + /// + /// - Parameter configuration: Default identifiers for keychain and app group storage. public func updateStorageConfiguration(_ configuration: StorageConfiguration) { self.storageConfiguration = configuration } @@ -64,6 +76,10 @@ public actor StorageRouter: StorageProviding { // MARK: - Key Material Providers /// Registers a key material provider for external encryption policies. + /// + /// - Parameters: + /// - provider: Object that supplies key material for encryption. + /// - source: Identifier used to look up the provider. public func registerKeyMaterialProvider( _ provider: any KeyMaterialProviding, for source: KeyMaterialSource @@ -77,6 +93,7 @@ public actor StorageRouter: StorageProviding { /// - Parameters: /// - catalog: The catalog type to register. /// - migrateImmediately: If true, triggers a proactive migration (sweep) for all keys in the catalog. + /// - Throws: ``StorageError/duplicateRegisteredKeys(_:)`` or ``StorageError/missingDescription(_:)``. public func registerCatalog(_ catalog: some StorageKeyCatalog, migrateImmediately: Bool = false) async throws { let entries = catalog.allKeys try validateDescription(entries) @@ -120,6 +137,7 @@ public actor StorageRouter: StorageProviding { /// Triggers a proactive migration (sweep) for all registered storage keys. /// This "drains" any legacy data into the modern storage locations. + /// - Throws: Migration or storage errors from individual keys. public func migrateAllRegisteredKeys() async throws { Logger.debug(">>> [STORAGE] STARTING GLOBAL MIGRATION SWEEP") for entry in registeredKeys.values { @@ -129,6 +147,9 @@ public actor StorageRouter: StorageProviding { } /// Returns the last migration date for a specific key, if available. + /// + /// - Parameter key: The key to look up. + /// - Returns: The date of the most recent successful migration. public func migrationHistory(for key: StorageKey) -> Date? { migrationHistory[key.name] } @@ -557,6 +578,10 @@ public actor StorageRouter: StorageProviding { /// Attempts to sync any registered keys that already have stored values. /// This is useful for bootstrapping watch data after app launch or reconnection. + /// + /// The router only syncs keys that: + /// - Are available on watch (`.all` or `.phoneWithWatchSync`) + /// - Have a non-`.never` sync policy public func syncRegisteredKeysIfNeeded() async { let isAvailable = await sync.isSyncAvailable() guard isAvailable else { @@ -595,6 +620,8 @@ public actor StorageRouter: StorageProviding { } /// Builds a snapshot of syncable key data for immediate watch requests. + /// + /// - Returns: A dictionary mapping key names to secured `Data` payloads. public func syncSnapshot() async -> [String: Data] { let isAvailable = await sync.isSyncAvailable() guard isAvailable else { diff --git a/Sources/LocalData/Utilities/Logger.swift b/Sources/LocalData/Utilities/Logger.swift index f6df9e4..83a1f9f 100644 --- a/Sources/LocalData/Utilities/Logger.swift +++ b/Sources/LocalData/Utilities/Logger.swift @@ -2,8 +2,10 @@ import Foundation /// Internal logging utility for the LocalData package. enum Logger { + /// Enables or disables logging output. static var isLoggingEnabled = true + /// Emits a debug-level log message. static func debug(_ message: String) { #if DEBUG if isLoggingEnabled { @@ -12,6 +14,7 @@ enum Logger { #endif } + /// Emits an info-level log message. static func info(_ message: String) { #if DEBUG if isLoggingEnabled { @@ -20,6 +23,11 @@ enum Logger { #endif } + /// Emits an error-level log message. + /// + /// - Parameters: + /// - message: The message to log. + /// - error: Optional error to include in the output. static func error(_ message: String, error: Error? = nil) { #if DEBUG var logMessage = " {LOCAL_DATA} ❌ \(message)"