Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codeSnippets/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ module("snippets", "tutorial-server-docker-compose")
module("snippets", "htmx-integration")
module("snippets", "server-http-request-lifecycle")
module("snippets", "openapi-spec-gen")
module("snippets", "server-di")

if(!System.getProperty("os.name").startsWith("Windows")) {
module("snippets", "embedded-server-native")
Expand Down
12 changes: 12 additions & 0 deletions codeSnippets/snippets/server-di/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Ktor Dependency Injection sample

This project demonstrates the usage of Ktor’s built-in Dependency Injection (DI) plugin.
> This sample is a part of the [codeSnippets](../../README.md) Gradle project.

## Running the Project

```bash
./gradlew :server-di:run
```

Then, navigate to [http://localhost:8080/greet/world](http://localhost:8080/greet/world).
29 changes: 29 additions & 0 deletions codeSnippets/snippets/server-di/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
val ktor_version: String by project
val kotlin_version: String by project
val logback_version: String by project

plugins {
application
kotlin("jvm")
}

application {
mainClass.set("io.ktor.server.netty.EngineMain")
}

repositories {
mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/ktor/eap") }
}

dependencies {
implementation("io.ktor:ktor-server-core-jvm:$ktor_version")
implementation("io.ktor:ktor-server-di:$ktor_version")
implementation("io.ktor:ktor-server-netty-jvm:$ktor_version")
implementation("io.ktor:ktor-server-config-yaml:${ktor_version}")
implementation("io.ktor:ktor-server-content-negotiation-jvm:$ktor_version")
implementation("io.ktor:ktor-serialization-kotlinx-json-jvm:$ktor_version")
implementation("ch.qos.logback:logback-classic:${logback_version}")
testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version")
testImplementation(kotlin("test"))
}
20 changes: 20 additions & 0 deletions codeSnippets/snippets/server-di/requests.http
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
### Successful checkout using cookies and amount
POST http://localhost:8080/checkout?amount=1500
Content-Type: application/x-www-form-urlencoded
Cookie: userId=alice; cartId=cart-123

###

### Missing userId cookie
POST http://localhost:8080/checkout?amount=1500
Content-Type: application/x-www-form-urlencoded
Cookie: cartId=cart-123

###

### Missing amount query parameter
POST http://localhost:8080/checkout
Content-Type: application/x-www-form-urlencoded
Cookie: userId=alice; cartId=cart-123

###
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.example

import io.ktor.server.application.Application
import io.ktor.server.plugins.di.dependencies
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.routing

fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)

fun Application.module(
greetingService: GreetingService,
userRepository: UserRepository,
) {
routing {
val optional: OptionalConfig? by dependencies

get("/greet/{name}") {
val name = call.parameters["name"] ?: "World"
call.respondText(greetingService.greet(name))
}

get("/db") {
call.respondText("DB = ${userRepository.db}")
}

get("/optional") {
call.respondText("Optional = $optional")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example

import io.ktor.server.application.Application
import io.ktor.server.application.log
import io.ktor.server.plugins.di.dependencies
import kotlinx.coroutines.delay

data class EventsConnection(val connected: Boolean)

suspend fun Application.installEvents() {
val conn: EventsConnection = dependencies.resolve()
log.info("Events connection ready: $conn")
}

suspend fun Application.loadEventsConnection() {
dependencies.provide {
delay(200) // simulate async work
EventsConnection(true)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example

interface Database

class PostgresDatabase(val url: String) : Database
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example

interface GreetingService {
fun greet(name: String): String
}

class GreetingServiceImpl : GreetingService {
override fun greet(name: String): String = "Hello, $name!"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example

import io.ktor.server.application.*
import io.ktor.server.plugins.di.dependencies
import java.io.PrintStream

class Logger(private val out: PrintStream) {
fun log(message: String) {
out.println("[LOG] $message")
}
}

fun Application.logging(printStreamProvider: () -> PrintStream) {
dependencies {
provide<Logger> {
Logger(printStreamProvider())
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.example

data class OptionalConfig(val value: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.example

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.di.annotations.*
import io.ktor.server.plugins.di.dependencies
import io.ktor.server.response.*
import io.ktor.server.routing.*

interface PaymentProcessor {
suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long)
}
class CreditCardPaymentProvider(
val baseUrl: String,
val clientKey: String,
val hashEncoding: (String) -> String
) : PaymentProcessor {
override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) {
call.response.header("X-Transaction-Id", "$userId:$cartId")
call.response.header("X-Digest", hashEncoding("$clientKey:$userId:$cartId:$amount"))
call.respondRedirect("$baseUrl/payment/$amount")
}
}

class PointsBalancePaymentProvider(
val updatePoints: suspend (String, Long) -> Long
) : PaymentProcessor {
override suspend fun handlePayment(call: ApplicationCall, userId: String, cartId: String, amount: Long) {
updatePoints(userId, amount)
call.respondRedirect("/paymentComplete?userId=$userId&cartId=$cartId&amount=$amount")
}
}

fun Application.configureExternalPaymentProvider(
@Property("payments.url") baseUrl: String,
@Property("payments.clientKey") clientKey: String,
) {
dependencies {
provide("external") { CreditCardPaymentProvider(baseUrl, clientKey) { it.reversed() } }
}
}


fun Application.paymentsHandling(
@Named("external") payments: PaymentProcessor
) {
log.info("Using payment processor: $payments")
routing {
post("/checkout") {
val userId = call.request.cookies["userId"]
?: return@post call.respondText("Login required", status = HttpStatusCode.Forbidden)
val cartId = call.request.cookies["cartId"]
?: return@post call.respondText("Cart ID missing", status = HttpStatusCode.Forbidden)
val amount = call.request.queryParameters["amount"]?.toLongOrNull() ?: return@post call.respondText("Amount missing", status = HttpStatusCode.BadRequest)

payments.handlePayment(call, userId, cartId, amount)
}
get("/payment/{amount}") {
call.respondText("Payment for ${call.parameters["amount"]} is pending...")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.example

import java.io.PrintStream

fun stdout(): () -> PrintStream = { System.out }
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.example

import io.ktor.server.plugins.di.annotations.Property

fun provideDatabase(
@Property("database.connectionUrl") connectionUrl: String
): Database = PostgresDatabase(connectionUrl)

open class UserRepository(val db: Database)
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
ktor:
deployment:
port: 8080
application:
dependencies:
- com.example.RepositoriesKt.provideDatabase
- com.example.UserRepository
- com.example.GreetingServiceImpl
- com.example.PrintStreamProviderKt.stdout
modules:
- com.example.ApplicationKt.module
- com.example.LoggingKt.logging
- com.example.PaymentServiceKt.configureExternalPaymentProvider
- com.example.PaymentServiceKt.paymentsHandling
database:
connectionUrl: postgres://localhost:5432/admin

connection:
domain: api.example.com
path: /v1
protocol: https

payments:
url: "http://localhost:8080"
clientKey: "super-secret-client-key"
29 changes: 29 additions & 0 deletions codeSnippets/snippets/server-di/src/test/kotlin/GreetingTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.example

import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import io.ktor.server.testing.*
import kotlin.test.*

class GreetingTest {
@Test
fun testGreeting() = testApplication {
application {
module(
greetingService = FakeGreetingService(),
userRepository = FakeUserRepository(),
)
}

val response = client.get("/greet/Test")
assertEquals("Fake greeting", response.bodyAsText())
}
}

class FakeGreetingService : GreetingService {
override fun greet(name: String) = "Fake greeting"
}

class FakeUserRepository : UserRepository(FakeDatabase())

class FakeDatabase : Database
48 changes: 48 additions & 0 deletions codeSnippets/snippets/server-di/src/test/kotlin/PaymentTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.example

import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.plugins.di.dependencies
import io.ktor.server.response.respondRedirect
import io.ktor.server.testing.*
import kotlin.test.*

class PaymentTest {
@Test
fun testCheckoutRedirectViaExternalProcessor() = testApplication {
application {
val mockProcessor = MockPaymentProcessor()
dependencies {
provide<PaymentProcessor>("external") { mockProcessor }
}
paymentsHandling(mockProcessor)
}

val response = client.post("/checkout") {
cookie("userId", "user-42")
cookie("cartId", "cart-7")
parameter("amount", "1999")
}

assertEquals(HttpStatusCode.Found, response.status)
assertEquals("/fake-payment-complete", response.headers[HttpHeaders.Location])
}
}

class MockPaymentProcessor : PaymentProcessor {
data class RecordedCall(val userId: String, val cartId: String, val amount: Long)

var lastCall: RecordedCall? = null
private set

override suspend fun handlePayment(
call: ApplicationCall,
userId: String,
cartId: String,
amount: Long,
) {
lastCall = RecordedCall(userId, cartId, amount)
call.respondRedirect("/fake-payment-complete")
}
}
9 changes: 8 additions & 1 deletion ktor.tree
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,14 @@
accepts-web-file-names="configuration.html,configurations.html,environments.html,configuration-file.html"/>
<toc-element topic="server-modules.md"
accepts-web-file-names="modules.html"/>
<toc-element topic="server-dependency-injection.md"/>
<toc-element toc-title="Dependency injection">
<toc-element topic="server-dependency-injection.md" toc-title="Overview"/>
<toc-element topic="server-di-configuration.md" toc-title="Configuration"/>
<toc-element topic="server-di-dependency-registration.md"/>
<toc-element topic="server-di-dependency-resolution.md"/>
<toc-element topic="server-di-resource-lifecycle-management.md"/>
<toc-element topic="server-di-testing.md" toc-title="Testing"/>
</toc-element>
<toc-element topic="server-plugins.md"
toc-title="Plugins"
accepts-web-file-names="zfeatures.html,features.html,plugins.html"/>
Expand Down
Loading