-
Notifications
You must be signed in to change notification settings - Fork 363
KTOR-9165 and KTOR-9193 Documentation for OpenAPI specification generation #744
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: 3.4.0
Are you sure you want to change the base?
Changes from all commits
c3623a8
ef10e78
61898e8
a189f03
2422d37
78a1528
320c30d
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,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. |
| 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
Contributor
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. 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.
Collaborator
Author
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. 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 { | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.