Browse Source

finish auth type metadata

main
James Fenn 8 months ago
parent
commit
44e785ddcb
6 changed files with 117 additions and 40 deletions
  1. +3
    -0
      docs/build.gradle
  2. +53
    -28
      docs/src/me/jfenn/ktordocs/HtmlBuilder.kt
  3. +7
    -1
      docs/src/me/jfenn/ktordocs/RestDocsFeature.kt
  4. +44
    -3
      docs/src/me/jfenn/ktordocs/model/AuthenticationInfo.kt
  5. +1
    -0
      docs/src/me/jfenn/ktordocs/model/ParameterInfo.kt
  6. +9
    -8
      example/src/Application.kt

+ 3
- 0
docs/build.gradle View File

@@ -14,5 +14,8 @@ 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"

implementation "com.atlassian.commonmark:commonmark:0.15.2"
}

+ 53
- 28
docs/src/me/jfenn/ktordocs/HtmlBuilder.kt View File

@@ -7,6 +7,8 @@ import me.jfenn.ktordocs.`interface`.HasParams
import me.jfenn.ktordocs.`interface`.HasReferences
import me.jfenn.ktordocs.model.EndpointInfo
import me.jfenn.ktordocs.model.ParameterInfo
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer

class HtmlBuilder(
val docs: RestDocs
@@ -48,7 +50,9 @@ class HtmlBuilder(
}
td("text-muted") { +param.type }
td("text-muted") { +param.location.value }
td("text-muted") { +param.desc }
td("text-muted") {
markdown(param.desc, inline = true)
}
}
}
}
@@ -80,7 +84,9 @@ class HtmlBuilder(
}
}
endpoint.desc?.let {
p("text-muted") { +it }
div("text-muted") {
markdown(it)
}
}
div("alert bg-light") {
span("mr-3 badge badge-" + when (endpoint.method) {
@@ -101,22 +107,18 @@ class HtmlBuilder(
parameterInfo(endpoint)

h5 { +"Code samples" }
pre("alert bg-light") {
code {
style = "tab-size: 2;"

+buildString {
appendln("curl -X ${endpoint.method.value} \\")
codeblock(tabSize = 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}' \\")
}
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}' \\")
if (endpoint.method == HttpMethod.Post || endpoint.method == HttpMethod.Put)
appendln("\t-d '{data}' \\")

appendln("\t${docs.config.baseUrl}${endpoint.path}")
}
appendln("\t${docs.config.baseUrl}${endpoint.path}")
}
}

@@ -139,15 +141,12 @@ class HtmlBuilder(
span("text-muted") { +" (${code.description})" }
}
}
td("text-muted") { +response.desc }
td("text-muted") {
markdown(response.desc, inline = true)
}
td {
response.example?.let {
pre("alert bg-light") {
code {
style = "tab-size: 4;"
+it
}
}
codeblock { +it }
}
}
}
@@ -173,25 +172,29 @@ class HtmlBuilder(
)

