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(-)
This commit is contained in:
Matt Bruce 2026-01-14 10:46:09 -06:00
parent 91aa0df7f4
commit 59e2be1306
8 changed files with 43 additions and 9 deletions

View File

@ -159,10 +159,10 @@ LocalData can generate a catalog of all configured storage keys, even if no data
```swift ```swift
struct AppStorageCatalog: StorageKeyCatalog { struct AppStorageCatalog: StorageKeyCatalog {
static var allKeys: [StorageKeyDescriptor] { static var allKeys: [StorageKeyEntry] {
[ [
.from(StorageKeys.AppVersionKey(), serializer: .json), StorageKeyEntry(StorageKeys.AppVersionKey()),
.from(StorageKeys.UserPreferencesKey(), serializer: .json) StorageKeyEntry(StorageKeys.UserPreferencesKey())
] ]
} }
} }
@ -175,6 +175,12 @@ let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
print(report) 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. For dynamic key names, use a placeholder name and a note to describe how it is generated.
## Sample App ## Sample App

View File

@ -1,6 +1,6 @@
import Foundation import Foundation
public struct Serializer<Value: Codable & Sendable>: Sendable { public struct Serializer<Value: Codable & Sendable>: Sendable, CustomStringConvertible {
public let encode: @Sendable (Value) throws -> Data public let encode: @Sendable (Value) throws -> Data
public let decode: @Sendable (Data) throws -> Value public let decode: @Sendable (Data) throws -> Value
public let name: String public let name: String
@ -15,6 +15,8 @@ public struct Serializer<Value: Codable & Sendable>: Sendable {
self.name = name self.name = name
} }
public var description: String { name }
public static var json: Serializer<Value> { public static var json: Serializer<Value> {
Serializer<Value>( Serializer<Value>(
encode: { try JSONEncoder().encode($0) }, encode: { try JSONEncoder().encode($0) },

View File

@ -10,6 +10,7 @@ public enum StorageError: Error {
case invalidUserDefaultsSuite(String) case invalidUserDefaultsSuite(String)
case dataTooLargeForSync case dataTooLargeForSync
case notFound case notFound
case unregisteredKey(String)
// ... // ...
} }

View File

@ -11,7 +11,7 @@ public struct StorageKeyDescriptor: Sendable {
public let syncPolicy: SyncPolicy public let syncPolicy: SyncPolicy
public let notes: String? public let notes: String?
public init( init(
name: String, name: String,
domain: StorageDomain, domain: StorageDomain,
security: SecurityPolicy, security: SecurityPolicy,
@ -35,14 +35,13 @@ public struct StorageKeyDescriptor: Sendable {
public static func from<Key: StorageKey>( public static func from<Key: StorageKey>(
_ key: Key, _ key: Key,
serializer: Serializer<Key.Value>,
notes: String? = nil notes: String? = nil
) -> StorageKeyDescriptor { ) -> StorageKeyDescriptor {
StorageKeyDescriptor( StorageKeyDescriptor(
name: key.name, name: key.name,
domain: key.domain, domain: key.domain,
security: key.security, security: key.security,
serializer: serializer.name, serializer: key.serializer.name,
valueType: String(describing: Key.Value.self), valueType: String(describing: Key.Value.self),
owner: key.owner, owner: key.owner,
availability: key.availability, availability: key.availability,

View File

@ -0,0 +1,7 @@
public struct StorageKeyEntry: Sendable {
public let descriptor: StorageKeyDescriptor
public init<Key: StorageKey>(_ key: Key, notes: String? = nil) {
self.descriptor = .from(key, notes: notes)
}
}

View File

@ -1,3 +1,3 @@
public protocol StorageKeyCatalog { public protocol StorageKeyCatalog {
static var allKeys: [StorageKeyDescriptor] { get } static var allKeys: [StorageKeyEntry] { get }
} }

View File

@ -2,7 +2,7 @@ import Foundation
public struct StorageAuditReport: Sendable { public struct StorageAuditReport: Sendable {
public static func items<C: StorageKeyCatalog>(for catalog: C.Type) -> [StorageKeyDescriptor] { public static func items<C: StorageKeyCatalog>(for catalog: C.Type) -> [StorageKeyDescriptor] {
catalog.allKeys catalog.allKeys.map(\.descriptor)
} }
public static func renderText<C: StorageKeyCatalog>(for catalog: C.Type) -> String { public static func renderText<C: StorageKeyCatalog>(for catalog: C.Type) -> String {

View File

@ -9,6 +9,8 @@ public actor StorageRouter: StorageProviding {
public static let shared = StorageRouter() public static let shared = StorageRouter()
private var registeredKeyNames: Set<String> = []
private init() {} private init() {}
// MARK: - Key Material Providers // MARK: - Key Material Providers
@ -21,6 +23,12 @@ public actor StorageRouter: StorageProviding {
await EncryptionHelper.shared.registerKeyMaterialProvider(provider, for: source) 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<C: StorageKeyCatalog>(_ catalog: C.Type) {
registeredKeyNames = Set(catalog.allKeys.map { $0.descriptor.name })
}
// MARK: - StorageProviding Implementation // MARK: - StorageProviding Implementation
/// Stores a value for the given key. /// Stores a value for the given key.
@ -29,6 +37,7 @@ public actor StorageRouter: StorageProviding {
/// - key: The storage key defining where and how to store. /// - key: The storage key defining where and how to store.
/// - Throws: Various errors depending on the storage domain and security policy. /// - Throws: Various errors depending on the storage domain and security policy.
public func set<Key: StorageKey>(_ value: Key.Value, for key: Key) async throws { public func set<Key: StorageKey>(_ value: Key.Value, for key: Key) async throws {
try validateCatalogRegistration(for: key)
try validatePlatformAvailability(for: key) try validatePlatformAvailability(for: key)
let data = try serialize(value, with: key.serializer) let data = try serialize(value, with: key.serializer)
@ -43,6 +52,7 @@ public actor StorageRouter: StorageProviding {
/// - Returns: The stored value. /// - Returns: The stored value.
/// - Throws: `StorageError.notFound` if no value exists, plus domain-specific errors. /// - Throws: `StorageError.notFound` if no value exists, plus domain-specific errors.
public func get<Key: StorageKey>(_ key: Key) async throws -> Key.Value { public func get<Key: StorageKey>(_ key: Key) async throws -> Key.Value {
try validateCatalogRegistration(for: key)
try validatePlatformAvailability(for: key) try validatePlatformAvailability(for: key)
guard let securedData = try await retrieve(for: key) else { guard let securedData = try await retrieve(for: key) else {
@ -57,6 +67,7 @@ public actor StorageRouter: StorageProviding {
/// - Parameter key: The storage key to remove. /// - Parameter key: The storage key to remove.
/// - Throws: Domain-specific errors if removal fails. /// - Throws: Domain-specific errors if removal fails.
public func remove<Key: StorageKey>(_ key: Key) async throws { public func remove<Key: StorageKey>(_ key: Key) async throws {
try validateCatalogRegistration(for: key)
try validatePlatformAvailability(for: key) try validatePlatformAvailability(for: key)
try await delete(for: key) try await delete(for: key)
} }
@ -65,6 +76,7 @@ public actor StorageRouter: StorageProviding {
/// - Parameter key: The storage key to check. /// - Parameter key: The storage key to check.
/// - Returns: True if a value exists. /// - Returns: True if a value exists.
public func exists<Key: StorageKey>(_ key: Key) async throws -> Bool { public func exists<Key: StorageKey>(_ key: Key) async throws -> Bool {
try validateCatalogRegistration(for: key)
try validatePlatformAvailability(for: key) try validatePlatformAvailability(for: key)
switch key.domain { switch key.domain {
@ -91,6 +103,13 @@ public actor StorageRouter: StorageProviding {
#endif #endif
} }
private func validateCatalogRegistration<Key: StorageKey>(for key: Key) throws {
guard !registeredKeyNames.isEmpty else { return }
guard registeredKeyNames.contains(key.name) else {
throw StorageError.unregisteredKey(key.name)
}
}
// MARK: - Serialization // MARK: - Serialization
private func serialize<Value: Codable & Sendable>( private func serialize<Value: Codable & Sendable>(