Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
d1054e1802
commit
b8ab728c37
12
README.md
12
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
|
||||
|
||||
@ -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) },
|
||||
|
||||
@ -10,6 +10,7 @@ public enum StorageError: Error {
|
||||
case invalidUserDefaultsSuite(String)
|
||||
case dataTooLargeForSync
|
||||
case notFound
|
||||
case unregisteredKey(String)
|
||||
// ...
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
7
Sources/LocalData/Models/StorageKeyEntry.swift
Normal file
7
Sources/LocalData/Models/StorageKeyEntry.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,3 @@
|
||||
public protocol StorageKeyCatalog {
|
||||
static var allKeys: [StorageKeyDescriptor] { get }
|
||||
static var allKeys: [StorageKeyEntry] { get }
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user