3 Commits
Author SHA1 Message Date
xrain cbe763ebb6 精简了openapi相关 2026-09-02 00:08:33 +08:00
xrain 06884a920c 精简了openapi相关 2026-09-01 23:36:07 +08:00
xrain eed230aab0 修正了部分lint 2026-09-01 20:31:26 +08:00
75 changed files with 544 additions and 1290 deletions
+49 -49
View File
@@ -368,25 +368,25 @@ SimApiExtensions.addSimApi(builder) { options =>
}
```
| 方法 | 说明 |
|------|------|
| `getUploadUrl(path, expire=7200)` | 上传预签名 URL,返回 `GetUploadUrlResponse(UploadUrl, DownloadUrl, Path)` |
| `getDownloadUrl(path, expire=600)` | 下载预签名 URL |
| `uploadFile(path, data, contentType="image/png")` | 直接 PUT 上传(字节数组) |
| `deleteFiles(paths)` | 批量删除对象(S3 原生 DeleteObjects,一次请求删多个) |
| `fullUrl(path)` / `getUrl(path)` | 补全访问 URL(`~/` 前缀依赖请求上下文) |
| `getPath(url)` | 从 URL 还原相对路径(去掉 Endpoint/Bucket 或 ServeUrl 前缀) |
| 方法 | 说明 |
| ------------------------------------------------- | ------------------------------------------------------------------------- |
| `getUploadUrl(path, expire=7200)` | 上传预签名 URL,返回 `GetUploadUrlResponse(UploadUrl, DownloadUrl, Path)` |
| `getDownloadUrl(path, expire=600)` | 下载预签名 URL |
| `uploadFile(path, data, contentType="image/png")` | 直接 PUT 上传(字节数组) |
| `deleteFiles(paths)` | 批量删除对象(S3 原生 DeleteObjects,一次请求删多个) |
| `fullUrl(path)` / `getUrl(path)` | 补全访问 URL(`~/` 前缀依赖请求上下文) |
| `getPath(url)` | 从 URL 还原相对路径(去掉 Endpoint/Bucket 或 ServeUrl 前缀) |
> 说明:桶不存在时自动创建(静态守卫只执行一次);
> 预签名与上传使用 AWS SigV4(HMAC-SHA256 基于 stdx SHA256 自实现),已用 AWS 官方测试向量验证签名正确。
### 6. 内置路由(UseSimApi 自动注册)
| 路由 | 方法 | 条件 | 说明 |
| ----------------- | -------- | ---------------------------- | -------------------------- |
| `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo |
| `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 |
| `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException,不出现在文档中) |
| 路由 | 方法 | 条件 | 说明 |
| ------------------- | ---- | ------------------ | ---------------------------------------------- |
| `/user/info` | POST | `enableSimApiAuth` | 需登录,返回 LoginInfo |
| `/auth/logout` | POST | `enableSimApiAuth` | 退出登录 |
| `/exception/{code}` | GET | 始终 | 错误反馈(抛 SimApiException,不出现在文档中) |
路由路径可自定义(`configureSimApiRoute`):
@@ -427,11 +427,11 @@ SimApiExtensions.addSimApi(builder) { options =>
}
```
| 类 | 说明 |
|----|------|
| `SimApiAuthClient` | `SimApiHttpClient` 子类,凭证取 AuthCenterOptions |
| `SimApiAuthCenter` | 群组/Profile/内部应用/系统登录/安全验证等 12 个接口 + `VerifySign` |
| `SimApiAuthIam` | 注册权限 / 获取权限标识 / 校验权限(无权限抛 403) |
| 类 | 说明 |
| ---------------------------- | ------------------------------------------------------------------------------ |
| `SimApiAuthClient` | `SimApiHttpClient` 子类,凭证取 AuthCenterOptions |
| `SimApiAuthCenter` | 群组/Profile/内部应用/系统登录/安全验证等 12 个接口 + `VerifySign` |
| `SimApiAuthIam` | 注册权限 / 获取权限标识 / 校验权限(无权限抛 403) |
| `SimApiAuthCenterMiddleware` | 网关透传:`X-SimApi-Gate-Auth/Time/Sign` 三头 MD5 校验 → Base64 解码 LoginInfo |
```cangjie
@@ -491,12 +491,12 @@ options.configureSimApiDoc { doc =>
}
```
| 路由 | 说明 |
|------|------|
| `/{prefix}/all.html` | 多文档切换页(顶部栏下拉选择所有文档) |
| 路由 | 说明 |
| --------------------- | ------------------------------------------ |
| `/{prefix}/all.html` | 多文档切换页(顶部栏下拉选择所有文档) |
| `/{prefix}/{id}.html` | 单文档页(无顶部栏,自动加载 `{id}.json`) |
| `/{prefix}/urls` | 文档列表 JSON(供 Swagger UI 下拉) |
| `/{prefix}/{id}.json` | OpenAPI 文档 JSON |
| `/{prefix}/urls` | 文档列表 JSON(供 Swagger UI 下拉) |
| `/{prefix}/{id}.json` | OpenAPI 文档 JSON |
> Swagger UI 静态资源(CSS/JS/HTML)以 Base64 内联编译,运行时由 `OpenApiUIMiddleware` 解码输出。资源更新后运行 `pwsh tools/gen-swagger-ui-resources.ps1` 重新生成。
@@ -504,13 +504,13 @@ options.configureSimApiDoc { doc =>
接口返回值自动封装为统一响应格式,**文档中 response schema 也体现封装**:
| 返回类型 | `@OriginResponse` | 文档 response schema |
|----------|-------------------|---------------------|
| `SimApiBaseResponse`/`SimApiResponse<T>`/`SimApiDataResponse` | - | 原样不封装 |
| 任意类型 | ✓ | 原样不封装 |
| `Unit` (void) | ✗ | `{code: int64, message: string}` |
| `String` | ✗ | `{code, message, data: {type: string}}` |
| DTO | ✗ | `{code, message, data: {$ref: DTO}}` |
| 返回类型 | `@OriginResponse` | 文档 response schema |
| ------------------------------------------------------------- | ----------------- | --------------------------------------- |
| `SimApiBaseResponse`/`SimApiResponse<T>`/`SimApiDataResponse` | - | 原样不封装 |
| 任意类型 | ✓ | 原样不封装 |
| `Unit` (void) | ✗ | `{code: int64, message: string}` |
| `String` | ✗ | `{code, message, data: {type: string}}` |
| DTO | ✗ | `{code, message, data: {$ref: DTO}}` |
### 认证锁图标
@@ -625,11 +625,11 @@ SimApiExtensions.addSimApi(builder) { options =>
simapi 提供 Spire MVC 控制器(继承 `SimApiBaseController`),`addSimApi` 自动注册内置控制器 + 自动扫描调用者包中的控制器:
| 控制器 | 路由 | 说明 |
|--------|------|------|
| `SimApiCommonController` | `/exception/{code}`、`/config`、`/user/info` | 通用内置路由 |
| `SimApiAuthController` | `/auth/logout` | 退出登录 |
| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken`(访问时自动校验登录,未登录抛 401) |
| 控制器 | 路由 | 说明 |
| ------------------------ | -------------------------------------------- | -------------------------------------------------------------------- |
| `SimApiCommonController` | `/exception/{code}`、`/config`、`/user/info` | 通用内置路由 |
| `SimApiAuthController` | `/auth/logout` | 退出登录 |
| `SimApiBaseController` | — | 基类:`loginInfo` / `loginToken`(访问时自动校验登录,未登录抛 401) |
```cangjie
import simcu::simapi.controllers.*
@@ -656,10 +656,10 @@ public class MyController <: SimApiBaseController {
以下原包功能因仓颉生态暂无对应库(Hangfire/MQTT),**选项保留但未实现**:
| 选项 | 原功能 | 状态 |
|------|--------|------|
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
| 选项 | 原功能 | 状态 |
| --------------- | ----------------- | -------- |
| `enableSynapse` | MQTT 通信 | ❌ 未实现 |
| `enableJob` | Hangfire 任务调度 | ❌ 未实现 |
> ✅ 已实现(曾为占位):`enableSimApiDoc`(OpenAPI 文档 + Swagger UI)、`enableSimApiStorage`(S3/MinIO,自实现 AWS SigV4)、`enableSimApiAuthGate`(AuthSDK 认证中心)、`SimApiAesUtil`(纯仓颉 AES-256-CBC)、`SimApiAuthChecker`、`@SimApiSign` / `@AesBody` 声明式注解、内置路由自定义路径。
@@ -667,16 +667,16 @@ public class MyController <: SimApiBaseController {
## 依赖
| 依赖 | 用途 |
|------|------|
| `soulsoft_web_http / routing / hosting` | Web 框架 |
| `soulsoft_extensions_logging` 系列 | 日志 |
| `soulsoft_extensions_injection` | 依赖注入 |
| `soulsoft_extensions_configuration` | 配置 |
| `simcu::serialization`(path 依赖) | JSON 序列化(simapi 自研,反射免标注) |
| `redis`(pkg.cangjie-lang.cn) | Redis 客户端(认证/缓存 Redis 模式) |
| `stdx`(CANGJIE_STDX_PATH) | 标准扩展库(md5/sha1/base64/http/tls) |
| `soulsoft_web_mvc` | MVC 框架(控制器路由、模型绑定) |
| 依赖 | 用途 |
| --------------------------------------- | -------------------------------------- |
| `soulsoft_web_http / routing / hosting` | Web 框架 |
| `soulsoft_extensions_logging` 系列 | 日志 |
| `soulsoft_extensions_injection` | 依赖注入 |
| `soulsoft_extensions_configuration` | 配置 |
| `simcu::serialization`(path 依赖) | JSON 序列化(simapi 自研,反射免标注) |
| `redis`(pkg.cangjie-lang.cn) | Redis 客户端(认证/缓存 Redis 模式) |
| `stdx`(CANGJIE_STDX_PATH) | 标准扩展库(md5/sha1/base64/http/tls) |
| `soulsoft_web_mvc` | MVC 框架(控制器路由、模型绑定) |
> 构建前需设置 `CANGJIE_STDX_PATH` 指向本地 stdx 的 `static/stdx` 目录。
> OpenAPI Swagger UI 静态资源内置(Base64 内联),更新资源后运行 `pwsh tools/gen-swagger-ui-resources.ps1`。
+12 -12
View File
@@ -3,18 +3,18 @@ version = 0
[requires]
soulsoft_extensions_hosting = {version = "1.0.20260528"}
soulsoft_extensions_options_configuration = {version = "1.0.20260528"}
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
redis = {version = "1.0.20260627"}
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
soulsoft_identity_claims = {version = "1.0.20260528"}
soulsoft_web_http = {version = "1.0.20260528"}
"simcu::serialization" = {version = "1.2.1"}
soulsoft_extensions_configuration = {version = "1.0.20260528"}
soulsoft_web_routing = {version = "1.0.20260528"}
soulsoft_web_cors = {version = "1.0.20260528"}
soulsoft_extensions_injection = {version = "1.0.20260528"}
soulsoft_web_mvc = {version = "1.0.20260528"}
soulsoft_serialization = {version = "1.0.20260528"}
soulsoft_extensions_options = {version = "1.0.20260528"}
soulsoft_extensions_logging = {version = "1.0.20260528"}
soulsoft_identity_claims = {version = "1.0.20260528"}
"simcu::serialization" = {version = "1.2.1"}
soulsoft_web_hosting = {version = "1.0.20260528"}
soulsoft_web_http = {version = "1.0.20260528"}
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
soulsoft_extensions_configuration = {version = "1.0.20260528"}
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
soulsoft_serialization = {version = "1.0.20260528"}
soulsoft_extensions_injection = {version = "1.0.20260528"}
redis = {version = "1.0.20260627"}
soulsoft_extensions_options = {version = "1.0.20260528"}
soulsoft_web_mvc = {version = "1.0.20260528"}
soulsoft_web_cors = {version = "1.0.20260528"}
+1 -1
View File
@@ -3,7 +3,7 @@ cjc-version = "1.1.3"
name = "simapi"
organization = "simcu"
description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)"
version = "1.1.0"
version = "1.1.5"
target-dir = ""
output-type = "static"
override-compile-option = ""
+63
View File
@@ -0,0 +1,63 @@
/*
* 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.annotations
/**
* @brief 为控制器或动作提供 API 文档元数据(对齐 C# [SimApiDoc] 特性)。
*
* 用法:
* @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 {
/**
* @brief API 名称。
*/
public let name: ?String
/**
* @brief 当前 API 是否应被忽略。
*/
public let ignore: Bool
/**
* @brief API 摘要信息。
*/
public let summary: ?String
/**
* @brief API 分组名称(逗号分隔,如 "api,admin");"*" 表示出现在所有文档;空串表示未分组(仅进默认文档)。
*/
public let groupNames: String
/**
* @brief API 描述信息。
*/
public let description: ?String
/**
* @brief API 标签字符串。
*/
public let tags: ?String
/**
* @brief 创建 SimApiDoc 注解实例。
* @param name API 名称。
* @param ignore 是否忽略当前 API。
* @param summary API 摘要。
* @param groupNames API 分组名称(逗号分隔,如 "api,admin");"*" 表示出现在所有文档;空串表示未分组(仅进默认文档)。
* @param description API 描述。
* @param tags API 标签字符串。
*/
public const init(name!: ?String = None, ignore!: Bool = false, summary!: ?String = None, groupNames!: String = "",
description!: ?String = None, tags!: ?String = None) {
this.tags = tags
this.name = name
this.ignore = ignore
this.summary = summary
this.groupNames = groupNames
this.description = description
}
}
+2 -3
View File
@@ -11,8 +11,7 @@ import soulsoft_web_mvc.annotations.*
import simcu::simapi.communications.*
import simcu::simapi.configurations.*
import simcu::simapi.helpers.*
import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAttribute}
import simcu::simapi.openapi.annotations.*
import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAttribute, SimApiDoc}
/**
* 通用控制器:错误反馈、WebConfig、用户信息。
@@ -31,7 +30,7 @@ public class SimApiCommonController <: SimApiBaseController {
*/
@HttpGet["exception/{code}"]
@SimApiDoc[ignore: true]
public func exceptionHandler(@FromRoute[] code: Int64) { // cjlint-ignore !G.FUN.02 注解绑定参数误报
public func exceptionHandler(@FromRoute[] code: Int64): Unit { // cjlint-ignore !G.FUN.02 注解绑定参数误报
SimApiError.error(code: code)
}
-9
View File
@@ -1,9 +0,0 @@
/*
* 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
public import simcu::simapi.openapi.annotations.*
@@ -1,38 +0,0 @@
// 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
import soulsoft_web_http.*
/**
* @brief 提供 OpenAPI 端点约定扩展。
*/
public interface OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 OpenAPI 元数据。
* @param metadata 要附加的 OpenAPI 元数据。
* @return 当前端点约定构建器实例。
*/
func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder
}
extend EndpointConventionBuilder <: OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 API 文档元数据。
* @param metadata 要附加的 API 文档元数据。
* @return 当前端点约定构建器实例。
*/
public func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(metadata)
}
return this
}
}
@@ -1,65 +0,0 @@
// 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
import soulsoft_web_http.*
import soulsoft_web_routing.*
import simcu::simapi.openapi.services.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.infrastructure.*
/**
* @brief 提供 OpenAPI 文档路由映射扩展。
*/
public interface OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射默认 OpenAPI 文档路由。
* @return 当前端点路由构建器实例。
*/
func mapOpenApi(): EndpointRouteBuilder {
mapOpenApi(OpenApiConstants.DefaultOpenApiRoute)
}
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
func mapOpenApi(pattern: String): EndpointRouteBuilder
}
extend EndpointRouteBuilder <: OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
public func mapOpenApi(pattern: String): EndpointRouteBuilder {
verifyOpenApiServicesAreRegistered()
this.mapGet(pattern) {
context =>
let documentName = context.request.routeValues.get("documentName").flatMap {f => f} ?? "v1"
let documentFactory = context.services.getOrThrow<IDocumentProvider>()
let document = documentFactory.create(documentName, context.services)
context.response.contentType = "application/json; charset=utf-8"
let writer = JsonOpenApiWriter(context.response.body)
document.serializeAsV3(writer)
writer.flush()
}.withOpenApi(SimApiDoc(ignore: true))
return this
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception(
"Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
@@ -1,71 +0,0 @@
// 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
import soulsoft_web_routing.*
import soulsoft_extensions_options.*
import simcu::simapi.openapi.services.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.infrastructure.*
/**
* @brief 提供 OpenAPI 服务注册扩展。
*/
public interface OpenApiServiceCollectionExtensions {
/**
* @brief 注册默认名称的 OpenAPI 服务。
* @return 当前服务集合实例。
*/
func addOpenApi(): ServiceCollection {
addOpenApi(OpenApiConstants.DefaultDocumentName, {_ =>})
}
/**
* @brief 注册指定文档名称的 OpenAPI 服务。
* @param documentName OpenAPI 文档名称。
* @return 当前服务集合实例。
*/
func addOpenApi(documentName: String): ServiceCollection {
addOpenApi(documentName, {_ =>})
}
/**
* @brief 注册默认文档名称的 OpenAPI 服务并配置选项。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
addOpenApi(OpenApiConstants.DefaultDocumentName, configureOptions)
}
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection
}
extend ServiceCollection <: OpenApiServiceCollectionExtensions {
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
public func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
this.addRouting()
this.configure<OpenApiOptions>(documentName, configureOptions)
this.addSingleton(NamedService(documentName))
this.addSingleton<IDocumentProvider, OpenApiDocumentProvider>()
return this
}
}
@@ -1,41 +0,0 @@
// 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
import soulsoft_web_http.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.services.*
/**
* @brief 提供 OpenAPI UI 中间件扩展。
*/
public interface OpenApiUIMiddlewareExtensions {
/**
* @brief 启用 OpenAPI UI 中间件。
*/
func useOpenApiUI(): Unit
}
extend ApplicationBuilder <: OpenApiUIMiddlewareExtensions{
/**
* @brief 启用 OpenAPI UI 中间件。
*/
public func useOpenApiUI(): Unit {
verifyOpenApiServicesAreRegistered()
use<OpenApiUIMiddleware>()
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception("Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
@@ -1,56 +0,0 @@
/*
* 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
}
}
-107
View File
@@ -1,107 +0,0 @@
/*
* 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.annotations
import simcu::simapi.openapi.metadata.*
/**
* @brief 为控制器或动作提供 API 文档元数据(对齐 C# [SimApiDoc] 特性)。
*
* 用法:
* @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 & IApiGroupNamesProvider & IApiDescriptionMetadata & IApiSummaryMetadata & IApiVisibilityProvider {
private let _ignore: Bool
private let _name: ?String
private let _tags: ?String
private let _summary: ?String
private let _groupNames: String
private let _description: ?String
/**
* @brief 创建 SimApiDoc 注解实例。
* @param name API 名称。
* @param ignore 是否忽略当前 API。
* @param summary API 摘要。
* @param groupNames API 分组名称(逗号分隔,如 "api,admin");"*" 表示出现在所有文档;空串表示未分组(仅进默认文档)。
* @param description API 描述。
* @param tags API 标签字符串。
*/
public const init(name!: ?String = None, ignore!: Bool = false, summary!: ?String = None,
groupNames!: String = "", description!: ?String = None, tags!: ?String = None) {
_tags = tags
_name = name
_ignore = ignore
_summary = summary
_groupNames = groupNames
_description = description
}
/**
* @brief 返回 API 标签字符串。
* @return 当前 API 的标签字符串。
*/
public prop tags: ?String {
get() {
_tags
}
}
/**
* @brief 返回 API 名称。
* @return 当前 API 的名称。
*/
public prop name: ?String {
get() {
_name
}
}
/**
* @brief 返回当前 API 是否应被忽略。
* @return 如果当前 API 需要从文档中忽略则返回 `true`。
*/
public prop ignore: Bool {
get() {
_ignore
}
}
/**
* @brief 返回 API 摘要信息。
* @return 当前 API 的摘要信息。
*/
public prop summary: ?String {
get() {
_summary
}
}
/**
* @brief 返回 API 分组名称(逗号分隔,"*" 表示所有文档)。
* @return 逗号分隔的分组名称字符串,空串表示未分组。
*/
public prop groupNames: String {
get() {
_groupNames
}
}
/**
* @brief 返回 API 描述信息。
* @return 当前 API 的描述信息。
*/
public prop description: ?String {
get() {
_description
}
}
}
-28
View File
@@ -1,28 +0,0 @@
/*
* 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.infrastructure
import std.reflect.*
/*
Option类型操作
*/
protected class Nullable {
/**
* @brief 返回 Option 类型的底层类型。
* @param typeInfo 要解析的类型信息。
* @return 如果是 Option 类型则返回其底层类型,否则返回 `None`。
*/
public static func getUnderlyingType(typeInfo: TypeInfo): ?TypeInfo {
let qualifiedName = typeInfo.qualifiedName
if (!(qualifiedName.startsWith("Option<") && qualifiedName.endsWith('>'))) {
return None
}
let underlyingTypeName = qualifiedName[7..qualifiedName.size - 1]
return TypeInfo.get(underlyingTypeName)
}
}
@@ -1,13 +0,0 @@
/*
* 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.infrastructure
protected class OpenApiConstants {
public static let DefaultOpenApiName = "default"
public static let DefaultDocumentName = "v1"
public static let DefaultOpenApiRoute = "/openapi/{documentName}.json"
}
+97
View File
@@ -0,0 +1,97 @@
/*
* 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
import std.reflect.*
import std.collection.*
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
import simcu::simapi.annotations.*
import simcu::simapi.openapi.models.*
/**
* @brief 提供 OpenAPI 端点约定扩展。
*/
public interface OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 OpenAPI 元数据。
* @param metadata 要附加的 OpenAPI 元数据。
* @return 当前端点约定构建器实例。
*/
func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder
}
/**
* @brief 提供 OpenAPI 文档路由映射扩展。
*/
public interface OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
func mapOpenApi(pattern: String): EndpointRouteBuilder
}
/**
* @brief 提供 OpenAPI 服务注册扩展。
*/
public interface OpenApiServiceCollectionExtensions {
/**
* @brief 注册默认文档名称的 OpenAPI 服务并配置选项。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
addOpenApi("v1", configureOptions)
}
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection
}
/**
* @brief 提供 OpenAPI UI 中间件扩展。
*/
public interface OpenApiUIMiddlewareExtensions {
/**
* @brief 启用 OpenAPI UI 中间件。
*/
func useOpenApiUI(): Unit
}
/**
* @brief 为动态注册的路由提供认证和响应类型元数据扩展。
*/
public interface SimApiEndpointMetadataExtensions {
func withSimApiAuth(auth: SimApiAuth): EndpointConventionBuilder
func withResponseType(typeInfo: TypeInfo): EndpointConventionBuilder
}
/**
* @brief 定义 OpenAPI 文档提供器接口。
*/
public interface IDocumentProvider {
/**
* @brief 返回已注册的文档名称集合。
* @return 当前可用的文档名称集合。
*/
func getDooucmentNames(): Collection<String>
/**
* @brief 按名称创建 OpenAPI 文档。
* @param documentName 要创建的文档名称。
* @return 生成后的 OpenAPI 文档。
*/
func create(documentName: String, services: IServiceProvider): OpenApiDocument
}
@@ -1,21 +0,0 @@
// 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.interfaces
/**
* @brief 定义 OpenAPI 对象序列化接口。
*/
public interface IOpenApiSerializable {
/**
* @brief 按 OpenAPI V3 格式写出当前对象。
* @param writer OpenAPI 写入器。
*/
func serializeAsV3(writer: IOpenApiWriter): Unit
}
@@ -1,21 +0,0 @@
// 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.metadata
/**
* @brief 提供 API 描述元数据。
*/
public interface IApiDescriptionMetadata {
/**
* @brief 返回 API 描述信息。
* @return 当前 API 的描述信息。
*/
prop description: ?String
}
@@ -1,21 +0,0 @@
// 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.metadata
/**
* @brief 提供 API 多分组名称元数据(逗号分隔,如 "api,admin")。
*/
public interface IApiGroupNamesProvider {
/**
* @brief 返回 API 所属的多个分组名称(逗号分隔)。
* @return 逗号分隔的分组名称字符串,空串表示未指定。
*/
prop groupNames: String
}
-21
View File
@@ -1,21 +0,0 @@
// 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.metadata
/**
* @brief 提供 API 名称元数据。
*/
public interface IApiNameMetadata {
/**
* @brief 返回 API 名称。
* @return 当前 API 的名称。
*/
prop name: ?String
}
@@ -1,28 +0,0 @@
// 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.metadata
import std.reflect.*
/**
* @brief 提供 API 响应元数据。
*/
public interface IApiResponseMetadataProvider {
/**
* @brief 返回响应状态码。
* @return 当前 API 响应的状态码。
*/
prop status: Int64
/**
* @brief 返回响应类型信息。
* @return 当前 API 响应的数据类型信息。
*/
prop typeInfo: TypeInfo
}
@@ -1,18 +0,0 @@
/*
* 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
}
@@ -1,21 +0,0 @@
// 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.metadata
/**
* @brief 提供 API 摘要元数据。
*/
public interface IApiSummaryMetadata {
/**
* @brief 返回 API 摘要信息。
* @return 当前 API 的摘要信息。
*/
prop summary: ?String
}
-21
View File
@@ -1,21 +0,0 @@
// 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.metadata
/**
* @brief 提供 API 标签元数据。
*/
public interface IApiTagsMetadata {
/**
* @brief 返回 API 标签字符串。
* @return 当前 API 的标签字符串。
*/
prop tags: ?String
}
@@ -1,21 +0,0 @@
// 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.metadata
/**
* @brief 提供 API 可见性元数据。
*/
public interface IApiVisibilityProvider {
/**
* @brief 返回当前 API 是否应被忽略。
* @return 如果当前 API 需要从 OpenAPI 文档中忽略则返回 `true`。
*/
prop ignore: Bool
}
-9
View File
@@ -1,9 +0,0 @@
/*
* 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
protected import simcu::simapi.openapi.interfaces.*
@@ -18,12 +18,12 @@ public class OpenApiComponents <: IOpenApiSerializable {
/**
* @brief 表示组件中的架构集合。
*/
public var schemas = HashMap<String, OpenApiSchema>()
public var schemas: HashMap<String, OpenApiSchema> = HashMap<String, OpenApiSchema>()
/**
* @brief 表示组件中的安全方案集合。
*/
public var securitySchemes = HashMap<String, OpenApiSecurityScheme>()
public var securitySchemes: HashMap<String, OpenApiSecurityScheme> = HashMap<String, OpenApiSecurityScheme>()
/**
* @brief 创建 OpenAPI 组件实例。
@@ -34,15 +34,15 @@ public class OpenApiDocument <: IOpenApiSerializable {
/**
* @brief 表示标签集合。
*/
public var tags = ArrayList<OpenApiTag>()
public var tags: ArrayList<OpenApiTag> = ArrayList<OpenApiTag>()
/**
* @brief 表示服务器集合。
*/
public var servers = ArrayList<OpenApiServer>()
public var servers: ArrayList<OpenApiServer> = ArrayList<OpenApiServer>()
/**
* @brief 表示安全需求集合。
*/
public var securityRequirements = ArrayList<OpenApiSecurityRequirement>()
public var securityRequirements: ArrayList<OpenApiSecurityRequirement> = ArrayList<OpenApiSecurityRequirement>()
/**
* @brief 创建 OpenAPI 文档对象。
@@ -30,15 +30,15 @@ public class OpenApiOperation <: IOpenApiSerializable {
/**
* @brief 表示操作响应集合。
*/
public var response = OpenApiResponses()
public var response: OpenApiResponses = OpenApiResponses()
/**
* @brief 表示操作标签集合。
*/
public var tags = ArrayList<OpenApiTag>()
public var tags: ArrayList<OpenApiTag> = ArrayList<OpenApiTag>()
/**
* @brief 表示操作服务器集合。
*/
public var servers = ArrayList<OpenApiServer>()
public var servers: ArrayList<OpenApiServer> = ArrayList<OpenApiServer>()
/**
* @brief 表示操作请求体。
*/
@@ -46,11 +46,11 @@ public class OpenApiOperation <: IOpenApiSerializable {
/**
* @brief 表示操作参数集合。
*/
public var parameters = ArrayList<OpenApiParameter>()
public var parameters: ArrayList<OpenApiParameter> = ArrayList<OpenApiParameter>()
/**
* @brief 表示操作安全需求集合。
*/
public var security = ArrayList<OpenApiSecurityRequirement>()
public var security: ArrayList<OpenApiSecurityRequirement> = ArrayList<OpenApiSecurityRequirement>()
/**
* @brief 创建 OpenAPI 操作对象。
@@ -34,15 +34,15 @@ public class OpenApiPathItem <: IOpenApiSerializable {
/**
* @brief 表示路径项服务器集合。
*/
public var servers = ArrayList<OpenApiServer>()
public var servers: ArrayList<OpenApiServer> = ArrayList<OpenApiServer>()
/**
* @brief 表示路径项参数集合。
*/
public var parameters = ArrayList<OpenApiParameter>()
public var parameters: ArrayList<OpenApiParameter> = ArrayList<OpenApiParameter>()
/**
* @brief 表示路径项操作集合。
*/
public var operations = HashMap<OperationType, OpenApiOperation>()
public var operations: HashMap<OperationType, OpenApiOperation> = HashMap<OperationType, OpenApiOperation>()
/**
* @brief 创建 OpenAPI 路径项对象。
@@ -26,7 +26,7 @@ public class OpenApiRequestBody <: IOpenApiSerializable {
/**
* @brief 表示请求体内容映射。
*/
public var content = HashMap<String, OpenApiMediaType>()
public var content: HashMap<String, OpenApiMediaType> = HashMap<String, OpenApiMediaType>()
/**
* @brief 创建 OpenAPI 请求体对象。
@@ -22,7 +22,7 @@ public class OpenApiResponse <: IOpenApiSerializable {
/**
* @brief 表示响应内容映射。
*/
public var content = HashMap<String, OpenApiMediaType>()
public var content: HashMap<String, OpenApiMediaType> = HashMap<String, OpenApiMediaType>()
/**
* @brief 创建 OpenAPI 响应对象。
@@ -46,11 +46,11 @@ public class OpenApiSchema <: IOpenApiSerializable {
/**
* @brief 表示对象属性集合。
*/
public var properties = HashMap<String, OpenApiSchema>()
public var properties: HashMap<String, OpenApiSchema> = HashMap<String, OpenApiSchema>()
/**
* @brief 表示 allOf 组合架构。
*/
public var allOf = ArrayList<OpenApiSchema>()
public var allOf: ArrayList<OpenApiSchema> = ArrayList<OpenApiSchema>()
/**
* @brief 表示是否允许附加属性。
*/
@@ -70,7 +70,7 @@ public class OpenApiSchema <: IOpenApiSerializable {
* @param reference 架构引用。
* @param description 架构描述。
*/
public init(title!: ?String = None, nullable!: ?Bool = None, `type`!: ?String = None, items!: ?OpenApiSchema = None,
public init(title!: ?String = None, nullable!: ?Bool = None, `type`!: ?String = None, items!: ?OpenApiSchema = None, // cjlint-ignore !G.FUN.02
format!: ?String = None, reference!: ?OpenApiReference = None, description!: ?String = None) {
this.title = title
this.format = format
@@ -13,9 +13,9 @@ private let numbers = ["Float16", "Float32", "Float64", "Decimal"]
private let integers = ["Int8", "Int16", "Int32", "Int64", "UInt8", "UInt16", "UInt32", "UInt64"]
protected class OpenApiSchemaTypes {
public static const OBJECT = "object"
public static const STRING = "string"
public static const ARRAY = "array"
public static const OBJECT: String = "object"
public static const STRING: String = "string"
public static const ARRAY: String = "array"
/**
* @brief 将类型信息转换为 OpenAPI Schema 类型名称。
@@ -1,13 +1,21 @@
// 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.interfaces
package simcu::simapi.openapi.models
/**
* @brief 定义 OpenAPI 对象序列化接口。
*/
public interface IOpenApiSerializable {
/**
* @brief 按 OpenAPI V3 格式写出当前对象。
* @param writer OpenAPI 写入器。
*/
func serializeAsV3(writer: IOpenApiWriter): Unit
}
/**
* @brief 定义 OpenAPI 文档写入接口。
@@ -7,13 +7,12 @@
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.services
package simcu::simapi.openapi
import std.collection.*
import simcu::simapi.openapi.models.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.transformers.*
/**
* @brief 提供 OpenAPI 文档创建能力。
@@ -42,10 +41,10 @@ protected class OpenApiDocumentProvider <: IDocumentProvider {
}
/**
* @brief 按名称创建 OpenAPI 文档并依次应用所有转换器。
* @brief 按名称创建 OpenAPI 文档。
* @param documentName 要创建的文档名称。
* @param services 请求作用域的服务提供器,传递给操作转换器上下文。
* @return 经过转换器处理后的 OpenAPI 文档。
* @param services 请求作用域的服务提供器。
* @return OpenAPI 文档。
*/
public func create(documentName: String, services: IServiceProvider): OpenApiDocument {
let options = _options.get(documentName)
@@ -57,11 +56,6 @@ protected class OpenApiDocumentProvider <: IDocumentProvider {
schemaService,
parameterService
)
let document = provider.getOpenApiDocument(services)
let context = OpenApiDocumentTransformerContext(services, documentName)
for (transformer in options.documentTransformers) {
transformer.transform(document, context)
}
return document
return provider.getOpenApiDocument(services)
}
}
@@ -7,7 +7,7 @@
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.services
package simcu::simapi.openapi
import std.reflect.*
import std.collection.*
@@ -20,10 +20,7 @@ import simcu::simapi.openapi.models.*
import soulsoft_extensions_options.*
import soulsoft_web_mvc.controllers.*
import soulsoft_extensions_injection.*
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.*
@@ -105,7 +102,6 @@ protected class OpenApiDocumentService {
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, openApiOptions.includeUnGrouped)) {
continue
@@ -132,10 +128,6 @@ protected class OpenApiDocumentService {
let operationType = OperationType.parse(httpMethod)
let actionDescriptor = endpoint.metadata.getMetadata<ControllerActionDescriptor>()
let operation = createOpenApiOperation(endpoint, actionDescriptor)
let context = OpenApiOperationTransformerContext(services, _documentName, actionDescriptor)
for (transformer in operationTransformers) {
transformer.transform(operation, context)
}
pathItem.operations.add(operationType, operation)
}
}
@@ -144,7 +136,7 @@ protected class OpenApiDocumentService {
}
private func isShouldInclude(endpoint: RouteEndpoint, includeUnGrouped: Bool) {
let rawGroupNames = endpoint.metadata.getOrderedMetadata<IApiGroupNamesProvider>() |>
let rawGroupNames = endpoint.metadata.getOrderedMetadata<SimApiDoc>() |>
filterMap { f => f.groupNames } |> collectArray
if (rawGroupNames.isEmpty()) {
// 未标注 groupNames 的接口:仅进入默认文档(includeUnGrouped=true 的文档)
@@ -204,7 +196,7 @@ protected class OpenApiDocumentService {
operation.response = createOpenApiOperationResponses(endpoint, actionDescriptor)
// summary
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiSummaryMetadata> {f => f.summary.isSome()}) {
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.summary.isSome()}) {
if (let Some(summary) <- metadata.summary) {
operation.summary = summary
}
@@ -213,14 +205,14 @@ protected class OpenApiDocumentService {
// description
if (let Some(metadata) <- endpoint
.metadata
.getLastMetadata<IApiDescriptionMetadata> {f => f.description.isSome()}) {
.getLastMetadata<SimApiDoc> {f => f.description.isSome()}) {
if (let Some(description) <- metadata.description) {
operation.description = description
}
}
// operationId
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiNameMetadata> {f => f.name.isSome()}) {
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.name.isSome()}) {
operation.operationId = metadata.name
}
@@ -237,7 +229,7 @@ protected class OpenApiDocumentService {
private func createOpenApiOperationTags(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) {
let result = ArrayList<OpenApiTag>()
// tags
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiTagsMetadata> {f => f.tags.isSome()}) {
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.tags.isSome()}) {
if (let Some(tags) <- metadata.tags) {
for (name in tags.split(',')) {
result.add(OpenApiTag(name: name))
@@ -251,7 +243,7 @@ protected class OpenApiDocumentService {
if (let Some(actionDescriptor) <- actionDescriptor) {
result.add(OpenApiTag(name: actionDescriptor.controllerName))
} else {
result.add(OpenApiTag(name: OpenApiConstants.DefaultOpenApiName))
result.add(OpenApiTag(name: "default"))
}
return result
@@ -260,7 +252,7 @@ protected class OpenApiDocumentService {
/*
来自请求体的参数
*/
private func createOpenApiOperationRequestBody(endpoint: RouteEndpoint, actionDescriptor: ControllerActionDescriptor): ?OpenApiRequestBody {
private func createOpenApiOperationRequestBody(endpoint: RouteEndpoint, actionDescriptor: ControllerActionDescriptor): ?OpenApiRequestBody { // cjlint-ignore !G.FUN.02 endpoint 未使用
var requestBody = OpenApiRequestBody(required: true)
let parameters = HashMap<String, OpenApiSchema>()
for (parameter in actionDescriptor.actionFunction.parameters) {
@@ -299,12 +291,12 @@ protected class OpenApiDocumentService {
private func createOpenApiOperationResponses(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) {
let responses = OpenApiResponses()
// 获取action的返回类型:优先从 actionDescriptor,其次从 IApiResponseTypeMetadata(动态路由)
// 获取action的返回类型:优先从 actionDescriptor,其次从 ApiResponseTypeMetadata(动态路由)
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>()) {
} else if (let Some(meta) <- endpoint.metadata.getMetadata<ApiResponseTypeMetadata>()) {
meta.responseType
} else {
None
@@ -357,30 +349,9 @@ protected class OpenApiDocumentService {
// api ignore
private func isIgnore(endpoint: RouteEndpoint) {
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiVisibilityProvider> {f => f.ignore}) {
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.ignore}) {
return true
}
return false
}
}
extend EndpointMetadataCollection {
/**
* @brief 返回最后一个满足条件的元数据。
* @param filter 用于筛选元数据的条件。
* @return 最后一个满足条件的元数据;如果不存在则返回 `None`。
*/
public func getLastMetadata<T>(filter: (T) -> Bool): ?T {
let metadatas = this.getOrderedMetadata<T>()
var index = metadatas.size - 1
while (index >= 0) {
let metadata = metadatas[index]
if (filter(metadata)) {
return metadata
}
index--
}
return None
}
}
+152
View File
@@ -0,0 +1,152 @@
/*
* 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
import std.reflect.*
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
import simcu::simapi.annotations.*
/**
* @brief 为端点附加 OpenAPI 元数据。
*/
extend EndpointConventionBuilder <: OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 API 文档元数据。
* @param metadata 要附加的 API 文档元数据。
* @return 当前端点约定构建器实例。
*/
public func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(metadata)
}
return this
}
}
/**
* @brief 映射默认 OpenAPI 文档路由。
*/
extend EndpointRouteBuilder <: OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
public func mapOpenApi(pattern: String): EndpointRouteBuilder {
verifyOpenApiServicesAreRegistered()
this.mapGet(pattern) {
context =>
let documentName = context.request.routeValues.get("documentName").flatMap {f => f} ?? "v1"
let documentFactory = context.services.getOrThrow<IDocumentProvider>()
let document = documentFactory.create(documentName, context.services)
context.response.contentType = "application/json; charset=utf-8"
let writer = JsonOpenApiWriter(context.response.body)
document.serializeAsV3(writer)
writer.flush()
}.withOpenApi(SimApiDoc(ignore: true))
return this
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception(
"Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
/**
* @brief 注册默认名称的 OpenAPI 服务。
*/
extend ServiceCollection <: OpenApiServiceCollectionExtensions {
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
public func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
this.addRouting()
this.configure<OpenApiOptions>(documentName, configureOptions)
this.addSingleton(NamedService(documentName))
this.addSingleton<IDocumentProvider, OpenApiDocumentProvider>()
return this
}
}
/**
* @brief 启用 OpenAPI UI 中间件。
*/
extend ApplicationBuilder <: OpenApiUIMiddlewareExtensions{
/**
* @brief 启用 OpenAPI UI 中间件。
*/
public func useOpenApiUI(): Unit {
verifyOpenApiServicesAreRegistered()
use<OpenApiUIMiddleware>()
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception("Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
/**
* @brief 为端点附加认证与响应类型元数据。
*/
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
}
}
/**
* @brief 提供元数据集合扩展。
*/
extend EndpointMetadataCollection {
/**
* @brief 返回最后一个满足条件的元数据。
* @param filter 用于筛选元数据的条件。
* @return 最后一个满足条件的元数据;如果不存在则返回 `None`。
*/
public func getLastMetadata<T>(filter: (T) -> Bool): ?T {
let metadatas = this.getOrderedMetadata<T>()
var index = metadatas.size - 1
while (index >= 0) {
let metadata = metadatas[index]
if (filter(metadata)) {
return metadata
}
index--
}
return None
}
}
+46
View File
@@ -0,0 +1,46 @@
// 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
import std.reflect.*
import std.collection.*
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 文档生成选项。
*/
public class OpenApiOptions {
/**
* 未标注 groupName 的接口是否进入本文档。
* 多文档分组时,仅默认组(第一个 apiGroup 或 isDefault 的组)应设为 true。
*/
public var includeUnGrouped: Bool = false
/**
* @brief 确定指定类型在 components/schemas 中使用的引用 ID。
* 返回 None 时该 Schema 始终内联,不生成 $ref。
*/
public var createSchemaReferenceId: (TypeInfo) -> ?String = {typeInfo =>
let name = typeInfo.name
if (let Some(ltIdx) <- name.indexOf('<') && let Some(gtIdx) <- name.lastIndexOf('>') &&
gtIdx > ltIdx) {
let baseName = name[0..ltIdx]
let argsStr = name[ltIdx + 1..gtIdx]
let simpleName = if (let Some(dotIdx) <- argsStr.lastIndexOf('.')) {
argsStr[dotIdx + 1..]
} else {
argsStr
}
"${baseName}Of${simpleName}"
} else {
name
}
}
}
@@ -4,7 +4,7 @@
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.services
package simcu::simapi.openapi
import std.reflect.*
import std.collection.*
@@ -12,7 +12,6 @@ import soulsoft_web_mvc.*
import soulsoft_web_mvc.utilities.*
import simcu::simapi.openapi.models.*
import soulsoft_web_mvc.controllers.*
import simcu::simapi.openapi.infrastructure.*
/**
* @brief 提供 OpenAPI 参数生成能力。
@@ -7,7 +7,7 @@
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.services
package simcu::simapi.openapi
import std.time.*
import std.reflect.*
@@ -17,10 +17,8 @@ import stdx.encoding.json.*
import soulsoft_web_http.*
import simcu::simapi.openapi.models.*
import soulsoft_web_mvc.IActionResult
import simcu::simapi.openapi.metadata.*
import simcu::simapi.annotations.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.transformers.*
import simcu::simapi.openapi.infrastructure.*
/**
* @brief 提供 OpenAPI Schema 生成功能。
@@ -105,21 +103,18 @@ protected class OpenApiSchemaService {
let `type` = OpenApiSchemaTypes.convert(typeInfo)
let format = convertToSchemaFormat(typeInfo)
let schema = OpenApiSchema(`type`: `type`, format: format, nullable: nullable)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema
}
// JsonValue/JsonObject/JsonArray 表示“任意 JSON 值树”,不应展开为固定 object schema。
if (typeInfo.isSubtypeOf(OpenApiSchemaService._jsonValueType)) {
let schema = OpenApiSchema(nullable: nullable)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema
}
// 处理文件上传
if (typeInfo == OpenApiSchemaService._formFileType) {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING, format: "binary")
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema
}
@@ -142,7 +137,6 @@ protected class OpenApiSchemaService {
if (let _: EnumTypeInfo <- typeInfo) {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING, nullable: nullable)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema
}
@@ -170,14 +164,12 @@ protected class OpenApiSchemaService {
// 先加入缓存,再处理属性,避免循环引用导致无限递归
_schemas.add(schemaId, schema)
buildObjectProperties(schema, typeInfo)
applySchemaTransformers(schema, schemaId)
}
return createReferenceSchema(schemaId, nullable)
} else {
// 强制内联:不缓存,不生成 $ref
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT, nullable: nullable)
buildObjectProperties(schema, typeInfo)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema
}
}
@@ -195,7 +187,6 @@ protected class OpenApiSchemaService {
private func createInlineObjectSchema(typeInfo: TypeInfo, nullable: Bool): OpenApiSchema {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT, nullable: nullable)
buildObjectProperties(schema, typeInfo)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema
}
@@ -207,7 +198,7 @@ protected class OpenApiSchemaService {
let propertyTypeInfo = underlyingType ?? property.typeInfo
let propertySchema = buildSchema(propertyTypeInfo, underlyingType.isSome(), true)
for (annotation in property.annotations) {
if (let meta: IApiDescriptionMetadata <- annotation) {
if (let meta: SimApiDoc <- annotation) {
propertySchema.description = meta.description
}
}
@@ -223,7 +214,7 @@ protected class OpenApiSchemaService {
let variableTypeInfo = underlyingType ?? variable.typeInfo
let variableSchema = buildSchema(variableTypeInfo, underlyingType.isSome(), true)
for (annotation in variable.annotations) {
if (let meta: IApiDescriptionMetadata <- annotation) {
if (let meta: SimApiDoc <- annotation) {
variableSchema.description = meta.description
}
}
@@ -244,8 +235,6 @@ protected class OpenApiSchemaService {
private func createArraySchema(elementTypeInfo: TypeInfo, preferReference: Bool): OpenApiSchema {
let itemSchema = buildSchema(elementTypeInfo, false, preferReference)
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.ARRAY, items: itemSchema)
// transformer 面向当前数组 schema,本处使用容器类型名最准确;现阶段退化为元素类型名。
applySchemaTransformers(schema, elementTypeInfo.qualifiedName)
return schema
}
@@ -258,24 +247,12 @@ protected class OpenApiSchemaService {
if (keyTypeInfo != OpenApiSchemaService._stringType) {
// 非字符串 key 无法直接表达为标准 JSON object key;当前保守输出为封闭 object。
schema.additionalProperties = false
applySchemaTransformers(schema, keyTypeInfo.qualifiedName)
return schema
}
schema.additionalPropertiesSchema = buildSchema(valueTypeInfo, false, true)
applySchemaTransformers(schema, valueTypeInfo.qualifiedName)
return schema
}
private func applySchemaTransformers(schema: OpenApiSchema, schemaName: String): Unit {
if (_documentName.isEmpty() || _options.schemaTransformers.isEmpty()) {
return
}
let context = OpenApiSchemaTransformerContext(_services, _documentName, schemaName)
for (transformer in _options.schemaTransformers) {
transformer.transform(schema, context)
}
}
/*
解析 Collection<T> 中的 T
*/
@@ -1,17 +1,32 @@
// 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.services
package simcu::simapi.openapi
import std.io.*
import std.reflect.*
import stdx.encoding.json.stream.*
import simcu::simapi.openapi.interfaces.*
import simcu::simapi.openapi.models.*
/**
* @brief 响应类型元数据(供动态注册的路由指定返回类型)。
*/
public class ApiResponseTypeMetadata {
/**
* @brief 响应类型的类型信息。
*/
public let responseType: ?TypeInfo
/**
* @brief 创建响应类型元数据实例。
* @param responseType 响应类型的类型信息。
*/
public init(responseType: ?TypeInfo) {
this.responseType = responseType
}
}
/**
* @brief 提供基于 JSON 的 OpenAPI 写入器实现。
@@ -94,3 +109,40 @@ public class JsonOpenApiWriter <: IOpenApiWriter {
_writer.flush()
}
}
/**
* @brief 表示带名称的服务标识。
*/
public class NamedService {
/**
* @brief 表示服务名称。
*/
public let name: String
/**
* @brief 创建带名称的服务标识实例。
* @param name 服务名称。
*/
public init(name: String) {
this.name = name
}
}
/*
Option类型操作
*/
protected class Nullable {
/**
* @brief 返回 Option 类型的底层类型。
* @param typeInfo 要解析的类型信息。
* @return 如果是 Option 类型则返回其底层类型,否则返回 `None`。
*/
public static func getUnderlyingType(typeInfo: TypeInfo): ?TypeInfo {
let qualifiedName = typeInfo.qualifiedName
if (!(qualifiedName.startsWith("Option<") && qualifiedName.endsWith('>'))) {
return None
}
let underlyingTypeName = qualifiedName[7..qualifiedName.size - 1]
return TypeInfo.get(underlyingTypeName)
}
}
@@ -92,7 +92,10 @@ public class OpenApiUIMiddleware <: IMiddleware {
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)) {
if (!exists(path)) {
return false
}
if (!isPathWithinRoot(path)) {
return false
}
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.infrastructure
package simcu::simapi.openapi
import std.reflect.*
import std.collection.*
-30
View File
@@ -1,30 +0,0 @@
// 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.services
import simcu::simapi.openapi.models.*
import soulsoft_extensions_injection.*
/**
* @brief 定义 OpenAPI 文档提供器接口。
*/
public interface IDocumentProvider {
/**
* @brief 返回已注册的文档名称集合。
* @return 当前可用的文档名称集合。
*/
func getDooucmentNames(): Collection<String>
/**
* @brief 按名称创建 OpenAPI 文档。
* @param documentName 要创建的文档名称。
* @return 生成后的 OpenAPI 文档。
*/
func create(documentName: String, services: IServiceProvider): OpenApiDocument
}
-28
View File
@@ -1,28 +0,0 @@
// 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.services
/**
* @brief 表示带名称的服务标识。
*/
public class NamedService {
/**
* @brief 表示服务名称。
*/
public let name: String
/**
* @brief 创建带名称的服务标识实例。
* @param name 服务名称。
*/
public init(name: String) {
this.name = name
}
}
-129
View File
@@ -1,129 +0,0 @@
// 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.services
import std.reflect.*
import std.collection.*
import simcu::simapi.openapi.models.*
import simcu::simapi.openapi.transformers.*
/**
* @brief 定义 OpenAPI 文档生成选项。
*/
public class OpenApiOptions {
private let _documentTransformers = ArrayList<IOpenApiDocumentTransformer>()
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。
*/
public var createSchemaReferenceId: (TypeInfo) -> ?String = {typeInfo =>
let name = typeInfo.name
if (let Some(ltIdx) <- name.indexOf('<') && let Some(gtIdx) <- name.lastIndexOf('>') &&
gtIdx > ltIdx) {
let baseName = name[0..ltIdx]
let argsStr = name[ltIdx + 1..gtIdx]
let simpleName = if (let Some(dotIdx) <- argsStr.lastIndexOf('.')) {
argsStr[dotIdx + 1..]
} else {
argsStr
}
"${baseName}Of${simpleName}"
} else {
name
}
}
/**
* @brief 返回当前文档转换器集合。
* @return 已注册的文档转换器集合。
*/
protected prop documentTransformers: List<IOpenApiDocumentTransformer> {
get() {
_documentTransformers
}
}
/**
* @brief 返回当前操作转换器集合。
* @return 已注册的操作转换器集合。
*/
protected prop operationTransformers: List<IOpenApiOperationTransformer> {
get() {
_operationTransformer
}
}
/**
* @brief 返回当前架构转换器集合。
* @return 已注册的架构转换器集合。
*/
protected prop schemaTransformers: List<IOpenApiSchemaTransformer> {
get() {
_schemaTransformers
}
}
/**
* @brief 添加文档转换器实例。
* @param transformer 要添加的 OpenAPI 文档转换器。
*/
public func addDocumentTransformer(transformer: IOpenApiDocumentTransformer): Unit {
_documentTransformers.add(transformer)
}
/**
* @brief 以委托形式添加文档转换器。
* @param transformer 表示文档转换逻辑的委托。
*/
public func addDocumentTransformer(transformer: (OpenApiDocument, OpenApiDocumentTransformerContext) -> Unit): Unit {
_documentTransformers.add(DelegateOpenApiDocumentTransformer(transformer))
}
/**
* @brief 添加操作转换器实例。
* @param transformer 要添加的 OpenAPI 操作转换器。
*/
public func addOperationTransformer(transformer: IOpenApiOperationTransformer): Unit {
_operationTransformer.add(transformer)
}
/**
* @brief 以委托形式添加操作转换器。
* @param transformer 表示操作转换逻辑的委托。
*/
public func addOperationTransformer(transformer: (OpenApiOperation, OpenApiOperationTransformerContext) -> Unit): Unit {
_operationTransformer.add(DelegateOpenApiOperationTransformer(transformer))
}
/**
* @brief 添加架构转换器实例。
* @param transformer 要添加的 OpenAPI 架构转换器。
*/
public func addSchemaTransformer(transformer: IOpenApiSchemaTransformer): Unit {
_schemaTransformers.add(transformer)
}
/**
* @brief 以委托形式添加架构转换器。
* @param transformer 表示架构转换逻辑的委托。
*/
public func addSchemaTransformer(transformer: (OpenApiSchema, OpenApiSchemaTransformerContext) -> Unit): Unit {
_schemaTransformers.add(DelegateOpenApiSchemaTransformer(transformer))
}
}
@@ -1,36 +0,0 @@
// 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.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 将委托包装为 IOpenApiDocumentTransformer 的适配器实现。
*/
protected class DelegateOpenApiDocumentTransformer <: IOpenApiDocumentTransformer {
private let _transformer: (OpenApiDocument, OpenApiDocumentTransformerContext) -> Unit
/**
* @brief 创建委托文档转换器实例。
* @param transformer 表示文档转换逻辑的委托。
*/
public init(transformer: (OpenApiDocument, OpenApiDocumentTransformerContext) -> Unit) {
_transformer = transformer
}
/**
* @brief 调用委托转换指定的 OpenAPI 文档。
* @param document 要转换的 OpenAPI 文档。
* @param context 文档转换器上下文。
*/
public func transform(document: OpenApiDocument, context: OpenApiDocumentTransformerContext): Unit {
_transformer(document, context)
}
}
@@ -1,24 +0,0 @@
// 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.transformers
import simcu::simapi.openapi.models.*
protected class DelegateOpenApiOperationTransformer <: IOpenApiOperationTransformer {
private let _transformer: (OpenApiOperation, OpenApiOperationTransformerContext) -> Unit
public init(transformer: (OpenApiOperation, OpenApiOperationTransformerContext) -> Unit) {
_transformer = transformer
}
public func transform(operation: OpenApiOperation, context: OpenApiOperationTransformerContext): Unit {
_transformer(operation, context)
}
}
@@ -1,36 +0,0 @@
// 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.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 将委托包装为 IOpenApiSchemaTransformer 的适配器实现。
*/
protected class DelegateOpenApiSchemaTransformer <: IOpenApiSchemaTransformer {
private let _transformer: (OpenApiSchema, OpenApiSchemaTransformerContext) -> Unit
/**
* @brief 创建委托架构转换器实例。
* @param transformer 表示架构转换逻辑的委托。
*/
public init(transformer: (OpenApiSchema, OpenApiSchemaTransformerContext) -> Unit) {
_transformer = transformer
}
/**
* @brief 调用委托转换指定的 OpenAPI 架构。
* @param schema 要转换的 OpenAPI 架构。
* @param context 架构转换器上下文。
*/
public func transform(schema: OpenApiSchema, context: OpenApiSchemaTransformerContext): Unit {
_transformer(schema, context)
}
}
@@ -1,24 +0,0 @@
// 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.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 文档转换器接口。
*/
public interface IOpenApiDocumentTransformer {
/**
* @brief 转换指定的 OpenAPI 文档。
* @param document 要转换的 OpenAPI 文档。
* @param context 文档转换器上下文。
*/
func transform(document: OpenApiDocument, context: OpenApiDocumentTransformerContext): Unit
}
@@ -1,24 +0,0 @@
// 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.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 操作转换器接口。
*/
public interface IOpenApiOperationTransformer {
/**
* @brief 转换指定的 OpenAPI 操作对象。
* @param operation 要转换的 OpenAPI 操作对象。
* @param context 操作转换器上下文。
*/
func transform(operation: OpenApiOperation, context: OpenApiOperationTransformerContext): Unit
}
@@ -1,24 +0,0 @@
// 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.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 架构转换器接口。
*/
public interface IOpenApiSchemaTransformer {
/**
* @brief 转换指定的 OpenAPI 架构。
* @param schema 要转换的 OpenAPI 架构。
* @param context 架构转换器上下文。
*/
func transform(schema: OpenApiSchema, context: OpenApiSchemaTransformerContext): Unit
}
@@ -1,36 +0,0 @@
// 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.transformers
import soulsoft_extensions_injection.*
/**
* @brief 提供文档转换器执行时的上下文信息。
*/
public class OpenApiDocumentTransformerContext {
/**
* @brief 当前正在生成的文档名称。
*/
public let documentName: String
/**
* @brief 请求作用域的服务提供器。
*/
public let services: IServiceProvider
/**
* @brief 创建文档转换器上下文实例。
* @param services 请求作用域的服务提供器。
* @param documentName 当前正在生成的文档名称。
*/
public init(services: IServiceProvider, documentName: String) {
this.services = services
this.documentName = documentName
}
}
@@ -1,43 +0,0 @@
// 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.transformers
import soulsoft_web_mvc.controllers.*
import soulsoft_extensions_injection.*
/**
* @brief 提供操作转换器执行时的上下文信息。
*/
public class OpenApiOperationTransformerContext {
/**
* @brief 当前正在生成的文档名称。
*/
public let documentName: String
/**
* @brief 请求作用域的服务提供器。
*/
public let services: IServiceProvider
/**
* @brief 当前操作关联的控制器动作描述。
*/
public let description: ?ControllerActionDescriptor
/**
* @brief 创建操作转换器上下文实例。
* @param services 请求作用域的服务提供器。
* @param documentName 当前正在生成的文档名称。
* @param description 当前操作关联的控制器动作描述。
*/
public init(services: IServiceProvider, documentName: String, description: ?ControllerActionDescriptor) {
this.services = services
this.documentName = documentName
this.description = description
}
}
@@ -1,42 +0,0 @@
// 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.transformers
import soulsoft_extensions_injection.*
/**
* @brief 提供架构转换器执行时的上下文信息。
*/
public class OpenApiSchemaTransformerContext {
/**
* @brief 当前正在生成的文档名称。
*/
public let documentName: String
/**
* @brief 请求作用域的服务提供器。
*/
public let services: IServiceProvider
/**
* @brief 当前架构在 components/schemas 中的键名(即类型限定名)。
*/
public let schemaName: String
/**
* @brief 创建架构转换器上下文实例。
* @param services 请求作用域的服务提供器。
* @param documentName 当前正在生成的文档名称。
* @param schemaName 当前架构在 components/schemas 中的键名。
*/
public init(services: IServiceProvider, documentName: String, schemaName: String) {
this.services = services
this.documentName = documentName
this.schemaName = schemaName
}
}
@@ -4,7 +4,7 @@
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.infrastructure
package simcu::simapi.openapi
import std.collection.*
+1 -2
View File
@@ -44,8 +44,7 @@ import simcu::simapi.interfaces.*
import simcu::simapi.logger.*
import simcu::simapi.middlewares.*
import simcu::simapi.openapi.*
import simcu::simapi.openapi.annotations.*
import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAnnotation}
import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAnnotation, SimApiDoc}
/**
* SimApi 扩展入口。
+2 -2
View File
@@ -1,11 +1,11 @@
# 生成 SwaggerUIResources.cj:将 resources/openapi/ 下静态资源以 Base64 内联为 CJ 源常量。
# 生成 swagger_ui_resources.cj:将 resources/openapi/ 下静态资源以 Base64 内联为 CJ 源常量。
# 用法:pwsh tools/gen-swagger-ui-resources.ps1
# 资源更新后重新运行此脚本即可。
$ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot
$inDir = Join-Path $root "resources\openapi"
$outFile = Join-Path $root "src\openapi\SwaggerUIResources.cj"
$outFile = Join-Path $root "src\openapi\swagger_ui_resources.cj"
$files = @(
@{ Name = "allHtml"; File = "all.html" },