Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
# Store API Implementation

This directory contains the complete implementation of the Store API for the iOS Render SDK as specified in `/docs/specs/store.spec.md`.

## Architecture

The Store API is organized into the following layers:

```
Domain/Store/
├── StoreValue.swift # Core data model for all storable values
├── StorePatch.swift # Represents changes to store values
├── StoreEnums.swift # Scope and Storage enums
├── StoreValidation.swift # Validation system
├── StoreProtocol.swift # Store and StoreFactory protocols
└── StoreError.swift # Error handling

Infrastructure/Store/
├── BaseStore.swift # Base implementation of Store protocol
├── StoreStorageBackend.swift # Storage backend protocol and utilities
├── StoreFactory.swift # Default StoreFactory implementation
├── ScenarioStoreFactory.swift # Scenario-specific factory
├── MemoryStorageBackend.swift # In-memory storage
├── UserDefaultsStorageBackend.swift # UserDefaults storage
├── FileStorageBackend.swift # File-based storage
└── ScenarioSessionStorageBackend.swift # Scenario session storage

SDK/
├── RenderSDK.swift # Integration with RenderSDK
├── ComponentStore.swift # Component binding helpers
├── StoreDebugInspector.swift # Debug tools (DEBUG builds only)
└── StoreUsageExample.swift # Usage examples
```

## Key Features Implemented

✅ **Core Data Models**
- `StoreValue` enum supporting all JSON-compatible types plus `color` and `url`
- `StorePatch` and `StoreChange` for representing mutations
- Full Codable support for serialization

✅ **Scope and Storage**
- `Scope` enum: `.app` and `.scenario(id: String)`
- `Storage` enum: `.memory`, `.userPrefs()`, `.file(url:)`, `.backend(namespace:)`, `.scenarioSession`
- Semantic versioning with major version handling

✅ **Validation System**
- Strict and lenient validation modes
- Configurable validation rules per key path
- Type, range, pattern, and length validation

✅ **Reactive Publishers**
- Combine publishers for reactive data observation
- Component lifecycle binding support
- Thread-safe operations with serial queues

✅ **Storage Backends**
- **Memory**: Fast, ephemeral storage
- **UserPrefs**: Persistent UserDefaults storage
- **File**: Atomic JSON file storage
- **ScenarioSession**: Scenario lifecycle storage
- **Backend**: Placeholder for remote storage

✅ **Store Factory**
- Singleton-like store reuse for identical scope+storage combinations
- Version management with automatic reset on major version bumps
- Scenario session lifecycle management

✅ **SDK Integration**
- Full integration into `RenderSDK`
- Component binding helpers (`ComponentStore`)
- Debug inspector for development builds
- Comprehensive usage examples

## Usage Examples

### Basic Usage

```swift
let sdk = RenderSDK.shared

// Get stores
let appStore = sdk.getAppStore() // App-level persistent storage
let sessionStore = sdk.getScenarioStore(scenarioID: "checkout") // Session storage

// Set and get values
appStore.set("user.name", .string("John Doe"))
let userName = appStore.get("user.name") // StoreValue?

// Typed access
let name: String = try appStore.get("user.name", as: String.self)

// Reactive updates
let cancellable = appStore.publisher(for: "cart.total")
.sink { value in print("Total changed: \(value)") }
```

### Component Binding

```swift
class MyComponent: NSObject {
@Published var itemCount: Int?

func setupBindings() {
componentStore.bindStoreValue(
"cart.itemCount",
to: \.itemCount,
in: store
)
}
}
```

### Validation

```swift
let validationOptions = ValidationOptions(
mode: .strict,
schema: [
"user.age": ValidationRule(
kind: .integer,
required: true,
min: 0,
max: 150
)
]
)
store.configureValidation(validationOptions)
```

### Transactions

```swift
store.transaction { store in
store.set("cart.item1", .string("Apple"))
store.set("cart.item2", .string("Banana"))
// All changes committed atomically
}
```

## Debug Tools (DEBUG builds only)

```swift
#if DEBUG
let inspector = sdk.getDebugInspector()
let debugInfo = inspector.getStoreDebugInfo(store)
let jsonData = inspector.exportAllData()
#endif
```

## Thread Safety

- All store operations are thread-safe
- Each store has its own serial dispatch queue
- Publishers deliver updates on the main thread
- Transactions ensure atomicity

## Performance Considerations

- Memory storage: Fastest, but data lost on app termination
- UserDefaults storage: Persistent, but limited to small data sizes
- File storage: Best for larger datasets, atomic writes
- Scenario session: Automatically cleaned up when scenario ends

## Error Handling

The API provides comprehensive error handling through the `StoreError` enum:
- Key path validation errors
- Decode/encode failures
- Validation errors
- Storage unavailability
- Concurrency issues

