DI with colleen
package io.github.cymoo.colleen
// ============================================================================
// Colleen — Dependency Injection API
// ============================================================================
//
// This file shows only the DI-related portion of the Colleen class.
// Paste these members into your existing Colleen class body.
//
// All APIs are fully backward-compatible:
// - Existing calls without a qualifier continue to work unchanged.
// - Qualifier support is additive via default parameter `qualifier = null`.
//
// Usage examples at the bottom of this file.
// ============================================================================
class Colleen {
// ========================================================================
// Internal container
// ========================================================================
@PublishedApi
internal val serviceContainer = ServiceContainer()
// ========================================================================
// provide — register a pre-built instance
// ========================================================================
/**
* Registers a pre-built service instance.
*
* ```kotlin
* // No qualifier (original API — unchanged)
* app.provide(MyService())
*
* // With qualifier
* app.provide(HikariDataSource(primaryConfig), qualifier = Primary)
* app.provide(HikariDataSource(replicaConfig), qualifier = Replica)
* ```
*
* @param instance the service instance to register.
* @param qualifier optional qualifier to distinguish instances of the same type.
*/
inline fun <reified T : Any> provide(
instance: T,
qualifier: Any? = null
) {
serviceContainer.registerInstance(instance, qualifier)
}
// ========================================================================
// provide — register a factory
// ========================================================================
/**
* Registers a service using a factory function.
*
* How often the factory is invoked depends on [lifetime]:
* - [Lifetime.Singleton] *(default)*: factory is invoked lazily and only
* once; the same instance is returned for all subsequent resolutions.
* - [Lifetime.Transient]: factory is invoked on every resolution,
* producing a new instance each time.
*
* ```kotlin
* // Original API — all unchanged
* app.provide { Database.connect(config) }
* app.provide(Lifetime.Transient) { HttpClient() }
*
* // With qualifier (new)
* app.provide(qualifier = Primary) { HikariDataSource(primaryConfig) }
* app.provide(qualifier = Replica, lifetime = Lifetime.Singleton) {
* HikariDataSource(replicaConfig)
* }
* ```
*
* @param qualifier optional qualifier to distinguish instances of the same type.
* @param lifetime defines the lifetime and caching behaviour. Defaults to
* [Lifetime.Singleton].
* @param factory a function that creates instances of the service.
*/
inline fun <reified T : Any> provide(
qualifier: Any? = null,
lifetime: Lifetime = Lifetime.Singleton,
noinline factory: () -> T
) {
when (lifetime) {
Lifetime.Singleton -> serviceContainer.registerSingleton(qualifier, factory)
Lifetime.Transient -> serviceContainer.registerTransient(qualifier, factory)
}
}
}
// ============================================================================
// Context — service retrieval API
// ============================================================================
//
// Paste these members into your existing Context class body.
// ============================================================================
data class Context(
val request: Request,
val response: Response = Response(),
val app: Colleen,
val parentContext: Context? = null,
) {
// ========================================================================
// getService / getServiceOrNull
// ========================================================================
/**
* Retrieves a required service instance.
*
* Resolution order:
* 1. Current app's service container.
* 2. Parent contexts (if any).
*
* ```kotlin
* // Original API — unchanged
* val db = ctx.getService<Database>()
*
* // With qualifier (new)
* val primary = ctx.getService<DataSource>(Primary)
* val replica = ctx.getService<DataSource>(Replica)
* ```
*
* @param qualifier optional qualifier identifying the specific registration.
* @throws IllegalStateException if the service is not registered.
*/
inline fun <reified T : Any> getService(qualifier: Any? = null): T =
getServiceOrNull<T>(qualifier)
?: error("Service ${T::class.simpleName}(qualifier=$qualifier) not registered")
/**
* Retrieves an optional service instance, or `null` if not registered.
*
* Resolution order:
* 1. Current app's service container.
* 2. Parent contexts (if any).
*
* ```kotlin
* // Original API — unchanged
* val db = ctx.getServiceOrNull<Database>()
*
* // With qualifier (new)
* val primary = ctx.getServiceOrNull<DataSource>(Primary)
* ```
*
* @param qualifier optional qualifier identifying the specific registration.
*/
inline fun <reified T : Any> getServiceOrNull(qualifier: Any? = null): T? =
app.serviceContainer.getOrNull<T>(qualifier)
?: parentContext?.getServiceOrNull(qualifier)
/**
* Retrieves all registered instances of type [T] from the current app,
* regardless of qualifier.
*
* Useful for plugin-style registrations.
*
* ```kotlin
* val handlers = ctx.getServices<EventHandler>()
* handlers.forEach { it.handle(event) }
* ```
*/
inline fun <reified T : Any> getServices(): List<T> =
app.serviceContainer.getAll()
@PublishedApi
internal inline fun <reified T : Any> getService(kClass: kotlin.reflect.KClass<T>): T? =
app.serviceContainer.getOrNull(kClass)
?: parentContext?.getService(kClass)
}
// ============================================================================
// Usage examples (not compiled — for reference only)
// ============================================================================
/*
// ── Qualifier objects (recommended) ────────────────────────────────────────
object Primary
object Replica
// ── Registration ───────────────────────────────────────────────────────────
// 1. Backward-compatible: no qualifier needed
app.provide { Database.connect(config) }
app.provide(MyService())
app.provide(Lifetime.Transient) { HttpClient() }
// 2. Multiple instances of the same type via qualifier
app.provide<DataSource>(qualifier = Primary) { HikariDataSource(primaryConfig) }
app.provide<DataSource>(qualifier = Replica) { HikariDataSource(replicaConfig) }
// 3. String qualifier (simple alternative)
app.provide<DataSource>("primary") { HikariDataSource(primaryConfig) }
app.provide<DataSource>("replica") { HikariDataSource(replicaConfig) }
// 4. Plugin-style: multiple handlers registered with distinct qualifiers
app.provide<EventHandler>(qualifier = "audit") { AuditHandler() }
app.provide<EventHandler>(qualifier = "metrics") { MetricsHandler() }
// ── Retrieval ───────────────────────────────────────────────────────────────
// No qualifier — original API unchanged
val db = ctx.getService<Database>()
val svc = ctx.getServiceOrNull<MyService>()
// With object qualifier
val primary = ctx.getService<DataSource>(Primary)
val replica = ctx.getService<DataSource>(Replica)
// With string qualifier
val primary = ctx.getService<DataSource>("primary")
// Get all instances of a type (ignores qualifier)
val handlers = ctx.getServices<EventHandler>()
handlers.forEach { it.handle(event) }
*/