Skip to content
Open
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
8 changes: 4 additions & 4 deletions codeSnippets/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ buildscript {
}
repositories {
mavenLocal()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
}

configurations.classpath {
Expand Down Expand Up @@ -70,14 +70,14 @@ allprojects {
kotlin_version = rootProject.properties['kotlin_snapshot_version']
repositories {
mavenLocal()
maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") }
}
}

repositories {
mavenCentral()
maven {
url "https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev"
url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/dev")
}
}
}
Expand All @@ -89,7 +89,7 @@ def ktorRepositoryDir = file("$buildDir/m2")
if (ktorRepositoryDir.exists()) {
allprojects {
repositories {
maven { url ktorRepositoryDir.absolutePath }
maven { url = uri(ktorRepositoryDir.absolutePath) }
}
}
} else {
Expand Down
2 changes: 2 additions & 0 deletions codeSnippets/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ kotlin.code.style = official
kotlin.native.binary.memoryModel = experimental
# gradle configuration
org.gradle.configureondemand = false
kotlin.mpp.applyDefaultHierarchyTemplate=false
org.gradle.java.installations.auto-download=false
# versions
kotlin_version = 2.2.20
ktor_version = 3.4.0-eap-1477
Expand Down
1 change: 1 addition & 0 deletions codeSnippets/settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ module("snippets", "tutorial-server-websockets")
module("snippets", "tutorial-server-docker-compose")
module("snippets", "htmx-integration")
module("snippets", "server-http-request-lifecycle")
module("snippets", "openapi-spec-gen")

if(!System.getProperty("os.name").startsWith("Windows")) {
module("snippets", "embedded-server-native")
Expand Down
20 changes: 20 additions & 0 deletions codeSnippets/snippets/openapi-spec-gen/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# OpenAPI documentation

A sample Ktor project showing how to build OpenAPI documentation using routing annotations and the compiler
extension of the Ktor Gradle plugin.

> This sample is a part of the [`codeSnippets`](../../README.md) Gradle project.

## Run the application

To run the application, execute the following command in the repository's root directory:

```bash
./gradlew :openapi-spec-gen:run
```

To view the OpenAPI documentation, navigate to the following URLs:

- [http://0.0.0.0:8080/docs.json](http://0.0.0.0:8080/docs.json) to view a JSON document of the API spec.
- [http://0.0.0.0:8080/openApi](http://0.0.0.0:8080/openApi) to view the OpenAPI UI for the API spec.
- [http://0.0.0.0:8080/swaggerUI](http://0.0.0.0:8080/swaggerUI) to view the Swagger UI for the API spec.
40 changes: 40 additions & 0 deletions codeSnippets/snippets/openapi-spec-gen/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
val ktor_version = "3.4.0-eap-1511"
val kotlin_version: String by project
val logback_version: String by project

plugins {
application
kotlin("jvm")
id("io.ktor.plugin") version "3.3.3"
}

application {
mainClass = "io.ktor.server.netty.EngineMain"
}

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

ktor {
openApi {
enabled = true
codeInferenceEnabled = true
onlyCommented = false
}
}


dependencies {
implementation("io.ktor:ktor-server-core:$ktor_version")
implementation("io.ktor:ktor-server-routing-annotate:$ktor_version")
implementation("io.ktor:ktor-server-openapi:$ktor_version")
implementation("io.ktor:ktor-server-content-negotiation:${ktor_version}")
implementation("io.ktor:ktor-serialization-kotlinx-json:${ktor_version}")
implementation("io.ktor:ktor-server-swagger:${ktor_version}")
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version")
testImplementation("io.ktor:ktor-server-test-host-jvm:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package com.example

import io.ktor.annotate.OpenApiDocSource
import io.ktor.annotate.annotate
import io.ktor.annotate.generateOpenApiDoc
import io.ktor.http.*
import io.ktor.openapi.OpenApiDoc
import io.ktor.openapi.OpenApiInfo
import io.ktor.openapi.jsonSchema
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.*
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
import io.ktor.server.plugins.openapi.*
import io.ktor.server.plugins.swagger.swaggerUI
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json

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

fun Application.module() {
install(ContentNegotiation) {
json()
json(Json {
encodeDefaults = false
})
}
routing {
// Main page for marketing
get("/") {
call.respondText("<html><body><h1>Hello, World</h1></body></html>", ContentType.Text.Html)
}

/**
* API endpoints for users.
*
* These will appear in the resulting OpenAPI document.
*/
val apiRoute = userCrud()

get("/docs.json") {
val docs = generateOpenApiDoc(
OpenApiDoc(info = OpenApiInfo("My API", "1.0")),
apiRoute.descendants()
)
call.respond(docs)
}

/**
* View the generated UI for the API spec.
*/
openAPI("/openApi"){
outputPath = "docs/routes"
// title, version, etc.
info = OpenApiInfo("My API from routes", "1.0.0")
// which routes to read from to build the model
// by default, it will check for `openapi/documentation.yaml` then use the routing root as a fallback
source = OpenApiDocSource.RoutingSource(ContentType.Application.Json) {
apiRoute.descendants()
}
}

/**
* View the Swagger flavor of the UI for the API spec.
*/
swaggerUI("/swaggerUI") {
info = OpenApiInfo("My API", "1.0")
source = OpenApiDocSource.RoutingSource(ContentType.Application.Json) {
apiRoute.descendants()
}
}
}
}

fun Routing.userCrud(): Route =
route("/api") {
route("/users") {
val list = mutableListOf<User>()

/**
* Get a single user by ID.
*
* – Path: id [ULong] the ID of the user
* – Response: 400 The ID parameter is malformatted or missing.
* – Response: 404 The user for the given ID does not exist.
* – Response: 200 [User] The user found with the given ID.
*/
get("/{id}") {
val id = call.parameters["id"]?.toULongOrNull()
?: return@get call.respond(HttpStatusCode.BadRequest)
val user = list.find { it.id == id }
?: return@get call.respond(HttpStatusCode.NotFound)
call.respond(user)
}

/**
* Get a list of users.
*
* – Response: 200 The list of items.
*/
get("/users") {
val query = call.parameters["q"]
val result = if (query != null) {
list.filter {it.name.contains(query, ignoreCase = true) }
} else {
list
}

call.respond(result)
}.annotate {
summary = "Get users"
description = "Retrieves a list of users."
parameters {
query("q") {
description = "An encoded query"
required = false
}
}
responses {
HttpStatusCode.OK {
description = "A list of users"
schema = jsonSchema<List<User>>()
}
HttpStatusCode.BadRequest {
description = "Invalid query"
ContentType.Text.Plain()
}
}
}
Comment on lines +112 to +131
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would be good to leave a comment here that describes the order of precedence here in building the final model. In this case, we have the comment API and code inference that will supply some of the details, which will merged with the runtime API call from the annotate function.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a short paragraph below the example and a new section called "Metadata precedence". Not sure if the placement of it makes sense though and if it's better to place it higher up 🤔


/**
* Save a new user.
*
* – Response: 204 The new user was saved.
*/
post {
list += call.receive<User>()
call.respond(HttpStatusCode.NoContent)
}

/**
* Delete the user with the given ID.
*
* – Path id [ULong] the ID of the user to remove
* – Response: 400 The ID parameter is malformatted or missing.
* – Response: 404 The user for the given ID does not exist.
* – Response: 204 The user was deleted.
*/
delete("/{id}") {
val id = call.parameters["id"]?.toULongOrNull()
?: return@delete call.respond(HttpStatusCode.BadRequest)
if (!list.removeIf { it.id == id })
return@delete call.respond(HttpStatusCode.NotFound)
call.respond(HttpStatusCode.NoContent)
}

}
}

@Serializable
data class User(val id: ULong, val name: String)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ktor {
deployment {
port = 8080
}
application {
modules = [ com.example.ApplicationKt.module ]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example

import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.testing.*
import kotlin.test.*

class ApplicationTest {
@Test
fun testRoot() = testApplication {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ logback-classic = { module = "ch.qos.logback:logback-classic", version.ref = "lo
ktor-server-config-yaml = { module = "io.ktor:ktor-server-config-yaml-jvm", version.ref = "ktor-version" }
ktor-server-test-host = { module = "io.ktor:ktor-server-test-host-jvm", version.ref = "ktor-version" }
kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin-version" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation-jvm", version.ref = "ktor-version" }
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor-version" }

[plugins]
kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin-version" }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,6 @@ dependencies {
implementation("ch.qos.logback:logback-classic:$logback_version")
implementation("io.ktor:ktor-server-config-yaml:$ktor_version")
testImplementation("io.ktor:ktor-server-test-host-jvm")
testImplementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version")
testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,5 @@ dependencies {
implementation("ch.qos.logback:logback-classic:$logback_version")
testImplementation("io.ktor:ktor-server-test-host-jvm")
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
testImplementation("io.ktor:ktor-client-content-negotiation-jvm:$ktor_version")
testImplementation("io.ktor:ktor-client-content-negotiation:$ktor_version")
}
Loading