From 59e2be1306dc9704b06771160a4e541bb26cd158 Mon Sep 17 00:00:00 2001 From: Matt Bruce Date: Wed, 14 Jan 2026 10:46:09 -0600 Subject: [PATCH] Update Models, Protocols, Services + docs Summary: - Sources: Models, Protocols, Services - Docs: README - Added symbols: struct Serializer, struct StorageKeyEntry, func registerCatalog, func validateCatalogRegistration - Removed symbols: struct Serializer Stats: - 8 files changed, 43 insertions(+), 9 deletions(-) --- README.md | 12 +++++++++--- Sources/LocalData/Models/Serializer.swift | 4 +++- Sources/LocalData/Models/StorageError.swift | 1 + .../Models/StorageKeyDescriptor.swift | 5 ++--- .../LocalData/Models/StorageKeyEntry.swift | 7 +++++++ .../Protocols/StorageKeyCatalog.swift | 2 +- .../Services/StorageAuditReport.swift | 2 +- .../LocalData/Services/StorageRouter.swift | 19 +++++++++++++++++++ 8 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 Sources/LocalData/Models/StorageKeyEntry.swift diff --git a/README.md b/README.md index a35777b..3ecd082 100644 --- a/README.md +++ b/README.md @@ -159,10 +159,10 @@ LocalData can generate a catalog of all configured storage keys, even if no data ```swift struct AppStorageCatalog: StorageKeyCatalog { - static var allKeys: [StorageKeyDescriptor] { + static var allKeys: [StorageKeyEntry] { [ - .from(StorageKeys.AppVersionKey(), serializer: .json), - .from(StorageKeys.UserPreferencesKey(), serializer: .json) + StorageKeyEntry(StorageKeys.AppVersionKey()), + StorageKeyEntry(StorageKeys.UserPreferencesKey()) ] } } @@ -175,6 +175,12 @@ let report = StorageAuditReport.renderText(for: AppStorageCatalog.self) print(report) ``` +3) Register the catalog to enforce usage: + +```swift +StorageRouter.shared.registerCatalog(AppStorageCatalog.self) +``` + For dynamic key names, use a placeholder name and a note to describe how it is generated. ## Sample App diff --git a/Sources/LocalData/Models/Serializer.swift b/Sources/LocalData/Models/Serializer.swift index 7eb5056..8ae3ee9 100644 --- a/Sources/LocalData/Models/Serializer.swift +++ b/Sources/LocalData/Models/Serializer.swift @@ -1,6 +1,6 @@ import Foundation -public struct Serializer: Sendable { +public struct Serializer: Sendable, CustomStringConvertible { public let encode: @Sendable (Value) throws -> Data public let decode: @Sendable (Data) throws -> Value public let name: String @@ -15,6 +15,8 @@ public struct Serializer: Sendable { self.name = name } + public var description: String { name } + public static var json: Serializer { Serializer( encode: { try JSONEncoder().encode($0) }, diff --git a/Sources/LocalData/Models/StorageError.swift b/Sources/LocalData/Models/StorageError.swift index fc1901a..a138375 100644 --- a/Sources/LocalData/Models/StorageError.swift +++ b/Sources/LocalData/Models/StorageError.swift @@ -10,6 +10,7 @@ public enum StorageError: Error { case invalidUserDefaultsSuite(String) case dataTooLargeForSync case notFound + case unregisteredKey(String) // ... } diff --git a/Sources/LocalData/Models/StorageKeyDescriptor.swift b/Sources/LocalData/Models/StorageKeyDescriptor.swift index cb0f071..cb4d9c5 100644 --- a/Sources/LocalData/Models/StorageKeyDescriptor.swift +++ b/Sources/LocalData/Models/StorageKeyDescriptor.swift @@ -11,7 +11,7 @@ public struct StorageKeyDescriptor: Sendable { public let syncPolicy: SyncPolicy public let notes: String? - public init( + init( name: String, domain: StorageDomain, security: SecurityPolicy, @@ -35,14 +35,13 @@ public struct StorageKeyDescriptor: Sendable { public static func from( _ key: Key, - serializer: Serializer, notes: String? = nil ) -> StorageKeyDescriptor { StorageKeyDescriptor( name: key.name, domain: key.domain, security: key.security, - serializer: serializer.name, + serializer: key.serializer.name, valueType: String(describing: Key.Value.self), owner: key.owner, availability: key.availability, diff --git a/Sources/LocalData/Models/StorageKeyEntry.swift b/Sources/LocalData/Models/StorageKeyEntry.swift new file mode 100644 index 0000000..b3f1ac9 --- /dev/null +++ b/Sources/LocalData/Models/StorageKeyEntry.swift @@ -0,0 +1,7 @@ +public struct StorageKeyEntry: Sendable { + public let descriptor: StorageKeyDescriptor + + public init(_ key: Key, notes: String? = nil) { + self.descriptor = .from(key, notes: notes) + } +} diff --git a/Sources/LocalData/Protocols/StorageKeyCatalog.swift b/Sources/LocalData/Protocols/StorageKeyCatalog.swift index a2c7f00..9c2786f 100644 --- a/Sources/LocalData/Protocols/StorageKeyCatalog.swift +++ b/Sources/LocalData/Protocols/StorageKeyCatalog.swift @@ -1,3 +1,3 @@ public protocol StorageKeyCatalog { - static var allKeys: [StorageKeyDescriptor] { get } + static var allKeys: [StorageKeyEntry] { get } } diff --git a/Sources/LocalData/Services/StorageAuditReport.swift b/Sources/LocalData/Services/StorageAuditReport.swift index 50c6c71..b446ba3 100644 --- a/Sources/LocalData/Services/StorageAuditReport.swift +++ b/Sources/LocalData/Services/StorageAuditReport.swift @@ -2,7 +2,7 @@ import Foundation public struct StorageAuditReport: Sendable { public static func items(for catalog: C.Type) -> [StorageKeyDescriptor] { - catalog.allKeys + catalog.allKeys.map(\.descriptor) } public static func renderText(for catalog: C.Type) -> String { diff --git a/Sources/LocalData/Services/StorageRouter.swift b/Sources/LocalData/Services/StorageRouter.swift index c555e13..617b372 100644 --- a/Sources/LocalData/Services/StorageRouter.swift +++ b/Sources/LocalData/Services/StorageRouter.swift @@ -9,6 +9,8 @@ public actor StorageRouter: StorageProviding { public static let shared = StorageRouter() + private var registeredKeyNames: Set = [] + private init() {} // MARK: - Key Material Providers @@ -20,6 +22,12 @@ public actor StorageRouter: StorageProviding { ) async { await EncryptionHelper.shared.registerKeyMaterialProvider(provider, for: source) } + + /// Registers a catalog of known storage keys for audit and validation. + /// When registered, all storage operations will verify keys are listed. + public func registerCatalog(_ catalog: C.Type) { + registeredKeyNames = Set(catalog.allKeys.map { $0.descriptor.name }) + } // MARK: - StorageProviding Implementation @@ -29,6 +37,7 @@ public actor StorageRouter: StorageProviding { /// - 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 { + try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) let data = try serialize(value, with: key.serializer) @@ -43,6 +52,7 @@ public actor StorageRouter: StorageProviding { /// - Returns: The stored value. /// - Throws: `StorageError.notFound` if no value exists, plus domain-specific errors. public func get(_ key: Key) async throws -> Key.Value { + try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) guard let securedData = try await retrieve(for: key) else { @@ -57,6 +67,7 @@ public actor StorageRouter: StorageProviding { /// - Parameter key: The storage key to remove. /// - Throws: Domain-specific errors if removal fails. public func remove(_ key: Key) async throws { + try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) try await delete(for: key) } @@ -65,6 +76,7 @@ public actor StorageRouter: StorageProviding { /// - Parameter key: The storage key to check. /// - Returns: True if a value exists. public func exists(_ key: Key) async throws -> Bool { + try validateCatalogRegistration(for: key) try validatePlatformAvailability(for: key) switch key.domain { @@ -90,6 +102,13 @@ public actor StorageRouter: StorageProviding { } #endif } + + private func validateCatalogRegistration(for key: Key) throws { + guard !registeredKeyNames.isEmpty else { return } + guard registeredKeyNames.contains(key.name) else { + throw StorageError.unregisteredKey(key.name) + } + } // MARK: - Serialization