aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLinnea Gräf <nea@nea.moe>2025-01-17 13:57:58 +0100
committerLinnea Gräf <nea@nea.moe>2025-01-17 13:57:58 +0100
commit8a9f076d826cb93dcce292180de6fc2be66a7872 (patch)
treef6f92e71ee918d3ea9a55b5392eec52f70d7b747
parente08c8778640967cc086a922f178b18e08b313a29 (diff)
downloadLocalTransactionLedger-8a9f076d826cb93dcce292180de6fc2be66a7872.tar.gz
LocalTransactionLedger-8a9f076d826cb93dcce292180de6fc2be66a7872.tar.bz2
LocalTransactionLedger-8a9f076d826cb93dcce292180de6fc2be66a7872.zip
feat(openapi): Add openapi method routes
-rw-r--r--server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt3
-rw-r--r--server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt1
-rw-r--r--server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt124
3 files changed, 85 insertions, 43 deletions
diff --git a/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt
index 0f13606..56492db 100644
--- a/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt
+++ b/server/core/src/main/kotlin/moe/nea/ledger/server/core/Application.kt
@@ -6,6 +6,8 @@ import io.ktor.server.application.install
import io.ktor.server.netty.EngineMain
import io.ktor.server.plugins.compression.Compression
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
+import io.ktor.server.response.respondRedirect
+import io.ktor.server.routing.get
import io.ktor.server.routing.route
import io.ktor.server.routing.routing
import kotlinx.serialization.json.Json
@@ -43,6 +45,7 @@ fun Application.module() {
routing {
route("/api") {
this.apiRouting(database)
+ get { call.respondRedirect("/openapi/") }
}
route("/api.json") {
openApiDocsJson()
diff --git a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt
index d447df0..ebee503 100644
--- a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt
+++ b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/OpenApiModel.kt
@@ -28,6 +28,7 @@ data class OpenApiRoute(
val summary: String,
val description: String,
val get: OpenApiOperation?,
+ val put: OpenApiOperation?,
val patch: OpenApiOperation?,
val post: OpenApiOperation?,
val delete: OpenApiOperation?,
diff --git a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt
index 86ed99d..fd63f81 100644
--- a/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt
+++ b/server/swagger/src/main/kotlin/moe/nea/ledger/server/core/api/docs.kt
@@ -1,6 +1,7 @@
package moe.nea.ledger.server.core.api
import io.ktor.http.ContentType
+import io.ktor.http.HttpMethod
import io.ktor.http.HttpStatusCode
import io.ktor.http.content.OutgoingContent
import io.ktor.http.defaultForFilePath
@@ -99,36 +100,55 @@ internal class InputStreamContent(
override fun readFrom(): ByteReadChannel = input.toByteReadChannel(pool = KtorDefaultPool)
}
-class DocumentationPath(val path: String) {
+class DocumentationPath(val path: String)
- companion object {
- fun createDocumentationPath(baseRoute: Route?, route: Route): DocumentationPath {
- return DocumentationPath(createRoutePath(baseRoute, route as RoutingNode))
- }
+class DocumentationEndpoint private constructor() {
+ var method: HttpMethod = HttpMethod.Get
+ private set
+ lateinit var path: DocumentationPath
+ private set
- private fun createRoutePath(
- baseRoute: Route?,
- route: RoutingNode,
- ): String {
- if (baseRoute == route)
- return "/"
- val parent = route.parent
- if (parent == null) {
- if (baseRoute != null)
- error("Could not find $route in $baseRoute")
- return "/"
- }
- var parentPath = createRoutePath(baseRoute, parent)
- var parentPathAppendable = parentPath
- if (!parentPathAppendable.endsWith("/"))
- parentPathAppendable += "/"
- return when (val selector = route.selector) {
- is TrailingSlashRouteSelector -> parentPathAppendable
- is RootRouteSelector -> parentPath
- is PathSegmentConstantRouteSelector -> parentPathAppendable + selector.value
- is HttpMethodRouteSelector -> parentPath // TODO: generate a separate path here
- else -> error("Could not comprehend $selector (${selector.javaClass})")
+ private fun initFromPath(
+ baseRoute: Route?,
+ route: RoutingNode,
+ ) {
+ path = DocumentationPath(createRoutePath(baseRoute, route))
+ }
+
+ private fun createRoutePath(
+ baseRoute: Route?,
+ route: RoutingNode,
+ ): String {
+ if (baseRoute == route)
+ return "/"
+ val parent = route.parent
+ if (parent == null) {
+ if (baseRoute != null)
+ error("Could not find $route in $baseRoute")
+ return "/"
+ }
+ val parentPath = createRoutePath(baseRoute, parent)
+ var parentPathAppendable = parentPath
+ if (!parentPathAppendable.endsWith("/"))
+ parentPathAppendable += "/"
+ return when (val selector = route.selector) {
+ is TrailingSlashRouteSelector -> parentPathAppendable
+ is RootRouteSelector -> parentPath
+ is PathSegmentConstantRouteSelector -> parentPathAppendable + selector.value
+ is HttpMethodRouteSelector -> {
+ method = selector.method
+ parentPath
}
+
+ else -> error("Could not comprehend $selector (${selector.javaClass})")
+ }
+ }
+
+ companion object {
+ fun createDocumentationPath(baseRoute: Route?, route: Route): DocumentationEndpoint {
+ val path = DocumentationEndpoint()
+ path.initFromPath(baseRoute, route as RoutingNode)
+ return path
}
}
}
@@ -153,7 +173,7 @@ interface IntoTag {
fun intoTag(): String
}
-class DocumentationContext(val path: DocumentationPath) {
+class DocumentationOperationContext(val route: DocumentationContext) {
val responses = mutableMapOf<HttpStatusCode, Response>()
fun responds(statusCode: HttpStatusCode, block: Response.() -> Unit) {
responses.getOrPut(statusCode) { Response() }.also(block)
@@ -176,23 +196,40 @@ class DocumentationContext(val path: DocumentationPath) {
tag.mapTo(tags) { it.intoTag() }
}
+ fun intoJson(): OpenApiOperation {
+ return OpenApiOperation(
+ tags = tags.map { Tag(it) },
+ summary = summary,
+ description = description,
+ operationId = operationId,
+ deprecated = deprecated,
+ responses = responses.mapValues {
+ it.value.intoJson()
+ }
+ )
+
+ }
+}
+
+class DocumentationContext(val path: DocumentationPath) {
+ val ops: MutableMap<HttpMethod, DocumentationOperationContext> = mutableMapOf()
+ var summary: String = ""
+ var description = ""
fun intoJson(): OpenApiRoute {
return OpenApiRoute(
summary,
description,
- get = OpenApiOperation(
- tags = tags.map { Tag(it) },
- summary = summary,
- description = description,
- operationId = operationId,
- deprecated = deprecated,
- responses = responses.mapValues {
- it.value.intoJson()
- }
- ),
- post = null, patch = null, delete = null, // TODO: generate separate contexts for those
+ get = ops[HttpMethod.Get]?.intoJson(),
+ put = ops[HttpMethod.Put]?.intoJson(),
+ post = ops[HttpMethod.Post]?.intoJson(),
+ patch = ops[HttpMethod.Patch]?.intoJson(),
+ delete = ops[HttpMethod.Delete]?.intoJson(),
)
}
+
+ fun createOperationNode(method: HttpMethod): DocumentationOperationContext {
+ return ops.getOrPut(method) { DocumentationOperationContext(this) }
+ }
}
@@ -210,8 +247,9 @@ class Documentation(config: Configuration) {
val info = config.info
var root: RoutingNode? = null
private val documentationNodes = mutableMapOf<DocumentationPath, DocumentationContext>()
- fun createDocumentationNode(path: DocumentationPath) =
- documentationNodes.getOrPut(path) { DocumentationContext(path) }
+ fun createDocumentationNode(endpoint: DocumentationEndpoint) =
+ documentationNodes.getOrPut(endpoint.path) { DocumentationContext(endpoint.path) }
+ .createOperationNode(endpoint.method)
private val openApiJson by lazy {
OpenApiModel(
@@ -237,9 +275,9 @@ class Documentation(config: Configuration) {
}
}
-fun Route.docs(block: DocumentationContext.() -> Unit) {
+fun Route.docs(block: DocumentationOperationContext.() -> Unit) {
val documentation = plugin(Documentation)
- val documentationPath = DocumentationPath.createDocumentationPath(documentation.root, this)
+ val documentationPath = DocumentationEndpoint.createDocumentationPath(documentation.root, this)
val node = documentation.createDocumentationNode(documentationPath)
block(node)
}