Update Protocols, Services, Utilities and docs
Summary: - Sources: update Protocols, Services, Utilities - Docs: update docs for README Stats: - 4 files changed, 108 insertions(+), 36 deletions(-)
This commit is contained in:
parent
71e142f36d
commit
8e63375b34
31
README.md
31
README.md
@ -131,7 +131,36 @@ let token = try await StorageRouter.shared.get(key)
|
||||
try await StorageRouter.shared.remove(key)
|
||||
```
|
||||
|
||||
## Storage Domains
|
||||
### Complex Codable Support
|
||||
LocalData handles complex `Codable` types automatically. You are not limited to simple strings or integers.
|
||||
|
||||
```swift
|
||||
struct UserProfile: Codable {
|
||||
let id: UUID
|
||||
let name: String
|
||||
let settings: [String: String]
|
||||
}
|
||||
|
||||
extension StorageKeys {
|
||||
struct ProfileKey: StorageKey {
|
||||
typealias Value = UserProfile // Library handles serialization
|
||||
let name = "user_profile"
|
||||
let domain: StorageDomain = .fileSystem(directory: .documents)
|
||||
// ... other properties
|
||||
}
|
||||
}
|
||||
|
||||
## Storage Design Philosophy
|
||||
|
||||
This app intentionally uses a **Type-Safe Storage Design**. Unlike standard iOS development which uses string keys (e.g., `UserDefaults.standard.string(forKey: "user_name")`), this library requires you to define a `StorageKey` type.
|
||||
|
||||
### Why types instead of strings?
|
||||
1. **Safety**: The compiler prevents typos. You can't accidentally load from `"user_name"` and save to `"username"`.
|
||||
2. **Codable Support**: Keys define their own value types. You can store complex `Codable` structs or classes just as easily as strings, and the library handles the JSON/Plist serialization automatically.
|
||||
3. **Visibility**: All data your app stores is discoverable in the `StorageKeys/` folder. It serves as a manifest of your app's persistence layer.
|
||||
4. **Migration**: You can move a piece of data from `UserDefaults` to `EncryptedFileSystem` just by changing the `domain` in the Key definition. No UI code needs to change.
|
||||
|
||||
## Storage Key Examples
|
||||
|
||||
| Domain | Use Case |
|
||||
|--------|----------|
|
||||
|
||||
@ -11,4 +11,11 @@ public protocol StorageKey: Sendable, CustomStringConvertible {
|
||||
|
||||
var availability: PlatformAvailability { get }
|
||||
var syncPolicy: SyncPolicy { get }
|
||||
|
||||
/// Optional list of legacy keys to migrate data from if this key is empty.
|
||||
var migrationSources: [AnyStorageKey] { get }
|
||||
}
|
||||
|
||||
extension StorageKey {
|
||||
public var migrationSources: [AnyStorageKey] { [] }
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ public actor StorageRouter: StorageProviding {
|
||||
try validatePlatformAvailability(for: key)
|
||||
|
||||
let data = try serialize(value, with: key.serializer)
|
||||
let securedData = try await applySecurity(data, for: key, isEncrypt: true)
|
||||
let securedData = try await applySecurity(data, for: .from(key), isEncrypt: true)
|
||||
|
||||
try await store(securedData, for: key)
|
||||
try await handleSync(key, data: securedData)
|
||||
@ -88,15 +88,42 @@ public actor StorageRouter: StorageProviding {
|
||||
try validateCatalogRegistration(for: key)
|
||||
try validatePlatformAvailability(for: key)
|
||||
|
||||
guard let securedData = try await retrieve(for: key) else {
|
||||
Logger.debug("<<< [STORAGE] GET NOT FOUND: \(key.name)")
|
||||
throw StorageError.notFound
|
||||
// 1. Try primary location
|
||||
if let securedData = try await retrieve(for: .from(key)) {
|
||||
let data = try await applySecurity(securedData, for: .from(key), isEncrypt: false)
|
||||
let result = try deserialize(data, with: key.serializer)
|
||||
Logger.debug("<<< [STORAGE] GET SUCCESS: \(key.name)")
|
||||
return result
|
||||
}
|
||||
|
||||
let data = try await applySecurity(securedData, for: key, isEncrypt: false)
|
||||
let result = try deserialize(data, with: key.serializer)
|
||||
Logger.debug("<<< [STORAGE] GET SUCCESS: \(key.name)")
|
||||
return result
|
||||
// 2. Try migration sources
|
||||
for source in key.migrationSources {
|
||||
let descriptor = source.descriptor
|
||||
Logger.debug("!!! [STORAGE] MIGRATION: Checking source '\(descriptor.name)' for key '\(key.name)'")
|
||||
|
||||
if let securedOldData = try await retrieve(for: descriptor) {
|
||||
Logger.info("!!! [STORAGE] MIGRATION: Found data for '\(key.name)' at legacy source '\(descriptor.name)'. Migrating...")
|
||||
|
||||
// Unsecure using OLD key's policy
|
||||
let oldData = try await applySecurity(securedOldData, for: descriptor, isEncrypt: false)
|
||||
|
||||
// Decode using NEW key's serializer (assuming types are compatible)
|
||||
let value = try deserialize(oldData, with: key.serializer)
|
||||
|
||||
// Store in NEW location with NEW security
|
||||
// This will also handle sync and catalog validation via recursive call to public set
|
||||
try await self.set(value, for: key)
|
||||
|
||||
// Delete OLD data
|
||||
try await delete(for: descriptor)
|
||||
|
||||
Logger.info("!!! [STORAGE] MIGRATION: Successfully migrated '\(key.name)' from '\(descriptor.name)'")
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
Logger.debug("<<< [STORAGE] GET NOT FOUND: \(key.name)")
|
||||
throw StorageError.notFound
|
||||
}
|
||||
|
||||
/// Removes the value for the given key.
|
||||
@ -105,7 +132,7 @@ public actor StorageRouter: StorageProviding {
|
||||
public func remove<Key: StorageKey>(_ key: Key) async throws {
|
||||
try validateCatalogRegistration(for: key)
|
||||
try validatePlatformAvailability(for: key)
|
||||
try await delete(for: key)
|
||||
try await delete(for: .from(key))
|
||||
}
|
||||
|
||||
/// Checks if a value exists for the given key.
|
||||
@ -216,10 +243,10 @@ public actor StorageRouter: StorageProviding {
|
||||
|
||||
private func applySecurity(
|
||||
_ data: Data,
|
||||
for key: any StorageKey,
|
||||
for descriptor: StorageKeyDescriptor,
|
||||
isEncrypt: Bool
|
||||
) async throws -> Data {
|
||||
switch key.security {
|
||||
switch descriptor.security {
|
||||
case .none:
|
||||
return data
|
||||
|
||||
@ -227,13 +254,13 @@ public actor StorageRouter: StorageProviding {
|
||||
if isEncrypt {
|
||||
return try await EncryptionHelper.shared.encrypt(
|
||||
data,
|
||||
keyName: key.name,
|
||||
keyName: descriptor.name,
|
||||
policy: encryptionPolicy
|
||||
)
|
||||
} else {
|
||||
return try await EncryptionHelper.shared.decrypt(
|
||||
data,
|
||||
keyName: key.name,
|
||||
keyName: descriptor.name,
|
||||
policy: encryptionPolicy
|
||||
)
|
||||
}
|
||||
@ -247,23 +274,24 @@ public actor StorageRouter: StorageProviding {
|
||||
// MARK: - Storage Operations
|
||||
|
||||
private func store(_ data: Data, for key: any StorageKey) async throws {
|
||||
switch key.domain {
|
||||
let descriptor = StorageKeyDescriptor.from(key)
|
||||
switch descriptor.domain {
|
||||
case .userDefaults(let suite):
|
||||
try await UserDefaultsHelper.shared.set(data, forKey: key.name, suite: suite)
|
||||
try await UserDefaultsHelper.shared.set(data, forKey: descriptor.name, suite: suite)
|
||||
|
||||
case .appGroupUserDefaults(let identifier):
|
||||
let resolvedId = try resolveIdentifier(identifier)
|
||||
try await UserDefaultsHelper.shared.set(data, forKey: key.name, appGroupIdentifier: resolvedId)
|
||||
try await UserDefaultsHelper.shared.set(data, forKey: descriptor.name, appGroupIdentifier: resolvedId)
|
||||
|
||||
case .keychain(let service):
|
||||
guard case let .keychain(accessibility, accessControl) = key.security else {
|
||||
guard case let .keychain(accessibility, accessControl) = descriptor.security else {
|
||||
throw StorageError.securityApplicationFailed
|
||||
}
|
||||
let resolvedService = try resolveService(service)
|
||||
try await KeychainHelper.shared.set(
|
||||
data,
|
||||
service: resolvedService,
|
||||
key: key.name,
|
||||
key: descriptor.name,
|
||||
accessibility: accessibility,
|
||||
accessControl: accessControl
|
||||
)
|
||||
@ -272,7 +300,7 @@ public actor StorageRouter: StorageProviding {
|
||||
try await FileStorageHelper.shared.write(
|
||||
data,
|
||||
to: directory,
|
||||
fileName: key.name,
|
||||
fileName: descriptor.name,
|
||||
useCompleteFileProtection: false
|
||||
)
|
||||
|
||||
@ -280,7 +308,7 @@ public actor StorageRouter: StorageProviding {
|
||||
try await FileStorageHelper.shared.write(
|
||||
data,
|
||||
to: directory,
|
||||
fileName: key.name,
|
||||
fileName: descriptor.name,
|
||||
useCompleteFileProtection: true
|
||||
)
|
||||
|
||||
@ -289,56 +317,56 @@ public actor StorageRouter: StorageProviding {
|
||||
try await FileStorageHelper.shared.write(
|
||||
data,
|
||||
to: directory,
|
||||
fileName: key.name,
|
||||
fileName: descriptor.name,
|
||||
appGroupIdentifier: resolvedId,
|
||||
useCompleteFileProtection: false
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func retrieve(for key: any StorageKey) async throws -> Data? {
|
||||
switch key.domain {
|
||||
private func retrieve(for descriptor: StorageKeyDescriptor) async throws -> Data? {
|
||||
switch descriptor.domain {
|
||||
case .userDefaults(let suite):
|
||||
return try await UserDefaultsHelper.shared.get(forKey: key.name, suite: suite)
|
||||
return try await UserDefaultsHelper.shared.get(forKey: descriptor.name, suite: suite)
|
||||
case .appGroupUserDefaults(let identifier):
|
||||
let resolvedId = try resolveIdentifier(identifier)
|
||||
return try await UserDefaultsHelper.shared.get(forKey: key.name, appGroupIdentifier: resolvedId)
|
||||
return try await UserDefaultsHelper.shared.get(forKey: descriptor.name, appGroupIdentifier: resolvedId)
|
||||
|
||||
case .keychain(let service):
|
||||
let resolvedService = try resolveService(service)
|
||||
return try await KeychainHelper.shared.get(service: resolvedService, key: key.name)
|
||||
return try await KeychainHelper.shared.get(service: resolvedService, key: descriptor.name)
|
||||
|
||||
case .fileSystem(let directory), .encryptedFileSystem(let directory):
|
||||
return try await FileStorageHelper.shared.read(from: directory, fileName: key.name)
|
||||
return try await FileStorageHelper.shared.read(from: directory, fileName: descriptor.name)
|
||||
case .appGroupFileSystem(let identifier, let directory):
|
||||
let resolvedId = try resolveIdentifier(identifier)
|
||||
return try await FileStorageHelper.shared.read(
|
||||
from: directory,
|
||||
fileName: key.name,
|
||||
fileName: descriptor.name,
|
||||
appGroupIdentifier: resolvedId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private func delete(for key: any StorageKey) async throws {
|
||||
switch key.domain {
|
||||
private func delete(for descriptor: StorageKeyDescriptor) async throws {
|
||||
switch descriptor.domain {
|
||||
case .userDefaults(let suite):
|
||||
try await UserDefaultsHelper.shared.remove(forKey: key.name, suite: suite)
|
||||
try await UserDefaultsHelper.shared.remove(forKey: descriptor.name, suite: suite)
|
||||
case .appGroupUserDefaults(let identifier):
|
||||
let resolvedId = try resolveIdentifier(identifier)
|
||||
try await UserDefaultsHelper.shared.remove(forKey: key.name, appGroupIdentifier: resolvedId)
|
||||
try await UserDefaultsHelper.shared.remove(forKey: descriptor.name, appGroupIdentifier: resolvedId)
|
||||
|
||||
case .keychain(let service):
|
||||
let resolvedService = try resolveService(service)
|
||||
try await KeychainHelper.shared.delete(service: resolvedService, key: key.name)
|
||||
try await KeychainHelper.shared.delete(service: resolvedService, key: descriptor.name)
|
||||
|
||||
case .fileSystem(let directory), .encryptedFileSystem(let directory):
|
||||
try await FileStorageHelper.shared.delete(from: directory, fileName: key.name)
|
||||
try await FileStorageHelper.shared.delete(from: directory, fileName: descriptor.name)
|
||||
case .appGroupFileSystem(let identifier, let directory):
|
||||
let resolvedId = try resolveIdentifier(identifier)
|
||||
try await FileStorageHelper.shared.delete(
|
||||
from: directory,
|
||||
fileName: key.name,
|
||||
fileName: descriptor.name,
|
||||
appGroupIdentifier: resolvedId
|
||||
)
|
||||
}
|
||||
|
||||
@ -12,6 +12,14 @@ enum Logger {
|
||||
#endif
|
||||
}
|
||||
|
||||
static func info(_ message: String) {
|
||||
#if DEBUG
|
||||
if isLoggingEnabled {
|
||||
print(" {LOCAL_DATA} 📢 \(message)")
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
static func error(_ message: String, error: Error? = nil) {
|
||||
#if DEBUG
|
||||
var logMessage = " {LOCAL_DATA} ❌ \(message)"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user