Add Documentation.docc
Summary: - Sources: add Documentation.docc Stats: - 7 files changed, 339 insertions(+)
This commit is contained in:
parent
aa33326198
commit
1954ce1ff0
58
Sources/LocalData/Documentation.docc/GettingStarted.md
Normal file
58
Sources/LocalData/Documentation.docc/GettingStarted.md
Normal file
@ -0,0 +1,58 @@
|
||||
# Getting Started
|
||||
|
||||
Follow this guide to define keys, configure LocalData, and register catalogs for auditing.
|
||||
|
||||
## 1) Define keys
|
||||
|
||||
Keys are static, typed constants. Do not use dynamic key names.
|
||||
|
||||
```swift
|
||||
import LocalData
|
||||
|
||||
extension StorageKey where Value == String {
|
||||
static let userToken = StorageKey(
|
||||
name: "user_token",
|
||||
domain: .keychain(service: "com.myapp"),
|
||||
owner: "Auth",
|
||||
description: "Current user auth token."
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 2) Configure the router (optional)
|
||||
|
||||
Set defaults once at app launch to avoid repeating service identifiers.
|
||||
|
||||
```swift
|
||||
let storageConfig = StorageConfiguration(
|
||||
defaultKeychainService: "com.myapp.keychain",
|
||||
defaultAppGroupIdentifier: "group.com.myapp"
|
||||
)
|
||||
await StorageRouter.shared.updateStorageConfiguration(storageConfig)
|
||||
```
|
||||
|
||||
## 3) Register catalogs
|
||||
|
||||
Catalogs are mandatory if you want auditing and duplicate detection.
|
||||
|
||||
```swift
|
||||
struct AuthCatalog: StorageKeyCatalog {
|
||||
let allKeys: [AnyStorageKey] = [
|
||||
.key(StorageKey.userToken)
|
||||
]
|
||||
}
|
||||
|
||||
try await StorageRouter.shared.registerCatalog(AuthCatalog())
|
||||
```
|
||||
|
||||
## 4) Store and read values
|
||||
|
||||
```swift
|
||||
try await StorageRouter.shared.set("token", for: .userToken)
|
||||
let token = try await StorageRouter.shared.get(.userToken)
|
||||
```
|
||||
|
||||
## Next steps
|
||||
|
||||
- Read <doc:KeyAndCatalogDiscipline> to avoid audit gaps.
|
||||
- Add migrations in <doc:Migrations>.
|
||||
@ -0,0 +1,39 @@
|
||||
# Key and Catalog Discipline
|
||||
|
||||
This package is designed for static, discoverable keys. Treat your key catalog as your storage manifest.
|
||||
|
||||
## Static keys only
|
||||
|
||||
Do not generate dynamic key names at runtime. If a value can exist, it should have a corresponding
|
||||
static ``StorageKey`` definition. This keeps audits accurate and prevents invisible storage usage.
|
||||
|
||||
If you must represent multiple instances (for example, per-account settings), create a static key
|
||||
for each supported slot or store a dictionary keyed by account ID inside a single ``StorageKey``.
|
||||
|
||||
## Avoid audit gaps
|
||||
|
||||
Catalog registration enforces correctness:
|
||||
|
||||
- Missing descriptions are rejected.
|
||||
- Duplicate key names across catalogs are rejected.
|
||||
- Unregistered keys throw ``StorageError/unregisteredKey(_:)`` at runtime.
|
||||
|
||||
If you skip catalog registration, LocalData cannot guarantee that all writes are auditable.
|
||||
|
||||
## Modular catalogs
|
||||
|
||||
Split catalogs by feature or module and register them incrementally:
|
||||
|
||||
```swift
|
||||
try await StorageRouter.shared.registerCatalog(AuthCatalog())
|
||||
try await StorageRouter.shared.registerCatalog(ProfileCatalog())
|
||||
```
|
||||
|
||||
Registration is additive. A collision in key names across catalogs throws an error.
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Define keys in `StorageKeys/` per feature.
|
||||
2. Create a catalog for each feature.
|
||||
3. Register all catalogs on app launch.
|
||||
4. Run ``StorageAuditReport`` to verify output.
|
||||
43
Sources/LocalData/Documentation.docc/LocalData.md
Normal file
43
Sources/LocalData/Documentation.docc/LocalData.md
Normal file
@ -0,0 +1,43 @@
|
||||
# LocalData
|
||||
|
||||
LocalData is a typed, auditable storage layer that unifies UserDefaults, Keychain, and file storage under a single API. It favors static, discoverable keys and explicit catalogs so teams can reason about what is stored, where it lives, and how it is secured.
|
||||
|
||||
## Overview
|
||||
|
||||
LocalData revolves around three concepts:
|
||||
|
||||
- ``StorageKey`` defines storage metadata for a single value.
|
||||
- ``StorageRouter`` coordinates serialization, security, and routing.
|
||||
- ``StorageKeyCatalog`` registers keys for auditing and migration.
|
||||
|
||||
## Topics
|
||||
|
||||
### Getting Started
|
||||
|
||||
- <doc:GettingStarted>
|
||||
- <doc:KeyAndCatalogDiscipline>
|
||||
|
||||
### Migrations
|
||||
|
||||
- <doc:Migrations>
|
||||
|
||||
### Sync
|
||||
|
||||
- <doc:WatchSync>
|
||||
|
||||
### Security
|
||||
|
||||
- <doc:Security>
|
||||
|
||||
### Testing
|
||||
|
||||
- <doc:Testing>
|
||||
|
||||
### Reference
|
||||
|
||||
- ``StorageRouter``
|
||||
- ``StorageKey``
|
||||
- ``StorageKeyCatalog``
|
||||
- ``StorageDomain``
|
||||
- ``SecurityPolicy``
|
||||
- ``StorageError``
|
||||
72
Sources/LocalData/Documentation.docc/Migrations.md
Normal file
72
Sources/LocalData/Documentation.docc/Migrations.md
Normal file
@ -0,0 +1,72 @@
|
||||
# Migrations
|
||||
|
||||
LocalData supports lazy and proactive migrations to move data from legacy keys to modern keys.
|
||||
|
||||
## Lazy migrations (on read)
|
||||
|
||||
Attach a migration to a key. If the destination is missing, `StorageRouter.get(_:)` will run it.
|
||||
|
||||
```swift
|
||||
extension StorageKey where Value == String {
|
||||
static let legacyToken = StorageKey(
|
||||
name: "legacy_token",
|
||||
domain: .userDefaults(suite: nil),
|
||||
security: .none,
|
||||
owner: "Auth",
|
||||
description: "Legacy token."
|
||||
)
|
||||
|
||||
static let userToken = StorageKey(
|
||||
name: "user_token",
|
||||
domain: .keychain(service: "com.myapp"),
|
||||
owner: "Auth",
|
||||
description: "Modern token.",
|
||||
migration: { destination in
|
||||
AnyStorageMigration(
|
||||
SimpleLegacyMigration(
|
||||
destinationKey: destination,
|
||||
sourceKey: .key(StorageKey.legacyToken)
|
||||
)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Proactive sweeps
|
||||
|
||||
Run migrations at startup to drain legacy values:
|
||||
|
||||
```swift
|
||||
try await StorageRouter.shared.registerCatalog(AuthCatalog(), migrateImmediately: true)
|
||||
```
|
||||
|
||||
## Transforming migrations
|
||||
|
||||
Use `DefaultTransformingMigration` when types change.
|
||||
|
||||
```swift
|
||||
let migration = DefaultTransformingMigration(
|
||||
destinationKey: StorageKey.userAge,
|
||||
sourceKey: StorageKey.legacyAgeString
|
||||
) { value in
|
||||
guard let intValue = Int(value) else {
|
||||
throw MigrationError.transformationFailed("Invalid age")
|
||||
}
|
||||
return intValue
|
||||
}
|
||||
```
|
||||
|
||||
## Aggregating migrations
|
||||
|
||||
Combine multiple legacy values into one destination:
|
||||
|
||||
```swift
|
||||
let migration = DefaultAggregatingMigration(
|
||||
destinationKey: StorageKey.profileSummary,
|
||||
sourceKeys: [.key(StorageKey.firstName), .key(StorageKey.lastName)]
|
||||
) { sources in
|
||||
let parts = sources.compactMap { $0.value as? String }
|
||||
return parts.joined(separator: " ")
|
||||
}
|
||||
```
|
||||
61
Sources/LocalData/Documentation.docc/Security.md
Normal file
61
Sources/LocalData/Documentation.docc/Security.md
Normal file
@ -0,0 +1,61 @@
|
||||
# Security
|
||||
|
||||
Security is declared per key using ``SecurityPolicy``.
|
||||
|
||||
## Recommended default
|
||||
|
||||
Use the default security policy unless you have a specific reason not to:
|
||||
|
||||
```swift
|
||||
let key = StorageKey(
|
||||
name: "secure_value",
|
||||
domain: .fileSystem(directory: .documents),
|
||||
owner: "Security",
|
||||
description: "Sensitive value stored with recommended policy."
|
||||
)
|
||||
```
|
||||
|
||||
## Keychain security
|
||||
|
||||
Store data directly in Keychain with accessibility and access control:
|
||||
|
||||
```swift
|
||||
let key = StorageKey(
|
||||
name: "token",
|
||||
domain: .keychain(service: "com.myapp"),
|
||||
security: .keychain(
|
||||
accessibility: .afterFirstUnlock,
|
||||
accessControl: .biometryAny
|
||||
),
|
||||
owner: "Auth",
|
||||
description: "Auth token."
|
||||
)
|
||||
```
|
||||
|
||||
## Encrypted file storage
|
||||
|
||||
Use encryption for file-based storage:
|
||||
|
||||
```swift
|
||||
let key = StorageKey(
|
||||
name: "secret_file",
|
||||
domain: .encryptedFileSystem(directory: .documents),
|
||||
owner: "Vault",
|
||||
description: "Encrypted file data."
|
||||
)
|
||||
```
|
||||
|
||||
## External key material
|
||||
|
||||
Register a provider and reference it in the policy:
|
||||
|
||||
```swift
|
||||
struct RemoteKeyProvider: KeyMaterialProviding {
|
||||
func keyMaterial(for keyName: String) async throws -> Data {
|
||||
Data(repeating: 1, count: 32)
|
||||
}
|
||||
}
|
||||
|
||||
let source = KeyMaterialSource(id: "remote.key")
|
||||
await StorageRouter.shared.registerKeyMaterialProvider(RemoteKeyProvider(), for: source)
|
||||
```
|
||||
32
Sources/LocalData/Documentation.docc/Testing.md
Normal file
32
Sources/LocalData/Documentation.docc/Testing.md
Normal file
@ -0,0 +1,32 @@
|
||||
# Testing
|
||||
|
||||
Use isolated helpers and custom configurations to avoid polluting real storage.
|
||||
|
||||
## Use temporary file locations
|
||||
|
||||
```swift
|
||||
let testURL = FileManager.default.temporaryDirectory
|
||||
.appending(path: "LocalDataTests-\(UUID().uuidString)")
|
||||
|
||||
let router = StorageRouter(
|
||||
keychain: MockKeychainHelper(),
|
||||
encryption: EncryptionHelper(keychain: MockKeychainHelper()),
|
||||
file: FileStorageHelper(configuration: FileStorageConfiguration(baseURL: testURL)),
|
||||
defaults: UserDefaultsHelper(defaults: UserDefaults(suiteName: "LocalDataTests")!)
|
||||
)
|
||||
```
|
||||
|
||||
## App Group identifiers
|
||||
|
||||
Use a mock `StorageConfiguration` to avoid real entitlements:
|
||||
|
||||
```swift
|
||||
await router.updateStorageConfiguration(
|
||||
StorageConfiguration(defaultAppGroupIdentifier: "group.test")
|
||||
)
|
||||
```
|
||||
|
||||
## Keychain in simulator
|
||||
|
||||
Keychain tests in the simulator often require a host app with entitlements.
|
||||
If you see `-34018`, ensure the test target has a host app and keychain sharing.
|
||||
34
Sources/LocalData/Documentation.docc/WatchSync.md
Normal file
34
Sources/LocalData/Documentation.docc/WatchSync.md
Normal file
@ -0,0 +1,34 @@
|
||||
# Watch Sync
|
||||
|
||||
LocalData supports WatchConnectivity application context sync for eligible keys.
|
||||
|
||||
## Eligibility rules
|
||||
|
||||
A key syncs only when:
|
||||
|
||||
- `availability` is `.all` or `.phoneWithWatchSync`
|
||||
- `syncPolicy` is `.manual` or `.automaticSmall`
|
||||
|
||||
For `.automaticSmall`, the payload must be below `SyncConfiguration.maxAutoSyncSize`.
|
||||
|
||||
## Bootstrapping at launch
|
||||
|
||||
Call this on iOS after session activation or reachability changes:
|
||||
|
||||
```swift
|
||||
await StorageRouter.shared.syncRegisteredKeysIfNeeded()
|
||||
```
|
||||
|
||||
## Responding to watch requests
|
||||
|
||||
Build a snapshot and return it via `WCSession`:
|
||||
|
||||
```swift
|
||||
let snapshot = await StorageRouter.shared.syncSnapshot()
|
||||
```
|
||||
|
||||
## Best practices
|
||||
|
||||
- Activate `WCSession` in the app, not in LocalData.
|
||||
- Use a launch-order-safe handshake (watch first or phone first).
|
||||
- Avoid large payloads; prefer smaller derived values for watch display.
|
||||
Loading…
Reference in New Issue
Block a user