Skip to content

Commit

Permalink
feat: kodein di support
Browse files Browse the repository at this point in the history
Get instances from Kodein DI inside routes
  • Loading branch information
programadorthi committed Aug 10, 2024
1 parent 4ec0afb commit 47884bf
Show file tree
Hide file tree
Showing 10 changed files with 493 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package dev.programadorthi.routing.core

import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.ROUTE_INSTANCE
import io.ktor.http.Parameters
import io.ktor.util.Attributes
import kotlinx.coroutines.CoroutineScope
Expand All @@ -15,7 +16,6 @@ internal class RoutingApplicationCall(
parameters: Parameters,
) : ApplicationCall, CoroutineScope {
override val application: Application get() = previousCall.application
override val attributes: Attributes get() = previousCall.attributes
override val name: String get() = previousCall.name
override val uri: String get() = previousCall.uri

Expand All @@ -26,5 +26,11 @@ internal class RoutingApplicationCall(
}
}

override val attributes: Attributes by lazy(LazyThreadSafetyMode.NONE) {
previousCall.attributes.apply {
put(ROUTE_INSTANCE, route)
}
}

override fun toString(): String = "RoutingApplicationCall(route=$route)"
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
package dev.programadorthi.routing.core.application

import dev.programadorthi.routing.core.RedirectApplicationCall
import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.RouteMethod
import io.ktor.http.Parameters
import io.ktor.util.AttributeKey
import io.ktor.util.Attributes
import io.ktor.util.pipeline.PipelineContext
import io.ktor.util.pipeline.execute
import kotlinx.coroutines.launch

private val RECEIVE_TYPE: AttributeKey<Any> = AttributeKey("KotlinRoutingReceiveType")
internal val ROUTE_INSTANCE: AttributeKey<Route> = AttributeKey("KotlinRouteInstance")

/**
* A single act of communication between a client and server.
Expand Down Expand Up @@ -134,6 +137,8 @@ public fun ApplicationCall.redirectToPath(
redirect(path = path, name = "", parameters = parameters, routeMethod = method)
}

public fun PipelineContext<*, ApplicationCall>.route(): Route? = call.attributes.getOrNull(ROUTE_INSTANCE)

private fun ApplicationCall.redirect(
path: String,
name: String,
Expand Down
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ ktor = "3.0.0-beta-1"
serialization = "1.6.2"
slf4j = "2.0.4"
voyager = "1.1.0-alpha03"
kodein = "7.21.2"

junit = "4.13.2"
robolectric = "4.11.1"
Expand Down Expand Up @@ -46,6 +47,7 @@ serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-jso
slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" }
slf4j-simple = { module = "org.slf4j:slf4j-simple", version.ref = "slf4j" }
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
kodein-di = { module = "org.kodein.di:kodein-di", version.ref = "kodein" }

test-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
test-coroutines-debug = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-debug", version.ref = "coroutines" }
Expand Down
25 changes: 25 additions & 0 deletions integration/kodein/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
plugins {
kotlin("multiplatform")
id("org.jlleitschuh.gradle.ktlint")
id("org.jetbrains.kotlinx.kover")
alias(libs.plugins.maven.publish)
}

applyBasicSetup()

kotlin {
sourceSets {
commonMain {
dependencies {
implementation(projects.core)
implementation(libs.kodein.di)
}
}

commonTest {
dependencies {
implementation(projects.statusPages)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package dev.programadorthi.routing.kodein

import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCall
import dev.programadorthi.routing.core.application.call
import dev.programadorthi.routing.core.application.route
import io.ktor.util.AttributeKey
import io.ktor.util.pipeline.PipelineContext
import org.kodein.di.DI
import org.kodein.di.LazyDI

// attribute key for storing injector in a call
public val KodeinDIKey: AttributeKey<DI> = AttributeKey<DI>("KodeinDI")

/**
* Getting the global [DI] container from the [Application]
*/
public fun Application.closestDI(): LazyDI = LazyDI { attributes[KodeinDIKey] }

