Update Audit, Protocols, Services + tests + docs
Summary: - Sources: Audit, Protocols, Services - Tests: AuditTests.swift, ModularRegistryTests.swift, RouterErrorTests.swift, StorageCatalogTests.swift, SyncIntegrationTests.swift - Docs: README - Added symbols: protocol StorageKeyCatalog, extension StorageKeyCatalog, func registerCatalog - Removed symbols: protocol StorageKeyCatalog, func registerCatalog Stats: - 9 files changed, 55 insertions(+), 46 deletions(-)
This commit is contained in:
parent
8c8d4a0db8
commit
73d6f7ec8b
15
README.md
15
README.md
@ -201,10 +201,10 @@ When registering a catalog, you can enable `migrateImmediately` to perform a glo
|
|||||||
|
|
||||||
```swift
|
```swift
|
||||||
// Module A
|
// Module A
|
||||||
try await StorageRouter.shared.registerCatalog(AuthCatalog.self)
|
try await StorageRouter.shared.registerCatalog(AuthCatalog())
|
||||||
|
|
||||||
// Module B
|
// Module B
|
||||||
try await StorageRouter.shared.registerCatalog(ProfileCatalog.self)
|
try await StorageRouter.shared.registerCatalog(ProfileCatalog())
|
||||||
```
|
```
|
||||||
|
|
||||||
## Storage Design Philosophy
|
## Storage Design Philosophy
|
||||||
@ -378,27 +378,26 @@ 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: [AnyStorageKey] {
|
let allKeys: [AnyStorageKey] = [
|
||||||
[
|
|
||||||
.key(StorageKeys.AppVersionKey()),
|
.key(StorageKeys.AppVersionKey()),
|
||||||
.key(StorageKeys.UserPreferencesKey())
|
.key(StorageKeys.UserPreferencesKey())
|
||||||
]
|
]
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
2) Generate a report:
|
2) Generate a report:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
let report = StorageAuditReport.renderText(for: AppStorageCatalog.self)
|
let report = StorageAuditReport.renderText(AppStorageCatalog())
|
||||||
print(report)
|
print(report)
|
||||||
```
|
```
|
||||||
|
|
||||||
3) Register the catalog to enforce usage and catch duplicates:
|
3) Create and register the catalog to enforce usage and catch duplicates:
|
||||||
|
|
||||||
```swift
|
```swift
|
||||||
|
let appCatalog = AppStorageCatalog()
|
||||||
do {
|
do {
|
||||||
try await StorageRouter.shared.registerCatalog(AppStorageCatalog.self)
|
try await StorageRouter.shared.registerCatalog(appCatalog)
|
||||||
} catch {
|
} catch {
|
||||||
assertionFailure("Storage catalog registration failed: \(error)")
|
assertionFailure("Storage catalog registration failed: \(error)")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import Foundation
|
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(for catalog: some StorageKeyCatalog) -> [StorageKeyDescriptor] {
|
||||||
catalog.allKeys.map(\.descriptor)
|
catalog.allKeys.map { $0.descriptor.withCatalog(catalog.name) }
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func renderText<C: StorageKeyCatalog>(for catalog: C.Type) -> String {
|
public static func renderText(_ catalog: some StorageKeyCatalog) -> String {
|
||||||
renderText(items(for: catalog))
|
renderText(items(for: catalog))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,3 +1,13 @@
|
|||||||
public protocol StorageKeyCatalog {
|
public protocol StorageKeyCatalog: Sendable {
|
||||||
static var allKeys: [AnyStorageKey] { get }
|
var name: String { get }
|
||||||
|
var allKeys: [AnyStorageKey] { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
extension StorageKeyCatalog {
|
||||||
|
public var name: String {
|
||||||
|
let fullName = String(describing: type(of: self))
|
||||||
|
// Simple cleanup for generic or nested names if needed,
|
||||||
|
// but describes the type clearly enough for audit.
|
||||||
|
return fullName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,13 +76,13 @@ public actor StorageRouter: StorageProviding {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - catalog: The catalog type to register.
|
/// - catalog: The catalog type to register.
|
||||||
/// - migrateImmediately: If true, triggers a proactive migration (sweep) for all keys in the catalog.
|
/// - migrateImmediately: If true, triggers a proactive migration (sweep) for all keys in the catalog.
|
||||||
public func registerCatalog<C: StorageKeyCatalog>(_ catalog: C.Type, migrateImmediately: Bool = false) async throws {
|
public func registerCatalog(_ catalog: some StorageKeyCatalog, migrateImmediately: Bool = false) async throws {
|
||||||
let entries = catalog.allKeys
|
let entries = catalog.allKeys
|
||||||
try validateDescription(entries)
|
try validateDescription(entries)
|
||||||
try validateUniqueKeys(entries)
|
try validateUniqueKeys(entries)
|
||||||
|
|
||||||
// Validate against existing registrations to prevent collisions across catalogs
|
// Validate against existing registrations to prevent collisions across catalogs
|
||||||
let catalogName = String(describing: catalog)
|
let catalogName = catalog.name
|
||||||
Logger.info(">>> [STORAGE] Registering Catalog: \(catalogName) (\(entries.count) keys)")
|
Logger.info(">>> [STORAGE] Registering Catalog: \(catalogName) (\(entries.count) keys)")
|
||||||
|
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import Testing
|
|||||||
@Suite struct AuditTests {
|
@Suite struct AuditTests {
|
||||||
|
|
||||||
private struct AuditCatalog: StorageKeyCatalog {
|
private struct AuditCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[
|
[
|
||||||
.key(TestKey(name: "k1", domain: .userDefaults(suite: nil))),
|
.key(TestKey(name: "k1", domain: .userDefaults(suite: nil))),
|
||||||
.key(TestKey(name: "k2", domain: .keychain(service: "s"), security: .keychain(accessibility: .afterFirstUnlock, accessControl: .userPresence))),
|
.key(TestKey(name: "k2", domain: .keychain(service: "s"), security: .keychain(accessibility: .afterFirstUnlock, accessControl: .userPresence))),
|
||||||
@ -35,7 +35,7 @@ import Testing
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func renderCatalogText() {
|
@Test func renderCatalogText() {
|
||||||
let text = StorageAuditReport.renderText(for: AuditCatalog.self)
|
let text = StorageAuditReport.renderText(AuditCatalog())
|
||||||
|
|
||||||
#expect(text.contains("name=k1"))
|
#expect(text.contains("name=k1"))
|
||||||
#expect(text.contains("domain=userDefaults(standard)"))
|
#expect(text.contains("domain=userDefaults(standard)"))
|
||||||
|
|||||||
@ -21,19 +21,19 @@ private struct TestRegistryKey: StorageKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct CatalogA: StorageKeyCatalog {
|
private struct CatalogA: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[.key(TestRegistryKey(name: "key.a", owner: "ModuleA"))]
|
[.key(TestRegistryKey(name: "key.a", owner: "ModuleA"))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct CatalogB: StorageKeyCatalog {
|
private struct CatalogB: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[.key(TestRegistryKey(name: "key.b", owner: "ModuleB"))]
|
[.key(TestRegistryKey(name: "key.b", owner: "ModuleB"))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct CatalogCollision: StorageKeyCatalog {
|
private struct CatalogCollision: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[.key(TestRegistryKey(name: "key.a", owner: "ModuleCollision"))]
|
[.key(TestRegistryKey(name: "key.a", owner: "ModuleCollision"))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -44,11 +44,11 @@ struct ModularRegistryTests {
|
|||||||
@Test func testAdditiveRegistration() async throws {
|
@Test func testAdditiveRegistration() async throws {
|
||||||
let router = StorageRouter(keychain: MockKeychainHelper())
|
let router = StorageRouter(keychain: MockKeychainHelper())
|
||||||
|
|
||||||
try await router.registerCatalog(CatalogA.self)
|
try await router.registerCatalog(CatalogA())
|
||||||
#expect(await router.allRegisteredEntries().count == 1)
|
#expect(await router.allRegisteredEntries().count == 1)
|
||||||
#expect(await router.allRegisteredEntries().contains { $0.descriptor.name == "key.a" })
|
#expect(await router.allRegisteredEntries().contains { $0.descriptor.name == "key.a" })
|
||||||
|
|
||||||
try await router.registerCatalog(CatalogB.self)
|
try await router.registerCatalog(CatalogB())
|
||||||
#expect(await router.allRegisteredEntries().count == 2)
|
#expect(await router.allRegisteredEntries().count == 2)
|
||||||
#expect(await router.allRegisteredEntries().contains { $0.descriptor.name == "key.b" })
|
#expect(await router.allRegisteredEntries().contains { $0.descriptor.name == "key.b" })
|
||||||
}
|
}
|
||||||
@ -56,18 +56,18 @@ struct ModularRegistryTests {
|
|||||||
@Test func testCollisionDetectionAcrossCatalogs() async throws {
|
@Test func testCollisionDetectionAcrossCatalogs() async throws {
|
||||||
let router = StorageRouter(keychain: MockKeychainHelper())
|
let router = StorageRouter(keychain: MockKeychainHelper())
|
||||||
|
|
||||||
try await router.registerCatalog(CatalogA.self)
|
try await router.registerCatalog(CatalogA())
|
||||||
|
|
||||||
await #expect(throws: StorageError.self) {
|
await #expect(throws: StorageError.self) {
|
||||||
try await router.registerCatalog(CatalogCollision.self)
|
try await router.registerCatalog(CatalogCollision())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func testGlobalAuditReport() async throws {
|
@Test func testGlobalAuditReport() async throws {
|
||||||
let router = StorageRouter(keychain: MockKeychainHelper())
|
let router = StorageRouter(keychain: MockKeychainHelper())
|
||||||
|
|
||||||
try await router.registerCatalog(CatalogA.self)
|
try await router.registerCatalog(CatalogA())
|
||||||
try await router.registerCatalog(CatalogB.self)
|
try await router.registerCatalog(CatalogB())
|
||||||
|
|
||||||
// Note: StorageAuditReport.renderGlobalRegistry() uses the shared router.
|
// Note: StorageAuditReport.renderGlobalRegistry() uses the shared router.
|
||||||
// For testing isolation, we should probably add a parameter to it or
|
// For testing isolation, we should probably add a parameter to it or
|
||||||
@ -88,8 +88,8 @@ struct ModularRegistryTests {
|
|||||||
@Test func testGlobalAuditReportGrouped() async throws {
|
@Test func testGlobalAuditReportGrouped() async throws {
|
||||||
let router = StorageRouter(keychain: MockKeychainHelper())
|
let router = StorageRouter(keychain: MockKeychainHelper())
|
||||||
|
|
||||||
try await router.registerCatalog(CatalogA.self)
|
try await router.registerCatalog(CatalogA())
|
||||||
try await router.registerCatalog(CatalogB.self)
|
try await router.registerCatalog(CatalogB())
|
||||||
|
|
||||||
let catalogs = await router.allRegisteredCatalogs()
|
let catalogs = await router.allRegisteredCatalogs()
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,7 @@ private struct MockKey: StorageKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct PartialCatalog: StorageKeyCatalog {
|
private struct PartialCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[.key(MockKey(name: "registered.key", domain: .userDefaults(suite: nil)))]
|
[.key(MockKey(name: "registered.key", domain: .userDefaults(suite: nil)))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -35,7 +35,7 @@ struct RouterErrorTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func unregisteredKeyThrows() async throws {
|
@Test func unregisteredKeyThrows() async throws {
|
||||||
try await router.registerCatalog(PartialCatalog.self)
|
try await router.registerCatalog(PartialCatalog())
|
||||||
|
|
||||||
let badKey = MockKey(name: "unregistered.key", domain: .userDefaults(suite: nil))
|
let badKey = MockKey(name: "unregistered.key", domain: .userDefaults(suite: nil))
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,7 @@ private struct TestCatalogKey: StorageKey {
|
|||||||
// MARK: - Test Catalogs
|
// MARK: - Test Catalogs
|
||||||
|
|
||||||
private struct ValidCatalog: StorageKeyCatalog {
|
private struct ValidCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[
|
[
|
||||||
.key(TestCatalogKey(name: "valid.key1", description: "First test key")),
|
.key(TestCatalogKey(name: "valid.key1", description: "First test key")),
|
||||||
.key(TestCatalogKey(name: "valid.key2", description: "Second test key"))
|
.key(TestCatalogKey(name: "valid.key2", description: "Second test key"))
|
||||||
@ -34,7 +34,7 @@ private struct ValidCatalog: StorageKeyCatalog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct DuplicateNameCatalog: StorageKeyCatalog {
|
private struct DuplicateNameCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[
|
[
|
||||||
.key(TestCatalogKey(name: "duplicate.name", description: "First instance")),
|
.key(TestCatalogKey(name: "duplicate.name", description: "First instance")),
|
||||||
.key(TestCatalogKey(name: "duplicate.name", description: "Second instance"))
|
.key(TestCatalogKey(name: "duplicate.name", description: "Second instance"))
|
||||||
@ -43,11 +43,11 @@ private struct DuplicateNameCatalog: StorageKeyCatalog {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct EmptyCatalog: StorageKeyCatalog {
|
private struct EmptyCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] { [] }
|
var allKeys: [AnyStorageKey] { [] }
|
||||||
}
|
}
|
||||||
|
|
||||||
private struct MissingDescriptionCatalog: StorageKeyCatalog {
|
private struct MissingDescriptionCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[
|
[
|
||||||
.key(TestCatalogKey(name: "missing.desc", description: " "))
|
.key(TestCatalogKey(name: "missing.desc", description: " "))
|
||||||
]
|
]
|
||||||
@ -61,7 +61,7 @@ struct StorageCatalogTests {
|
|||||||
private let router = StorageRouter(keychain: MockKeychainHelper())
|
private let router = StorageRouter(keychain: MockKeychainHelper())
|
||||||
|
|
||||||
@Test func auditReportContainsAllKeys() {
|
@Test func auditReportContainsAllKeys() {
|
||||||
let items = StorageAuditReport.items(for: ValidCatalog.self)
|
let items = StorageAuditReport.items(for: ValidCatalog())
|
||||||
|
|
||||||
#expect(items.count == 2)
|
#expect(items.count == 2)
|
||||||
#expect(items[0].name == "valid.key1")
|
#expect(items[0].name == "valid.key1")
|
||||||
@ -69,7 +69,7 @@ struct StorageCatalogTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func auditReportRendersText() {
|
@Test func auditReportRendersText() {
|
||||||
let report = StorageAuditReport.renderText(for: ValidCatalog.self)
|
let report = StorageAuditReport.renderText(ValidCatalog())
|
||||||
|
|
||||||
#expect(report.contains("valid.key1"))
|
#expect(report.contains("valid.key1"))
|
||||||
#expect(report.contains("valid.key2"))
|
#expect(report.contains("valid.key2"))
|
||||||
@ -89,30 +89,30 @@ struct StorageCatalogTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test func emptyReportForEmptyCatalog() {
|
@Test func emptyReportForEmptyCatalog() {
|
||||||
let items = StorageAuditReport.items(for: EmptyCatalog.self)
|
let items = StorageAuditReport.items(for: EmptyCatalog())
|
||||||
#expect(items.isEmpty)
|
#expect(items.isEmpty)
|
||||||
|
|
||||||
let report = StorageAuditReport.renderText(for: EmptyCatalog.self)
|
let report = StorageAuditReport.renderText(EmptyCatalog())
|
||||||
#expect(report.isEmpty)
|
#expect(report.isEmpty)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func catalogRegistrationDetectsDuplicates() async {
|
@Test func catalogRegistrationDetectsDuplicates() async {
|
||||||
// Attempting to register a catalog with duplicate key names should throw
|
// Attempting to register a catalog with duplicate key names should throw
|
||||||
await #expect(throws: StorageError.self) {
|
await #expect(throws: StorageError.self) {
|
||||||
try await router.registerCatalog(DuplicateNameCatalog.self)
|
try await router.registerCatalog(DuplicateNameCatalog())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func catalogRegistrationDetectsMissingDescriptions() async {
|
@Test func catalogRegistrationDetectsMissingDescriptions() async {
|
||||||
// Attempting to register a catalog with missing descriptions should throw
|
// Attempting to register a catalog with missing descriptions should throw
|
||||||
await #expect(throws: StorageError.self) {
|
await #expect(throws: StorageError.self) {
|
||||||
try await router.registerCatalog(MissingDescriptionCatalog.self)
|
try await router.registerCatalog(MissingDescriptionCatalog())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func migrateAllRegisteredKeysInvokesMigrationOnKeys() async throws {
|
@Test func migrateAllRegisteredKeysInvokesMigrationOnKeys() async throws {
|
||||||
// This test verifies that migrateAllRegisteredKeys calling logic works.
|
// This test verifies that migrateAllRegisteredKeys calling logic works.
|
||||||
try await router.registerCatalog(ValidCatalog.self)
|
try await router.registerCatalog(ValidCatalog())
|
||||||
|
|
||||||
// No error should occur
|
// No error should occur
|
||||||
try await router.migrateAllRegisteredKeys()
|
try await router.migrateAllRegisteredKeys()
|
||||||
|
|||||||
@ -29,14 +29,14 @@ struct SyncIntegrationTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private struct SyncCatalog: StorageKeyCatalog {
|
private struct SyncCatalog: StorageKeyCatalog {
|
||||||
static var allKeys: [AnyStorageKey] {
|
var allKeys: [AnyStorageKey] {
|
||||||
[.key(SyncKey(name: "sync.test.key"))]
|
[.key(SyncKey(name: "sync.test.key"))]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test func updateFromSyncStoresDataLocally() async throws {
|
@Test func updateFromSyncStoresDataLocally() async throws {
|
||||||
// 1. Register catalog so router knows about the key
|
// 1. Register catalog so router knows about the key
|
||||||
try await router.registerCatalog(SyncCatalog.self)
|
try await router.registerCatalog(SyncCatalog())
|
||||||
|
|
||||||
let keyName = "sync.test.key"
|
let keyName = "sync.test.key"
|
||||||
let expectedValue = "Updated from Watch"
|
let expectedValue = "Updated from Watch"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user