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

Official Plugins

Support

Found an issue with this page? Report it on GitHub