All errors implement `LocalizedError` for user-friendly error messages.
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import Foundation

/// Represents the scope of a store instance
public enum Scope: Equatable {
case app
case scenario(id: String)

public var description: String {
switch self {
case .app: return "app"
case .scenario(let id): return "scenario(\(id))"
}
}
}

/// Represents the storage mechanism for a store instance
public enum Storage: Equatable {
case memory
case userPrefs(suite: String? = nil)
case file(url: URL)
case backend(namespace: String)
case scenarioSession

public var description: String {
switch self {
case .memory: return "memory"
case .userPrefs(let suite): return "userPrefs(\(suite ?? "default"))"
case .file(let url): return "file(\(url.lastPathComponent))"
case .backend(let namespace): return "backend(\(namespace))"
case .scenarioSession: return "scenarioSession"
}
}

/// Get a unique identifier for this storage configuration
public var identifier: String {
switch self {
case .memory: return "memory"
case .userPrefs(let suite): return "userPrefs_\(suite ?? "default")"
case .file(let url): return "file_\(url.absoluteString.hashValue)"
case .backend(let namespace): return "backend_\(namespace)"
case .scenarioSession: return "scenarioSession"
}
}
}

/// Semantic version for store versioning
public struct SemanticVersion: Equatable, Comparable {
public let major: Int
public let minor: Int
public let patch: Int

public init(major: Int, minor: Int, patch: Int) {
self.major = major
self.minor = minor
self.patch = patch
}

public init(_ versionString: String) throws {
let components = versionString.split(separator: ".").map { Int($0) }
guard components.count == 3, let major = components[0], let minor = components[1], let patch = components[2] else {
throw StoreError.invalidVersionFormat("Invalid version format: \(versionString)")
}
self.major = major
self.minor = minor
self.patch = patch
}

public var description: String {
return "\(major).\(minor).\(patch)"
}

public static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool {
if lhs.major != rhs.major { return lhs.major < rhs.major }
if lhs.minor != rhs.minor { return lhs.minor < rhs.minor }
return lhs.patch < rhs.patch
}

/// Check if this is a major version bump compared to another version
public func isMajorBump(from other: SemanticVersion) -> Bool {
return major > other.major
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import Foundation

/// Errors that can occur during store operations
public enum StoreError: LocalizedError {
case keyPathNotFound(String)
case invalidKeyPath(String)
case decodeFailed(String)
case encodeFailed(String)
case invalidVersionFormat(String)
case validationFailed(String)
case storageUnavailable(String)
case concurrencyError(String)
case transactionFailed(String)

public var errorDescription: String? {
switch self {
case .keyPathNotFound(let keyPath):
return "Key path not found: \(keyPath)"
case .invalidKeyPath(let keyPath):
return "Invalid key path: \(keyPath)"
case .decodeFailed(let reason):
return "Failed to decode value: \(reason)"
case .encodeFailed(let reason):
return "Failed to encode value: \(reason)"
case .invalidVersionFormat(let version):
return "Invalid version format: \(version)"
case .validationFailed(let reason):
return "Validation failed: \(reason)"
case .storageUnavailable(let reason):
return "Storage unavailable: \(reason)"
case .concurrencyError(let reason):
return "Concurrency error: \(reason)"
case .transactionFailed(let reason):
return "Transaction failed: \(reason)"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation

/// Represents a single patch operation on a store value
public struct StorePatch: Equatable {
public enum Op: String, Codable, Equatable {
case set
case remove
case merge
}

public let op: Op
public let keyPath: String
public let oldValue: StoreValue?
public let newValue: StoreValue?

public init(op: Op, keyPath: String, oldValue: StoreValue? = nil, newValue: StoreValue? = nil) {
self.op = op
self.keyPath = keyPath
self.oldValue = oldValue
self.newValue = newValue
}
}

/// Represents a collection of patches within a single transaction
public struct StoreChange: Equatable {
public let patches: [StorePatch]
public let transactionID: UUID?

public init(patches: [StorePatch], transactionID: UUID? = nil) {
self.patches = patches
self.transactionID = transactionID ?? UUID()
}

/// Check if this change affects a specific key path
public func affects(keyPath: String) -> Bool {
return patches.contains { patch in
patch.keyPath == keyPath || keyPath.hasPrefix(patch.keyPath + ".")
}
}

/// Get all key paths affected by this change
public var affectedKeyPaths: Set<String> {
var keyPaths = Set<String>()

for patch in patches {
keyPaths.insert(patch.keyPath)

// Add parent paths for nested changes
var components = patch.keyPath.split(separator: ".")
while !components.isEmpty {
components.removeLast()
if !components.isEmpty {
keyPaths.insert(components.joined(separator: "."))
}
}
}

return keyPaths
}
}
Loading