Add a Kotlin DSL for endpoint documentation (#249)

This commit is contained in:
Mitchell Syer
2021-11-14 09:46:39 -05:00
committed by GitHub
parent 845b588426
commit b02884f58d
5 changed files with 450 additions and 11 deletions

View File

@@ -16,7 +16,8 @@ dependencies {
implementation("com.squareup.okio:okio:2.10.0")
// Javalin api
implementation("io.javalin:javalin:4.0.0")
implementation("io.javalin:javalin:4.1.1")
implementation("io.javalin:javalin-openapi:4.1.1")
// jackson version locked by javalin, ref: `io.javalin.core.util.OptionalDependency`
val jacksonVersion = "2.12.4"
implementation("com.fasterxml.jackson.core:jackson-databind:$jacksonVersion")

View File

@@ -52,7 +52,7 @@ object MangaAPI {
}
path("manga") {
get("{mangaId}", MangaController::retrieve)
get("{mangaId}", MangaController.retrieve)
get("{mangaId}/thumbnail", MangaController::thumbnail)
get("{mangaId}/category", MangaController::categoryList)

View File

@@ -13,20 +13,35 @@ import suwayomi.tachidesk.manga.impl.Chapter
import suwayomi.tachidesk.manga.impl.Library
import suwayomi.tachidesk.manga.impl.Manga
import suwayomi.tachidesk.manga.impl.Page
import suwayomi.tachidesk.manga.model.dataclass.MangaDataClass
import suwayomi.tachidesk.server.JavalinSetup.future
import suwayomi.tachidesk.server.util.handler
import suwayomi.tachidesk.server.util.pathParam
import suwayomi.tachidesk.server.util.queryParam
import suwayomi.tachidesk.server.util.withOperation
object MangaController {
/** get manga info */
fun retrieve(ctx: Context) {
val mangaId = ctx.pathParam("mangaId").toInt()
val onlineFetch = ctx.queryParam("onlineFetch")?.toBoolean() ?: false
ctx.future(
future {
Manga.getManga(mangaId, onlineFetch)
val retrieve = handler(
pathParam<Int>("mangaId"),
queryParam("onlineFetch", false),
documentWith = {
withOperation {
summary("Get a manga")
description("Get a manga from the database using a specific id")
}
)
}
},
behaviorOf = { ctx, mangaId, onlineFetch ->
ctx.future(
future {
Manga.getManga(mangaId, onlineFetch)
}
)
},
withResults = {
json<MangaDataClass>("OK")
}
)
/** manga thumbnail */
fun thumbnail(ctx: Context) {

View File

@@ -11,6 +11,10 @@ import io.javalin.Javalin
import io.javalin.apibuilder.ApiBuilder.path
import io.javalin.core.security.RouteRole
import io.javalin.http.staticfiles.Location
import io.javalin.plugin.openapi.OpenApiOptions
import io.javalin.plugin.openapi.OpenApiPlugin
import io.javalin.plugin.openapi.ui.SwaggerOptions
import io.swagger.v3.oas.models.info.Info
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
@@ -46,6 +50,7 @@ object JavalinSetup {
logger.info { "Serving webUI static files" }
config.addStaticFiles(applicationDirs.webUIRoot, Location.EXTERNAL)
config.addSinglePageRoot("/", applicationDirs.webUIRoot + "/index.html", Location.EXTERNAL)
config.registerPlugin(OpenApiPlugin(getOpenApiOptions()))
}
config.enableCorsForAllOrigins()
@@ -98,6 +103,21 @@ object JavalinSetup {
}
}
private fun getOpenApiOptions(): OpenApiOptions {
val applicationInfo = Info().apply {
version("1.0")
description("Tachidesk Api")
}
return OpenApiOptions(applicationInfo).apply {
path("/api/openapi.json")
swagger(
SwaggerOptions("/api/swagger-ui").apply {
title("Tachidesk Swagger Documentation")
}
)
}
}
object Auth {
enum class Role : RouteRole { ANYONE, USER_READ, USER_WRITE }
}

View File

@@ -0,0 +1,403 @@
package suwayomi.tachidesk.server.util
import io.javalin.http.Context
import io.javalin.plugin.openapi.dsl.DocumentedHandler
import io.javalin.plugin.openapi.dsl.OpenApiDocumentation
import io.javalin.plugin.openapi.dsl.documented
import io.swagger.v3.oas.models.Operation
fun <T> getSimpleParamItem(ctx: Context, param: Param<T>): String? {
return when (param) {
is Param.FormParam -> ctx.formParam(param.key)
is Param.PathParam -> ctx.pathParam(param.key)
is Param.QueryParam -> ctx.queryParam(param.key)
}
}
@Suppress("UNCHECKED_CAST")
fun <T> getParam(ctx: Context, param: Param<T>): T {
val typedItem: Any? = when (param.clazz) {
String::class.java -> getSimpleParamItem(ctx, param)
Int::class.java -> getSimpleParamItem(ctx, param)?.toIntOrNull()
Long::class.java -> getSimpleParamItem(ctx, param)?.toLongOrNull()
Boolean::class.java -> getSimpleParamItem(ctx, param)?.toBoolean()
Float::class.java -> getSimpleParamItem(ctx, param)?.toFloatOrNull()
Double::class.java -> getSimpleParamItem(ctx, param)?.toDoubleOrNull()
else -> {
when (param) {
is Param.FormParam -> ctx.formParamAsClass(param.key, param.clazz)
is Param.PathParam -> ctx.pathParamAsClass(param.key, param.clazz)
is Param.QueryParam -> ctx.queryParamAsClass(param.key, param.clazz)
}.let {
if (param.nullable) {
it.allowNullable().get() ?: param.defaultValue
} else {
if (param.defaultValue != null) {
it.getOrDefault(param.defaultValue!!)
} else {
it.get()
}
}
}
}
}
return if (param.nullable) {
typedItem as T
} else {
typedItem!! as T
}
}
inline fun getDocumentation(
documentWith: OpenApiDocumentation.() -> Unit,
noinline withResults: ResultsBuilder.() -> Unit,
vararg params: Param<*>
): OpenApiDocumentation {
return OpenApiDocumentation().apply(documentWith).apply {
applyResults(withResults)
params.forEach {
when (it) {
is Param.FormParam -> formParam(it.key, it.clazz, !it.nullable && it.defaultValue == null)
is Param.PathParam -> pathParam(it.key, it.clazz)
is Param.QueryParam -> queryParam(it.key, it.clazz,)
}
}
}
}
fun OpenApiDocumentation.applyResults(withResults: ResultsBuilder.() -> Unit) {
ResultsBuilder().apply(withResults).results.forEach {
it.applyTo(this)
}
}
fun OpenApiDocumentation.withOperation(block: Operation.() -> Unit) {
operation(block)
}
inline fun <reified T> formParam(key: String, defaultValue: T? = null): Param.FormParam<T> {
return Param.FormParam(key, T::class.java, defaultValue, null is T)
}
inline fun <reified T> queryParam(key: String, defaultValue: T? = null): Param.QueryParam<T> {
return Param.QueryParam(key, T::class.java, defaultValue, null is T)
}
inline fun <reified T> pathParam(key: String): Param.PathParam<T> {
return Param.PathParam(key, T::class.java, null, false)
}
sealed class Param<T> {
abstract val key: String
abstract val clazz: Class<T>
abstract val defaultValue: T?
abstract val nullable: Boolean
data class FormParam<T>(
override val key: String,
override val clazz: Class<T>,
override val defaultValue: T?,
override val nullable: Boolean
) : Param<T>()
data class QueryParam<T>(
override val key: String,
override val clazz: Class<T>,
override val defaultValue: T?,
override val nullable: Boolean
) : Param<T>()
data class PathParam<T>(
override val key: String,
override val clazz: Class<T>,
override val defaultValue: T?,
override val nullable: Boolean
) : Param<T>()
}
class ResultsBuilder {
val results = mutableListOf<ResultType<*>>()
inline fun <reified T> json(status: String) {
results += ResultType.MimeType(status, "application/json", T::class.java)
}
inline fun <reified T> plainText(status: String) {
results += ResultType.MimeType(status, "text/plain", String::class.java)
}
}
sealed class ResultType <T> {
abstract fun applyTo(documentation: OpenApiDocumentation)
data class MimeType<T>(val status: String, val mime: String, private val clazz: Class<T>) : ResultType<T>() {
override fun applyTo(documentation: OpenApiDocumentation) {
documentation.result(status, clazz)
}
}
}
inline fun handler(
documentWith: OpenApiDocumentation.() -> Unit = {},
noinline behaviorOf: (ctx: Context) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults),
handle = behaviorOf
)
}
inline fun <reified P1> handler(
param1: Param<P1>,
documentWith: OpenApiDocumentation.() -> Unit,
noinline behaviorOf: (ctx: Context, P1) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1),
handle = {
behaviorOf(
it,
getParam(it, param1)
)
}
)
}
inline fun <reified P1, reified P2> handler(
param1: Param<P1>,
param2: Param<P2>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2)
)
}
)
}
inline fun <reified P1, reified P2, reified P3> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
)
}
)
}
inline fun <reified P1, reified P2, reified P3, reified P4> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
param4: Param<P4>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3, P4) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3, param4),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
getParam(it, param4),
)
}
)
}
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
param4: Param<P4>,
param5: Param<P5>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3, P4, P5) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3, param4, param5),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
getParam(it, param4),
getParam(it, param5),
)
}
)
}
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5, reified P6> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
param4: Param<P4>,
param5: Param<P5>,
param6: Param<P6>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3, P4, P5, P6) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3, param4, param5, param6),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
getParam(it, param4),
getParam(it, param5),
getParam(it, param6),
)
}
)
}
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5, reified P6, reified P7> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
param4: Param<P4>,
param5: Param<P5>,
param6: Param<P6>,
param7: Param<P7>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3, P4, P5, P6, P7) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3, param4, param5, param6, param7),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
getParam(it, param4),
getParam(it, param5),
getParam(it, param6),
getParam(it, param7),
)
}
)
}
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5, reified P6, reified P7, reified P8> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
param4: Param<P4>,
param5: Param<P5>,
param6: Param<P6>,
param7: Param<P7>,
param8: Param<P8>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3, P4, P5, P6, P7, P8) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3, param4, param5, param6, param7, param8),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
getParam(it, param4),
getParam(it, param5),
getParam(it, param6),
getParam(it, param7),
getParam(it, param8),
)
}
)
}
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5, reified P6, reified P7, reified P8, reified P9> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
param4: Param<P4>,
param5: Param<P5>,
param6: Param<P6>,
param7: Param<P7>,
param8: Param<P8>,
param9: Param<P9>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3, P4, P5, P6, P7, P8, P9) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3, param4, param5, param6, param7, param8, param9),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
getParam(it, param4),
getParam(it, param5),
getParam(it, param6),
getParam(it, param7),
getParam(it, param8),
getParam(it, param9),
)
}
)
}
inline fun <reified P1, reified P2, reified P3, reified P4, reified P5, reified P6, reified P7, reified P8, reified P9, reified P10> handler(
param1: Param<P1>,
param2: Param<P2>,
param3: Param<P3>,
param4: Param<P4>,
param5: Param<P5>,
param6: Param<P6>,
param7: Param<P7>,
param8: Param<P8>,
param9: Param<P9>,
param10: Param<P10>,
documentWith: OpenApiDocumentation.() -> Unit = {},
crossinline behaviorOf: (ctx: Context, P1, P2, P3, P4, P5, P6, P7, P8, P9, P10) -> Unit,
noinline withResults: ResultsBuilder.() -> Unit
): DocumentedHandler {
return documented(
documentation = getDocumentation(documentWith, withResults, param1, param2, param3, param4, param5, param6, param7, param8, param9, param10),
handle = {
behaviorOf(
it,
getParam(it, param1),
getParam(it, param2),
getParam(it, param3),
getParam(it, param4),
getParam(it, param5),
getParam(it, param6),
getParam(it, param7),
getParam(it, param8),
getParam(it, param9),
getParam(it, param10),
)
}
)
}