DI
package io.github.cymoo.colleen
import java.util.concurrent.ConcurrentHashMap
import kotlin.reflect.KClass
// ============================================================================
// Lifetime
// ============================================================================
enum class Lifetime {
Singleton,
Transient
}
// ============================================================================
// ServiceKey
// ============================================================================
/**
* Composite key used to uniquely identify a service registration.
*
* A service is uniquely identified by its [type] combined with an optional
* [qualifier]. This allows multiple instances of the same type to coexist
* in the container under different qualifiers.
*
* Qualifiers can be any object — typically a Kotlin `object` singleton
* (preferred for type safety and IDE refactoring support) or a plain [String]
* (convenient for simple cases). Both work because [ServiceKey] relies on
* standard `equals`/`hashCode` semantics.
*
* Examples:
* ```kotlin
* // No qualifier (default)
* ServiceKey(Database::class)
*
* // String qualifier
* ServiceKey(DataSource::class, "primary")
*
* // Object qualifier (recommended)
* object Primary
* ServiceKey(DataSource::class, Primary)
* ```
*/
data class ServiceKey(
val type: KClass<*>,
val qualifier: Any? = null
)
// ============================================================================
// ServiceContainer
// ============================================================================
/**
* A lightweight, thread-safe dependency injection container.
*
* Supports:
* - **Singleton** services: created once and reused across all resolutions.
* - **Transient** services: a fresh instance is created on every resolution.
* - **Qualifier-based** multi-registration: multiple instances of the same
* type can be registered and retrieved using a qualifier (string or object).
* - **Interface binding**: bind an interface to a concrete implementation.
* - **Bulk retrieval**: retrieve all registered instances of a given type.
*
* All public mutating operations are thread-safe via [ConcurrentHashMap].
*
* ### Basic usage
* ```kotlin
* val container = ServiceContainer()
*
* // Register
* container.registerInstance(MyService())
* container.registerSingleton { Database.connect(config) }
* container.registerTransient { HttpClient() }
*
* // Retrieve
* val svc = container.get<MyService>()
* ```
*
* ### Qualifier usage
* ```kotlin
* object Primary
* object Replica
*
* container.registerSingleton(Primary) { HikariDataSource(primaryConfig) }
* container.registerSingleton(Replica) { HikariDataSource(replicaConfig) }
*
* val primary = container.get<DataSource>(Primary)
* val replica = container.get<DataSource>(Replica)
* ```
*/
class ServiceContainer {
@PublishedApi
internal val instances = ConcurrentHashMap<ServiceKey, Any>()
@PublishedApi
internal val factories = ConcurrentHashMap<ServiceKey, () -> Any>()
// ========================================================================
// Registration
// ========================================================================
/**
* Registers a pre-built service instance.
*
* The instance is stored directly and returned as-is on every retrieval.
*
* @param instance the service instance to register.
* @param qualifier optional qualifier to distinguish instances of the same type.
*/
inline fun <reified T : Any> registerInstance(
instance: T,
qualifier: Any? = null
): ServiceContainer {
instances[ServiceKey(T::class, qualifier)] = instance
return this
}
/**
* Registers a lazily initialized singleton service.
*
* The [factory] is invoked at most once; the resulting instance is cached
* and returned for all subsequent resolutions of the same key.
*
* @param qualifier optional qualifier to distinguish instances of the same type.
* @param factory the factory function used to create the service instance.
*/
inline fun <reified T : Any> registerSingleton(
qualifier: Any? = null,
noinline factory: () -> T
): ServiceContainer {
val key = ServiceKey(T::class, qualifier)
factories[key] = { instances.getOrPut(key) { factory() } }
return this
}
/**
* Registers a transient service.
*
* The [factory] is invoked on every retrieval, producing a new instance
* each time.
*
* @param qualifier optional qualifier to distinguish instances of the same type.
* @param factory the factory function used to create each service instance.
*/
inline fun <reified T : Any> registerTransient(
qualifier: Any? = null,
noinline factory: () -> T
): ServiceContainer {
factories[ServiceKey(T::class, qualifier)] = factory
return this
}
/**
* Binds an interface [TInterface] to a concrete implementation [TImpl].
*
* Resolving [TInterface] will delegate to the registered [TImpl] service.
*
* @param qualifier optional qualifier applied to the interface binding.
*/
inline fun <reified TInterface : Any, reified TImpl : TInterface> bind(
qualifier: Any? = null
): ServiceContainer {
factories[ServiceKey(TInterface::class, qualifier)] = { get<TImpl>() }
return this
}
// ========================================================================
// Retrieval
// ========================================================================
/**
* Retrieves a registered service instance.
*
* Resolution order:
* 1. Cached instances ([instances] map).
* 2. Registered factories ([factories] map).
*
* @param qualifier optional qualifier identifying the specific registration.
* @throws NoSuchElementException if no matching service is registered.
*/
inline fun <reified T : Any> get(qualifier: Any? = null): T =
get(T::class, qualifier)
@Suppress("UNCHECKED_CAST")
fun <T : Any> get(kClass: KClass<T>, qualifier: Any? = null): T {
val key = ServiceKey(kClass, qualifier)
instances[key]?.let { return it as T }
factories[key]?.let { return it() as T }
throw NoSuchElementException(
"Service ${kClass.simpleName}(qualifier=$qualifier) is not registered"
)
}
/**
* Retrieves a registered service instance, or `null` if not registered.
*
* @param qualifier optional qualifier identifying the specific registration.
*/
inline fun <reified T : Any> getOrNull(qualifier: Any? = null): T? =
getOrNull(T::class, qualifier)
fun <T : Any> getOrNull(kClass: KClass<T>, qualifier: Any? = null): T? =
runCatching { get(kClass, qualifier) }.getOrNull()
/**
* Retrieves a registered service instance, or returns [defaultValue] if
* not registered.
*
* @param qualifier optional qualifier identifying the specific registration.
* @param defaultValue the value to return when no service is found.
*/
inline fun <reified T : Any> getOrDefault(
defaultValue: T,
qualifier: Any? = null
): T = getOrNull<T>(qualifier) ?: defaultValue
/**
* Retrieves a registered service instance, or creates one using [factory]
* if not registered.
*
* @param qualifier optional qualifier identifying the specific registration.
* @param factory the fallback factory invoked when no service is found.
*/
inline fun <reified T : Any> getOrElse(
qualifier: Any? = null,
factory: () -> T
): T = getOrNull<T>(qualifier) ?: factory()
/**
* Retrieves all registered instances (and eagerly resolves all factories)
* for the given type [T], regardless of qualifier.
*
* Useful for plugin-style registrations where multiple implementations of
* the same interface coexist.
*
* ```kotlin
* container.registerSingleton<EventHandler>(qualifier = "audit") { AuditHandler() }
* container.registerSingleton<EventHandler>(qualifier = "metrics") { MetricsHandler() }
*
* val handlers = container.getAll<EventHandler>()
* handlers.forEach { it.handle(event) }
* ```
*/
@Suppress("UNCHECKED_CAST")
inline fun <reified T : Any> getAll(): List<T> {
val type = T::class
val cachedKeys = instances.keys.filter { it.type == type }.toSet()
val cached = cachedKeys.mapNotNull { instances[it] as? T }
val fromFactories = factories
.filterKeys { it.type == type && it !in cachedKeys }
.values
.mapNotNull { runCatching { it() as? T }.getOrNull() }
return cached + fromFactories
}
// ========================================================================
// Introspection
// ========================================================================
/**
* Returns `true` if a service of type [T] with the given [qualifier] is registered.
*/
inline fun <reified T : Any> has(qualifier: Any? = null): Boolean {
val key = ServiceKey(T::class, qualifier)
return instances.containsKey(key) || factories.containsKey(key)
}
/**
* Removes the service of type [T] with the given [qualifier].
*
* @return `true` if at least one registration (instance or factory) was removed.
*/
inline fun <reified T : Any> remove(qualifier: Any? = null): Boolean {
val key = ServiceKey(T::class, qualifier)
return (instances.remove(key) != null) or (factories.remove(key) != null)
}
/**
* Removes all registered services.
*/
fun clear() {
instances.clear()
factories.clear()
}
/**
* Returns the set of all registered [ServiceKey]s.
*/
val registeredServices: Set<ServiceKey>
get() = instances.keys + factories.keys
// ========================================================================
// Java Compatibility
// ========================================================================
fun <T : Any> registerInstance(
clazz: Class<T>,
instance: T,
qualifier: Any? = null
): ServiceContainer {
instances[ServiceKey(clazz.kotlin, qualifier)] = instance
return this
}
fun <T : Any> registerSingleton(
clazz: Class<T>,
qualifier: Any? = null,
factory: () -> T
): ServiceContainer {
val key = ServiceKey(clazz.kotlin, qualifier)
factories[key] = { instances.getOrPut(key) { factory() } }
return this
}
fun <T : Any> registerTransient(
clazz: Class<T>,
qualifier: Any? = null,
factory: () -> T
): ServiceContainer {
factories[ServiceKey(clazz.kotlin, qualifier)] = factory
return this
}
}