style {
+"table {font-size:0.8rem;display:inline-block;max-width:100%;overflow-x:auto;}"
+"table {font-size:0.8rem;display:inline-block;max-width:100%;overflow-x:auto;}.bg-dark a {color:#b1d4fb !important;}"
}
}
body {
div("row m-0") {
div("col-12 col-md-3 bg-dark text-light") {
style = "background-color: #05264c !important; font-size: 0.9rem;"

div("py-4") {
style = "position: sticky; top: 0;"

div("px-2") {
h3 { +docs.config.title }
p("text-white-50") { +docs.config.desc }
div("text-white-50") {
markdown(docs.config.desc)
}

span("font-weight-bold") { +"Contents" }
h5 { +"Contents" }
}

docs.endpoints.forEach {
div("p-2 border-bottom border-secondary") {
a(classes = "text-light", href = "#" + it.id) { +it.title }
div("p-2 border-bottom border-primary") {
a(href = "#" + it.id) { +it.title }
}
}
}
@@ -206,7 +209,7 @@ class HtmlBuilder(

docs.config.authMethods.forEach { (name, auth) ->
modal("auth_${name}", auth.title) {
p { +auth.desc }
markdown(auth.desc)

auth.subDesc?.let {
p { small("text-muted") { +it } }
@@ -247,3 +250,25 @@ fun FlowContent.modal(modalId: String, modalTitle: String, content: DIV.() -> Un
}
}
}

fun FlowContent.codeblock(tabSize: Int = 4, content: CODE.() -> Unit) {
pre("alert bg-light") {
code {
style = "tab-size: ${tabSize};"
content()
}
}
}

fun HTMLTag.markdown(markdown: String, inline: Boolean = false) {
val parser = Parser.builder().build()
val renderer = HtmlRenderer.builder().build()

val html = renderer.render(parser.parse(markdown))

unsafe {
if (inline)
+html.replace(Regex("</?p>"), "")
else +html
}
}

+ 7
- 1
docs/src/me/jfenn/ktordocs/RestDocsFeature.kt View File

@@ -42,7 +42,13 @@ class RestDocsFeature(
// add auth methods
newEndpoint = inheritEndpoint.copy(
authentication = selector.names.filterNotNull()
)
).apply {
selector.names.firstOrNull()?.let {
docs.config.authMethods[it]
}?.also {
params.putAll(it.params)
}
}
}

(route.selector as? HttpMethodRouteSelector)?.let { selector ->


+ 44
- 3
docs/src/me/jfenn/ktordocs/model/AuthenticationInfo.kt View File

@@ -20,19 +20,60 @@ data class AuthenticationInfo(
sealed class Type(val value: String) {
object Unknown : Type("")
object Basic : Type("basic")
object Form : Type("form")
class Form(
val userParamName: String = "user",
val passwordParamName: String = "password"
) : Type("form")
object Digest : Type("digest")
object JWT : Type("jwt")
object OAuth : Type("oauth")
}

fun type(type: Type) {
when (type) {
Type.Basic -> {
is 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."
desc = "A string containing the username and password separated by a colon (':') character."
location = ParameterInfo.In.Header
example = "username:password"
}
}
is Type.Form -> {
subDesc = "The username and password are provided as raw values from a form submission."
param(type.userParamName) {
desc = "The user's username / identifier."
location = ParameterInfo.In.FormData
}
param(type.passwordParamName) {
desc = "The user's raw password."
location = ParameterInfo.In.FormData
}
reference("https://developer.mozilla.org/en-US/docs/Web/API/FormData/Using_FormData_Objects", "Web FormData API")
}
is Type.Digest -> {
subDesc = "The username and password are provided in a `Digest` authorization header."
param("Authorization") {
desc = "A string containing various request authorization properties."
location = ParameterInfo.In.Header
example = """Digest realm="{realm}" username="{username}" uri="{uri}" nonce="{nonce}" opaque="{opaque}" nc="{nc}" algorithm="MD5" response="{response}" cnonce="{cnonce}" qop="{qop}""""
}
reference("https://en.wikipedia.org/wiki/Digest_access_authentication", "Digest access authentication (Wikipedia)")
}
is Type.JWT -> {
subDesc = "The client provides a stateless JSON Web Token in a `Bearer` authorization header."
param("Authorization") {
desc = "A string containing various request authorization properties."
location = ParameterInfo.In.Header
example = "Bearer {token}"
}
reference("https://jwt.io/", "JWT.io")
}
is Type.OAuth -> {
subDesc = "The client must authenticate with an external OAuth provider."
reference("https://ktor.io/servers/features/authentication/oauth.html", "Ktor OAuth Guide")
reference("https://oauth.net/2/", "OAuth 2.0 Specification")
}
else -> {}
}
}


+ 1
- 0
docs/src/me/jfenn/ktordocs/model/ParameterInfo.kt View File

@@ -13,6 +13,7 @@ data class ParameterInfo(
object Header : In("header")
object Path : In("path")
object Query : In("query")
object FormData : In("FormData")
}

companion object {


+ 9
- 8
example/src/Application.kt View File

@@ -1,16 +1,13 @@
package me.jfenn

import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.ContentType
import io.ktor.response.*
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
@@ -37,17 +34,21 @@ fun Application.module() {
baseUrl = "https://example.com"

title = "Example API"
desc = "Basic documentation generated for testing & demo purposes."
desc = """
Basic documentation generated for **testing** and _demo_ purposes.
Find the source code [here](https://code.horrific.dev/james/ktordocs)!
""".trimIndent()

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."
desc = "A simple authentication method **intended for debugging**, which _only_ checks whether a username is equal to the provided password."
}

defaultProperties {
param("accept") {
desc = "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"
@@ -60,7 +61,7 @@ fun Application.module() {
get("/hello") {
docs {
title = "Hello world"
desc = "Responds with 'hello world' :)"
desc = "Responds with _\"hello world\"_ :)"
}

call.respondText("""{"hello": "world"}""", ContentType.Application.Json)


Loading…
Cancel
Save