aboutsummaryrefslogtreecommitdiff
path: root/dependency-injection
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-01-07 14:07:14 +0100
committerLinnea Gräf <nea@nea.moe>2025-01-07 14:07:14 +0100
commit34334396178eec3f72f1228b400cb9ec81c4ce4c (patch)
tree416d05a1193aa787c3fcbe44431a87d9ec555f3b /dependency-injection
parentf2783ceea23147d2702c47c9b47c06ad7fc707a8 (diff)
downloadLocalTransactionLedger-34334396178eec3f72f1228b400cb9ec81c4ce4c.tar.gz
LocalTransactionLedger-34334396178eec3f72f1228b400cb9ec81c4ce4c.tar.bz2
LocalTransactionLedger-34334396178eec3f72f1228b400cb9ec81c4ce4c.zip
build: Split up dependency injection into its own package
Diffstat (limited to 'dependency-injection')
-rw-r--r--dependency-injection/build.gradle.kts8
-rw-r--r--dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt83
-rw-r--r--dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt56
-rw-r--r--dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt6
4 files changed, 153 insertions, 0 deletions
diff --git a/dependency-injection/build.gradle.kts b/dependency-injection/build.gradle.kts
new file mode 100644
index 0000000..5a51941
--- /dev/null
+++ b/dependency-injection/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ `java-library`
+ kotlin("jvm")
+}
+
+java {
+ toolchain.languageVersion.set(JavaLanguageVersion.of(8))
+}
diff --git a/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt
new file mode 100644
index 0000000..0683063
--- /dev/null
+++ b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DI.kt
@@ -0,0 +1,83 @@
+package moe.nea.ledger.utils.di
+
+import java.lang.reflect.AnnotatedElement
+import java.util.Collections
+import java.util.Stack
+
+@Suppress("UNCHECKED_CAST")
+class DI {
+ private fun formatInjectionStack() =
+ injectionStack.joinToString(" -> ")
+
+ fun <T : Any> getProvider(type: Class<T>): BaseDIProvider<T, *> {
+ val provider = providers[type] as BaseDIProvider<T, *>?
+ ?: error("Could not find provider for type $type")
+ return provider
+ }
+
+ private fun <T : Any, C> internalProvide(type: Class<T>, element: AnnotatedElement? = null): T {
+ try {
+ val provider = getProvider(type) as BaseDIProvider<T, C>
+ val context = if (element == null) provider.createEmptyContext() else provider.createContext(element)
+ val key = Pair(type, context)
+ val existingValue = values[key]
+ if (existingValue != null) return existingValue as T
+ if (type in injectionStack) {
+ error("Found injection cycle: ${formatInjectionStack()} -> $type")
+ }
+ injectionStack.push(type)
+ val value =
+ provider.provideWithContext(this, context)
+ val cycleCheckCookie = injectionStack.pop()
+ require(cycleCheckCookie == type) { "Unbalanced stack cookie: $cycleCheckCookie != $type" }
+ values[key] = value
+ return value
+ } catch (ex: Exception) {
+ throw RuntimeException("Could not create instance for type $type (in stack ${formatInjectionStack()})", ex)
+ }
+ }
+
+ fun <T : Any> provide(type: Class<T>, element: AnnotatedElement? = null): T {
+ return internalProvide<T, Any>(type, element)
+ }
+
+ inline fun <reified T : Any> provide(): T = provide(T::class.java)
+
+ fun <T : Any> register(type: Class<T>, provider: BaseDIProvider<T, *>) {
+ providers[type] = provider
+ }
+
+ fun <I : Any, T : I> registerInjectableInterface(parent: Class<I>, type: Class<T>) {
+ internalRegisterInjectableClass(type)
+ register(parent, DIProvider.fromInheritance(type))
+ }
+
+ fun registerInjectableClasses(vararg type: Class<*>) {
+ type.forEach { internalRegisterInjectableClass(it) }
+ }
+
+ private fun <T : Any> internalRegisterInjectableClass(type: Class<T>) {
+ register(type, DIProvider.fromInjectableClass(type))
+ }
+
+ fun instantiateAll() {
+ providers.keys.forEach {
+ provide(it, null)
+ }
+ }
+
+ fun getAllInstances(): Collection<Any> =
+ Collections.unmodifiableCollection(values.values)
+
+ fun <T : Any> registerSingleton(value: T) {
+ register(value.javaClass, DIProvider.singeleton(value))
+ }
+
+ private val injectionStack: Stack<Class<*>> = Stack()
+ private val values = mutableMapOf<Pair<Class<*>, *>, Any>()
+ private val providers = mutableMapOf<Class<*>, BaseDIProvider<*, *>>()
+
+ init {
+ registerSingleton(this)
+ }
+} \ No newline at end of file
diff --git a/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt
new file mode 100644
index 0000000..8a54d5f
--- /dev/null
+++ b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/DIProvider.kt
@@ -0,0 +1,56 @@
+package moe.nea.ledger.utils.di
+
+import java.lang.reflect.AnnotatedElement
+import java.lang.reflect.Constructor
+
+fun interface DIProvider<T : Any> : BaseDIProvider<T, Unit> {
+ override fun provideWithContext(di: DI, context: Unit): T {
+ return provide(di)
+ }
+
+ override fun createContext(element: AnnotatedElement) {
+ }
+
+ override fun createEmptyContext() {
+ }
+
+ fun provide(di: DI): T
+
+ companion object {
+
+ fun <T : Any> fromInjectableClass(clazz: Class<out T>): DIProvider<T> {
+ @Suppress("UNCHECKED_CAST")
+ val cons = (clazz.constructors.find { it.getAnnotation(Inject::class.java) != null }
+ ?: clazz.constructors.find { it.parameterCount == 0 }
+ ?: error("Could not find DI injection entrypoint for class $clazz"))
+ as Constructor<out T>
+ // TODO: consider using unsafe init to inject the parameters *before* calling the constructor
+ return DIProvider { di ->
+ val typArgs = cons.parameters.map {
+ di.provide(it.type, it)
+ }.toTypedArray()
+ val instance = cons.newInstance(*typArgs)
+ for (it in clazz.fields) {
+ if (it.getAnnotation(Inject::class.java) == null) continue
+ it.set(instance, di.provide(it.type, it))
+ }
+ instance
+ }
+ }
+
+ fun <T : Any> singeleton(value: T): DIProvider<T> {
+ return DIProvider { _ -> value }
+ }
+
+ fun <I : Any> fromInheritance(type: Class<out I>): DIProvider<I> {
+ return DIProvider { it.provide(type) }
+ }
+ }
+
+}
+
+interface BaseDIProvider<T : Any, C> {
+ fun createContext(element: AnnotatedElement): C
+ fun provideWithContext(di: DI, context: C): T
+ fun createEmptyContext(): C
+}
diff --git a/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt
new file mode 100644
index 0000000..a8fdd87
--- /dev/null
+++ b/dependency-injection/src/main/kotlin/moe/nea/ledger/utils/di/Inject.kt
@@ -0,0 +1,6 @@
+package moe.nea.ledger.utils.di
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CONSTRUCTOR, AnnotationTarget.FIELD)
+annotation class Inject(
+)