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
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

View File

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

View File

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

View File

@ -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: StorageKey>(
_ key: Key,
serializer: Serializer<Key.Value>,
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,

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 {
static var allKeys: [StorageKeyDescriptor] { get }
static var allKeys: [StorageKeyEntry] { get }
}

View File

@ -2,7 +2,7 @@ import Foundation
public struct StorageAuditReport: Sendable {
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 {

View File

@ -9,6 +9,8 @@ public actor StorageRouter: StorageProviding {
public static let shared = StorageRouter()
private var registeredKeyNames: Set<String> = []
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<C: StorageKeyCatalog>(_ 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<Key: StorageKey>(_ 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: StorageKey>(_ 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: StorageKey>(_ 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: StorageKey>(_ 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<Key: StorageKey>(for key: Key) throws {
guard !registeredKeyNames.isEmpty else { return }
guard registeredKeyNames.contains(key.name) else {
throw StorageError.unregisteredKey(key.name)
}
}
// MARK: - Serialization