@@ -13,5 +13,6 @@ sourceSets { | |||
dependencies { | |||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | |||
implementation "io.ktor:ktor-server-core:$ktor_version" | |||
implementation "io.ktor:ktor-auth:$ktor_version" | |||
implementation "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.1" | |||
} |
@@ -0,0 +1,249 @@ | |||
package me.jfenn.ktordocs | |||
import io.ktor.http.HttpMethod | |||
import kotlinx.html.* | |||
import kotlinx.html.stream.appendHTML | |||
import me.jfenn.ktordocs.`interface`.HasParams | |||
import me.jfenn.ktordocs.`interface`.HasReferences | |||
import me.jfenn.ktordocs.model.EndpointInfo | |||
import me.jfenn.ktordocs.model.ParameterInfo | |||
class HtmlBuilder( | |||
val docs: RestDocs | |||
) { | |||
private fun DIV.referenceInfo(item: HasReferences) { | |||
if (item.references.isNotEmpty()) { | |||
h5 { +"Links" } | |||
ul { | |||
item.references.forEach { reference -> | |||
li { | |||
a(href = reference.url) { +reference.title } | |||
} | |||
} | |||
} | |||
} | |||
} | |||
private fun DIV.parameterInfo(item: HasParams) { | |||
if (item.params.isNotEmpty()) { | |||
h5 { +"Parameters" } | |||
table("table") { | |||
thead { | |||
tr { | |||
th { +"Name" } | |||
th { +"Type" } | |||
th { +"In" } | |||
th { +"Description" } | |||
} | |||
} | |||
tbody { | |||
item.params.forEach { (name, param) -> | |||
tr { | |||
td { | |||
span("text-monospace font-weight-bold") { +name } | |||
if (param.isRequired) { | |||
span("text-danger") { +"*" } | |||
} | |||
} | |||
td("text-muted") { +param.type } | |||
td("text-muted") { +param.location.value } | |||
td("text-muted") { +param.desc } | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
private fun DIV.endpointInfo(endpoint: EndpointInfo) { | |||
div("mb-5") { | |||
div { | |||
h4("d-inline") { | |||
id = endpoint.id | |||
a(href = "#" + endpoint.id) { +endpoint.title } | |||
} | |||
if (endpoint.auth.isNotEmpty()) { | |||
span("float-right") { | |||
endpoint.auth.forEach { | |||
button(type = ButtonType.button, classes = "btn btn-link text-monospace") { | |||
attributes["data-toggle"] = "modal" | |||
attributes["data-target"] = "#auth_${it}" | |||
+it | |||
} | |||
} | |||
// "lock" icon | |||
+"\uD83D\uDD12" | |||
} | |||
} | |||
} | |||
endpoint.desc?.let { | |||
p("text-muted") { +it } | |||
} | |||
div("alert bg-light") { | |||
span("mr-3 badge badge-" + when (endpoint.method) { | |||
HttpMethod.Get -> "primary" | |||
HttpMethod.Post -> "success" | |||
HttpMethod.Put -> "warning" | |||
HttpMethod.Patch -> "info" | |||
HttpMethod.Delete -> "danger" | |||
else -> "secondary" | |||
}) { | |||
+endpoint.method.value | |||
} | |||
span("text-monospace") { | |||
+endpoint.path | |||
} | |||
} | |||
parameterInfo(endpoint) | |||
h5 { +"Code samples" } | |||
pre("alert bg-light") { | |||
code { | |||
style = "tab-size: 2;" | |||
+buildString { | |||
appendln("curl -X ${endpoint.method.value} \\") | |||
endpoint.params.filterValues { it.location == ParameterInfo.In.Header }.forEach { | |||
appendln("\t-H '${it.value.name}: ${it.value.example}' \\") | |||
} | |||
if (endpoint.method == HttpMethod.Post || endpoint.method == HttpMethod.Put) | |||
appendln("\t-d '{data}' \\") | |||
appendln("\t${docs.config.baseUrl}${endpoint.path}") | |||
} | |||
} | |||
} | |||
if (endpoint.responses.isNotEmpty()) { | |||
h5 { +"Responses" } | |||
table("table") { | |||
thead { | |||
tr { | |||
th { +"Code" } | |||
th { +"Description" } | |||
th { +"Value" } | |||
} | |||
} | |||
tbody { | |||
endpoint.responses.forEach { (code, response) -> | |||
tr { | |||
td { | |||
a(href = "https://http.cat/${code.value}") { | |||
span("text-monospace") { +code.value.toString() } | |||
span("text-muted") { +" (${code.description})" } | |||
} | |||
} | |||
td("text-muted") { +response.desc } | |||
td { | |||
response.example?.let { | |||
pre("alert bg-light") { | |||
code { | |||
style = "tab-size: 4;" | |||
+it | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
referenceInfo(endpoint) | |||
} | |||
} | |||
fun toHtml() : String = buildString { | |||
appendln("<!DOCTYPE html>") | |||
appendHTML().html { | |||
head { | |||
meta(charset = "utf-8") | |||
title(docs.config.title) | |||
link( | |||
href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.1/css/bootstrap.min.css", | |||
rel = "stylesheet" | |||
) | |||
style { | |||
+"table {font-size:0.8rem;display:inline-block;max-width:100%;overflow-x:auto;}" | |||
} | |||
} | |||
body { | |||
div("row m-0") { | |||
div("col-12 col-md-3 bg-dark text-light") { | |||
div("py-4") { | |||
style = "position: sticky; top: 0;" | |||
div("px-2") { | |||
h3 { +docs.config.title } | |||
p("text-white-50") { +docs.config.desc } | |||
span("font-weight-bold") { +"Contents" } | |||
} | |||
docs.endpoints.forEach { | |||
div("p-2 border-bottom border-secondary") { | |||
a(classes = "text-light", href = "#" + it.id) { +it.title } | |||
} | |||
} | |||
} | |||
} | |||
div("col-12 col-md-9") { | |||
div("container my-4") { | |||
h1("mb-4") { +"Endpoints" } | |||
docs.endpoints.forEach { endpointInfo(it) } | |||
} | |||
} | |||
} | |||
docs.config.authMethods.forEach { (name, auth) -> | |||
modal("auth_${name}", auth.title) { | |||
p { +auth.desc } | |||
auth.subDesc?.let { | |||
p { small("text-muted") { +it } } | |||
} | |||
parameterInfo(auth) | |||
referenceInfo(auth) | |||
} | |||
} | |||
script(src = "https://code.jquery.com/jquery-3.5.1.slim.min.js") {} | |||
script(src = "https://cdn.jsdelivr.net/npm/popper.js@1.16.1/dist/umd/popper.min.js") {} | |||
script(src = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js") {} | |||
} | |||
} | |||
appendln() | |||
} | |||
} | |||
fun FlowContent.modal(modalId: String, modalTitle: String, content: DIV.() -> Unit) { | |||
div("modal fade") { | |||
id = modalId | |||
attributes["tabindex"] = "-1" | |||
div("modal-dialog") { | |||
div("modal-content") { | |||
div("modal-header") { | |||
h5("modal-title") { +modalTitle } | |||
button(type = ButtonType.button, classes = "close") { | |||
attributes["data-dismiss"] = "modal" | |||
unsafe { +"×" } | |||
} | |||
} | |||
div("modal-body") { | |||
content() | |||
} | |||
} | |||
} | |||
} | |||
} |
@@ -3,182 +3,22 @@ package me.jfenn.ktordocs | |||
import io.ktor.http.HttpMethod | |||
import kotlinx.html.* | |||
import kotlinx.html.stream.appendHTML | |||
import me.jfenn.ktordocs.model.AuthenticationInfo | |||
import me.jfenn.ktordocs.model.Configuration | |||
import me.jfenn.ktordocs.model.EndpointInfo | |||
import me.jfenn.ktordocs.model.ParameterInfo | |||
import me.jfenn.ktordocs.util.slugify | |||
class RestDocs( | |||
configure: Configuration.() -> Unit | |||
) { | |||
val config = Configuration().apply { configure() } | |||
val htmlBuilder = HtmlBuilder(this) | |||
val endpoints = ArrayList<EndpointInfo>() | |||
fun add(info: EndpointInfo) { | |||
fun endpoint(info: EndpointInfo) { | |||
endpoints.add(info) | |||
} | |||
fun FlowContent.endpointInfo(endpoint: EndpointInfo) { | |||
div("my-5") { | |||
h4 { | |||
id = endpoint.id | |||
a(href = "#" + endpoint.id) { +endpoint.title } | |||
} | |||
endpoint.description?.let { | |||
p("text-muted") { +it } | |||
} | |||
div("alert bg-light") { | |||
span("mr-3 badge badge-" + when (endpoint.method) { | |||
HttpMethod.Get -> "primary" | |||
HttpMethod.Post -> "success" | |||
HttpMethod.Put -> "warning" | |||
HttpMethod.Patch -> "info" | |||
HttpMethod.Delete -> "danger" | |||
else -> "secondary" | |||
}) { | |||
+endpoint.method.value | |||
} | |||
span("text-monospace") { | |||
+endpoint.path | |||
} | |||
} | |||
if (endpoint.params.isNotEmpty()) { | |||
h5 { +"Parameters" } | |||
table("table") { | |||
thead { | |||
tr { | |||
th { +"Name" } | |||
th { +"Type" } | |||
th { +"In" } | |||
th { +"Description" } | |||
} | |||
} | |||
tbody { | |||
endpoint.params.forEach { (name, param) -> | |||
tr { | |||
td { | |||
span("text-monospace") { +name } | |||
if (param.isRequired) { | |||
span("text-danger") { +"*" } | |||
} | |||
} | |||
td("text-muted") { +param.type } | |||
td("text-muted") { +param.location.value } | |||
td("text-muted") { +param.description } | |||
} | |||
} | |||
} | |||
} | |||
} | |||
h5 { +"Code samples" } | |||
pre("alert bg-light") { | |||
code { | |||
style = "tab-size: 2;" | |||
+buildString { | |||
appendln("curl -X ${endpoint.method.value} \\") | |||
endpoint.params.filterValues { it.location == ParameterInfo.In.Header }.forEach { | |||
appendln("\t-H '${it.value.name}: ${it.value.example}' \\") | |||
} | |||
if (endpoint.method == HttpMethod.Post || endpoint.method == HttpMethod.Put) | |||
appendln("\t-d '{data}' \\") | |||
appendln("\t${config.baseUrl}${endpoint.path}") | |||
} | |||
} | |||
} | |||
if (endpoint.responses.isNotEmpty()) { | |||
h5 { +"Responses" } | |||
table("table") { | |||
thead { | |||
tr { | |||
th { +"Code" } | |||
th { +"Description" } | |||
th { +"Value" } | |||
} | |||
} | |||
tbody { | |||
endpoint.responses.forEach { (code, response) -> | |||
tr { | |||
td { | |||
a(href = "https://http.cat/${code.value}") { | |||
span("text-monospace") { +code.value.toString() } | |||
span("text-muted") { +" (${code.description})" } | |||
} | |||
} | |||
td("text-muted") { +(response.description ?: "") } | |||
td { | |||
response.example?.let { | |||
pre("p-3 bg-dark text-light rounded") { | |||
code("py-3") { | |||
style = "tab-size: 4;" | |||
+it | |||
} | |||
} | |||
} ?: run { | |||
+"No example provided." | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
} | |||
fun toHtml() : String = buildString { | |||
appendln("<!DOCTYPE html>") | |||
appendHTML().html { | |||
head { | |||
meta(charset = "utf-8") | |||
title(config.title) | |||
link( | |||
href = "https://stackpath.bootstrapcdn.com/bootstrap/4.5.1/css/bootstrap.min.css", | |||
rel = "stylesheet" | |||
) | |||
style { | |||
+"table {font-size:0.8rem;display:inline-block;max-width:100%;overflow-x:auto;}" | |||
} | |||
} | |||
body { | |||
div("row m-0") { | |||
div("col-12 col-md-3 bg-dark text-light") { | |||
div("py-4") { | |||
style = "position: sticky; top: 0;" | |||
div("px-2") { | |||
h3 { +config.title } | |||
p { +config.description } | |||
h4 { +"Contents" } | |||
} | |||
endpoints.forEach { | |||
div("p-2 border-bottom border-secondary") { | |||
a(classes = "text-light", href = "#" + it.id) { +it.title } | |||
} | |||
} | |||
} | |||
} | |||
div("col-12 col-md-9") { | |||
div("container my-5") { | |||
endpoints.forEach { endpointInfo(it) } | |||
} | |||
} | |||
} | |||
} | |||
} | |||
appendln() | |||
} | |||
} |
@@ -1,6 +1,8 @@ | |||
package me.jfenn.ktordocs | |||
import io.ktor.application.* | |||
import io.ktor.auth.AuthenticationProvider | |||
import io.ktor.auth.AuthenticationRouteSelector | |||
import io.ktor.http.ContentType | |||
import io.ktor.response.respondText | |||
import io.ktor.routing.* | |||
@@ -33,10 +35,19 @@ class RestDocsFeature( | |||
}.joinToString("/") | |||
} | |||
suspend fun updateRoutes(route: Route, selectors: List<RouteSelector> = listOf()) { | |||
suspend fun updateRoutes(route: Route, selectors: List<RouteSelector> = listOf(), inheritEndpoint: EndpointInfo = docs.config.defaultEndpoint) { | |||
var newEndpoint = inheritEndpoint | |||
(route.selector as? AuthenticationRouteSelector)?.let { selector -> | |||
// add auth methods | |||
newEndpoint = inheritEndpoint.copy( | |||
authentication = selector.names.filterNotNull() | |||
) | |||
} | |||
(route.selector as? HttpMethodRouteSelector)?.let { selector -> | |||
val path = buildPathString(selectors) | |||
val endpoint = docs.config.defaultEndpoint.copy( | |||
val endpoint = inheritEndpoint.copy( | |||
path = path, | |||
method = selector.method | |||
) | |||
@@ -70,7 +81,7 @@ class RestDocsFeature( | |||
} catch (e: DocsProxyException) { | |||
// caught proxy extension; add endpoint to docs | |||
e.configure.invoke(endpoint) | |||
docs.add(endpoint) | |||
docs.endpoint(endpoint) | |||
return | |||
} catch (e: Throwable) { | |||
// do nothing | |||
@@ -79,7 +90,7 @@ class RestDocsFeature( | |||
} | |||
route.children.forEach { | |||
updateRoutes(it, selectors + it.selector) | |||
updateRoutes(it, selectors + it.selector, newEndpoint) | |||
} | |||
} | |||
@@ -107,7 +118,7 @@ fun Route.restDocumentation(path: String = "/") { | |||
get(path) { | |||
application.feature(RestDocsFeature).apply { | |||
call.respondText(docs.toHtml(), ContentType.Text.Html) | |||
call.respondText(docs.htmlBuilder.toHtml(), ContentType.Text.Html) | |||
} | |||
} | |||
} | |||
@@ -0,0 +1,13 @@ | |||
package me.jfenn.ktordocs.`interface` | |||
import me.jfenn.ktordocs.model.ParameterInfo | |||
interface HasParams { | |||
val params: HashMap<String, ParameterInfo> | |||
fun param(name: String, configure: ParameterInfo.() -> Unit = {}) { | |||
params[name] = (params[name] ?: ParameterInfo(name)).apply { configure() } | |||
} | |||
} |
@@ -0,0 +1,13 @@ | |||
package me.jfenn.ktordocs.`interface` | |||
import me.jfenn.ktordocs.model.ReferenceInfo | |||
interface HasReferences { | |||
val references: ArrayList<ReferenceInfo> | |||
fun reference(url: String, title: String = url) { | |||
references.add(ReferenceInfo(url, title)) | |||
} | |||
} |
@@ -0,0 +1,40 @@ | |||
package me.jfenn.ktordocs.model | |||
import me.jfenn.ktordocs.`interface`.HasParams | |||
import me.jfenn.ktordocs.`interface`.HasReferences | |||
data class AuthenticationInfo( | |||
val name: String, | |||
var title: String = name, | |||
var desc: String = "No description provided.", | |||
var subDesc: String? = null, | |||
internal var type: Type = Type.Unknown | |||
) : HasReferences, HasParams { | |||
override var references = arrayListOf( | |||
ReferenceInfo("https://ktor.io/servers/features/authentication", "Ktor Documentation") | |||
) | |||
override val params = HashMap<String, ParameterInfo>() | |||
sealed class Type(val value: String) { | |||
object Unknown : Type("") | |||
object Basic : Type("basic") | |||
object Form : Type("form") | |||
} | |||
fun type(type: Type) { | |||
when (type) { | |||
Type.Basic -> { | |||
subDesc = "The username and password are provided as raw values in a request header." | |||
param("Basic") { | |||
desc = "A header containing the username and password, separated by a colon (':') character." | |||
location = ParameterInfo.In.Header | |||
example = "username:password" | |||
} | |||
} | |||
else -> {} | |||
} | |||
} | |||
} |
@@ -7,7 +7,13 @@ class Configuration { | |||
var baseUrl = "http://localhost:8080" | |||
var title = "REST API" | |||
var description = "This documentation describes the website's API endpoints." | |||
var desc = "This documentation describes the website's API endpoints." | |||
internal val authMethods = HashMap<String, AuthenticationInfo>() | |||
fun authMethod(name: String, configure: AuthenticationInfo.() -> Unit) { | |||
authMethods[name] = (authMethods[name] ?: AuthenticationInfo(name)).apply { configure() } | |||
} | |||
internal var defaultEndpoint = EndpointInfo("/", HttpMethod.Get) | |||
@@ -2,46 +2,50 @@ package me.jfenn.ktordocs.model | |||
import io.ktor.http.HttpMethod | |||
import io.ktor.http.HttpStatusCode | |||
import me.jfenn.ktordocs.`interface`.HasParams | |||
import me.jfenn.ktordocs.`interface`.HasReferences | |||
import me.jfenn.ktordocs.util.slugify | |||
class EndpointInfo( | |||
val path: String, | |||
val method: HttpMethod, | |||
var title: String = path, | |||
var description: String? = null | |||
) { | |||
var desc: String? = null, | |||
var auth: List<String> = listOf() | |||
) : HasReferences, HasParams { | |||
val id get() = title.slugify() | |||
internal val params = HashMap<String, ParameterInfo>() | |||
override var references = ArrayList<ReferenceInfo>() | |||
override val params = HashMap<String, ParameterInfo>() | |||
internal val responses = HashMap<HttpStatusCode, ResponseInfo>() | |||
fun param(name: String, configure: ParameterInfo.() -> Unit = {}) { | |||
params[name] = (params[name] ?: ParameterInfo(name)).apply { configure() } | |||
} | |||
fun responds(code: HttpStatusCode = HttpStatusCode.OK, configure: ResponseInfo.() -> Unit = {}) { | |||
responses[code] = (responses[code] ?: ResponseInfo(code)).apply { configure() } | |||
} | |||
fun copy( | |||
path: String = this.path, | |||
method: HttpMethod = this.method | |||
method: HttpMethod = this.method, | |||
authentication: List<String> = this.auth | |||
) : EndpointInfo { | |||
return EndpointInfo( | |||
path = path, | |||
method = method, | |||
title = title, | |||
description = description | |||
).also { | |||
desc = desc, | |||
auth = authentication | |||
).also { new -> | |||
params.forEach { (name, param) -> | |||
it.params[name] = param.copy() | |||
new.params[name] = param.copy() | |||
} | |||
responses.forEach { (code, response) -> | |||
it.responses[code] = response.copy() | |||
new.responses[code] = response.copy() | |||
} | |||
new.references = ArrayList(references.map { it.copy() }) | |||
} | |||
} | |||
@@ -2,7 +2,7 @@ package me.jfenn.ktordocs.model | |||
data class ParameterInfo( | |||
val name: String, | |||
var description: String = "No description provided.", | |||
var desc: String = "No description provided.", | |||
var type: String = TYPE_STRING, | |||
var isRequired: Boolean = false, | |||
var location: In = In.Query, | |||
@@ -0,0 +1,10 @@ | |||
package me.jfenn.ktordocs.model | |||
import io.ktor.http.HttpStatusCode | |||
import kotlin.reflect.KClass | |||
import kotlin.reflect.full.declaredMemberProperties | |||
data class ReferenceInfo( | |||
val url: String, | |||
val title: String = url | |||
) |
@@ -6,7 +6,7 @@ import kotlin.reflect.full.declaredMemberProperties | |||
data class ResponseInfo( | |||
val code: HttpStatusCode, | |||
var description: String? = null, | |||
var desc: String = "No description provided.", | |||
var example: String? = null | |||
) { | |||
@@ -15,6 +15,7 @@ sourceSets { | |||
dependencies { | |||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" | |||
implementation "io.ktor:ktor-server-netty:$ktor_version" | |||
implementation "io.ktor:ktor-auth:$ktor_version" | |||
implementation "ch.qos.logback:logback-classic:$logback_version" | |||
testImplementation "io.ktor:ktor-server-tests:$ktor_version" | |||
@@ -2,15 +2,18 @@ package me.jfenn | |||
import io.ktor.application.* | |||
import io.ktor.http.ContentType | |||
import io.ktor.http.HttpStatusCode | |||
import io.ktor.response.* | |||
import io.ktor.request.* | |||
import io.ktor.routing.get | |||
import io.ktor.routing.post | |||
import io.ktor.routing.route | |||
import io.ktor.routing.routing | |||
import io.ktor.auth.Authentication | |||
import io.ktor.auth.UserIdPrincipal | |||
import io.ktor.auth.authenticate | |||
import io.ktor.auth.basic | |||
import me.jfenn.ktordocs.RestDocsFeature | |||
import me.jfenn.ktordocs.docs | |||
import me.jfenn.ktordocs.model.AuthenticationInfo | |||
import me.jfenn.ktordocs.model.ParameterInfo | |||
import me.jfenn.ktordocs.restDocumentation | |||
@@ -19,16 +22,32 @@ fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args) | |||
@Suppress("unused") // Referenced in application.conf | |||
@kotlin.jvm.JvmOverloads | |||
fun Application.module() { | |||
install(Authentication) { | |||
basic("basicAuth") { | |||
realm = "Ktor Server" | |||
validate { credentials -> | |||
if (credentials.name == credentials.password) | |||
UserIdPrincipal(credentials.name) | |||
else null | |||
} | |||
} | |||
} | |||
install(RestDocsFeature) { | |||
baseUrl = "https://example.com" | |||
title = "Example API" | |||
description = "Basic documentation generated for testing & demo purposes." | |||
desc = "Basic documentation generated for testing & demo purposes." | |||
authMethod("basicAuth") { | |||
type(AuthenticationInfo.Type.Basic) | |||
title = "Basic Authentication" | |||
desc = "A simple authentication method intended for debugging, which only checks whether a username is equal to the provided password." | |||
} | |||
defaultProperties { | |||
param("accept") { | |||
description = "The content type to return - such as 'application/json'" | |||
desc = "The content type to return - such as 'application/json'" | |||
location = ParameterInfo.In.Header | |||
type = "Content Type" | |||
example = "application/json" | |||
@@ -41,7 +60,7 @@ fun Application.module() { | |||
get("/hello") { | |||
docs { | |||
title = "Hello world" | |||
description = "Responds with 'hello world' :)" | |||
desc = "Responds with 'hello world' :)" | |||
} | |||
call.respondText("""{"hello": "world"}""", ContentType.Application.Json) | |||
@@ -50,13 +69,13 @@ fun Application.module() { | |||
get("/user/{username?}") { | |||
docs { | |||
title = "Get user info" | |||
description = "Returns an object representation of the requested user ID." | |||
desc = "Returns an object representation of the requested user ID." | |||
param("username") { | |||
type = "SHA1 hash" | |||
description = "A valid ID or username of the user to fetch." | |||
desc = "A valid ID or username of the user to fetch." | |||
} | |||
responds(HttpStatusCode.OK) { | |||
description = "If the user has been found" | |||
responds { | |||
desc = "If the user has been found" | |||
exampleJson(UserInfo::class) | |||
} | |||
} | |||
@@ -66,22 +85,24 @@ fun Application.module() { | |||
""".trimIndent()) | |||
} | |||
post("/user/{username}") { | |||
docs { | |||
title = "Modify user info" | |||
description = "Updates the provided user info in the database." | |||
param("username") { | |||
type = "SHA1 hash" | |||
description = "A valid ID or username of the user to update." | |||
} | |||
responds(HttpStatusCode.OK) { | |||
description = "If the user was successfully updated" | |||
authenticate("basicAuth") { | |||
post("/user/{username}") { | |||
docs { | |||
title = "Modify user info" | |||
desc = "Updates the provided user info in the database." | |||
param("username") { | |||
type = "SHA1 hash" | |||
desc = "A valid ID or username of the user to update." | |||
} | |||
responds { | |||
desc = "If the user was successfully updated" | |||
} | |||
} | |||
} | |||
} | |||
} | |||
restDocumentation("/docs") | |||
restDocumentation("/") | |||
} | |||
} | |||
@@ -1,4 +1,4 @@ | |||
ktor_version=1.3.2 | |||
kotlin.code.style=official | |||
kotlin_version=1.3.70 | |||
kotlin_version=1.3.72 | |||
logback_version=1.2.1 |