/**
* Getting the global [DI] container from the [Application] parameter
*/
public fun closestDI(getApplication: () -> Application): LazyDI = getApplication().closestDI()

/**
* Getting the global [DI] container from the [ApplicationCall]
*/
public fun ApplicationCall.closestDI(): LazyDI = closestDI { application }

/**
* Getting the global [DI] container from the [Routing] feature
*/
public fun Routing.closestDI(): LazyDI = closestDI { application }

/**
* Getting the global or local (if extended) [DI] container from the current [Route]
* by browsering all the routing tree until we get to the root level, the [Routing] feature
*
* @throws IllegalStateException if there is no [DI] container
*/
public fun Route.closestDI(): LazyDI {
val attrs =
when {
this is Routing -> application.attributes
else -> attributes
}
// Is there an inner DI container for this Route ?
val routeDI = attrs.getOrNull(KodeinDIKey)
if (routeDI is LazyDI) {
return routeDI
}
return parent?.closestDI() ?: error("No DI container found for [$this]")
}

/**
* Getting the global [DI] container from the [ApplicationCall]
*/
public fun PipelineContext<*, ApplicationCall>.closestDI(): LazyDI {
val route =
requireNotNull(route()) {
"Invalid context to get the closestDI: $call"
}
return route.closestDI()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package dev.programadorthi.routing.kodein

import dev.programadorthi.routing.core.application.Application
import dev.programadorthi.routing.core.application.ApplicationCallPipeline
import dev.programadorthi.routing.core.application.BaseApplicationPlugin
import dev.programadorthi.routing.core.application.install
import dev.programadorthi.routing.kodein.DIPlugin.Plugin
import io.ktor.util.AttributeKey
import org.kodein.di.DI
import org.kodein.di.bindInstance

/**
* Ktor [Plugin] that provide a global [DI] container
* that would be accessible from everywhere in the Ktor application
*/
public class DIPlugin {
/**
* Configure the [DI] container then put it in the [Application.attributes],
* thus it would be easily accessible (e.g. [Application.di]
*/
internal companion object Plugin :
BaseApplicationPlugin<ApplicationCallPipeline, DI.MainBuilder, DIPlugin> {
override val key: AttributeKey<DIPlugin> = AttributeKey("DIPlugin")

override fun install(
pipeline: ApplicationCallPipeline,
configure: DI.MainBuilder.() -> Unit,
): DIPlugin {
pipeline.attributes.put(KodeinDIKey, DI.lazy { configure() })

return DIPlugin()
}
}
}

/**
* Installs a [DIPlugin] feature for the this [Application] and runs a [configuration] script on it
*
* @throws [dev.programadorthi.routing.core.application.DuplicatePluginException] if the plugin has already been installed.
*/
public fun Application.di(configuration: DI.MainBuilder.() -> Unit): DIPlugin =
install(DIPlugin) {
bindInstance { this@di }
configuration()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package dev.programadorthi.routing.kodein

import dev.programadorthi.routing.core.Route
import dev.programadorthi.routing.core.Routing
import dev.programadorthi.routing.core.application
import org.kodein.di.Copy
import org.kodein.di.DI
import org.kodein.di.subDI

/**
* Extend the nearest [DI] container, Global (from the Application) or Local (from a parent)
*/
public inline fun Route.subDI(
allowSilentOverride: Boolean = false,
copy: Copy = Copy.NonCached,
crossinline init: DI.MainBuilder.() -> Unit,
) {
// Get any DI container in the parent # avoid infinite loop / StackOverflowError
val parentDI = parent?.closestDI() ?: closestDI { application }
val attrs =
when {
this is Routing -> application.attributes
else -> attributes
}
attrs.put(KodeinDIKey, subDI(parentDI, allowSilentOverride, copy, init))
}
Loading

0 comments on commit 47884bf

Please sign in to comment.