增加了api文档
This commit is contained in:
1 parent
e0e57a7eb4
commit
73e770db80
25 files changed
+913
-140
No files matched your search
@@ -11,22 +11,45 @@ package simcu::simapi.openapi
|
||||
|
||||
import std.io.*
|
||||
import std.fs.*
|
||||
import std.unicode.*
|
||||
import stdx.encoding.base64.*
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_web_hosting.*
|
||||
import simcu::simapi.configurations.*
|
||||
|
||||
/**
|
||||
* @brief 提供 OpenAPI UI 静态资源中间件。
|
||||
*
|
||||
* 资源加载顺序:优先读取应用 wwwroot/{prefix} 目录(允许使用方覆盖),
|
||||
* 缺失时回退到 simapi-cj 包内置资源(Base64 内联)。
|
||||
*
|
||||
* 路由约定:
|
||||
* /{prefix} → 重定向到 /{prefix}/all.html
|
||||
* /{prefix}/all.html → 多文档切换页(含顶部栏下拉)
|
||||
* /{prefix}/{id}.html → 单文档页(无顶部栏,JS 从 URL 提取 id 加载 {id}.json)
|
||||
* /{prefix}/urls → 文档列表 JSON(由 simapi_extensions 注册)
|
||||
* /{prefix}/{id}.json → OpenAPI 文档 JSON(由 mapOpenApi 注册)
|
||||
*/
|
||||
public class OpenApiUIMiddleware <: IMiddleware {
|
||||
private static let _openApiRootPath = PathString("/openapi")
|
||||
private static var _allHtmlCache: ?Array<Byte> = None
|
||||
private static var _singleHtmlCache: ?Array<Byte> = None
|
||||
private static var _bundleJsCache: ?Array<Byte> = None
|
||||
private static var _presetJsCache: ?Array<Byte> = None
|
||||
private static var _cssCache: ?Array<Byte> = None
|
||||
private let _env: IWebHostEnvironment
|
||||
private let _rootPath: PathString
|
||||
private let _wwwRootSubDir: String
|
||||
|
||||
/**
|
||||
* @brief 创建 OpenAPI UI 中间件实例。
|
||||
* @param evn 当前 Web 主机环境。
|
||||
* @param options SimApi 全局配置,用于读取文档路由前缀。
|
||||
*/
|
||||
public init(evn: IWebHostEnvironment) {
|
||||
public init(evn: IWebHostEnvironment, options: SimApiOptions) {
|
||||
_env = evn
|
||||
let prefix = options.simApiDocOptions.urlPrefix.trim().trimStart('/').trimEnd('/')
|
||||
_rootPath = PathString(if (prefix.isEmpty()) { "/docs" } else { "/${prefix}" })
|
||||
_wwwRootSubDir = if (prefix.isEmpty()) { "docs" } else { prefix }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -36,7 +59,7 @@ public class OpenApiUIMiddleware <: IMiddleware {
|
||||
*/
|
||||
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
|
||||
let remainingPath = if (let Some(path) <- context.request.path.startsWithSegments(
|
||||
OpenApiUIMiddleware._openApiRootPath
|
||||
_rootPath
|
||||
)) {
|
||||
path
|
||||
} else {
|
||||
@@ -44,41 +67,151 @@ public class OpenApiUIMiddleware <: IMiddleware {
|
||||
return
|
||||
}
|
||||
|
||||
if (context.request.path == "/openapi") {
|
||||
context.response.redirect("/openapi/index.html")
|
||||
return
|
||||
}
|
||||
|
||||
let path = if (let Some(path) <- resolveAssetPath(remainingPath)) {
|
||||
path
|
||||
} else {
|
||||
next(context)
|
||||
return
|
||||
}
|
||||
|
||||
if (exists(path) && isPathWithinRoot(path)) {
|
||||
try (fs = File(path, OpenMode.Read)) {
|
||||
let data = readToEnd(fs)
|
||||
context.response.write(data)
|
||||
if (let Some(relativePath) <- resolveRelativePath(remainingPath)) {
|
||||
if (serveFromWebRoot(context, relativePath) || serveFromEmbedded(context, relativePath)) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
next(context)
|
||||
}
|
||||
|
||||
next(context)
|
||||
}
|
||||
|
||||
private func resolveRelativePath(remainingPath: PathString): ?String {
|
||||
if (remainingPath == "/" || !remainingPath.hasValue) {
|
||||
return None
|
||||
}
|
||||
|
||||
let candidate = remainingPath.value.trimStart('/')
|
||||
if (!isSafeRelativePath(candidate)) {
|
||||
return None
|
||||
}
|
||||
return Some(candidate)
|
||||
}
|
||||
|
||||
private func serveFromWebRoot(context: HttpContext, relativePath: String): Bool {
|
||||
let openApiRoot = Path(_env.webRootPath).join(_wwwRootSubDir)
|
||||
let path = openApiRoot.join(relativePath).normalize()
|
||||
if (!exists(path) || !isPathWithinRoot(path)) {
|
||||
return false
|
||||
}
|
||||
|
||||
var served = false
|
||||
try (fs = File(path, OpenMode.Read)) {
|
||||
let data = readToEnd(fs)
|
||||
context.response.contentType = contentTypeFor(relativePath)
|
||||
context.response.write(data)
|
||||
served = true
|
||||
} catch (_: Exception) {
|
||||
served = false
|
||||
}
|
||||
return served
|
||||
}
|
||||
|
||||
private func serveFromEmbedded(context: HttpContext, relativePath: String): Bool {
|
||||
let data = embeddedResource(relativePath)
|
||||
if (let Some(bytes) <- data) {
|
||||
context.response.contentType = contentTypeFor(relativePath)
|
||||
context.response.write(bytes)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func embeddedResource(relativePath: String): ?Array<Byte> {
|
||||
if (relativePath == "all.html") {
|
||||
return cachedResource(SwaggerUIResources.allHtml, "allHtml")
|
||||
}
|
||||
// index.html 不支持(已废弃,使用 all.html 或 {id}.html)
|
||||
if (relativePath == "index.html") {
|
||||
return None
|
||||
}
|
||||
// 任意 {id}.html(非 all.html)→ 返回 single.html 模板(JS 从 URL 提取 id)
|
||||
if (relativePath.endsWith(".html")) {
|
||||
return cachedResource(SwaggerUIResources.singleHtml, "singleHtml")
|
||||
}
|
||||
if (relativePath == "swagger-ui-bundle.js") {
|
||||
return cachedResource(SwaggerUIResources.swaggerUiBundleJs, "bundleJs")
|
||||
}
|
||||
if (relativePath == "swagger-ui-standalone-preset.js") {
|
||||
return cachedResource(SwaggerUIResources.swaggerUiStandalonePresetJs, "presetJs")
|
||||
}
|
||||
if (relativePath == "swagger-ui.css") {
|
||||
return cachedResource(SwaggerUIResources.swaggerUiCss, "css")
|
||||
}
|
||||
return None
|
||||
}
|
||||
|
||||
private func cachedResource(b64: String, key: String): ?Array<Byte> {
|
||||
if (let Some(cached) <- OpenApiUIMiddleware.embeddedCache(key)) {
|
||||
return Some(cached)
|
||||
}
|
||||
let decoded = OpenApiUIMiddleware.decodeBase64(b64)
|
||||
if (let Some(bytes) <- decoded) {
|
||||
OpenApiUIMiddleware.setEmbeddedCache(key, bytes)
|
||||
}
|
||||
return decoded
|
||||
}
|
||||
|
||||
private static func embeddedCache(key: String): ?Array<Byte> {
|
||||
if (key == "allHtml") {
|
||||
return OpenApiUIMiddleware._allHtmlCache
|
||||
}
|
||||
if (key == "singleHtml") {
|
||||
return OpenApiUIMiddleware._singleHtmlCache
|
||||
}
|
||||
if (key == "bundleJs") {
|
||||
return OpenApiUIMiddleware._bundleJsCache
|
||||
}
|
||||
if (key == "presetJs") {
|
||||
return OpenApiUIMiddleware._presetJsCache
|
||||
}
|
||||
if (key == "css") {
|
||||
return OpenApiUIMiddleware._cssCache
|
||||
}
|
||||
return None
|
||||
}
|
||||
|
||||
private static func setEmbeddedCache(key: String, bytes: Array<Byte>): Unit {
|
||||
if (key == "allHtml") {
|
||||
OpenApiUIMiddleware._allHtmlCache = Some(bytes)
|
||||
} else if (key == "singleHtml") {
|
||||
OpenApiUIMiddleware._singleHtmlCache = Some(bytes)
|
||||
} else if (key == "bundleJs") {
|
||||
OpenApiUIMiddleware._bundleJsCache = Some(bytes)
|
||||
} else if (key == "presetJs") {
|
||||
OpenApiUIMiddleware._presetJsCache = Some(bytes)
|
||||
} else if (key == "css") {
|
||||
OpenApiUIMiddleware._cssCache = Some(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
private func resolveAssetPath(remainingPath: PathString): ?Path {
|
||||
let relativePath = if (remainingPath == "/" || !remainingPath.hasValue) {
|
||||
"index.html"
|
||||
} else {
|
||||
let candidate = remainingPath.value.trimStart('/')
|
||||
if (!isSafeRelativePath(candidate)) {
|
||||
private static func decodeBase64(b64: String): ?Array<Byte> {
|
||||
if (b64.isEmpty()) {
|
||||
return None
|
||||
}
|
||||
try {
|
||||
let bytes = fromBase64String(b64).getOrThrow { Exception("内置资源 Base64 解码失败") }
|
||||
if (bytes.size == 0) {
|
||||
return None
|
||||
}
|
||||
candidate
|
||||
return Some(bytes)
|
||||
} catch (_: Exception) {
|
||||
return None
|
||||
}
|
||||
}
|
||||
|
||||
let openApiRoot = Path(_env.webRootPath).join("openapi")
|
||||
return openApiRoot.join(relativePath).normalize()
|
||||
private func contentTypeFor(relativePath: String): String {
|
||||
if (relativePath.endsWith(".html")) {
|
||||
return "text/html; charset=utf-8"
|
||||
}
|
||||
if (relativePath.endsWith(".css")) {
|
||||
return "text/css; charset=utf-8"
|
||||
}
|
||||
if (relativePath.endsWith(".js")) {
|
||||
return "application/javascript; charset=utf-8"
|
||||
}
|
||||
return "application/octet-stream"
|
||||
}
|
||||
|
||||
private func isSafeRelativePath(relativePath: String): Bool {
|
||||
@@ -97,7 +230,7 @@ public class OpenApiUIMiddleware <: IMiddleware {
|
||||
|
||||
private func isPathWithinRoot(path: Path): Bool {
|
||||
try {
|
||||
let rootPath = canonicalize(Path(_env.webRootPath).join("openapi"))
|
||||
let rootPath = canonicalize(Path(_env.webRootPath).join(_wwwRootSubDir))
|
||||
let targetPath = canonicalize(path)
|
||||
let rootPathString = rootPath.toString()
|
||||
let targetPathString = targetPath.toString()
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimCuTeam. All rights reserved.
|
||||
* 遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simcu::simapi.openapi
|
||||
|
||||
import std.reflect.*
|
||||
import soulsoft_web_http.*
|
||||
import simcu::simapi.annotations.*
|
||||
import simcu::simapi.openapi.metadata.*
|
||||
|
||||
/**
|
||||
* @brief 响应类型元数据实现。
|
||||
*/
|
||||
public class ApiResponseTypeMetadata <: IApiResponseTypeMetadata {
|
||||
private let _responseType: ?TypeInfo
|
||||
public init(responseType: ?TypeInfo) {
|
||||
_responseType = responseType
|
||||
}
|
||||
public prop responseType: ?TypeInfo {
|
||||
get() {
|
||||
_responseType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 为动态注册的路由提供认证和响应类型元数据扩展。
|
||||
*/
|
||||
public interface SimApiEndpointMetadataExtensions {
|
||||
func withSimApiAuth(auth: SimApiAuth): EndpointConventionBuilder
|
||||
func withResponseType(typeInfo: TypeInfo): EndpointConventionBuilder
|
||||
}
|
||||
|
||||
extend EndpointConventionBuilder <: SimApiEndpointMetadataExtensions {
|
||||
/**
|
||||
* @brief 为端点附加 SimApiAuth 认证元数据(使 OpenAPI 文档显示锁图标)。
|
||||
*/
|
||||
public func withSimApiAuth(auth: SimApiAuth): EndpointConventionBuilder {
|
||||
this.add {
|
||||
builder => builder.metadata.add(auth)
|
||||
}
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 为端点附加响应类型元数据(使动态路由也能生成 response schema)。
|
||||
*/
|
||||
public func withResponseType(typeInfo: TypeInfo): EndpointConventionBuilder {
|
||||
this.add {
|
||||
builder => builder.metadata.add(ApiResponseTypeMetadata(Some(typeInfo)))
|
||||
}
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* 自动生成文件,请勿手动编辑。
|
||||
* 由 tools/gen-swagger-ui-resources.ps1 从 resources/openapi/ 生成。
|
||||
* Swagger UI 静态资源以 Base64 内联,运行时由 OpenApiUIMiddleware 解码输出。
|
||||
*/
|
||||
|
||||
package simcu::simapi.openapi
|
||||
|
||||
/**
|
||||
* Swagger UI 内置静态资源(Base64 编码)。
|
||||
*/
|
||||
public class SwaggerUIResources {
|
||||
public static let allHtml: String = "PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJ6aC1DTiI+DQoNCjxoZWFkPg0KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04Ij4NCiAgICA8dGl0bGU+QVBJIOaWh+aho++8iOWFqOmDqO+8iTwvdGl0bGU+DQogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJzd2FnZ2VyLXVpLmNzcyI+DQogICAgPHN0eWxlPg0KICAgICAgICBodG1sIHsNCiAgICAgICAgICAgIGJveC1zaXppbmc6IGJvcmRlci1ib3g7DQogICAgICAgICAgICBvdmVyZmxvdzogLW1vei1zY3JvbGxiYXJzLXZlcnRpY2FsOw0KICAgICAgICAgICAgb3ZlcmZsb3cteTogc2Nyb2xsOw0KICAgICAgICB9DQoNCiAgICAgICAgYm9keSB7DQogICAgICAgICAgICBtYXJnaW46IDA7DQogICAgICAgICAgICBiYWNrZ3JvdW5kOiAjZmFmYWZhOw0KICAgICAgICB9DQogICAgPC9zdHlsZT4NCjwvaGVhZD4NCg0KPGJvZHk+DQogICAgPGRpdiBpZD0ic3dhZ2dlci11aSI+PC9kaXY+DQogICAgPHNjcmlwdCBzcmM9InN3YWdnZXItdWktYnVuZGxlLmpzIj48L3NjcmlwdD4NCiAgICA8c2NyaXB0IHNyYz0ic3dhZ2dlci11aS1zdGFuZGFsb25lLXByZXNldC5qcyI+PC9zY3JpcHQ+DQogICAgPHNjcmlwdD4NCiAgICAgICAgd2luZG93Lm9ubG9hZCA9IGFzeW5jIGZ1bmN0aW9uICgpIHsNCiAgICAgICAgICAgIGxldCB1cmxzID0gW107DQogICAgICAgICAgICB0cnkgew0KICAgICAgICAgICAgICAgIGNvbnN0IHJlcyA9IGF3YWl0IGZldGNoKCd1cmxzJyk7DQogICAgICAgICAgICAgICAgaWYgKHJlcy5vaykgew0KICAgICAgICAgICAgICAgICAgICBjb25zdCBsaXN0ID0gYXdhaXQgcmVzLmpzb24oKTsNCiAgICAgICAgICAgICAgICAgICAgaWYgKEFycmF5LmlzQXJyYXkobGlzdCkgJiYgbGlzdC5sZW5ndGggPiAwKSB7DQogICAgICAgICAgICAgICAgICAgICAgICB1cmxzID0gbGlzdDsNCiAgICAgICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0gY2F0Y2ggKGUpIHsgfQ0KICAgICAgICAgICAgaWYgKHVybHMubGVuZ3RoID09PSAwKSB7DQogICAgICAgICAgICAgICAgZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ3N3YWdnZXItdWknKS5pbm5lckhUTUwgPQ0KICAgICAgICAgICAgICAgICAgICAnPGRpdiBzdHlsZT0icGFkZGluZzoyMHB4O2NvbG9yOiM2NjY7Ij7ml6Dms5XliqDovb3mlofmoaPliJfooajvvIzor7fnoa7orqTmnI3liqHmraPluLjov5DooYzjgII8L2Rpdj4nOw0KICAgICAgICAgICAgICAgIHJldHVybjsNCiAgICAgICAgICAgIH0NCiAgICAgICAgICAgIHdpbmRvdy51aSA9IFN3YWdnZXJVSUJ1bmRsZSh7DQogICAgICAgICAgICAgICAgdXJsczogdXJscywNCiAgICAgICAgICAgICAgICAidXJscy5wcmltYXJ5TmFtZSI6IHVybHNbMF0ubmFtZSwNCiAgICAgICAgICAgICAgICBkb21faWQ6ICIjc3dhZ2dlci11aSIsDQogICAgICAgICAgICAgICAgZGVlcExpbmtpbmc6IHRydWUsDQogICAgICAgICAgICAgICAgcHJlc2V0czogWw0KICAgICAgICAgICAgICAgICAgICBTd2FnZ2VyVUlCdW5kbGUucHJlc2V0cy5hcGlzLA0KICAgICAgICAgICAgICAgICAgICBTd2FnZ2VyVUlTdGFuZGFsb25lUHJlc2V0DQogICAgICAgICAgICAgICAgXSwNCiAgICAgICAgICAgICAgICBsYXlvdXQ6ICJTdGFuZGFsb25lTGF5b3V0Ig0KICAgICAgICAgICAgfSk7DQogICAgICAgIH07DQogICAgPC9zY3JpcHQ+DQo8L2JvZHk+DQoNCjwvaHRtbD4="
|
||||
|
||||
public static let singleHtml: String = "PCFET0NUWVBFIGh0bWw+DQo8aHRtbCBsYW5nPSJ6aC1DTiI+DQoNCjxoZWFkPg0KICAgIDxtZXRhIGNoYXJzZXQ9IlVURi04Ij4NCiAgICA8dGl0bGU+QVBJIOaWh+ahozwvdGl0bGU+DQogICAgPGxpbmsgcmVsPSJzdHlsZXNoZWV0IiBocmVmPSJzd2FnZ2VyLXVpLmNzcyI+DQogICAgPHN0eWxlPg0KICAgICAgICBodG1sIHsNCiAgICAgICAgICAgIGJveC1zaXppbmc6IGJvcmRlci1ib3g7DQogICAgICAgICAgICBvdmVyZmxvdzogLW1vei1zY3JvbGxiYXJzLXZlcnRpY2FsOw0KICAgICAgICAgICAgb3ZlcmZsb3cteTogc2Nyb2xsOw0KICAgICAgICB9DQoNCiAgICAgICAgYm9keSB7DQogICAgICAgICAgICBtYXJnaW46IDA7DQogICAgICAgICAgICBiYWNrZ3JvdW5kOiAjZmFmYWZhOw0KICAgICAgICB9DQogICAgPC9zdHlsZT4NCjwvaGVhZD4NCg0KPGJvZHk+DQogICAgPGRpdiBpZD0ic3dhZ2dlci11aSI+PC9kaXY+DQogICAgPHNjcmlwdCBzcmM9InN3YWdnZXItdWktYnVuZGxlLmpzIj48L3NjcmlwdD4NCiAgICA8c2NyaXB0Pg0KICAgICAgICB3aW5kb3cub25sb2FkID0gZnVuY3Rpb24gKCkgew0KICAgICAgICAgICAgdmFyIHBhdGggPSB3aW5kb3cubG9jYXRpb24ucGF0aG5hbWU7DQogICAgICAgICAgICB2YXIgbWF0Y2ggPSBwYXRoLm1hdGNoKC9cLyhbXlwvXSspXC5odG1sJC8pOw0KICAgICAgICAgICAgdmFyIGlkID0gbWF0Y2ggPyBtYXRjaFsxXSA6ICdhcGknOw0KICAgICAgICAgICAgU3dhZ2dlclVJQnVuZGxlKHsNCiAgICAgICAgICAgICAgICB1cmw6IGlkICsgJy5qc29uJywNCiAgICAgICAgICAgICAgICBkb21faWQ6ICcjc3dhZ2dlci11aScsDQogICAgICAgICAgICAgICAgZGVlcExpbmtpbmc6IHRydWUsDQogICAgICAgICAgICAgICAgcHJlc2V0czogW1N3YWdnZXJVSUJ1bmRsZS5wcmVzZXRzLmFwaXNdDQogICAgICAgICAgICB9KTsNCiAgICAgICAgfTsNCiAgICA8L3NjcmlwdD4NCjwvYm9keT4NCg0KPC9odG1sPg=="
|
||||
|
||||
public static let swaggerUiBundleJs: String = "LyohIEZvciBsaWNlbnNlIGluZm9ybWF0aW9uIHBsZWFzZSBzZWUgc3dhZ2dlci11aS1idW5kbGUuanMuTElDRU5TRS50eHQgKi8KIWZ1bmN0aW9uIHdlYnBhY2tVbml2ZXJzYWxNb2R1bGVEZWZpbml0aW9uKHMsbyl7Im9iamVjdCI9PXR5cGVvZiBleHBvcnRzJiYib2JqZWN0Ij09dHlwZW9mIG1vZHVsZT9tb2R1bGUuZXhwb3J0cz1vKCk6ImZ1bmN0aW9uIj09dHlwZW9mIGRlZmluZSYmZGVmaW5lLmFtZD9kZWZpbmUoW10sbyk6Im9iamVjdCI9PXR5cGVvZiBleHBvcnRzP2V4cG9ydHMuU3dhZ2dlclVJQnVuZGxlPW8oKTpzLlN3YWdnZXJVSUJ1bmRsZT1vKCl9KHRoaXMsKCgpPT4oKCk9Pnt2YXIgcz17Njc1MjYocyxvKXsidXNlIHN0cmljdCI7by5ieXRlTGVuZ3RoPWZ1bmN0aW9uIGJ5dGVMZW5ndGgocyl7dmFyIG89Z2V0TGVucyhzKSxpPW9bMF0sYT1vWzFdO3JldHVybiAzKihpK2EpLzQtYX0sby50b0J5dGVBcnJheT1mdW5jdGlvbiB0b0J5dGVBcnJheShzKXt2YXIgbyxpLF89Z2V0TGVucyhzKSx3PV9bMF0seD1fWzFdLEM9bmV3IHUoZnVuY3Rpb24gX2J5dGVMZW5ndGgocyxvLGkpe3JldHVybiAzKihvK2kpLzQtaX0oMCx3LHgpKSxqPTAsTD14PjA/dy00Onc7Zm9yKGk9MDtpPEw7aSs9NClvPWFbcy5jaGFyQ29kZUF0KGkpXTw8MTh8YVtzLmNoYXJDb2RlQXQoaSsxKV08PDEyfGFbcy5jaGFyQ29kZUF0KGkrMildPDw2fGFbcy5jaGFyQ29kZUF0KGkrMyldLENbaisrXT1vPj4xNiYyNTUsQ1tqKytdPW8+PjgmMjU1LENbaisrXT0yNTUmbzsyPT09eCYmKG89YVtzLmNoYXJDb2RlQXQoaSldPDwyfGFbcy5jaGFyQ29kZUF0KGkrMSldPj40LENbaisrXT0yNTUmbyk7MT09PXgmJihvPWFbcy5jaGFyQ29kZUF0KGkpXTw8MTB8YVtzLmNoYXJDb2RlQXQoaSsxKV08PDR8YVtzLmNoYXJDb2RlQXQoaSsyKV0+PjIsQ1tqKytdPW8+PjgmMjU1LENbaisrXT0yNTUmbyk7cmV0dXJuIEN9LG8uZnJvbUJ5dGVBcnJheT1mdW5jdGlvbiBmcm9tQnl0ZUFycmF5KHMpe2Zvcih2YXIgbyxhPXMubGVuZ3RoLHU9YSUzLF89W10sdz0xNjM4Myx4PTAsQz1hLXU7eDxDO3grPXcpXy5wdXNoKGVuY29kZUNodW5rKHMseCx4K3c+Qz9DOngrdykpOzE9PT11PyhvPXNbYS0xXSxfLnB1c2goaVtvPj4yXStpW288PDQmNjNdKyI9PSIpKToyPT09dSYmKG89KHNbYS0yXTw8OCkrc1thLTFdLF8ucHVzaChpW28+PjEwXStpW28+PjQmNjNdK2lbbzw8MiY2M10rIj0iKSk7cmV0dXJuIF8uam9pbigiIil9O2Zvcih2YXIgaT1bXSxhPVtdLHU9InVuZGVmaW5lZCIhPXR5cGVvZiBVaW50OEFycmF5P1VpbnQ4QXJyYXk6QXJyYXksXz0iQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLyIsdz0wO3c8NjQ7Kyt3KWlbd109X1t3XSxhW18uY2hhckNvZGVBdCh3KV09dztmdW5jdGlvbiBnZXRMZW5zKHMpe3ZhciBvPXMubGVuZ3RoO2lmKG8lND4wKXRocm93IG5ldyBFcnJvcigiSW52YWxpZCBzdHJpbmcuIExlbmd0aCBtdXN0IGJlIGEgbXVsdGlwbGUgb2YgNCIpO3ZhciBpPXMuaW5kZXhPZigiPSIpO3JldHVybi0xPT09aSYmKGk9byksW2ksaT09PW8/MDo0LWklNF19ZnVuY3Rpb24gZW5jb2RlQ2h1bmsocyxvLGEpe2Zvcih2YXIgdSxfLHc9W10seD1vO3g8YTt4Kz0zKXU9KHNbeF08PDE2JjE2NzExNjgwKSsoc1t4KzFdPDw4JjY1MjgwKSsoMjU1JnNbeCsyXSksdy5wdXNoKGlbKF89dSk+PjE4JjYzXStpW18+PjEyJjYzXStpW18+PjYmNjNdK2lbNjMmX10pO3JldHVybiB3LmpvaW4oIiIpfWFbIi0iLmNoYXJDb2RlQXQoMCldPTYyLGFbIl8iLmNoYXJDb2RlQXQoMCldPTYzfSw0ODI4NyhzLG8saSl7InVzZSBzdHJpY3QiO2NvbnN0IGE9aSg2NzUyNiksdT1pKDI1MSksXz0iZnVuY3Rpb24iPT10eXBlb2YgU3ltYm9sJiYiZnVuY3Rpb24iPT10eXBlb2YgU3ltYm9sLmZvcj9TeW1ib2wuZm9yKCJub2RlanMudXRpbC5pbnNwZWN0LmN1c3RvbSIpOm51bGw7by5CdWZmZXI9QnVmZmVyLG8uU2xvd0J1ZmZlcj1mdW5jdGlvbiBTbG93QnVmZmVyKHMpeytzIT1zJiYocz0wKTtyZXR1cm4gQnVmZmVyLmFsbG9jKCtzKX0sby5JTlNQRUNUX01BWF9CWVRFUz01MDtjb25zdCB3PTIxNDc0ODM2NDc7ZnVuY3Rpb24gY3JlYXRlQnVmZmVyKHMpe2lmKHM+dyl0aHJvdyBuZXcgUmFuZ2VFcnJvcignVGhlIHZhbHVlICInK3MrJyIgaXMgaW52YWxpZCBmb3Igb3B0aW9uICJzaXplIicpO2NvbnN0IG89bmV3IFVpbnQ4QXJyYXkocyk7cmV0dXJuIE9iamVjdC5zZXRQcm90b3R5cGVPZihvLEJ1ZmZlci5wcm90b3R5cGUpLG99ZnVuY3Rpb24gQnVmZmVyKHMsbyxpKXtpZigibnVtYmVyIj09dHlwZW9mIHMpe2lmKCJzdHJpbmciPT10eXBlb2Ygbyl0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgInN0cmluZyIgYXJndW1lbnQgbXVzdCBiZSBvZiB0eXBlIHN0cmluZy4gUmVjZWl2ZWQgdHlwZSBudW1iZXInKTtyZXR1cm4gYWxsb2NVbnNhZmUocyl9cmV0dXJuIGZyb20ocyxvLGkpfWZ1bmN0aW9uIGZyb20ocyxvLGkpe2lmKCJzdHJpbmciPT10eXBlb2YgcylyZXR1cm4gZnVuY3Rpb24gZnJvbVN0cmluZyhzLG8peyJzdHJpbmciPT10eXBlb2YgbyYmIiIhPT1vfHwobz0idXRmOCIpO2lmKCFCdWZmZXIuaXNFbmNvZGluZyhvKSl0aHJvdyBuZXcgVHlwZUVycm9yKCJVbmtub3duIGVuY29kaW5nOiAiK28pO2NvbnN0IGk9MHxieXRlTGVuZ3RoKHMsbyk7bGV0IGE9Y3JlYXRlQnVmZmVyKGkpO2NvbnN0IHU9YS53cml0ZShzLG8pO3UhPT1pJiYoYT1hLnNsaWNlKDAsdSkpO3JldHVybiBhfShzLG8pO2lmKEFycmF5QnVmZmVyLmlzVmlldyhzKSlyZXR1cm4gZnVuY3Rpb24gZnJvbUFycmF5VmlldyhzKXtpZihpc0luc3RhbmNlKHMsVWludDhBcnJheSkpe2NvbnN0IG89bmV3IFVpbnQ4QXJyYXkocyk7cmV0dXJuIGZyb21BcnJheUJ1ZmZlcihvLmJ1ZmZlcixvLmJ5dGVPZmZzZXQsby5ieXRlTGVuZ3RoKX1yZXR1cm4gZnJvbUFycmF5TGlrZShzKX0ocyk7aWYobnVsbD09cyl0aHJvdyBuZXcgVHlwZUVycm9yKCJUaGUgZmlyc3QgYXJndW1lbnQgbXVzdCBiZSBvbmUgb2YgdHlwZSBzdHJpbmcsIEJ1ZmZlciwgQXJyYXlCdWZmZXIsIEFycmF5LCBvciBBcnJheS1saWtlIE9iamVjdC4gUmVjZWl2ZWQgdHlwZSAiK3R5cGVvZiBzKTtpZihpc0luc3RhbmNlKHMsQXJyYXlCdWZmZXIpfHxzJiZpc0luc3RhbmNlKHMuYnVmZmVyLEFycmF5QnVmZmVyKSlyZXR1cm4gZnJvbUFycmF5QnVmZmVyKHMsbyxpKTtpZigidW5kZWZpbmVkIiE9dHlwZW9mIFNoYXJlZEFycmF5QnVmZmVyJiYoaXNJbnN0YW5jZShzLFNoYXJlZEFycmF5QnVmZmVyKXx8cyYmaXNJbnN0YW5jZShzLmJ1ZmZlcixTaGFyZWRBcnJheUJ1ZmZlcikpKXJldHVybiBmcm9tQXJyYXlCdWZmZXIocyxvLGkpO2lmKCJudW1iZXIiPT10eXBlb2Ygcyl0aHJvdyBuZXcgVHlwZUVycm9yKCdUaGUgInZhbHVlIiBhcmd1bWVudCBtdXN0IG5vdCBiZSBvZiB0eXBlIG51bWJlci4gUmVjZWl2ZWQgdHlwZSBudW1iZXInKTtjb25zdCBhPXMudmFsdWVPZiYmcy52YWx1ZU9mKCk7aWYobnVsbCE9YSYmYSE9PXMpcmV0dXJuIEJ1ZmZlci5mcm9tKGEsbyxpKTtjb25zdCB1PWZ1bmN0aW9uIGZyb21PYmplY3Qocyl7aWYoQnVmZmVyLmlzQnVmZmVyKHMpKXtjb25zdCBvPTB8Y2hlY2tlZChzLmxlbmd0aCksaT1jcmVhdGVCdWZmZXIobyk7cmV0dXJuIDA9PT1pLmxlbmd0aHx8cy5jb3B5KGksMCwwLG8pLGl9aWYodm9pLine truncated
|
||||
|
||||
public static let swaggerUiStandalonePresetJs: String = "LyohIEZvciBsaWNlbnNlIGluZm9ybWF0aW9uIHBsZWFzZSBzZWUgc3dhZ2dlci11aS1zdGFuZGFsb25lLXByZXNldC5qcy5MSUNFTlNFLnR4dCAqLwohZnVuY3Rpb24gd2VicGFja1VuaXZlcnNhbE1vZHVsZURlZmluaXRpb24odCxlKXsib2JqZWN0Ij09dHlwZW9mIGV4cG9ydHMmJiJvYmplY3QiPT10eXBlb2YgbW9kdWxlP21vZHVsZS5leHBvcnRzPWUoKToiZnVuY3Rpb24iPT10eXBlb2YgZGVmaW5lJiZkZWZpbmUuYW1kP2RlZmluZShbXSxlKToib2JqZWN0Ij09dHlwZW9mIGV4cG9ydHM/ZXhwb3J0cy5Td2FnZ2VyVUlTdGFuZGFsb25lUHJlc2V0PWUoKTp0LlN3YWdnZXJVSVN0YW5kYWxvbmVQcmVzZXQ9ZSgpfSh0aGlzLCgoKT0+KCgpPT57dmFyIHQ9ezc1MjYodCxlKXsidXNlIHN0cmljdCI7ZS5ieXRlTGVuZ3RoPWZ1bmN0aW9uIGJ5dGVMZW5ndGgodCl7dmFyIGU9Z2V0TGVucyh0KSxyPWVbMF0sbj1lWzFdO3JldHVybiAzKihyK24pLzQtbn0sZS50b0J5dGVBcnJheT1mdW5jdGlvbiB0b0J5dGVBcnJheSh0KXt2YXIgZSxyLGk9Z2V0TGVucyh0KSxhPWlbMF0scz1pWzFdLHU9bmV3IG8oZnVuY3Rpb24gX2J5dGVMZW5ndGgodCxlLHIpe3JldHVybiAzKihlK3IpLzQtcn0oMCxhLHMpKSxjPTAsbD1zPjA/YS00OmE7Zm9yKHI9MDtyPGw7cis9NCllPW5bdC5jaGFyQ29kZUF0KHIpXTw8MTh8blt0LmNoYXJDb2RlQXQocisxKV08PDEyfG5bdC5jaGFyQ29kZUF0KHIrMildPDw2fG5bdC5jaGFyQ29kZUF0KHIrMyldLHVbYysrXT1lPj4xNiYyNTUsdVtjKytdPWU+PjgmMjU1LHVbYysrXT0yNTUmZTsyPT09cyYmKGU9blt0LmNoYXJDb2RlQXQocildPDwyfG5bdC5jaGFyQ29kZUF0KHIrMSldPj40LHVbYysrXT0yNTUmZSk7MT09PXMmJihlPW5bdC5jaGFyQ29kZUF0KHIpXTw8MTB8blt0LmNoYXJDb2RlQXQocisxKV08PDR8blt0LmNoYXJDb2RlQXQocisyKV0+PjIsdVtjKytdPWU+PjgmMjU1LHVbYysrXT0yNTUmZSk7cmV0dXJuIHV9LGUuZnJvbUJ5dGVBcnJheT1mdW5jdGlvbiBmcm9tQnl0ZUFycmF5KHQpe2Zvcih2YXIgZSxuPXQubGVuZ3RoLG89biUzLGk9W10sYT0xNjM4MyxzPTAsdT1uLW87czx1O3MrPWEpaS5wdXNoKGVuY29kZUNodW5rKHQscyxzK2E+dT91OnMrYSkpOzE9PT1vPyhlPXRbbi0xXSxpLnB1c2gocltlPj4yXStyW2U8PDQmNjNdKyI9PSIpKToyPT09byYmKGU9KHRbbi0yXTw8OCkrdFtuLTFdLGkucHVzaChyW2U+PjEwXStyW2U+PjQmNjNdK3JbZTw8MiY2M10rIj0iKSk7cmV0dXJuIGkuam9pbigiIil9O2Zvcih2YXIgcj1bXSxuPVtdLG89InVuZGVmaW5lZCIhPXR5cGVvZiBVaW50OEFycmF5P1VpbnQ4QXJyYXk6QXJyYXksaT0iQUJDREVGR0hJSktMTU5PUFFSU1RVVldYWVphYmNkZWZnaGlqa2xtbm9wcXJzdHV2d3h5ejAxMjM0NTY3ODkrLyIsYT0wO2E8NjQ7KythKXJbYV09aVthXSxuW2kuY2hhckNvZGVBdChhKV09YTtmdW5jdGlvbiBnZXRMZW5zKHQpe3ZhciBlPXQubGVuZ3RoO2lmKGUlND4wKXRocm93IG5ldyBFcnJvcigiSW52YWxpZCBzdHJpbmcuIExlbmd0aCBtdXN0IGJlIGEgbXVsdGlwbGUgb2YgNCIpO3ZhciByPXQuaW5kZXhPZigiPSIpO3JldHVybi0xPT09ciYmKHI9ZSksW3Iscj09PWU/MDo0LXIlNF19ZnVuY3Rpb24gZW5jb2RlQ2h1bmsodCxlLG4pe2Zvcih2YXIgbyxpLGE9W10scz1lO3M8bjtzKz0zKW89KHRbc108PDE2JjE2NzExNjgwKSsodFtzKzFdPDw4JjY1MjgwKSsoMjU1JnRbcysyXSksYS5wdXNoKHJbKGk9byk+PjE4JjYzXStyW2k+PjEyJjYzXStyW2k+PjYmNjNdK3JbNjMmaV0pO3JldHVybiBhLmpvaW4oIiIpfW5bIi0iLmNoYXJDb2RlQXQoMCldPTYyLG5bIl8iLmNoYXJDb2RlQXQoMCldPTYzfSw4Mjg3KHQsZSxyKXsidXNlIHN0cmljdCI7Y29uc3Qgbj1yKDc1MjYpLG89cigyNTEpLGk9ImZ1bmN0aW9uIj09dHlwZW9mIFN5bWJvbCYmImZ1bmN0aW9uIj09dHlwZW9mIFN5bWJvbC5mb3I/U3ltYm9sLmZvcigibm9kZWpzLnV0aWwuaW5zcGVjdC5jdXN0b20iKTpudWxsO2UuQnVmZmVyPUJ1ZmZlcixlLlNsb3dCdWZmZXI9ZnVuY3Rpb24gU2xvd0J1ZmZlcih0KXsrdCE9dCYmKHQ9MCk7cmV0dXJuIEJ1ZmZlci5hbGxvYygrdCl9LGUuSU5TUEVDVF9NQVhfQllURVM9NTA7Y29uc3QgYT0yMTQ3NDgzNjQ3O2Z1bmN0aW9uIGNyZWF0ZUJ1ZmZlcih0KXtpZih0PmEpdGhyb3cgbmV3IFJhbmdlRXJyb3IoJ1RoZSB2YWx1ZSAiJyt0KyciIGlzIGludmFsaWQgZm9yIG9wdGlvbiAic2l6ZSInKTtjb25zdCBlPW5ldyBVaW50OEFycmF5KHQpO3JldHVybiBPYmplY3Quc2V0UHJvdG90eXBlT2YoZSxCdWZmZXIucHJvdG90eXBlKSxlfWZ1bmN0aW9uIEJ1ZmZlcih0LGUscil7aWYoIm51bWJlciI9PXR5cGVvZiB0KXtpZigic3RyaW5nIj09dHlwZW9mIGUpdGhyb3cgbmV3IFR5cGVFcnJvcignVGhlICJzdHJpbmciIGFyZ3VtZW50IG11c3QgYmUgb2YgdHlwZSBzdHJpbmcuIFJlY2VpdmVkIHR5cGUgbnVtYmVyJyk7cmV0dXJuIGFsbG9jVW5zYWZlKHQpfXJldHVybiBmcm9tKHQsZSxyKX1mdW5jdGlvbiBmcm9tKHQsZSxyKXtpZigic3RyaW5nIj09dHlwZW9mIHQpcmV0dXJuIGZ1bmN0aW9uIGZyb21TdHJpbmcodCxlKXsic3RyaW5nIj09dHlwZW9mIGUmJiIiIT09ZXx8KGU9InV0ZjgiKTtpZighQnVmZmVyLmlzRW5jb2RpbmcoZSkpdGhyb3cgbmV3IFR5cGVFcnJvcigiVW5rbm93biBlbmNvZGluZzogIitlKTtjb25zdCByPTB8Ynl0ZUxlbmd0aCh0LGUpO2xldCBuPWNyZWF0ZUJ1ZmZlcihyKTtjb25zdCBvPW4ud3JpdGUodCxlKTtvIT09ciYmKG49bi5zbGljZSgwLG8pKTtyZXR1cm4gbn0odCxlKTtpZihBcnJheUJ1ZmZlci5pc1ZpZXcodCkpcmV0dXJuIGZ1bmN0aW9uIGZyb21BcnJheVZpZXcodCl7aWYoaXNJbnN0YW5jZSh0LFVpbnQ4QXJyYXkpKXtjb25zdCBlPW5ldyBVaW50OEFycmF5KHQpO3JldHVybiBmcm9tQXJyYXlCdWZmZXIoZS5idWZmZXIsZS5ieXRlT2Zmc2V0LGUuYnl0ZUxlbmd0aCl9cmV0dXJuIGZyb21BcnJheUxpa2UodCl9KHQpO2lmKG51bGw9PXQpdGhyb3cgbmV3IFR5cGVFcnJvcigiVGhlIGZpcnN0IGFyZ3VtZW50IG11c3QgYmUgb25lIG9mIHR5cGUgc3RyaW5nLCBCdWZmZXIsIEFycmF5QnVmZmVyLCBBcnJheSwgb3IgQXJyYXktbGlrZSBPYmplY3QuIFJlY2VpdmVkIHR5cGUgIit0eXBlb2YgdCk7aWYoaXNJbnN0YW5jZSh0LEFycmF5QnVmZmVyKXx8dCYmaXNJbnN0YW5jZSh0LmJ1ZmZlcixBcnJheUJ1ZmZlcikpcmV0dXJuIGZyb21BcnJheUJ1ZmZlcih0LGUscik7aWYoInVuZGVmaW5lZCIhPXR5cGVvZiBTaGFyZWRBcnJheUJ1ZmZlciYmKGlzSW5zdGFuY2UodCxTaGFyZWRBcnJheUJ1ZmZlcil8fHQmJmlzSW5zdGFuY2UodC5idWZmZXIsU2hhcmVkQXJyYXlCdWZmZXIpKSlyZXR1cm4gZnJvbUFycmF5QnVmZmVyKHQsZSxyKTtpZigibnVtYmVyIj09dHlwZW9mIHQpdGhyb3cgbmV3IFR5cGVFcnJvcignVGhlICJ2YWx1ZSIgYXJndW1lbnQgbXVzdCBub3QgYmUgb2YgdHlwZSBudW1iZXIuIFJlY2VpdmVkIHR5cGUgbnVtYmVyJyk7Y29uc3Qgbj10LnZhbHVlT2YmJnQudmFsdWVPZigpO2lmKG51bGwhPW4mJm4hPT10KXJldHVybiBCdWZmZXIuZnJvbShuLGUscik7Y29uc3Qgbz1mdW5jdGlvbiBmcm9tT2JqZWN0KHQpe2lmKEJ1ZmZlci5pc0J1ZmZlcih0KSl7Y29uc3QgZT0wfGNoZWNrZWQodC5sZW5ndGgpLHI9Y3JlYXRlQnVmZmVyKGUpO3JldHVybiAwPTLine truncated
|
||||
|
||||
public static let swaggerUiCss: String = "LnN3YWdnZXItdWl7Y29sb3I6IzNiNDE1MTtjb250YWluZXItbmFtZTpzd2FnZ2VyLXVpO2NvbnRhaW5lci10eXBlOmlubGluZS1zaXplOy8qISBub3JtYWxpemUuY3NzIHY3LjAuMCB8IE1JVCBMaWNlbnNlIHwgZ2l0aHViLmNvbS9uZWNvbGFzL25vcm1hbGl6ZS5jc3MgKi9mb250LWZhbWlseTpzYW5zLXNlcmlmfS5zd2FnZ2VyLXVpIGh0bWx7bGluZS1oZWlnaHQ6MS4xNTstbXMtdGV4dC1zaXplLWFkanVzdDoxMDAlOy13ZWJraXQtdGV4dC1zaXplLWFkanVzdDoxMDAlfS5zd2FnZ2VyLXVpIGJvZHl7bWFyZ2luOjB9LnN3YWdnZXItdWkgYXJ0aWNsZSwuc3dhZ2dlci11aSBhc2lkZSwuc3dhZ2dlci11aSBmb290ZXIsLnN3YWdnZXItdWkgaGVhZGVyLC5zd2FnZ2VyLXVpIG5hdiwuc3dhZ2dlci11aSBzZWN0aW9ue2Rpc3BsYXk6YmxvY2t9LnN3YWdnZXItdWkgaDF7Zm9udC1zaXplOjJlbTttYXJnaW46LjY3ZW0gMH0uc3dhZ2dlci11aSBmaWdjYXB0aW9uLC5zd2FnZ2VyLXVpIGZpZ3VyZSwuc3dhZ2dlci11aSBtYWlue2Rpc3BsYXk6YmxvY2t9LnN3YWdnZXItdWkgZmlndXJle21hcmdpbjoxZW0gNDBweH0uc3dhZ2dlci11aSBocntib3gtc2l6aW5nOmNvbnRlbnQtYm94O2hlaWdodDowO292ZXJmbG93OnZpc2libGV9LnN3YWdnZXItdWkgcHJle2ZvbnQtZmFtaWx5Om1vbm9zcGFjZSxtb25vc3BhY2U7Zm9udC1zaXplOjFlbX0uc3dhZ2dlci11aSBhe2JhY2tncm91bmQtY29sb3I6dHJhbnNwYXJlbnQ7LXdlYmtpdC10ZXh0LWRlY29yYXRpb24tc2tpcDpvYmplY3RzfS5zd2FnZ2VyLXVpIGFiYnJbdGl0bGVde2JvcmRlci1ib3R0b206bm9uZTt0ZXh0LWRlY29yYXRpb246dW5kZXJsaW5lOy13ZWJraXQtdGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZSBkb3R0ZWQ7dGV4dC1kZWNvcmF0aW9uOnVuZGVybGluZSBkb3R0ZWR9LnN3YWdnZXItdWkgYiwuc3dhZ2dlci11aSBzdHJvbmd7Zm9udC13ZWlnaHQ6aW5oZXJpdDtmb250LXdlaWdodDpib2xkZXJ9LnN3YWdnZXItdWkgY29kZSwuc3dhZ2dlci11aSBrYmQsLnN3YWdnZXItdWkgc2FtcHtmb250LWZhbWlseTptb25vc3BhY2UsbW9ub3NwYWNlO2ZvbnQtc2l6ZToxZW19LnN3YWdnZXItdWkgZGZue2ZvbnQtc3R5bGU6aXRhbGljfS5zd2FnZ2VyLXVpIG1hcmt7YmFja2dyb3VuZC1jb2xvcjojZmYwO2NvbG9yOiMwMDB9LnN3YWdnZXItdWkgc21hbGx7Zm9udC1zaXplOjgwJX0uc3dhZ2dlci11aSBzdWIsLnN3YWdnZXItdWkgc3Vwe2ZvbnQtc2l6ZTo3NSU7bGluZS1oZWlnaHQ6MDtwb3NpdGlvbjpyZWxhdGl2ZTt2ZXJ0aWNhbC1hbGlnbjpiYXNlbGluZX0uc3dhZ2dlci11aSBzdWJ7Ym90dG9tOi0uMjVlbX0uc3dhZ2dlci11aSBzdXB7dG9wOi0uNWVtfS5zd2FnZ2VyLXVpIGF1ZGlvLC5zd2FnZ2VyLXVpIHZpZGVve2Rpc3BsYXk6aW5saW5lLWJsb2NrfS5zd2FnZ2VyLXVpIGF1ZGlvOm5vdChbY29udHJvbHNdKXtkaXNwbGF5Om5vbmU7aGVpZ2h0OjB9LnN3YWdnZXItdWkgaW1ne2JvcmRlci1zdHlsZTpub25lfS5zd2FnZ2VyLXVpIHN2Zzpub3QoOnJvb3Qpe292ZXJmbG93OmhpZGRlbn0uc3dhZ2dlci11aSBidXR0b24sLnN3YWdnZXItdWkgaW5wdXQsLnN3YWdnZXItdWkgb3B0Z3JvdXAsLnN3YWdnZXItdWkgc2VsZWN0LC5zd2FnZ2VyLXVpIHRleHRhcmVhe2ZvbnQtZmFtaWx5OnNhbnMtc2VyaWY7Zm9udC1zaXplOjEwMCU7bGluZS1oZWlnaHQ6MS4xNTttYXJnaW46MH0uc3dhZ2dlci11aSBidXR0b24sLnN3YWdnZXItdWkgaW5wdXR7b3ZlcmZsb3c6dmlzaWJsZX0uc3dhZ2dlci11aSBidXR0b24sLnN3YWdnZXItdWkgc2VsZWN0e3RleHQtdHJhbnNmb3JtOm5vbmV9LnN3YWdnZXItdWkgW3R5cGU9cmVzZXRdLC5zd2FnZ2VyLXVpIFt0eXBlPXN1Ym1pdF0sLnN3YWdnZXItdWkgYnV0dG9uLC5zd2FnZ2VyLXVpIGh0bWwgW3R5cGU9YnV0dG9uXXstd2Via2l0LWFwcGVhcmFuY2U6YnV0dG9ufS5zd2FnZ2VyLXVpIFt0eXBlPWJ1dHRvbl06Oi1tb3otZm9jdXMtaW5uZXIsLnN3YWdnZXItdWkgW3R5cGU9cmVzZXRdOjotbW96LWZvY3VzLWlubmVyLC5zd2FnZ2VyLXVpIFt0eXBlPXN1Ym1pdF06Oi1tb3otZm9jdXMtaW5uZXIsLnN3YWdnZXItdWkgYnV0dG9uOjotbW96LWZvY3VzLWlubmVye2JvcmRlci1zdHlsZTpub25lO3BhZGRpbmc6MH0uc3dhZ2dlci11aSBbdHlwZT1idXR0b25dOi1tb3otZm9jdXNyaW5nLC5zd2FnZ2VyLXVpIFt0eXBlPXJlc2V0XTotbW96LWZvY3VzcmluZywuc3dhZ2dlci11aSBbdHlwZT1zdWJtaXRdOi1tb3otZm9jdXNyaW5nLC5zd2FnZ2VyLXVpIGJ1dHRvbjotbW96LWZvY3VzcmluZ3tvdXRsaW5lOjFweCBkb3R0ZWQgQnV0dG9uVGV4dH0uc3dhZ2dlci11aSBmaWVsZHNldHtwYWRkaW5nOi4zNWVtIC43NWVtIC42MjVlbX0uc3dhZ2dlci11aSBsZWdlbmR7Ym94LXNpemluZzpib3JkZXItYm94O2NvbG9yOmluaGVyaXQ7ZGlzcGxheTp0YWJsZTttYXgtd2lkdGg6MTAwJTtwYWRkaW5nOjA7d2hpdGUtc3BhY2U6bm9ybWFsfS5zd2FnZ2VyLXVpIHByb2dyZXNze2Rpc3BsYXk6aW5saW5lLWJsb2NrO3ZlcnRpY2FsLWFsaWduOmJhc2VsaW5lfS5zd2FnZ2VyLXVpIHRleHRhcmVhe292ZXJmbG93OmF1dG99LnN3YWdnZXItdWkgW3R5cGU9Y2hlY2tib3hdLC5zd2FnZ2VyLXVpIFt0eXBlPXJhZGlvXXtib3gtc2l6aW5nOmJvcmRlci1ib3g7cGFkZGluZzowfS5zd2FnZ2VyLXVpIFt0eXBlPW51bWJlcl06Oi13ZWJraXQtaW5uZXItc3Bpbi1idXR0b24sLnN3YWdnZXItdWkgW3R5cGU9bnVtYmVyXTo6LXdlYmtpdC1vdXRlci1zcGluLWJ1dHRvbntoZWlnaHQ6YXV0b30uc3dhZ2dlci11aSBbdHlwZT1zZWFyY2hdey13ZWJraXQtYXBwZWFyYW5jZTp0ZXh0ZmllbGQ7b3V0bGluZS1vZmZzZXQ6LTJweH0uc3dhZ2dlci11aSBbdHlwZT1zZWFyY2hdOjotd2Via2l0LXNlYXJjaC1jYW5jZWwtYnV0dG9uLC5zd2FnZ2VyLXVpIFt0eXBlPXNlYXJjaF06Oi13ZWJraXQtc2VhcmNoLWRlY29yYXRpb257LXdlYmtpdC1hcHBlYXJhbmNlOm5vbmV9LnN3YWdnZXItdWkgOjotd2Via2l0LWZpbGUtdXBsb2FkLWJ1dHRvbnstd2Via2l0LWFwcGVhcmFuY2U6YnV0dG9uO2ZvbnQ6aW5oZXJpdH0uc3dhZ2dlci11aSBkZXRhaWxzLC5zd2FnZ2VyLXVpIG1lbnV7ZGlzcGxheTpibG9ja30uc3dhZ2dlci11aSBzdW1tYXJ5e2Rpc3BsYXk6bGlzdC1pdGVtfS5zd2FnZ2VyLXVpIGNhbnZhc3tkaXNwbGF5OmlubGluZS1ibG9ja30uc3dhZ2dlci11aSBbaGlkZGVuXSwuc3dhZ2dlci11aSB0ZW1wbGF0ZXtkaXNwbGF5Om5vbmV9LnN3YWdnZXItdWkgLmRlYnVnICp7b3V0bGluZToxcHggc29saWQgZ29sZH0uc3dhZ2dlci11aSAuZGVidWctd2hpdGUgKntvdXRsaW5lOjFweCBzb2xpZCAjZmZmfS5zd2FnZ2VyLXVpIC5kZWJ1Zy1ibGFjayAqe291dGxpbmU6MXB4IHNvbGlkICMwMDB9LnN3YWdnZXItdWkgLmRlYnVnLWdyaWR7YmFja2dyb3VuZDp0cmFuc3BhcmVudCB1cmwoZGF0YTppbWFnZS9wbmc7YmFzZTY0LGlWQk9SdzBLR2dvQUFBQU5TVWhFVWdBQUFBZ0FBQUFJQ0FZQUFBREVENzZMQUFBQUdYUkZXSFJUYjJaMGQyRnlaUUJCWkc5aVpTQkpiV0ZuWlZKbFlXUjVjY2xsUEFBQUF5aHBWRmgwV0UxTU9tTnZiUzVoWkc5aVpTNTRiWEFBQUFBQUFEdy9lSEJoWTJ0bGRDQmlaV2RwYmowaTc3dS9JaUJwWkQwaVZ6Vk5NRTF3UTJWb2FVaDZjbVZUZWs1VVkzcHJZemxrSWo4K0lEeDRPbmh0Y0cxbGRHRWdlRzFzYLine truncated
|
||||
|
||||
private init() {}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
* This source file is licensed under the MIT License found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
|
||||
package simcu::simapi.openapi.annotations
|
||||
|
||||
import simcu::simapi.openapi.metadata.*
|
||||
@@ -13,14 +13,17 @@ import simcu::simapi.openapi.metadata.*
|
||||
*
|
||||
* 用法:
|
||||
* @SimApiDoc[tags: "登录", summary: "用户登录相关接口"]
|
||||
* @SimApiDoc[tags: "认证", summary: "后台登录", groupNames: "admin"]
|
||||
* @SimApiDoc[tags: "公共", groupNames: "api,admin"] // 同时出现在 api 和 admin 文档
|
||||
* @SimApiDoc[tags: "公共", groupNames: "*"] // 出现在所有文档
|
||||
*/
|
||||
@Annotation[target: [MemberFunction, Type, MemberProperty, MemberVariable, Parameter]]
|
||||
public class SimApiDoc <: IApiTagsMetadata & IApiNameMetadata & IApiGroupNameProvider & IApiDescriptionMetadata & IApiSummaryMetadata & IApiVisibilityProvider {
|
||||
public class SimApiDoc <: IApiTagsMetadata & IApiNameMetadata & IApiGroupNamesProvider & IApiDescriptionMetadata & IApiSummaryMetadata & IApiVisibilityProvider {
|
||||
private let _ignore: Bool
|
||||
private let _name: ?String
|
||||
private let _tags: ?String
|
||||
private let _summary: ?String
|
||||
private let _groupName: ?String
|
||||
private let _groupNames: String
|
||||
private let _description: ?String
|
||||
|
||||
/**
|
||||
@@ -28,17 +31,17 @@ public class SimApiDoc <: IApiTagsMetadata & IApiNameMetadata & IApiGroupNamePro
|
||||
* @param name API 名称。
|
||||
* @param ignore 是否忽略当前 API。
|
||||
* @param summary API 摘要。
|
||||
* @param groupName API 分组名称。
|
||||
* @param groupNames API 分组名称(逗号分隔,如 "api,admin");"*" 表示出现在所有文档;空串表示未分组(仅进默认文档)。
|
||||
* @param description API 描述。
|
||||
* @param tags API 标签字符串。
|
||||
*/
|
||||
public const init(name!: ?String = None, ignore!: Bool = false, summary!: ?String = None,
|
||||
groupName!: ?String = None, description!: ?String = None, tags!: ?String = None) {
|
||||
groupNames!: String = "", description!: ?String = None, tags!: ?String = None) {
|
||||
_tags = tags
|
||||
_name = name
|
||||
_ignore = ignore
|
||||
_summary = summary
|
||||
_groupName = groupName
|
||||
_groupNames = groupNames
|
||||
_description = description
|
||||
}
|
||||
|
||||
@@ -58,7 +61,7 @@ public class SimApiDoc <: IApiTagsMetadata & IApiNameMetadata & IApiGroupNamePro
|
||||
*/
|
||||
public prop name: ?String {
|
||||
get() {
|
||||
_name
|
||||
_name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,12 +86,12 @@ public class SimApiDoc <: IApiTagsMetadata & IApiNameMetadata & IApiGroupNamePro
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 返回 API 分组名称。
|
||||
* @return 当前 API 的分组名称。
|
||||
* @brief 返回 API 分组名称(逗号分隔,"*" 表示所有文档)。
|
||||
* @return 逗号分隔的分组名称字符串,空串表示未分组。
|
||||
*/
|
||||
public prop groupName: ?String {
|
||||
public prop groupNames: String {
|
||||
get() {
|
||||
_groupName
|
||||
_groupNames
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+7
-7
@@ -6,16 +6,16 @@
|
||||
* This source file is licensed under the MIT License found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
|
||||
package simcu::simapi.openapi.metadata
|
||||
|
||||
/**
|
||||
* @brief 提供 API 分组名称元数据。
|
||||
* @brief 提供 API 多分组名称元数据(逗号分隔,如 "api,admin")。
|
||||
*/
|
||||
public interface IApiGroupNameProvider {
|
||||
public interface IApiGroupNamesProvider {
|
||||
/**
|
||||
* @brief 返回 API 分组名称。
|
||||
* @return 当前 API 的分组名称。
|
||||
* @brief 返回 API 所属的多个分组名称(逗号分隔)。
|
||||
* @return 逗号分隔的分组名称字符串,空串表示未指定。
|
||||
*/
|
||||
prop groupName: ?String
|
||||
}
|
||||
prop groupNames: String
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 SimCuTeam. All rights reserved.
|
||||
* 遵循 MIT 许可证。
|
||||
*/
|
||||
|
||||
package simcu::simapi.openapi.metadata
|
||||
|
||||
import std.reflect.*
|
||||
|
||||
/**
|
||||
* @brief 提供 API 响应类型元数据(供动态注册的路由指定返回类型)。
|
||||
*/
|
||||
public interface IApiResponseTypeMetadata {
|
||||
/**
|
||||
* @brief 返回 API 响应的类型信息。
|
||||
*/
|
||||
prop responseType: ?TypeInfo
|
||||
}
|
||||
@@ -20,6 +20,11 @@ public class OpenApiComponents <: IOpenApiSerializable {
|
||||
*/
|
||||
public var schemas = HashMap<String, OpenApiSchema>()
|
||||
|
||||
/**
|
||||
* @brief 表示组件中的安全方案集合。
|
||||
*/
|
||||
public var securitySchemes = HashMap<String, OpenApiSecurityScheme>()
|
||||
|
||||
/**
|
||||
* @brief 创建 OpenAPI 组件实例。
|
||||
*/
|
||||
@@ -41,6 +46,15 @@ public class OpenApiComponents <: IOpenApiSerializable {
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
if (!securitySchemes.isEmpty()) {
|
||||
writer.writeName("securitySchemes")
|
||||
writer.startObject()
|
||||
for ((key, value) in securitySchemes) {
|
||||
writer.writeName(key)
|
||||
value.serializeAsV3(writer)
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
@@ -6,22 +6,56 @@
|
||||
* This source file is licensed under the MIT License found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
|
||||
package simcu::simapi.openapi.models
|
||||
|
||||
import std.collection.*
|
||||
|
||||
/**
|
||||
* @brief 表示 OpenAPI 安全需求对象。
|
||||
*
|
||||
* 每个实例对应一个安全需求条目,例如 {"Token": []}。
|
||||
*/
|
||||
public class OpenApiSecurityRequirement <: IOpenApiSerializable {
|
||||
private let _requirements = ArrayList<(String, ArrayList<String>)>()
|
||||
|
||||
/**
|
||||
* @brief 创建 OpenAPI 安全需求对象。
|
||||
*/
|
||||
public init() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加一个安全方案需求。
|
||||
* @param schemeName 安全方案名称。
|
||||
*/
|
||||
public func addScheme(schemeName: String): Unit {
|
||||
_requirements.add((schemeName, ArrayList<String>()))
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 添加一个带授权范围的安全方案需求。
|
||||
* @param schemeName 安全方案名称。
|
||||
* @param scopes 该方案要求的授权范围集合(OAuth2 场景)。
|
||||
*/
|
||||
public func addScheme(schemeName: String, scopes: ArrayList<String>): Unit {
|
||||
_requirements.add((schemeName, scopes))
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按 OpenAPI V3 格式写出当前安全需求对象。
|
||||
* @param writer OpenAPI 写入器。
|
||||
*/
|
||||
public func serializeAsV3(writer: IOpenApiWriter): Unit {}
|
||||
public func serializeAsV3(writer: IOpenApiWriter): Unit {
|
||||
writer.startObject()
|
||||
for ((schemeName, scopes) in _requirements) {
|
||||
writer.writeName(schemeName)
|
||||
writer.startArray()
|
||||
for (scope in scopes) {
|
||||
writer.writeValue(scope)
|
||||
}
|
||||
writer.endArray()
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
// Licensed to the .NET Foundation under one or more agreements.
|
||||
// The .NET Foundation licenses this file to you under the MIT license.
|
||||
|
||||
/*
|
||||
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
|
||||
* This source file is licensed under the MIT License found in the
|
||||
* LICENSE file in the root directory of this source tree.
|
||||
*/
|
||||
|
||||
package simcu::simapi.openapi.models
|
||||
|
||||
/**
|
||||
* @brief 表示 OpenAPI 安全方案对象。
|
||||
*/
|
||||
public class OpenApiSecurityScheme <: IOpenApiSerializable {
|
||||
/**
|
||||
* @brief 表示安全方案类型(apiKey/http/oauth2/openIdConnect)。
|
||||
*/
|
||||
public var schemeType: String = "apiKey"
|
||||
/**
|
||||
* @brief 表示用于安全方案的参数名称(apiKey 时有效)。
|
||||
*/
|
||||
public var name: ?String = None
|
||||
/**
|
||||
* @brief 表示 apiKey 参数所在位置(header/query/cookie)。
|
||||
*/
|
||||
public var location: ?String = None
|
||||
/**
|
||||
* @brief 表示 HTTP 认证方案名称(http 时有效,如 bearer)。
|
||||
*/
|
||||
public var scheme: ?String = None
|
||||
/**
|
||||
* @brief 表示安全方案的描述信息。
|
||||
*/
|
||||
public var description: ?String = None
|
||||
/**
|
||||
* @brief 表示 bearer 令牌的格式提示(http+bearer 时有效)。
|
||||
*/
|
||||
public var bearerFormat: ?String = None
|
||||
|
||||
/**
|
||||
* @brief 创建 OpenAPI 安全方案对象。
|
||||
*/
|
||||
public init() {
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief 按 OpenAPI V3 格式写出当前安全方案对象。
|
||||
* @param writer OpenAPI 写入器。
|
||||
*/
|
||||
public func serializeAsV3(writer: IOpenApiWriter): Unit {
|
||||
writer.startObject()
|
||||
writer.writeName("type")
|
||||
writer.writeValue(schemeType)
|
||||
if (let Some(value) <- description) {
|
||||
writer.writeName("description")
|
||||
writer.writeValue(value)
|
||||
}
|
||||
if (let Some(value) <- name) {
|
||||
writer.writeName("name")
|
||||
writer.writeValue(value)
|
||||
}
|
||||
if (let Some(value) <- location) {
|
||||
writer.writeName("in")
|
||||
writer.writeValue(value)
|
||||
}
|
||||
if (let Some(value) <- scheme) {
|
||||
writer.writeName("scheme")
|
||||
writer.writeValue(value)
|
||||
}
|
||||
if (let Some(value) <- bearerFormat) {
|
||||
writer.writeName("bearerFormat")
|
||||
writer.writeValue(value)
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ package simcu::simapi.openapi.services
|
||||
|
||||
import std.reflect.*
|
||||
import std.collection.*
|
||||
import std.unicode.*
|
||||
import soulsoft_web_mvc.*
|
||||
import soulsoft_web_http.*
|
||||
import soulsoft_web_routing.http.*
|
||||
@@ -23,6 +24,8 @@ import simcu::simapi.openapi.metadata.*
|
||||
import soulsoft_web_mvc.abstractions.*
|
||||
import simcu::simapi.openapi.transformers.*
|
||||
import simcu::simapi.openapi.infrastructure.*
|
||||
import simcu::simapi.configurations.*
|
||||
import simcu::simapi.annotations.*
|
||||
|
||||
/**
|
||||
* @brief 提供 OpenAPI 文档生成功能。
|
||||
@@ -59,18 +62,52 @@ protected class OpenApiDocumentService {
|
||||
*/
|
||||
public func getOpenApiDocument(services: IServiceProvider): OpenApiDocument {
|
||||
let document = OpenApiDocument()
|
||||
document.info = OpenApiInfo(title: "OpenApi | ${_documentName}", version: "1.0.0")
|
||||
// 从 SimApiDocOptions 中查找当前文档组的标题和描述
|
||||
var docTitle = "OpenApi | ${_documentName}"
|
||||
var docDescription: ?String = None
|
||||
if (let Some(simOptions) <- services.get<SimApiOptions>()) {
|
||||
docTitle = simOptions.simApiDocOptions.documentTitle
|
||||
for (g in simOptions.simApiDocOptions.distinctGroups()) {
|
||||
if (g.id == _documentName) {
|
||||
docTitle = g.name
|
||||
if (!g.description.isEmpty()) {
|
||||
docDescription = g.description
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
let info = OpenApiInfo(title: docTitle, version: "1.0.0")
|
||||
info.description = docDescription
|
||||
document.info = info
|
||||
document.paths = createOpenApiPaths(services)
|
||||
document.components = createOpenApiComponents()
|
||||
applyAuthSecurity(document, services)
|
||||
return document
|
||||
}
|
||||
|
||||
/// 当启用 SimApiAuth 时,为文档注册 Token 认证方案(apiKey Header)。
|
||||
/// 仅注册 scheme,不添加全局 security;由 createOpenApiOperation 按接口注解逐个添加。
|
||||
private func applyAuthSecurity(document: OpenApiDocument, services: IServiceProvider): Unit {
|
||||
let enableAuth = services.get<SimApiOptions>().flatMap {f => Some(f.enableSimApiAuth)} ?? false
|
||||
if (!enableAuth) {
|
||||
return
|
||||
}
|
||||
if (let Some(components) <- document.components) {
|
||||
let scheme = OpenApiSecurityScheme()
|
||||
scheme.name = Some("Token")
|
||||
scheme.location = Some("header")
|
||||
scheme.description = Some("登录后返回的 Token(Header: Token)")
|
||||
components.securitySchemes.add("Token", scheme)
|
||||
}
|
||||
}
|
||||
|
||||
private func createOpenApiPaths(services: IServiceProvider) {
|
||||
let paths = OpenApiPaths()
|
||||
let openApiOptions = services.getOrThrow<IOptionsMonitor<OpenApiOptions>>().get(_documentName)
|
||||
let operationTransformers = openApiOptions.operationTransformers
|
||||
for (endpoint in _endpointSource.endpoints |> filterMap {f => f as RouteEndpoint} where !isIgnore(endpoint)) {
|
||||
if (!isShouldInclude(endpoint)) {
|
||||
if (!isShouldInclude(endpoint, openApiOptions.includeUnGrouped)) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -106,13 +143,35 @@ protected class OpenApiDocumentService {
|
||||
return paths
|
||||
}
|
||||
|
||||
private func isShouldInclude(endpoint: RouteEndpoint) {
|
||||
let groupNames = endpoint.metadata.getOrderedMetadata<IApiGroupNameProvider>() |> filterMap {f => f.groupName} |>
|
||||
collectArray
|
||||
if (groupNames.isEmpty() || groupNames.contains(_documentName)) {
|
||||
private func isShouldInclude(endpoint: RouteEndpoint, includeUnGrouped: Bool) {
|
||||
let rawGroupNames = endpoint.metadata.getOrderedMetadata<IApiGroupNamesProvider>() |>
|
||||
filterMap { f => f.groupNames } |> collectArray
|
||||
if (rawGroupNames.isEmpty()) {
|
||||
// 未标注 groupNames 的接口:仅进入默认文档(includeUnGrouped=true 的文档)
|
||||
return includeUnGrouped
|
||||
}
|
||||
// 合并所有注解的 groupNames,按逗号拆分并去空白
|
||||
let names = ArrayList<String>()
|
||||
for (raw in rawGroupNames) {
|
||||
for (part in raw.split(",")) {
|
||||
let trimmed = part.trim()
|
||||
if (trimmed.isEmpty()) {
|
||||
continue
|
||||
}
|
||||
if (!names.contains(trimmed)) {
|
||||
names.add(trimmed)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (names.isEmpty()) {
|
||||
// 仅标注了空串:视为未分组
|
||||
return includeUnGrouped
|
||||
}
|
||||
// "*" 表示出现在所有文档
|
||||
if (names.contains("*")) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return names.contains(_documentName)
|
||||
}
|
||||
|
||||
private func createOpenApiComponents() {
|
||||
@@ -142,7 +201,7 @@ protected class OpenApiDocumentService {
|
||||
}
|
||||
|
||||
// response
|
||||
operation.response = createOpenApiOperationResponses(actionDescriptor)
|
||||
operation.response = createOpenApiOperationResponses(endpoint, actionDescriptor)
|
||||
|
||||
// summary
|
||||
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiSummaryMetadata> {f => f.summary.isSome()}) {
|
||||
@@ -165,6 +224,13 @@ protected class OpenApiDocumentService {
|
||||
operation.operationId = metadata.name
|
||||
}
|
||||
|
||||
// security:仅标注了 @SimApiAuth 的接口才显示锁图标
|
||||
if (let Some(_) <- endpoint.metadata.getMetadata<SimApiAuth>()) {
|
||||
let requirement = OpenApiSecurityRequirement()
|
||||
requirement.addScheme("Token")
|
||||
operation.security.add(requirement)
|
||||
}
|
||||
|
||||
return operation
|
||||
}
|
||||
|
||||
@@ -230,30 +296,65 @@ protected class OpenApiDocumentService {
|
||||
/*
|
||||
生成响应描述
|
||||
*/
|
||||
private func createOpenApiOperationResponses(actionDescriptor: ?ControllerActionDescriptor) {
|
||||
private func createOpenApiOperationResponses(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) {
|
||||
let responses = OpenApiResponses()
|
||||
|
||||
// 获取action的返回类型
|
||||
// 获取action的返回类型:优先从 actionDescriptor,其次从 IApiResponseTypeMetadata(动态路由)
|
||||
let returnType: ?TypeInfo = if (let Some(actionDescriptor) <- actionDescriptor) {
|
||||
Nullable.getUnderlyingType(actionDescriptor.actionFunction.returnType) ?? actionDescriptor
|
||||
.actionFunction
|
||||
.returnType
|
||||
} else if (let Some(meta) <- endpoint.metadata.getMetadata<IApiResponseTypeMetadata>()) {
|
||||
meta.responseType
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
// 判断是否跳过封装:@OriginResponse 或返回类型已是 SimApiBaseResponse/SimApiResponse/SimApiDataResponse
|
||||
var skipWrap = false
|
||||
if (let Some(_) <- endpoint.metadata.getMetadata<OriginResponse>()) {
|
||||
skipWrap = true
|
||||
}
|
||||
if (let Some(rt) <- returnType) {
|
||||
let rtName = rt.name
|
||||
if (rtName == "SimApiBaseResponse" || rtName == "SimApiDataResponse" || rtName.startsWith("SimApiResponse")) {
|
||||
skipWrap = true
|
||||
}
|
||||
}
|
||||
|
||||
if (let Some(returnType) <- returnType && returnType != TypeInfo.of<Unit>()) {
|
||||
let response = OpenApiResponse("OK")
|
||||
let schema = _openApiSchemaService.createSchema(returnType)
|
||||
let dataSchema = _openApiSchemaService.createSchema(returnType)
|
||||
let schema = if (skipWrap) { dataSchema } else { wrapResponseSchema(dataSchema) }
|
||||
response.content.add("text/plain", OpenApiMediaType(schema))
|
||||
response.content.add("application/json", OpenApiMediaType(schema))
|
||||
response.content.add("text/json", OpenApiMediaType(schema))
|
||||
responses.add("200", response)
|
||||
} else {
|
||||
responses.add("200", OpenApiResponse("OK"))
|
||||
// Unit(void)→ SimApiBaseResponse {code, message}
|
||||
let response = OpenApiResponse("OK")
|
||||
let schema = if (skipWrap) { OpenApiSchema() } else { wrapResponseSchema(None) }
|
||||
response.content.add("text/plain", OpenApiMediaType(schema))
|
||||
response.content.add("application/json", OpenApiMediaType(schema))
|
||||
response.content.add("text/json", OpenApiMediaType(schema))
|
||||
responses.add("200", response)
|
||||
}
|
||||
return responses
|
||||
}
|
||||
|
||||
/// 将 schema 包装为统一响应结构 {code, message, data?}。
|
||||
private func wrapResponseSchema(dataSchema: ?OpenApiSchema): OpenApiSchema {
|
||||
let wrapped = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT)
|
||||
let codeSchema = OpenApiSchema(`type`: "integer", format: "int64")
|
||||
let msgSchema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING)
|
||||
wrapped.properties.add("code", codeSchema)
|
||||
wrapped.properties.add("message", msgSchema)
|
||||
if (let Some(ds) <- dataSchema) {
|
||||
wrapped.properties.add("data", ds)
|
||||
}
|
||||
return wrapped
|
||||
}
|
||||
|
||||
// api ignore
|
||||
private func isIgnore(endpoint: RouteEndpoint) {
|
||||
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiVisibilityProvider> {f => f.ignore}) {
|
||||
|
||||
@@ -22,6 +22,12 @@ public class OpenApiOptions {
|
||||
private let _operationTransformer = ArrayList<IOpenApiOperationTransformer>()
|
||||
private let _schemaTransformers = ArrayList<IOpenApiSchemaTransformer>()
|
||||
|
||||
/**
|
||||
* 未标注 groupName 的接口是否进入本文档。
|
||||
* 多文档分组时,仅默认组(第一个 apiGroup 或 isDefault 的组)应设为 true。
|
||||
*/
|
||||
public var includeUnGrouped: Bool = false
|
||||
|
||||
/**
|
||||
* @brief 确定指定类型在 components/schemas 中使用的引用 ID。
|
||||
* 返回 None 时该 Schema 始终内联,不生成 $ref。
|
||||
|
||||
Reference in new issue
Block a user