Skip to content

Commit 218e4a6

Browse files
committed
feat: Rework features as DI.Module and DIScope Submodules
1 parent be47c7b commit 218e4a6

File tree

15 files changed

+336
-338
lines changed

15 files changed

+336
-338
lines changed

core/src/commonMain/kotlin/com/mineinabyss/features/DI.kt

Lines changed: 113 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,92 @@
11
package com.mineinabyss.features
22

3+
import com.mineinabyss.features.DI.Module
4+
import com.mineinabyss.features.impl.ModuleImpl
5+
import com.mineinabyss.features.impl.MutableDIImpl
36
import kotlin.reflect.KType
47
import kotlin.reflect.typeOf
58

9+
interface DIAware : AutoCloseable {
10+
val di: DI
11+
12+
override fun close() {
13+
di.close()
14+
}
15+
}
16+
617
/**
718
* A dependency injection context, values are evaluated lazily.
819
*
920
* A value *provider* can never change, though calling [Get] twice may return different values
1021
* when using a factory provider.
1122
*/
1223
@FeatureDSLMarker
13-
interface DI {
24+
interface DI : DIAware, AutoCloseable {
25+
val scope: DIScope
26+
27+
override val di: DI get() = this
28+
1429
fun <T> Get(type: Pair<KType, String?>): T
15-
fun <T> Lazy(type: Pair<KType, String?>): Lazy<T>
30+
fun <T> Lazy(type: Pair<KType, String?>): InjectedValue<T>
31+
32+
fun addCloseable(closeable: AutoCloseable)
1633

1734
val injected: List<Pair<Pair<KType, String?>, InjectedValue<*>>>
35+
36+
interface Module {
37+
val name: String
38+
val key: Key
39+
40+
fun create(parent: DI): DI
41+
42+
fun override(beforeLoad: MutableDI.() -> Unit): Module
43+
44+
interface Key
45+
}
46+
47+
fun childDI(): DI {
48+
val di = invoke(di.scope) { import(this@DI.di) }
49+
addCloseable(di)
50+
return di
51+
}
52+
53+
interface ModuleWithConfig<T> : Module {
54+
fun get(parent: DI): T
55+
}
56+
57+
companion object {
58+
operator fun invoke(scope: DIScope = DIScope(), builder: MutableDI.() -> Unit): DI {
59+
return MutableDIImpl(scope).apply(builder)
60+
}
61+
}
62+
}
63+
64+
fun module(
65+
name: String,
66+
block: MutableDI.() -> Unit,
67+
): Module {
68+
return ModuleImpl(name, configure = block)
69+
}
70+
71+
inline fun DIAware.addCloseable(crossinline closeable: () -> Unit) {
72+
di.addCloseable(AutoCloseable { closeable() })
73+
}
74+
75+
fun <T : AutoCloseable> DIAware.addCloseable(closeable: T): T {
76+
di.addCloseable(closeable)
77+
return closeable
78+
}
79+
80+
fun DIAware.addCloseables(vararg closeables: AutoCloseable) {
81+
closeables.forEach { addCloseable(it) }
82+
}
83+
84+
inline fun <reified T> Module.gets(): DI.ModuleWithConfig<T> {
85+
return object : DI.ModuleWithConfig<T>, Module by this {
86+
override fun get(parent: DI): T {
87+
return parent.get<T>()
88+
}
89+
}
1890
}
1991

2092
/**
@@ -23,8 +95,35 @@ interface DI {
2395
* @see DI
2496
*/
2597
interface MutableDI : DI {
26-
fun <T> Put(type: Pair<KType, String?>, property: InjectedValue<T>): Lazy<T>
98+
fun <T> Put(type: Pair<KType, String?>, property: InjectedValue<T>): InjectedValue<T>
2799

100+
/**
101+
* Loads a [DI.Module] as a singleton in this DI [scope]. If a module is already loaded, gets the instance.
102+
*
103+
* Closes this DI context when the included module is unloaded in [scope].
104+
*/
105+
fun singleModule(di: Module): DI
106+
107+
/**
108+
* Loads a [DI.Module] inside this [DI] context, copies dependencies at the point of the call into the submodule,
109+
* but does not [import] the submodule's new dependencies into this context.
110+
*
111+
* Closes the submodule when closing this DI context.
112+
*
113+
* @see import
114+
*/
115+
fun submodule(di: Module): DI
116+
117+
/**
118+
* Imports all keys from a DI [context] into this context.
119+
* Keys can never be overridden, so this may throw an error if the included context contains clashing providers.
120+
*
121+
* Identical providers do not throw an error, thus a submodule can be included via:
122+
*
123+
* ```kotlin
124+
* import(submodule(other))
125+
* ```
126+
*/
28127
fun import(context: DI)
29128
}
30129

@@ -33,15 +132,18 @@ interface MutableDI : DI {
33132
*
34133
* Immediately throws an error if not already registered.
35134
*/
36-
inline fun <reified T> DI.get(key: String? = null): T = Get(typeOf<T>() to key)
135+
inline fun <reified T> DIAware.get(key: String? = null): T = di.Get(typeOf<T>() to key)
136+
137+
//TODO simplify
138+
inline fun <reified T> DIAware.getOrNull(key: String? = null): T? = runCatching { di.Get<T>(typeOf<T>() to key) }.getOrNull()
37139

38140
/**
39141
* Gets a value of type [T], optionally keyed by [key] as a delegate.
40142
* Only evaluates when first read.
41143
*
42144
* Immediately throws an error if not already registered.
43145
*/
44-
inline fun <reified T> DI.getLazy(key: String? = null): Lazy<T> = Lazy(typeOf<T>() to key)
146+
inline fun <reified T> DIAware.getLazy(key: String? = null): InjectedValue<T> = di.Lazy(typeOf<T>() to key)
45147

46148
/**
47149
* Registers a value of type [T], optionally keyed by [key].
@@ -53,9 +155,9 @@ inline fun <reified T> MutableDI.single(
53155
key: String? = null,
54156
ignoreOverride: Boolean = false,
55157
crossinline block: DI.() -> T,
56-
): Lazy<T> = Put(
158+
): InjectedValue<T> = Put(
57159
typeOf<T>() to key,
58-
InjectedValue(ignoreOverride, lazy { block() })
160+
InjectedValueImpl(ignoreOverride, lazy { block() })
59161
)
60162

61163
/**
@@ -69,10 +171,10 @@ inline fun <reified T> MutableDI.factory(
69171
key: String? = null,
70172
ignoreOverride: Boolean = false,
71173
noinline block: DI.() -> T,
72-
): Lazy<T> {
174+
): InjectedValue<T> {
73175
return Put(
74176
typeOf<T>() to key,
75-
InjectedValue(ignoreOverride, object : Lazy<T> {
177+
InjectedValueImpl(ignoreOverride, object : Lazy<T> {
76178
override val value: T get() = block()
77179

78180
override fun isInitialized(): Boolean = false
@@ -81,6 +183,6 @@ inline fun <reified T> MutableDI.factory(
81183
}
82184

83185
context(context: MutableDI)
84-
inline fun <reified R> Lazy<R>.and(): Lazy<R> {
85-
return context.single<R> { this@and.value }
186+
inline fun <reified R> InjectedValue<R>.and(): InjectedValue<R> {
187+
return context.single<R>(ignoreOverride = ignoreOverride) { this@and.value }
86188
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
package com.mineinabyss.features
2+
3+
import co.touchlab.kermit.Logger
4+
5+
class DIScope(root: MutableDI.() -> Unit = {}) : DIAware, AutoCloseable {
6+
private val _loaded = mutableMapOf<DI.Module.Key, DI>()
7+
val loaded: List<DI.Module> = _loaded.keys.filterIsInstance<DI.Module>()
8+
9+
val root = DI.invoke(this) {
10+
single<DIScope>(ignoreOverride = true) { this@DIScope }
11+
single<Logger>(ignoreOverride = true) { Logger }
12+
root()
13+
}
14+
override val di: DI = this.root
15+
16+
val logger by getLazy<Logger>()
17+
18+
fun <T> load(feature: DI.ModuleWithConfig<T>, configure: T.() -> Unit): DI {
19+
return load(module("$feature-configuration") {
20+
feature.get(singleModule(feature)).configure()
21+
})
22+
}
23+
24+
fun load(feature: DI.Module): DI {
25+
val key = feature.key
26+
if (key in _loaded) return _loaded.getValue(key)
27+
return loadCatching(feature).getOrThrow()
28+
}
29+
30+
fun loadCatching(feature: DI.Module): Result<DI> {
31+
val key = feature.key
32+
if (key in _loaded) return Result.success(_loaded.getValue(key))
33+
return runCatching { feature.create(root) }.onSuccess {
34+
_loaded[key] = it
35+
it.addCloseable { _loaded.remove(key) }
36+
logger.i { "Loaded feature $feature" }
37+
}.onFailure {
38+
if (it is IllegalArgumentException) {
39+
logger.e { "Failed to load feature $feature: ${it.message}" }
40+
} else {
41+
logger.e(it) { "Failed to load feature $feature" }
42+
}
43+
}
44+
}
45+
46+
fun loadAll(vararg modules: DI.Module) {
47+
modules.forEach { load(it) }
48+
}
49+
50+
fun loadAllCatching(vararg modules: DI.Module) {
51+
modules.forEach { loadCatching(it) }
52+
}
53+
54+
operator fun get(module: DI.Module): DI? {
55+
return _loaded[module.key]?.di
56+
}
57+
58+
operator fun <T> get(module: DI.ModuleWithConfig<T>): T? {
59+
return _loaded[module.key]?.di?.let { module.get(it) }
60+
}
61+
62+
fun unload(feature: DI.Module) {
63+
val key = feature.key
64+
val feat = _loaded[key] ?: return
65+
runCatching { feat.close() }.onSuccess {
66+
logger.i { "Loaded feature $feature" }
67+
}.onFailure {
68+
logger.e(it) { "Failed to unload $feature" }
69+
}
70+
_loaded.remove(key)
71+
}
72+
73+
override fun close() {
74+
_loaded.toList().reversed().forEach { unload(it.first as DI.Module) } //TODO no cast
75+
}
76+
77+
companion object {
78+
fun new(builder: MutableDI.() -> Unit = {}): DIScope {
79+
return DIScope(builder)
80+
}
81+
}
82+
}

core/src/commonMain/kotlin/com/mineinabyss/features/Feature.kt

Lines changed: 0 additions & 31 deletions
This file was deleted.

core/src/commonMain/kotlin/com/mineinabyss/features/FeatureDSL.kt

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,3 @@ package com.mineinabyss.features
22

33
@DslMarker
44
annotation class FeatureDSLMarker
5-
6-
@FeatureDSLMarker
7-
interface FeatureDSL
8-
//
9-
//fun feature(name: String, block: FeatureBuilder.() -> Unit): Feature<Unit> {
10-
// return FeatureBuilder(name, Unit::class).apply(block).build(extract = { })
11-
//}
12-
//
13-
//@JvmName("featureWithType")
14-
//inline fun <reified T : Any> feature(name: String, block: FeatureBuilder.() -> Unit): Feature<T> {
15-
// return FeatureBuilder(name, T::class).apply(block).build(extract = { instance<T>() })
16-
//}

core/src/commonMain/kotlin/com/mineinabyss/features/FeatureHelpers.kt

Lines changed: 0 additions & 5 deletions
This file was deleted.

core/src/commonMain/kotlin/com/mineinabyss/features/FeatureKey.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.

0 commit comments

Comments
 (0)