-
Notifications
You must be signed in to change notification settings - Fork 363
KTOR-9176 KTOR-8997 Improve examples and flow in dependency injection + document configuration options #763
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
3ff9fa4
455df09
5e2ceb9
33dce55
4d2b6ab
989f3be
6fece44
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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). |
| 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")) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,22 @@ | ||
| @host = http://localhost:8080 | ||
|
|
||
| ### Successful checkout using cookies and amount | ||
| POST http://localhost:8080/checkout?amount=1500 | ||
| Content-Type: application/x-www-form-urlencoded | ||
| Cookie: userId=alice; cartId=cart123 | ||
|
|
||
| ### | ||
|
|
||
| ### 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("/paymment/{amount}") { | ||||||||||||||
| call.respondText("Payment for ${call.parameters["amount"]} is pending...") | ||||||||||||||
| } | ||||||||||||||
|
Comment on lines
+58
to
+60
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo in route path: Double 'm' in the route path. Proposed fix- get("/paymment/{amount}") {
+ get("/payment/{amount}") {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| } | ||||||||||||||
| } | ||||||||||||||
| 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:3037/admin | ||
|
Comment on lines
+15
to
+16
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Non-standard PostgreSQL port in sample configuration. Port 🤖 Prompt for AI Agents |
||
|
|
||
| connection: | ||
| domain: api.example.com | ||
| path: /v1 | ||
| protocol: https | ||
|
|
||
| payments: | ||
| url: "http://localhost:8080" | ||
| clientKey: "super-secret-client-key" | ||
| 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@hostvariable defined but never used;cartIdvalue inconsistent across requests.The
@hostvariable on line 1 is never referenced — all requests hardcodehttp://localhost:8080. Either use{{host}}/checkout...in the request URLs or remove the variable.Also, the successful checkout (line 6) uses
cartId=cart123while the other two scenarios (lines 13, 20) usecartId=cart-123. Consider making the format consistent to avoid confusing readers of the sample.Proposed fix
🤖 Prompt for AI Agents