CuppaCore
The foundational module of the Cuppa iOS framework, providing core utilities, plugin architecture, and shared protocols.
Features
- Plugin System: Extensible plugin architecture with dependency management
- Environment Configuration: Type-safe environment variable handling
- Logging: Structured logging with multiple levels
- Type-Safe Configuration: Protocol-based configuration system
- Cross-Platform Support: iOS 17+ and macOS 14+
Installation
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/cuppa-platform/cuppa-ios-v2", from: "1.0.0")
]
.target(
name: "YourApp",
dependencies: [
.product(name: "CuppaCore", package: "cuppa-ios-v2")
]
)
Plugin System
Plugin Protocol
All plugins must conform to the CuppaPlugin protocol:
public protocol CuppaPlugin: Sendable {
associatedtype Configuration: PluginConfiguration
/// Unique identifier for the plugin (e.g., "com.mycuppa.analytics")
static var identifier: String { get }
/// Semantic version of the plugin
static var version: String { get }
/// List of plugin identifiers this plugin depends on
static var dependencies: [String] { get }
/// Register the plugin with its configuration
static func register(with configuration: Configuration) throws
}
Plugin Configuration
public protocol PluginConfiguration: Sendable {
/// Dictionary representation of configuration for introspection
var settings: [String: Any] { get }
}
Plugin Registry
Central registry for managing plugins:
@MainActor
public final class PluginRegistry {
public static let shared = PluginRegistry()
/// Register a plugin with configuration
public func register<P: CuppaPlugin>(
_ pluginType: P.Type,
with configuration: P.Configuration
) throws
/// Check if a plugin is registered
public func isRegistered(_ identifier: String) -> Bool
/// Get all registered plugin identifiers
public var registeredPlugins: [String] { get }
}
Example Plugin
import CuppaCore
// 1. Define configuration
public struct MyPluginConfiguration: PluginConfiguration {
public let apiKey: String
public let enableDebugMode: Bool
public var settings: [String: Any] {
[
"apiKey": apiKey,
"enableDebugMode": enableDebugMode
]
}
public init(apiKey: String, enableDebugMode: Bool = false) {
self.apiKey = apiKey
self.enableDebugMode = enableDebugMode
}
}
// 2. Implement plugin
public struct MyPlugin: CuppaPlugin {
public static let identifier = "com.mycompany.my-plugin"
public static let version = "1.0.0"
public static let dependencies: [String] = []
public static func register(with configuration: MyPluginConfiguration) throws {
print("MyPlugin registered with API key: \(configuration.apiKey)")
// Initialize your plugin here
}
}
// 3. Register in your app
@main
struct MyApp: App {
init() {
try? PluginRegistry.shared.register(
MyPlugin.self,
with: MyPluginConfiguration(
apiKey: "your-api-key",
enableDebugMode: true
)
)
}
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
Plugin Errors
public enum PluginError: Error, LocalizedError {
case alreadyRegistered(String)
case dependencyNotFound(String)
case invalidConfiguration(reason: String)
public var errorDescription: String? {
switch self {
case .alreadyRegistered(let id):
return "Plugin '\(id)' is already registered"
case .dependencyNotFound(let id):
return "Required dependency '\(id)' not found"
case .invalidConfiguration(let reason):
return "Invalid configuration: \(reason)"
}
}
}
Environment Configuration
Type-safe environment variable handling:
import CuppaCore
// Access environment variables
if let apiUrl = ProcessInfo.processInfo.environment["API_URL"] {
print("API URL: \(apiUrl)")
}
// Or create a typed configuration
struct AppConfiguration {
let apiURL: String
let enableDebug: Bool
init() {
let env = ProcessInfo.processInfo.environment
self.apiURL = env["API_URL"] ?? "https://api.mycuppa.io"
self.enableDebug = env["DEBUG"] == "true"
}
}
Logging
Structured logging with multiple levels:
import CuppaCore
import OSLog
// Create a logger
let logger = Logger(subsystem: "com.mycuppa.app", category: "network")
// Log at different levels
logger.debug("Request started")
logger.info("User logged in")
logger.warning("API rate limit approaching")
logger.error("Network request failed")
logger.critical("Unable to connect to server")
Dependency Management
Plugins can declare dependencies on other plugins:
public struct AdvancedAnalyticsPlugin: CuppaPlugin {
public static let identifier = "com.mycuppa.advanced-analytics"
public static let version = "1.0.0"
// Requires base analytics plugin
public static let dependencies = ["com.mycuppa.analytics-plugin"]
public static func register(with configuration: Configuration) throws {
// This will only be called after "com.mycuppa.analytics-plugin" is registered
}
}
The registry will throw PluginError.dependencyNotFound if dependencies are not met.
Best Practices
1. Plugin Identifiers
Use reverse-DNS notation:
✅ Good: "com.mycuppa.analytics-plugin"
❌ Bad: "analyticsPlugin"
2. Semantic Versioning
Follow semantic versioning for plugin versions:
static let version = "1.2.3" // MAJOR.MINOR.PATCH
3. Thread Safety
All plugins must be Sendable:
public struct MyPlugin: CuppaPlugin { // ✅ Struct is Sendable
// ...
}
4. Error Handling
Always handle plugin registration errors:
do {
try PluginRegistry.shared.register(
MyPlugin.self,
with: configuration
)
} catch let error as PluginError {
logger.error("Failed to register plugin: \(error.localizedDescription)")
} catch {
logger.error("Unexpected error: \(error)")
}
5. Configuration Validation
Validate configuration in the register method:
public static func register(with configuration: Configuration) throws {
guard !configuration.apiKey.isEmpty else {
throw PluginError.invalidConfiguration(reason: "API key cannot be empty")
}
// Continue registration
}
Advanced Usage
Conditional Plugin Registration
#if DEBUG
try? PluginRegistry.shared.register(
DebugPlugin.self,
with: DebugPluginConfiguration()
)
#endif
Plugin Discovery
// Check if plugin is registered
if PluginRegistry.shared.isRegistered("com.mycuppa.analytics-plugin") {
print("Analytics plugin is available")
}
// List all registered plugins
for pluginId in PluginRegistry.shared.registeredPlugins {
print("Registered: \(pluginId)")
}
Custom Plugin Lifecycle
public protocol LifecyclePlugin: CuppaPlugin {
static func initialize() async throws
static func shutdown() async throws
}
// Implement lifecycle methods
extension MyPlugin: LifecyclePlugin {
public static func initialize() async throws {
// Async initialization
await setupResources()
}
public static func shutdown() async throws {
// Cleanup
await releaseResources()
}
}
Common Patterns
Singleton Manager Pattern
@MainActor
public final class AnalyticsManager: ObservableObject {
public static let shared = AnalyticsManager()
private var provider: AnalyticsProvider?
private init() {}
public func configure(provider: AnalyticsProvider) {
self.provider = provider
}
public func track(event: String) async {
await provider?.track(event: event, properties: nil)
}
}
// In plugin registration
public static func register(with configuration: Configuration) throws {
AnalyticsManager.shared.configure(provider: configuration.provider)
}
Protocol-Based Providers
public protocol StorageProvider: Sendable {
func save(_ data: Data, forKey key: String) async throws
func load(forKey key: String) async throws -> Data?
func delete(forKey key: String) async throws
}
public struct StoragePluginConfiguration: PluginConfiguration {
public let provider: StorageProvider
// ...
}
Testing Plugins
import XCTest
@testable import CuppaCore
final class PluginTests: XCTestCase {
func testPluginRegistration() throws {
let registry = PluginRegistry.shared
try registry.register(
MyPlugin.self,
with: MyPluginConfiguration(apiKey: "test-key")
)
XCTAssertTrue(registry.isRegistered("com.mycompany.my-plugin"))
}
func testDuplicateRegistration() {
let registry = PluginRegistry.shared
try? registry.register(
MyPlugin.self,
with: MyPluginConfiguration(apiKey: "test-key")
)
XCTAssertThrowsError(
try registry.register(
MyPlugin.self,
with: MyPluginConfiguration(apiKey: "test-key")
)
) { error in
XCTAssertTrue(error is PluginError)
}
}
}
API Reference
PluginRegistry
@MainActor
public final class PluginRegistry {
public static let shared: PluginRegistry
public func register<P: CuppaPlugin>(
_ pluginType: P.Type,
with configuration: P.Configuration
) throws
public func isRegistered(_ identifier: String) -> Bool
public var registeredPlugins: [String] { get }
}
CuppaPlugin Protocol
public protocol CuppaPlugin: Sendable {
associatedtype Configuration: PluginConfiguration
static var identifier: String { get }
static var version: String { get }
static var dependencies: [String] { get }
static func register(with configuration: Configuration) throws
}
PluginConfiguration Protocol
public protocol PluginConfiguration: Sendable {
var settings: [String: Any] { get }
}
Related Modules
- CuppaUI - SwiftUI components
- CuppaNavigation - Navigation system
- CuppaAnalytics - Analytics tracking
Official Plugins
- CuppaAnalyticsPlugin - Multi-provider analytics
- CuppaAuthPlugin - Authentication system
- CuppaErrorHandlingPlugin - Error reporting
- CuppaTheme - Design system