大多数的 Web 框架都有官方或社群开发的 OpenAPI Generator,自动把程序码转为 OpenAPI 文件。相对於 Spring Boot 有完整成熟的套件 springdoc-openapi,目前 Ktor 就只有2个 Github 不太成熟的套件,而且缺乏我想要的功能,使用方式也可以再改进。
另一方面,目前 Ktor YouTrack 上的 issue KTOR-774 仍处於 open 状态,官方团队在 2021-05-03 於 Slack 的回应是 We are investigating different ways on how to implement it using idiomatic kotlin and ktor style. No estimates yet.
所以看来短期是不会推出解决方案了。
综合以上原因,我打算自己动手实作 OpenAPI Generator,目标是…
我们先了解 OpenAPI Generator 及 Swagger UI 的运作方式才知道要如何实作
撰写 OpenAPI definition 及实作 Generator 等到明天再说明,在此我们先假设已经产生了 openapi.json 内容,然後要实作 Ktor OpenAPI Plugin 整合 Swagger UI (点此看完整程序码)。
OpenAPI Plugin 先读取设定档的 openapi 设定,然後建立 ProjectOpenApiManager
物件,後续每个子专案会注册自己的 openapi.json。接下来是建立 2 个 route,一个是下载 Swagger UI 的静态档案 js, css,另一个是下载 openapi.json,这样就能透过 Swagger UI 浏览 API 文件了。
// application.conf
openApi {
swaggerUI {
// swagger-ui 静态档案 js, css 路径,档案会一并打包至 shadow fat jar 及 docker image
dir = ${SWAGGER_UI_PATH}
}
}
install(OpenApiFeature)
override fun install(pipeline: Application, configure: Configuration.() -> Unit): OpenApiFeature {
val configuration = Configuration().apply(configure)
val feature = OpenApiFeature(configuration)
val appConfig = pipeline.get<MyApplicationConfig>()
val openApiConfig = appConfig.infra.openApi ?: configuration.build()
val projectOpenApiManager = ProjectOpenApiManager(openApiConfig)
pipeline.koin {
modules(
module(createdAtStart = true) {
single { projectOpenApiManager }
}
)
}
pipeline.routing {
// 下载 openapi.json
get("/apidocs/schema/{schemaJsonFileName}") {
val schemaJsonFileName = call.parameters["schemaJsonFileName"]
?: throw RequestException(InfraResponseCode.BAD_REQUEST_PATH, "schema json file name is required")
val projectId = schemaJsonFileName.substringBefore(".")
call.respond(projectOpenApiManager.getOpenApiJson(projectId))
}
// 下载 swagger-ui 静态档案 js, css
static("apidocs") {
resources("swagger-ui")
files(swaggerUiDir)
}
}
}
// 支援 multi-project 架构
class ProjectOpenApiManager(val config: OpenApiConfig) {
private val openApiMap: MutableMap<String, ProjectOpenApi> = mutableMapOf()
private val openApiJsonMap: MutableMap<String, String> = mutableMapOf()
fun register(projectOpenApi: ProjectOpenApi) {
val projectId = projectOpenApi.projectId
require(!openApiMap.containsKey(projectId))
openApiMap[projectId] = projectOpenApi
projectOpenApi.init(config)
}
fun getOpenApiJson(projectId: String): String {
return openApiJsonMap.getOrPut(projectId) {
val openAPIObject = openApiMap[projectId]?.openAPIObject
?: throw RequestException(InfraResponseCode.ENTITY_NOT_FOUND, "$projectId openapi json not found")
Jackson.toJsonString(openAPIObject)
}
}
}
假设在没有 CI/CD 的环境下,只有部署後端 API Server 而已,我们要怎麽知道目前在 Server 上的程序版本呢? 延伸的相关问题还有
既然我们已经可以自动产生 API 文件,那麽如果我们在建置部署阶段就把 Git 版本资讯加进文件,以上问题就可以解决了。
我们先定义 git 资讯的相关设定值於 application.conf 及对应的 config 类别 OpenApiInfoConfig
openApi {
info {
env = "@env@"
gitTagVersion = "@gitTagVersion@"
gitCommitVersion = "@gitCommitVersion@"
buildTime = "@buildTime@"
description = ""
}
swaggerUI {
dir = ${SWAGGER_UI_PATH}
}
}
data class OpenApiConfig(
val info: OpenApiInfoConfig,
val swaggerUI: SwaggerUIConfig? = null
)
data class OpenApiInfoConfig(
val env: String,
val gitTagVersion: String,
val gitCommitVersion: String,
val buildTime: String,
val description: String = ""
)
另一方面,我们也安装 Gradle Git Plugin,在 shadow plugin 执行 installShadowDist
task 打包档案时,先读取当下的 git branch 的 commit, tag 资讯、env 部署环境名称,还有 buildTime 建置当下的时间。然後再用 Ant filter 替换 application.conf 设定档里的变数值,这样 OpenAPI Plugin 就能透过设定档读取到 git 资讯了 (点此看完整 gradle build script)
plugins {
id("org.unbroken-dome.gitversion") version "0.10.0"
}
val branchToEnvMap: Map<String, String> = mapOf(
devBranchName to "dev",
testBranchName to "test",
releaseBranchName to "prod"
)
project.version = gitVersion.determineVersion()
val semVersion = (project.version as SemVersion)
val tagVersion = "${semVersion.major}.${semVersion.minor}.${semVersion.patch}"
val branch = semVersion.prereleaseTag!!
val env = branchToEnvMap[branch]
val shadowDistConfigFiles by tasks.register("shadowDistConfigFiles") {
group = "distribution"
doLast {
copy {
from("dist/config/$env")
into("src/dist")
filter<ReplaceTokens>(
"tokens" to mapOf(
"env" to env,
"gitTagVersion" to tagVersion,
"gitCommitVersion" to semVersion.toString(),
"buildTime" to DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now())
)
)
}
}
}
在 OpenAPI Plugin 读取 git 资讯之後,接下来就是想办法显示在 Swagger UI 的 description 区块,因为 Swagger UI 本身是 html 网页,所以我想使用 kotlinx.html library,以 Kotlin DSL 语法撰写 html,建立 div block 再崁入到 openapi.json 的 description 属性,经过排版後会比较美观且清楚。
fun init(config: OpenApiConfig) {
openAPIObject = OpenAPIObject(
info = Info(
title = "$projectId API (${config.info.env})", description = buildInfoDescription(config),
version = config.info.gitTagVersion
),
servers = listOf(Server(url = urlRootPath)),
tags = operations.flatMap { it.tags }
)
}
private fun buildInfoDescription(config: OpenApiConfig): String {
return buildString {
appendHTML(false).div {
p {
+config.info.description
}
ul {
li {
+"Server Start Time: ${
DateTimeUtils.LOCAL_DATE_TIME_FORMATTER.format(LocalDateTime.now(DateTimeUtils.TAIWAN_ZONE_ID))
}"
}
li { +"Build Time: ${config.info.buildTime}" }
li { +"Git Commit Version: ${config.info.gitCommitVersion}" }
}
}
}
}
另一个实务上的问题是API 文件权限控管,有些 API 文件是不可以对外公开的,虽然可以限制内网存取,最好还是可以设定帐密进行控管
我们先定义帐号密码设定值於 application.conf 及对应的 config 类别 SwaggerUIConfig
openApi {
swaggerUI {
dir = "/home/ec2-user/swagger-ui"
username = ${?SWAGGER_UI_AUTH_USER} #optional
password = ${?SWAGGER_UI_AUTH_PASSWORD} #optional
}
}
data class SwaggerUIConfig(
val dir: String?,
val username: String?,
val password: String?
)
然後在 OpenAPI Plugin 设定 Ktor Authentication Plugin,使用 HTTP Basic Authentication 保护 Swagger UI 的那2个 endpoints。
override fun install(pipeline: Application, configure: Configuration.() -> Unit): OpenApiFeature {
val configuration = Configuration().apply(configure)
val feature = OpenApiFeature(configuration)
val appConfig = pipeline.get<MyApplicationConfig>()
val openApiConfig = appConfig.infra.openApi ?: configuration.build()
val swaggerUIConfig = openApiConfig.swaggerUI
val swaggerUIAuth = swaggerUIConfig?.needAuth ?: false
if (swaggerUIAuth) {
requireNotNull(swaggerUIConfig)
pipeline.feature(Authentication).apply {
configure {
basic(swaggerUIAuthProviderName) {
realm = swaggerUIAuthProviderName
validate { userPasswordCredential ->
if (swaggerUIConfig.username == userPasswordCredential.name &&
swaggerUIConfig.password == userPasswordCredential.password
) UserIdPrincipal(userPasswordCredential.name) else null
}
}
}
}
}
pipeline.routing {
if (swaggerUIAuth) {
authenticate(swaggerUIAuthProviderName) {
apiDocsRoute(this, swaggerUIConfig, projectOpenApiManager)
}
} else {
apiDocsRoute(this, swaggerUIConfig, projectOpenApiManager)
}
}
}
今天说明如何实作 OpenAPI Plugin 整合 swaggerUI 产生 API 文件,明天再深入探讨开发者如何为 ktor route 撰写 OpenAPI Definition
延续昨日 今天我们且战且走 首先先把最简单的排序专案方法搞定 先创一个sortby function...
useCallback 如果父元件所传递的 props 包含 Object, 则在元件因状态改变而 ...
什麽是Github Actions呢?这是Github平台 在2019年上架的CI功能,使用Gith...
今天要先来介绍 Sass @each 与 map Sass map-get 这里要先宣告变数(有点类...
使用WooCommerce架设购物网站,预设的订单通知信里面,「商品名称」都是「纯文字」呈现,并不会...