增加了openapi

This commit is contained in:
2026-08-31 23:47:33 +08:00
parent 49465848a0
commit e0e57a7eb4
67 changed files with 3909 additions and 73 deletions
+8 -8
View File
@@ -1,20 +1,20 @@
version = 0
[requires]
soulsoft_extensions_hosting = {version = "1.0.20260528"}
soulsoft_web_http = {version = "1.0.20260528"}
soulsoft_web_hosting = {version = "1.0.20260528"}
soulsoft_extensions_hosting = {version = "1.0.20260528"}
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
soulsoft_extensions_options_configuration = {version = "1.0.20260528"}
soulsoft_web_hosting = {version = "1.0.20260528"}
soulsoft_extensions_configuration = {version = "1.0.20260528"}
soulsoft_web_routing = {version = "1.0.20260528"}
soulsoft_web_mvc = {version = "1.0.20260528"}
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
soulsoft_identity_claims = {version = "1.0.20260528"}
soulsoft_extensions_injection = {version = "1.0.20260528"}
soulsoft_extensions_options = {version = "1.0.20260528"}
soulsoft_web_cors = {version = "1.0.20260528"}
soulsoft_extensions_logging = {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_configuration = {version = "1.0.20260528"}
soulsoft_identity_claims = {version = "1.0.20260528"}
soulsoft_extensions_logging = {version = "1.0.20260528"}
"simcu::serialization" = {version = "1.2.1"}
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
+4
View File
@@ -7,6 +7,7 @@
package simcu::simapi.communications
import std.collection.*
import simcu::serialization.*
/**
* 登录信息项:Token 认证通过后注入请求上下文。
@@ -16,8 +17,11 @@ import std.collection.*
*/
public class SimApiLoginItem {
public var id: String = ""
@NotRequired
public var types: Array<String> = ["user"]
@NotRequired
public var meta: HashMap<String, String> = HashMap<String, String>()
@NotRequired
public var extra: HashMap<String, Any> = HashMap<String, Any>()
public init() {}
+1 -4
View File
@@ -6,7 +6,6 @@
package simcu::simapi.controllers
import soulsoft_web_mvc.annotations.*
import simcu::simapi.communications.*
import simcu::simapi.helpers.*
@@ -22,10 +21,8 @@ public class SimApiAuthController <: SimApiBaseController {
/**
* POST /auth/logout:退出登录(void 自动封装为 SimApiBaseResponse())。
* 对应路由 [HttpPost]
* 由 MapControllerRoute(pattern=LogoutRoute) 注册。
* 动态注册:路由路径由 SimApiRouteOptions.logoutRoute 决定(见 simapi_extensions.cj)。
*/
@HttpPost["/auth/logout"]
public func logout(): Unit {
if (let Some(token) <- request.headers.get("Token")) {
_auth.logout(token)
@@ -13,6 +13,7 @@ import simcu::simapi.communications.*
import simcu::simapi.helpers.*
import simcu::simapi.interfaces.*
/**
* 基础控制器:所有控制器均继承本控制器。
*/
+4 -9
View File
@@ -11,6 +11,7 @@ import soulsoft_web_mvc.annotations.*
import simcu::simapi.communications.*
import simcu::simapi.configurations.*
import simcu::simapi.helpers.*
import simcu::simapi.openapi.annotations.*
/**
* 通用控制器:错误反馈、WebConfig、用户信息。
@@ -28,30 +29,24 @@ public class SimApiCommonController <: SimApiBaseController {
* 抛 SimApiException,由异常中间件统一输出。
*/
@HttpGet["exception/{code}"]
@SimApiDoc[tags:"公共",summary:"异常报错"]
public func exceptionHandler(@FromRoute[] code: Int64): Unit { // cjlint-ignore !G.FUN.02 注解绑定参数误报
SimApiError.error(code: code)
}
/**
* POST/GET /config:给前端的自定义信息(含版本)。
* 对应路由 [HttpPost, HttpGet] 无路径,
* 由 MapControllerRoute(pattern=WebConfigRoute) 注册;soulsoft 约定路由不支持 defaults,故用特性路由直接对齐路径。
* 动态注册:路由路径由 SimApiRouteOptions.webConfigRoute 决定(见 simapi_extensions.cj)。
*/
@HttpGet["/config"]
public func webConfig(): HashMap<String, Any> {
webConfigMap()
}
@HttpPost["/config"]
public func webConfigPost(): HashMap<String, Any> {
webConfigMap()
}
/**
* POST /user/info:获取已登录用户信息(需登录)。
* 返回 SimApiLoginItem 由 SimApiResponseFilter 自动封装(data 为登录信息对象)。
* 动态注册:路由路径由 SimApiRouteOptions.userInfoRoute 决定(见 simapi_extensions.cj)。
*/
@HttpPost["/user/info"]
public func userInfo(): SimApiLoginItem {
requireLogin()
loginInfo
+9
View File
@@ -0,0 +1,9 @@
/*
* 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.*
@@ -0,0 +1,38 @@
// 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
}
}
@@ -0,0 +1,65 @@
// 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.")
}
}
}
@@ -0,0 +1,71 @@
// 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
}
}
+122
View File
@@ -0,0 +1,122 @@
// 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.io.*
import std.fs.*
import soulsoft_web_http.*
import soulsoft_web_hosting.*
/**
* @brief 提供 OpenAPI UI 静态资源中间件。
*/
public class OpenApiUIMiddleware <: IMiddleware {
private static let _openApiRootPath = PathString("/openapi")
private let _env: IWebHostEnvironment
/**
* @brief 创建 OpenAPI UI 中间件实例。
* @param evn 当前 Web 主机环境。
*/
public init(evn: IWebHostEnvironment) {
_env = evn
}
/**
* @brief 处理 OpenAPI UI 相关请求。
* @param context 当前 HTTP 上下文。
* @param next 下一个请求委托。
*/
public func invoke(context: HttpContext, next: RequestDelegate): Unit {
let remainingPath = if (let Some(path) <- context.request.path.startsWithSegments(
OpenApiUIMiddleware._openApiRootPath
)) {
path
} else {
next(context)
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)
}
} else {
next(context)
}
}
private func resolveAssetPath(remainingPath: PathString): ?Path {
let relativePath = if (remainingPath == "/" || !remainingPath.hasValue) {
"index.html"
} else {
let candidate = remainingPath.value.trimStart('/')
if (!isSafeRelativePath(candidate)) {
return None
}
candidate
}
let openApiRoot = Path(_env.webRootPath).join("openapi")
return openApiRoot.join(relativePath).normalize()
}
private func isSafeRelativePath(relativePath: String): Bool {
if (relativePath.isEmpty() || relativePath.startsWith('/') || relativePath.endsWith('/')) {
return false
}
for (segment in relativePath.split('/')) {
if (segment.isEmpty() || segment == "." || segment == "..") {
return false
}
}
return true
}
private func isPathWithinRoot(path: Path): Bool {
try {
let rootPath = canonicalize(Path(_env.webRootPath).join("openapi"))
let targetPath = canonicalize(path)
let rootPathString = rootPath.toString()
let targetPathString = targetPath.toString()
if (targetPathString == rootPathString) {
return true
}
if (rootPathString == Path.Separator) {
return targetPathString.startsWith(rootPathString)
}
let rootPathPrefix = if (rootPathString.endsWith(Path.Separator)) {
rootPathString
} else {
"${rootPathString}${Path.Separator}"
}
return targetPathString.startsWith(rootPathPrefix)
} catch (_: Exception) {
return false
}
}
}
@@ -0,0 +1,41 @@
// 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.")
}
}
}
+104
View File
@@ -0,0 +1,104 @@
/*
* 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: "用户登录相关接口"]
*/
@Annotation[target: [MemberFunction, Type, MemberProperty, MemberVariable, Parameter]]
public class SimApiDoc <: IApiTagsMetadata & IApiNameMetadata & IApiGroupNameProvider & 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 _description: ?String
/**
* @brief 创建 SimApiDoc 注解实例。
* @param name API 名称。
* @param ignore 是否忽略当前 API。
* @param summary API 摘要。
* @param groupName API 分组名称。
* @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) {
_tags = tags
_name = name
_ignore = ignore
_summary = summary
_groupName = groupName
_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 当前 API 的分组名称。
*/
public prop groupName: ?String {
get() {
_groupName
}
}
/**
* @brief 返回 API 描述信息。
* @return 当前 API 的描述信息。
*/
public prop description: ?String {
get() {
_description
}
}
}
+28
View File
@@ -0,0 +1,28 @@
/*
* 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)
}
}
@@ -0,0 +1,13 @@
/*
* 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"
}
@@ -0,0 +1,109 @@
/*
* 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.*
import std.collection.*
protected class ReflectUtilities {
/**
* 收集给定类型在最终可见视角下的 public 实例属性。
*
* 对 class 类型会沿继承链向上收集,并在子类 override 父类同名属性时,
* 仅保留子类的最终实现;非 class 类型返回空数组。
*/
public static func collectInstanceProperties(typeInfo: TypeInfo): Array<InstancePropertyInfo> {
if (let classTypeInfo: ClassTypeInfo <- typeInfo) {
let result = ArrayList<InstancePropertyInfo>(classTypeInfo.instanceProperties.size)
collectInstanceProperties(classTypeInfo, result)
return result.toArray()
}
return []
}
/**
* 递归收集继承链上的 public 实例属性。
*
* 先处理父类,再处理当前类;如果当前类 override 了父类同名属性,
* 则用当前类属性覆盖父类项,只保留最终可见的那一个。
*/
private static func collectInstanceProperties(classTypeInfo: ClassTypeInfo, result: ArrayList<InstancePropertyInfo>): Unit {
if (let Some(superClass) <- classTypeInfo.superClass && superClass != TypeInfo.of<Object>()) {
if (let superClassTypeInfo: ClassTypeInfo <- superClass) {
collectInstanceProperties(superClassTypeInfo, result)
}
}
for (pattern in classTypeInfo.instanceProperties) {
if (pattern.isAbstract()) {
continue
}
addOrReplaceProperty(result, pattern)
}
}
/**
* 子类同名属性覆盖父类属性,避免 open/override 场景下重复收集。
*/
private static func addOrReplaceProperty(result: ArrayList<InstancePropertyInfo>, propertyInfo: InstancePropertyInfo): Unit {
for ((index, existingProperty) in result |> enumerate) {
if (existingProperty.name == propertyInfo.name) {
result[index] = propertyInfo
return
}
}
result.add(propertyInfo)
}
/**
* 收集给定类型在最终可见视角下的 public 实例变量(var 字段)。
*
* std.reflect 的 instanceProperties 仅收录 prop 属性,而 DTO 常以 public var 字段定义,
* 需通过 instanceVariables 补充收集。对 class 类型会沿继承链向上收集,
* 子类覆盖父类同名变量时仅保留子类实现;非 class 类型返回空数组。
*/
public static func collectInstanceVariables(typeInfo: TypeInfo): Array<InstanceVariableInfo> {
if (let classTypeInfo: ClassTypeInfo <- typeInfo) {
let result = ArrayList<InstanceVariableInfo>(classTypeInfo.instanceVariables.size)
collectInstanceVariables(classTypeInfo, result)
return result.toArray()
}
return []
}
/**
* 递归收集继承链上的 public 实例变量(var 字段)。
*
* 先处理父类,再处理当前类;如果当前类覆盖了父类同名变量,
* 则用当前类变量覆盖父类项,只保留最终可见的那一个。
*/
private static func collectInstanceVariables(classTypeInfo: ClassTypeInfo, result: ArrayList<InstanceVariableInfo>): Unit {
if (let Some(superClass) <- classTypeInfo.superClass && superClass != TypeInfo.of<Object>()) {
if (let superClassTypeInfo: ClassTypeInfo <- superClass) {
collectInstanceVariables(superClassTypeInfo, result)
}
}
for (pattern in classTypeInfo.instanceVariables) {
addOrReplaceVariable(result, pattern)
}
}
/**
* 子类同名变量覆盖父类变量,避免 open/override 场景下重复收集。
*/
private static func addOrReplaceVariable(result: ArrayList<InstanceVariableInfo>, variableInfo: InstanceVariableInfo): Unit {
for ((index, existingVariable) in result |> enumerate) {
if (existingVariable.name == variableInfo.name) {
result[index] = variableInfo
return
}
}
result.add(variableInfo)
}
}
@@ -0,0 +1,95 @@
/*
* 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.collection.*
private let Space = UInt8(32)
private let Tab = UInt8(9)
private let LineFeed = UInt8(10)
private let CarriageReturn = UInt8(13)
private let LessThan = UInt8(60)
private let GreaterThan = UInt8(62)
private let Comma = UInt8(44)
/**
* @brief 提供基于类型限定名的轻量泛型解析能力。
*/
protected class TypeNameParser {
/**
* @brief 返回泛型类型的原始类型名;非泛型时返回自身。
* @param qualifiedName 类型限定名。
* @return 去掉泛型参数后的类型名。
*/
public static func getGenericBaseName(qualifiedName: String): String {
if (let Some(index) <- qualifiedName.indexOf('<')) {
return trimAsciiWhitespace(qualifiedName[0..index])
}
return trimAsciiWhitespace(qualifiedName)
}
/**
* @brief 解析限定名中的顶层泛型参数列表。
* @param qualifiedName 类型限定名,例如 `Map<String, Int64>`。
* @return 顶层泛型参数;若不是合法泛型限定名则返回 `None`。
*/
public static func getGenericArguments(qualifiedName: String): ?Array<String> {
if (!qualifiedName.endsWith('>')) {
return None
}
let start = qualifiedName.indexOf('<') ?? return None
let payload = qualifiedName[start + 1..qualifiedName.size - 1]
let result = ArrayList<String>()
var depth = 0
var segmentStart = 0
var index = 0
while (index < payload.size) {
let ch = payload[index]
if (ch == LessThan) {
depth++
} else if (ch == GreaterThan) {
if (depth == 0) {
return None
}
depth--
} else if (ch == Comma && depth == 0) {
let argument = trimAsciiWhitespace(payload[segmentStart..index])
if (argument.isEmpty()) {
return None
}
result.add(argument)
segmentStart = index + 1
}
index++
}
if (depth != 0) {
return None
}
let argument = trimAsciiWhitespace(payload[segmentStart..payload.size])
if (argument.isEmpty()) {
return None
}
result.add(argument)
return result.toArray()
}
private static func trimAsciiWhitespace(value: String): String {
var start = 0
var end = value.size
while (start < end && isAsciiWhitespace(value[start])) {
start++
}
while (end > start && isAsciiWhitespace(value[end - 1])) {
end--
}
return value[start..end]
}
private static func isAsciiWhitespace(ch: UInt8): Bool {
ch == Space || ch == Tab || ch == LineFeed || ch == CarriageReturn
}
}
@@ -0,0 +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
/**
* @brief 定义 OpenAPI 对象序列化接口。
*/
public interface IOpenApiSerializable {
/**
* @brief 按 OpenAPI V3 格式写出当前对象。
* @param writer OpenAPI 写入器。
*/
func serializeAsV3(writer: IOpenApiWriter): Unit
}
+56
View File
@@ -0,0 +1,56 @@
// 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 IOpenApiWriter {
/**
* @brief 刷新当前写入器缓冲区。
*/
func flush(): Unit
/**
* @brief 开始写入对象。
*/
func startObject(): Unit
/**
* @brief 结束写入对象。
*/
func endObject(): Unit
/**
* @brief 开始写入数组。
*/
func startArray(): Unit
/**
* @brief 结束写入数组。
*/
func endArray(): Unit
/**
* @brief 写入属性名称。
* @param name 要写入的属性名称。
*/
func writeName(name: String): Unit
/**
* @brief 写入整数值。
* @param value 要写入的整数值。
*/
func writeValue(value: Int64): Unit
/**
* @brief 写入布尔值。
* @param value 要写入的布尔值。
*/
func writeValue(value: Bool): Unit
/**
* @brief 写入字符串值。
* @param value 要写入的字符串值。
*/
func writeValue(value: String): Unit
}
@@ -0,0 +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.metadata
/**
* @brief 提供 API 描述元数据。
*/
public interface IApiDescriptionMetadata {
/**
* @brief 返回 API 描述信息。
* @return 当前 API 的描述信息。
*/
prop description: ?String
}
@@ -0,0 +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.metadata
/**
* @brief 提供 API 分组名称元数据。
*/
public interface IApiGroupNameProvider {
/**
* @brief 返回 API 分组名称。
* @return 当前 API 的分组名称。
*/
prop groupName: ?String
}
+21
View File
@@ -0,0 +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.metadata
/**
* @brief 提供 API 名称元数据。
*/
public interface IApiNameMetadata {
/**
* @brief 返回 API 名称。
* @return 当前 API 的名称。
*/
prop name: ?String
}
@@ -0,0 +1,28 @@
// 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
}
@@ -0,0 +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.metadata
/**
* @brief 提供 API 摘要元数据。
*/
public interface IApiSummaryMetadata {
/**
* @brief 返回 API 摘要信息。
* @return 当前 API 的摘要信息。
*/
prop summary: ?String
}
+21
View File
@@ -0,0 +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.metadata
/**
* @brief 提供 API 标签元数据。
*/
public interface IApiTagsMetadata {
/**
* @brief 返回 API 标签字符串。
* @return 当前 API 的标签字符串。
*/
prop tags: ?String
}
@@ -0,0 +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.metadata
/**
* @brief 提供 API 可见性元数据。
*/
public interface IApiVisibilityProvider {
/**
* @brief 返回当前 API 是否应被忽略。
* @return 如果当前 API 需要从 OpenAPI 文档中忽略则返回 `true`。
*/
prop ignore: Bool
}
+9
View File
@@ -0,0 +1,9 @@
/*
* 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.*
+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.models
import std.collection.*
/**
* @brief 表示 OpenAPI 组件集合。
*/
public class OpenApiComponents <: IOpenApiSerializable {
/**
* @brief 表示组件中的架构集合。
*/
public var schemas = HashMap<String, OpenApiSchema>()
/**
* @brief 创建 OpenAPI 组件实例。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前组件集合。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (!schemas.isEmpty()) {
writer.writeName("schemas")
writer.startObject()
for ((key, value) in schemas) {
writer.writeName(key)
value.serializeAsV3(writer)
}
writer.endObject()
}
writer.endObject()
}
}
+57
View File
@@ -0,0 +1,57 @@
// 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 OpenApiContact <: IOpenApiSerializable {
/**
* @brief 表示联系人名称。
*/
public var name: ?String = None
/**
* @brief 表示联系人地址。
*/
public var url: ?String = None
/**
* @brief 表示联系人邮箱。
*/
public var email: ?String = None
/**
* @brief 创建 OpenAPI 联系人实例。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前联系人信息。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(value) <- name) {
writer.writeName("name")
writer.writeValue(value)
}
if (let Some(value) <- url) {
writer.writeName("url")
writer.writeValue(value)
}
if (let Some(value) <- email) {
writer.writeName("email")
writer.writeValue(value)
}
writer.endObject()
}
}
+99
View File
@@ -0,0 +1,99 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 文档对象。
*/
public class OpenApiDocument <: IOpenApiSerializable {
/**
* @brief 表示 OpenAPI 版本号。
*/
public var openapi: String = "3.0.1"
/**
* @brief 表示文档基本信息。
*/
public var info: ?OpenApiInfo = None
/**
* @brief 表示路径集合。
*/
public var paths: ?OpenApiPaths = None
/**
* @brief 表示组件集合。
*/
public var components: ?OpenApiComponents = None
/**
* @brief 表示标签集合。
*/
public var tags = ArrayList<OpenApiTag>()
/**
* @brief 表示服务器集合。
*/
public var servers = ArrayList<OpenApiServer>()
/**
* @brief 表示安全需求集合。
*/
public var securityRequirements = ArrayList<OpenApiSecurityRequirement>()
/**
* @brief 创建 OpenAPI 文档对象。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前文档对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
writer.writeName("openapi")
writer.writeValue(openapi)
if (let Some(value) <- info) {
writer.writeName("info")
value.serializeAsV3(writer)
}
if (let Some(value) <- paths) {
writer.writeName("paths")
value.serializeAsV3(writer)
}
if (let Some(value) <- components) {
writer.writeName("components")
value.serializeAsV3(writer)
}
if (!tags.isEmpty()) {
writer.writeName("tags")
writer.startArray()
for (tag in tags) {
tag.serializeAsV3(writer)
}
writer.endArray()
}
if (!servers.isEmpty()) {
writer.writeName("servers")
writer.startArray()
for (server in servers) {
server.serializeAsV3(writer)
}
writer.endArray()
}
if (!securityRequirements.isEmpty()) {
writer.writeName("security")
writer.startArray()
for (sec in securityRequirements) {
sec.serializeAsV3(writer)
}
writer.endArray()
}
writer.endObject()
}
}
@@ -0,0 +1,101 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 可扩展字典基类。
*/
public abstract class OpenApiExtensibleDictionary<T> <: IOpenApiSerializable where T <: IOpenApiSerializable {
private let _items: HashMap<String, T>
/**
* @brief 创建一个空的 OpenAPI 可扩展字典。
*/
protected init() {
this(HashMap<String, T>())
}
/**
* @brief 使用已有项创建 OpenAPI 可扩展字典。
* @param items 初始字典项集合。
*/
protected init(items: HashMap<String, T>) {
_items = items
}
/**
* @brief 返回内部字典项集合。
* @return 当前字典中的所有项。
*/
protected prop items: HashMap<String, T> {
get() {
_items
}
}
/**
* @brief 添加字典项。
* @param key 字典键。
* @param value 字典值。
*/
public func add(key: String, value: T): Unit {
_items.add(key, value)
}
/**
* @brief 检查是否包含指定键。
* @param key 要检查的字典键。
* @return 如果包含指定键则返回 `true`。
*/
public func contains(key: String): Bool {
_items.contains(key)
}
/**
* @brief 通过键读取字典值。
* @param key 要读取的字典键。
* @return 对应的字典值。
*/
public operator func [](key: String): T {
_items[key]
}
/**
* @brief 通过键设置字典值。
* @param key 要设置的字典键。
* @param value 要设置的字典值。
*/
public operator func [](key: String, value!: T): Unit {
_items[key] = value
}
/**
* @brief 按 OpenAPI V3 格式写出当前字典对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
for ((key, value) in _items) {
writer.writeName(key)
value.serializeAsV3(writer)
}
writer.endObject()
}
/**
* @brief 检查当前字典是否为空。
* @return 如果当前字典没有任何项则返回 `true`。
*/
public func isEmpty(): Bool {
return _items.isEmpty()
}
}
+75
View File
@@ -0,0 +1,75 @@
// 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 OpenApiInfo <: IOpenApiSerializable {
/**
* @brief 表示文档标题。
*/
public var title: ?String = None
/**
* @brief 表示文档描述。
*/
public var description: ?String = None
/**
* @brief 表示文档版本。
*/
public var version: ?String = None
/**
* @brief 表示联系人信息。
*/
public var contact: ?OpenApiContact = None
/**
* @brief 表示许可证信息。
*/
public var license: ?OpenApiLicense = None
/**
* @brief 创建 OpenAPI 基本信息实例。
* @param title 文档标题。
* @param version 文档版本。
*/
public init(title!: String, version!: String) {
this.title = title
this.version = version
}
/**
* @brief 按 OpenAPI V3 格式写出当前基本信息。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(value) <- title) {
writer.writeName("title")
writer.writeValue(value)
}
if (let Some(value) <- description) {
writer.writeName("description")
writer.writeValue(value)
}
if (let Some(value) <- version) {
writer.writeName("version")
writer.writeValue(value)
}
if (let Some(value) <- contact) {
writer.writeName("contact")
value.serializeAsV3(writer)
}
if (let Some(value) <- license) {
writer.writeName("license")
value.serializeAsV3(writer)
}
writer.endObject()
}
}
+47
View File
@@ -0,0 +1,47 @@
// 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 OpenApiLicense <: IOpenApiSerializable {
/**
* @brief 表示许可证名称。
*/
public var name: ?String = None
/**
* @brief 表示许可证地址。
*/
public var url: ?String = None
/**
* @brief 创建 OpenAPI 许可证实例。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前许可证信息。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(value) <- name) {
writer.writeName("name")
writer.writeValue(value)
}
if (let Some(value) <- url) {
writer.writeName("url")
writer.writeValue(value)
}
writer.endObject()
}
}
+41
View File
@@ -0,0 +1,41 @@
// 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 OpenApiMediaType <: IOpenApiSerializable {
/**
* @brief 表示媒体类型对应的架构。
*/
public var schema: ?OpenApiSchema = None
/**
* @brief 创建 OpenAPI 媒体类型实例。
* @param schema 媒体类型对应的架构。
*/
public init(schema: ?OpenApiSchema) {
this.schema = schema
}
/**
* @brief 按 OpenAPI V3 格式写出当前媒体类型对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(value) <- schema) {
writer.writeName("schema")
value.serializeAsV3(writer)
}
writer.endObject()
}
}
+121
View File
@@ -0,0 +1,121 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 操作对象。
*/
public class OpenApiOperation <: IOpenApiSerializable {
/**
* @brief 表示操作摘要。
*/
public var summary: ?String = None
/**
* @brief 表示操作描述。
*/
public var description: ?String = None
/**
* @brief 表示操作标识。
*/
public var operationId: ?String = None
/**
* @brief 表示操作响应集合。
*/
public var response = OpenApiResponses()
/**
* @brief 表示操作标签集合。
*/
public var tags = ArrayList<OpenApiTag>()
/**
* @brief 表示操作服务器集合。
*/
public var servers = ArrayList<OpenApiServer>()
/**
* @brief 表示操作请求体。
*/
public var requestBody:? OpenApiRequestBody = None
/**
* @brief 表示操作参数集合。
*/
public var parameters = ArrayList<OpenApiParameter>()
/**
* @brief 表示操作安全需求集合。
*/
public var security = ArrayList<OpenApiSecurityRequirement>()
/**
* @brief 创建 OpenAPI 操作对象。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前操作对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (!tags.isEmpty()) {
writer.writeName("tags")
writer.startArray()
for (tag in tags) {
tag.serializeAsV3(writer)
}
writer.endArray()
}
if (let Some(value) <- summary) {
writer.writeName("summary")
writer.writeValue(value)
}
if (let Some(value) <- description) {
writer.writeName("description")
writer.writeValue(value)
}
if (let Some(value) <- operationId) {
writer.writeName("operationId")
writer.writeValue(value)
}
if (!servers.isEmpty()) {
writer.writeName("servers")
writer.startArray()
for (server in servers) {
server.serializeAsV3(writer)
}
writer.endArray()
}
if (!parameters.isEmpty()) {
writer.writeName("parameters")
writer.startArray()
for (param in parameters) {
param.serializeAsV3(writer)
}
writer.endArray()
}
if (!security.isEmpty()) {
writer.writeName("security")
writer.startArray()
for (sec in security) {
sec.serializeAsV3(writer)
}
writer.endArray()
}
if (!response.isEmpty()) {
writer.writeName("responses")
response.serializeAsV3(writer)
}
if (let Some(requestBody) <- requestBody) {
writer.writeName("requestBody")
requestBody.serializeAsV3(writer)
}
writer.endObject()
}
}
+92
View File
@@ -0,0 +1,92 @@
// 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 OpenApiParameter <: IOpenApiSerializable {
/**
* @brief 表示参数名称。
*/
public var name: ?String
/**
* @brief 表示参数是否必填。
*/
public var required: ?Bool
/**
* @brief 表示参数是否已弃用。
*/
public var deprecated: ?Bool
/**
* @brief 表示参数描述。
*/
public var description: ?String
/**
* @brief 表示参数架构。
*/
public var schema: ?OpenApiSchema = None
/**
* @brief 表示参数位置。
*/
public var location: ?ParameterLocation = None
/**
* @brief 创建 OpenAPI 参数对象。
* @param name 参数名称。
* @param location 参数位置。
* @param description 参数描述。
* @param required 参数是否必填。
* @param deprecated 参数是否已弃用。
* @param schema 参数架构。
*/
public init(name!: ?String, location!: ?ParameterLocation = None, description!: ?String = None,
required!: ?Bool = None, deprecated!: ?Bool = None, schema!: ?OpenApiSchema = None) {
this.name = name
this.schema = schema
this.location = location
this.required = required
this.deprecated = deprecated
this.description = description
}
/**
* @brief 按 OpenAPI V3 格式写出当前参数对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(value) <- name) {
writer.writeName("name")
writer.writeValue(value)
}
if (let Some(loc) <- location) {
writer.writeName("in")
writer.writeValue(loc.toString())
}
if (let Some(desc) <- description) {
writer.writeName("description")
writer.writeValue(desc)
}
if (let Some(req) <- required) {
writer.writeName("required")
writer.writeValue(req)
}
if (let Some(dep) <- deprecated) {
writer.writeName("deprecated")
writer.writeValue(dep)
}
if (let Some(s) <- schema) {
writer.writeName("schema")
s.serializeAsV3(writer)
}
writer.endObject()
}
}
+95
View File
@@ -0,0 +1,95 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 路径项对象。
*/
public class OpenApiPathItem <: IOpenApiSerializable {
/**
* @brief 表示路径项摘要。
*/
public var summary: ?String = None
/**
* @brief 表示路径项描述。
*/
public var description: ?String = None
/**
* @brief 表示当前路径项是否未解析引用。
*/
public var unresolvedReference: Bool = false
/**
* @brief 表示路径项引用。
*/
public var reference: ?OpenApiReference = None
/**
* @brief 表示路径项服务器集合。
*/
public var servers = ArrayList<OpenApiServer>()
/**
* @brief 表示路径项参数集合。
*/
public var parameters = ArrayList<OpenApiParameter>()
/**
* @brief 表示路径项操作集合。
*/
public var operations = HashMap<OperationType, OpenApiOperation>()
/**
* @brief 创建 OpenAPI 路径项对象。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前路径项对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(value) <- summary) {
writer.writeName("summary")
writer.writeValue(value)
}
if (let Some(value) <- description) {
writer.writeName("description")
writer.writeValue(value)
}
if (unresolvedReference) {
writer.writeName("$ref")
if (let Some(value) <- reference) {
writer.writeValue(value.toReferenceString())
}
}
if (!servers.isEmpty()) {
writer.writeName("servers")
writer.startArray()
for (server in servers) {
server.serializeAsV3(writer)
}
writer.endArray()
}
if (!parameters.isEmpty()) {
writer.writeName("parameters")
writer.startArray()
for (param in parameters) {
param.serializeAsV3(writer)
}
writer.endArray()
}
for ((key, value) in operations) {
writer.writeName(key.toString())
value.serializeAsV3(writer)
}
writer.endObject()
}
}
+29
View File
@@ -0,0 +1,29 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 路径集合。
*/
public class OpenApiPaths <: OpenApiExtensibleDictionary<OpenApiPathItem> {
/**
* @brief 创建 OpenAPI 路径集合实例。
*/
public init() {
}
private init(paths: HashMap<String, OpenApiPathItem>) {
super(paths)
}
}
+60
View File
@@ -0,0 +1,60 @@
// 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 OpenApiReference <: IOpenApiSerializable {
/**
* @brief 表示引用标识。
*/
public var id: String
/**
* @brief 表示引用类型。
*/
public var referenceType: ReferenceType
/**
* @brief 创建 OpenAPI 引用对象。
* @param referenceType 引用类型。
* @param id 引用标识。
*/
public init(referenceType: ReferenceType, id: String) {
this.id = id
this.referenceType = referenceType
}
/**
* @brief 返回当前引用在 OpenAPI 文档中的字符串值。
* @return 引用字符串。
*/
public func toReferenceString(): String {
if (ReferenceType.Tag == referenceType) {
return id
}
return "#/components/${referenceType.toString()}/${id}"
}
/**
* @brief 按 OpenAPI V3 格式写出当前引用对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
if (ReferenceType.Tag == referenceType) {
writer.writeValue(id)
return
}
writer.startObject()
writer.writeName("$ref")
writer.writeValue(toReferenceString())
writer.endObject()
}
}
+66
View File
@@ -0,0 +1,66 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 请求体对象。
*/
public class OpenApiRequestBody <: IOpenApiSerializable {
/**
* @brief 表示请求体是否必填。
*/
public var required: Bool = false
/**
* @brief 表示请求体描述。
*/
public var description: ?String = None
/**
* @brief 表示请求体内容映射。
*/
public var content = HashMap<String, OpenApiMediaType>()
/**
* @brief 创建 OpenAPI 请求体对象。
* @param required 请求体是否必填。
* @param description 请求体描述。
*/
public init(required!: Bool = false, description!: ?String = None) {
this.required = required
this.description = description
}
/**
* @brief 按 OpenAPI V3 格式写出当前请求体对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(desc) <- description) {
writer.writeName("description")
writer.writeValue(desc)
}
if (!content.isEmpty()) {
writer.writeName("content")
writer.startObject()
for ((key, value) in content) {
writer.writeName(key)
value.serializeAsV3(writer)
}
writer.endObject()
}
if (required) {
writer.writeName("required")
writer.writeValue(true)
}
writer.endObject()
}
}
+54
View File
@@ -0,0 +1,54 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 响应对象。
*/
public class OpenApiResponse <: IOpenApiSerializable {
/**
* @brief 表示响应描述。
*/
public var description: String
/**
* @brief 表示响应内容映射。
*/
public var content = HashMap<String, OpenApiMediaType>()
/**
* @brief 创建 OpenAPI 响应对象。
* @param description 响应描述。
*/
public init(description: String) {
this.description = description
}
/**
* @brief 按 OpenAPI V3 格式写出当前响应对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
writer.writeName("description")
writer.writeValue(description)
if (!content.isEmpty()) {
writer.writeName("content")
writer.startObject()
for ((key, value) in content) {
writer.writeName(key)
value.serializeAsV3(writer)
}
writer.endObject()
}
writer.endObject()
}
}
+21
View File
@@ -0,0 +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.models
/**
* @brief 表示 OpenAPI 响应集合。
*/
public class OpenApiResponses <: OpenApiExtensibleDictionary<OpenApiResponse> {
/**
* @brief 创建 OpenAPI 响应集合实例。
*/
public init() {
}
}
+148
View File
@@ -0,0 +1,148 @@
// 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
import std.collection.*
/**
* @brief 表示 OpenAPI 架构对象。
*/
public class OpenApiSchema <: IOpenApiSerializable {
/**
* @brief 表示架构标题。
*/
public var title: ?String = None
/**
* @brief 表示架构是否可为空。
*/
public var nullable: ?Bool = None
/**
* @brief 表示架构类型。
*/
public var `type`: ?String = None
/**
* @brief 表示数组元素架构。
*/
public var items: ?OpenApiSchema = None
/**
* @brief 表示架构格式。
*/
public var format: ?String = None
/**
* @brief 表示架构描述。
*/
public var description: ?String = None
/**
* @brief 表示架构引用。
*/
public var reference: ?OpenApiReference = None
/**
* @brief 表示对象属性集合。
*/
public var properties = HashMap<String, OpenApiSchema>()
/**
* @brief 表示 allOf 组合架构。
*/
public var allOf = ArrayList<OpenApiSchema>()
/**
* @brief 表示是否允许附加属性。
*/
public var additionalProperties: ?Bool = None
/**
* @brief 表示附加属性的值架构。
*/
public var additionalPropertiesSchema: ?OpenApiSchema = None
/**
* @brief 创建 OpenAPI 架构对象。
* @param title 架构标题。
* @param nullable 架构是否可为空。
* @param `type` 架构类型。
* @param items 数组元素架构。
* @param format 架构格式。
* @param reference 架构引用。
* @param description 架构描述。
*/
public init(title!: ?String = None, nullable!: ?Bool = None, `type`!: ?String = None, items!: ?OpenApiSchema = None,
format!: ?String = None, reference!: ?OpenApiReference = None, description!: ?String = None) {
this.title = title
this.format = format
this.`type` = `type`
this.items = items
this.nullable = nullable
this.reference = reference
this.description = description
}
/**
* @brief 按 OpenAPI V3 格式写出当前架构对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
if (let Some(reference) <- reference) {
reference.serializeAsV3(writer)
} else {
serializeAsV3WithoutReference(writer)
}
}
private func serializeAsV3WithoutReference(writer: IOpenApiWriter) {
writer.startObject()
if (let Some(value) <- title) {
writer.writeName("title")
writer.writeValue(value)
}
if (let Some(value) <- `type`) {
writer.writeName("type")
writer.writeValue(value)
}
if (let Some(value) <- items) {
writer.writeName("items")
value.serializeAsV3(writer)
}
if (let Some(value) <- nullable && value) {
writer.writeName("nullable")
writer.writeValue(value)
}
if (let Some(value) <- format) {
writer.writeName("format")
writer.writeValue(value)
}
if (let Some(value) <- description) {
writer.writeName("description")
writer.writeValue(value)
}
if (!properties.isEmpty()) {
writer.writeName("properties")
writer.startObject()
for ((key, value) in properties) {
writer.writeName(key)
value.serializeAsV3(writer)
}
writer.endObject()
}
if (!allOf.isEmpty()) {
writer.writeName("allOf")
writer.startArray()
for (schema in allOf) {
schema.serializeAsV3(writer)
}
writer.endArray()
}
if (let Some(value) <- additionalPropertiesSchema) {
writer.writeName("additionalProperties")
value.serializeAsV3(writer)
} else if (let Some(value) <- additionalProperties) {
writer.writeName("additionalProperties")
writer.writeValue(value)
}
writer.endObject()
}
}
+41
View File
@@ -0,0 +1,41 @@
/*
* 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
import std.reflect.*
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"
/**
* @brief 将类型信息转换为 OpenAPI Schema 类型名称。
* @param typeInfo 要转换的类型信息。
* @return 对应的 OpenAPI Schema 类型名称。
*/
public static func convert(typeInfo: TypeInfo): String {
if (typeInfo.name == "Bool") {
return "boolean"
}
if (typeInfo.name == "String" || typeInfo.name == "DateTime" || typeInfo.name == "BigInt" ||
typeInfo.name == "Rune") {
return "string"
}
if (numbers.contains(typeInfo.name)) {
return "number"
}
if (integers.contains(typeInfo.name)) {
return "integer"
}
return OpenApiSchemaTypes.OBJECT
}
}
@@ -0,0 +1,27 @@
// 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 OpenApiSecurityRequirement <: IOpenApiSerializable {
/**
* @brief 创建 OpenAPI 安全需求对象。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前安全需求对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {}
}
+47
View File
@@ -0,0 +1,47 @@
// 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 OpenApiServer <: IOpenApiSerializable {
/**
* @brief 表示服务器描述。
*/
public var description: ?String = None
/**
* @brief 表示服务器地址。
*/
public var url: ?String = None
/**
* @brief 创建 OpenAPI 服务器对象。
*/
public init() {
}
/**
* @brief 按 OpenAPI V3 格式写出当前服务器对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.startObject()
if (let Some(value) <- url) {
writer.writeName("url")
writer.writeValue(value)
}
if (let Some(value) <- description) {
writer.writeName("description")
writer.writeValue(value)
}
writer.endObject()
}
}
+54
View File
@@ -0,0 +1,54 @@
// 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 OpenApiTag <: IOpenApiSerializable {
/**
* @brief 表示标签名称。
*/
public var name: ?String = None
/**
* @brief 表示标签描述。
*/
public var description: ?String = None
/**
* @brief 表示标签引用。
*/
public var reference: ?OpenApiReference = None
/**
* @brief 创建 OpenAPI 标签对象。
* @param name 标签名称。
* @param reference 标签引用。
* @param description 标签描述。
*/
public init(name!: ?String = None, reference!: ?OpenApiReference = None, description!: ?String = None) {
this.name = name
this.reference = reference
this.description = description
}
/**
* @brief 按 OpenAPI V3 格式写出当前标签对象。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
if (let Some(reference) <- reference) {
reference.serializeAsV3(writer)
return
}
if (let Some(name) <- name) {
writer.writeValue(name)
}
}
}
+127
View File
@@ -0,0 +1,127 @@
// 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
import soulsoft_web_http.*
/**
* @brief 表示 OpenAPI 操作类型。
*/
public enum OperationType <: Equatable<OperationType> & Hashable & IOpenApiSerializable {
/**
* @brief 表示 GET 操作。
*/
Get
/**
* @brief 表示 PUT 操作。
*/
| Put
/**
* @brief 表示 POST 操作。
*/
| Post
/**
* @brief 表示 DELETE 操作。
*/
| Delete
/**
* @brief 表示 OPTIONS 操作。
*/
| Options
/**
* @brief 表示 HEAD 操作。
*/
| Head
/**
* @brief 表示 PATCH 操作。
*/
| Patch
/**
* @brief 表示 TRACE 操作。
*/
| Trace
/**
* @brief 表示未知操作。
*/
| Unkonw
/**
* @brief 比较两个操作类型是否相等。
* @param that 要比较的操作类型。
* @return 如果两个操作类型相等则返回 `true`。
*/
public operator func ==(that: OperationType): Bool {
match ((this, that)) {
case (Get, Get) => true
case (Put, Put) => true
case (Post, Post) => true
case (Delete, Delete) => true
case (Options, Options) => true
case (Head, Head) => true
case (Patch, Patch) => true
case (Trace, Trace) => true
case _ => false
}
}
/**
* @brief 按 OpenAPI V3 格式写出当前操作类型。
* @param writer OpenAPI 写入器。
*/
public func serializeAsV3(writer: IOpenApiWriter): Unit {
writer.writeValue(this.toString())
}
/**
* @brief 返回当前操作类型的哈希值。
* @return 当前操作类型的哈希值。
*/
public func hashCode(): Int64 {
this.toString().hashCode()
}
/**
* @brief 将 HTTP 方法字符串解析为操作类型。
* @param str 要解析的 HTTP 方法字符串。
* @return 解析得到的操作类型。
* @throws IllegalFormatException 当字符串不是支持的 HTTP 方法时抛出。
*/
public static func parse(str: String): OperationType {
match (str) {
case x where HttpMethods.isGet(x) => Get
case x where HttpMethods.isPut(x) => Put
case x where HttpMethods.isPost(x) => Post
case x where HttpMethods.isDelete(x) => Delete
case x where HttpMethods.isOptions(x) => Options
case x where HttpMethods.isHead(x) => Head
case x where HttpMethods.isPatch(x) => Patch
case x where HttpMethods.isTrace(x) => Trace
case _ => throw IllegalFormatException("Invalid OperationType format: ${str}")
}
}
/**
* @brief 返回操作类型对应的名称。
* @return 当前操作类型对应的名称字符串。
*/
public func toString(): String {
match (this) {
case Get => "get"
case Put => "put"
case Post => "post"
case Delete => "delete"
case Options => "options"
case Head => "head"
case Patch => "patch"
case Trace => "trace"
case Unkonw => "*"
}
}
}
+40
View File
@@ -0,0 +1,40 @@
// 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 enum ParameterLocation <: ToString {
/**
* @brief 表示查询字符串参数。
*/
Query
/**
* @brief 表示请求头参数。
*/
| Header
/**
* @brief 表示路径参数。
*/
| Path
/**
* @brief 返回参数位置对应的名称。
* @return 当前参数位置对应的名称字符串。
*/
public func toString(): String {
match (this) {
case Query => "query"
case Header => "header"
case Path => "path"
}
}
}
+54
View File
@@ -0,0 +1,54 @@
// 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 enum ReferenceType <: Equatable<ReferenceType> & ToString {
/**
* @brief 表示标签引用类型。
*/
Tag
/**
* @brief 表示链接引用类型。
*/
| Link
/**
* @brief 表示架构引用类型。
*/
| Schema
/**
* @brief 比较两个引用类型是否相等。
* @param that 要比较的引用类型。
* @return 如果两个引用类型相等则返回 `true`。
*/
public operator func ==(that: ReferenceType): Bool {
match ((this, that)) {
case (Tag, Tag) => true
case (Link, Link) => true
case (Schema, Schema) => true
case _ => false
}
}
/**
* @brief 返回引用类型对应的名称。
* @return 当前引用类型对应的名称字符串。
*/
public func toString(): String {
match (this) {
case Tag => "tags"
case Link => "links"
case Schema => "schemas"
}
}
}
+30
View File
@@ -0,0 +1,30 @@
// 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
}
+96
View File
@@ -0,0 +1,96 @@
// 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.io.*
import stdx.encoding.json.stream.*
import simcu::simapi.openapi.interfaces.*
/**
* @brief 提供基于 JSON 的 OpenAPI 写入器实现。
*/
public class JsonOpenApiWriter <: IOpenApiWriter {
private let _writer: JsonWriter
/**
* @brief 创建 JSON OpenAPI 写入器实例。
* @param stream 输出流。
*/
public init(stream: OutputStream) {
_writer = JsonWriter(stream)
}
/**
* @brief 开始写入对象。
*/
public func startObject(): Unit {
_writer.startObject()
}
/**
* @brief 结束写入对象。
*/
public func endObject(): Unit {
_writer.endObject()
}
/**
* @brief 开始写入数组。
*/
public func startArray(): Unit {
_writer.startArray()
}
/**
* @brief 结束写入数组。
*/
public func endArray(): Unit {
_writer.endArray()
}
/**
* @brief 写入属性名称。
* @param name 要写入的属性名称。
*/
public func writeName(name: String): Unit {
_writer.writeName(name)
}
/**
* @brief 写入字符串值。
* @param value 要写入的字符串值。
*/
public func writeValue(value: String): Unit {
_writer.writeValue(value)
}
/**
* @brief 写入整数值。
* @param value 要写入的整数值。
*/
public func writeValue(value: Int64): Unit {
_writer.writeValue(value)
}
/**
* @brief 写入布尔值。
* @param value 要写入的布尔值。
*/
public func writeValue(value: Bool): Unit {
_writer.writeValue(value)
}
/**
* @brief 刷新当前写入器。
*/
public func flush(): Unit {
_writer.flush()
}
}
+28
View File
@@ -0,0 +1,28 @@
// 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
}
}
@@ -0,0 +1,67 @@
// 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.collection.*
import simcu::simapi.openapi.models.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.transformers.*
/**
* @brief 提供 OpenAPI 文档创建能力。
*/
protected class OpenApiDocumentProvider <: IDocumentProvider {
private let _services: IServiceProvider
private let _options: IOptionsMonitor<OpenApiOptions>
/**
* @brief 创建 OpenAPI 文档提供器实例。
* @param services 服务提供器。
* @param options OpenAPI 选项监视器。
* @param provider OpenAPI 文档生成服务。
*/
public init(services: IServiceProvider, options: IOptionsMonitor<OpenApiOptions>) {
_services = services
_options = options
}
/**
* @brief 返回已注册的文档名称集合。
* @return 当前可用的文档名称集合。
*/
public func getDooucmentNames(): Collection<String> {
_services.getAll<NamedService>() |> map {f => f.name} |> collectArray
}
/**
* @brief 按名称创建 OpenAPI 文档并依次应用所有转换器。
* @param documentName 要创建的文档名称。
* @param services 请求作用域的服务提供器,传递给操作转换器上下文。
* @return 经过转换器处理后的 OpenAPI 文档。
*/
public func create(documentName: String, services: IServiceProvider): OpenApiDocument {
let options = _options.get(documentName)
let schemaService = OpenApiSchemaService(options, services, documentName)
let parameterService = OpenApiParameterService(schemaService)
let provider = ActivatorUtilities.createInstance<OpenApiDocumentService>(
services,
documentName,
schemaService,
parameterService
)
let document = provider.getOpenApiDocument(services)
let context = OpenApiDocumentTransformerContext(services, documentName)
for (transformer in options.documentTransformers) {
transformer.transform(document, context)
}
return document
}
}
@@ -0,0 +1,285 @@
// 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 soulsoft_web_mvc.*
import soulsoft_web_http.*
import soulsoft_web_routing.http.*
import soulsoft_web_mvc.utilities.*
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.*
/**
* @brief 提供 OpenAPI 文档生成功能。
*/
protected class OpenApiDocumentService {
private let _documentName: String
private let _endpointSource: EndpointDataSource
private let _openApiSchemaService: OpenApiSchemaService
private let _openApiParameterService: OpenApiParameterService
/**
* @brief 创建 OpenAPI 文档生成服务实例。
* @param endpointSource 终结点数据源。
* @param openApiSchemaService OpenAPI Schema 服务。
* @param openApiParameterService OpenAPI 参数服务。
* @param documentName 当前正在生成的文档名称。
*/
public init(
documentName: String,
endpointSource: EndpointDataSource,
openApiSchemaService: OpenApiSchemaService,
openApiParameterService: OpenApiParameterService
) {
_documentName = documentName
_endpointSource = endpointSource
_openApiSchemaService = openApiSchemaService
_openApiParameterService = openApiParameterService
}
/**
* @brief 生成 OpenAPI 文档。
* @param services 服务提供器,用于构建操作转换器上下文。
* @return 生成后的 OpenAPI 文档。
*/
public func getOpenApiDocument(services: IServiceProvider): OpenApiDocument {
let document = OpenApiDocument()
document.info = OpenApiInfo(title: "OpenApi | ${_documentName}", version: "1.0.0")
document.paths = createOpenApiPaths(services)
document.components = createOpenApiComponents()
return document
}
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)) {
continue
}
// 构建用于显示的url路径
let key = if (let Some(displayText) <- endpoint.routePattern.displayText) {
"/${displayText.trimStart('/')}"
} else {
"/${endpoint.routePattern.rawText.trimStart('/')}"
}
let pathItem = if (paths.contains(key)) {
paths[key]
} else {
let item = OpenApiPathItem()
paths[key] = item
item
}
// add operations
if (let Some(metadata) <- endpoint.metadata.getMetadata<IHttpMethodMetadata>()) {
for (httpMethod in metadata.httpMethods) {
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)
}
}
}
return paths
}
private func isShouldInclude(endpoint: RouteEndpoint) {
let groupNames = endpoint.metadata.getOrderedMetadata<IApiGroupNameProvider>() |> filterMap {f => f.groupName} |>
collectArray
if (groupNames.isEmpty() || groupNames.contains(_documentName)) {
return true
}
return false
}
private func createOpenApiComponents() {
let components = OpenApiComponents()
let schemas = _openApiSchemaService.getSchemas()
components.schemas = schemas
return components
}
/*
生成每个终结点的文档
*/
private func createOpenApiOperation(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor): OpenApiOperation {
let operation = OpenApiOperation()
// tags
operation.tags.add(all: createOpenApiOperationTags(endpoint, actionDescriptor))
// parameters
if (let Some(actionDescriptor) <- actionDescriptor) {
operation.parameters = _openApiParameterService.createParameters(actionDescriptor)
}
// requestBody
if (let Some(actionDescriptor) <- actionDescriptor) {
operation.requestBody = createOpenApiOperationRequestBody(endpoint, actionDescriptor)
}
// response
operation.response = createOpenApiOperationResponses(actionDescriptor)
// summary
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiSummaryMetadata> {f => f.summary.isSome()}) {
if (let Some(summary) <- metadata.summary) {
operation.summary = summary
}
}
// description
if (let Some(metadata) <- endpoint
.metadata
.getLastMetadata<IApiDescriptionMetadata> {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()}) {
operation.operationId = metadata.name
}
return operation
}
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(tags) <- metadata.tags) {
for (name in tags.split(',')) {
result.add(OpenApiTag(name: name))
}
}
}
if (!result.isEmpty()) {
return result
}
if (let Some(actionDescriptor) <- actionDescriptor) {
result.add(OpenApiTag(name: actionDescriptor.controllerName))
} else {
result.add(OpenApiTag(name: OpenApiConstants.DefaultOpenApiName))
}
return result
}
/*
来自请求体的参数
*/
private func createOpenApiOperationRequestBody(endpoint: RouteEndpoint, actionDescriptor: ControllerActionDescriptor): ?OpenApiRequestBody {
var requestBody = OpenApiRequestBody(required: true)
let parameters = HashMap<String, OpenApiSchema>()
for (parameter in actionDescriptor.actionFunction.parameters) {
let typeInfo = Nullable.getUnderlyingType(parameter.typeInfo) ?? parameter.typeInfo
let hasBindingSourceMetadata = parameter.annotations |> any {f => f is IBindingSourceMetadata}
if (parameter.findAnnotation<FromBody>().isSome() || !hasBindingSourceMetadata) {
let schema = _openApiSchemaService.createSchema(typeInfo)
requestBody.content.add("application/json", OpenApiMediaType(schema))
} else if (parameter.findAnnotation<FromForm>().isSome()) {
if (let classTypeInfo: ClassTypeInfo <- typeInfo) {
let schema = _openApiSchemaService.createInlineSchema(typeInfo)
for ((name, property) in schema.properties) {
parameters.add(name, property)
}
} else {
parameters.add(BindingNameUtilities.resolveFormName(parameter), _openApiSchemaService.createSchema(typeInfo))
}
}
}
if (parameters.size > 0) {
let formSchema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT)
for ((name, schema) in parameters) {
formSchema.properties.add(name, schema)
}
requestBody.content.add("multipart/form-data", OpenApiMediaType(formSchema))
}
if (requestBody.content.size > 0) {
return requestBody
}
return None
}
/*
生成响应描述
*/
private func createOpenApiOperationResponses(actionDescriptor: ?ControllerActionDescriptor) {
let responses = OpenApiResponses()
// 获取action的返回类型
let returnType: ?TypeInfo = if (let Some(actionDescriptor) <- actionDescriptor) {
Nullable.getUnderlyingType(actionDescriptor.actionFunction.returnType) ?? actionDescriptor
.actionFunction
.returnType
} else {
None
}
if (let Some(returnType) <- returnType && returnType != TypeInfo.of<Unit>()) {
let response = OpenApiResponse("OK")
let schema = _openApiSchemaService.createSchema(returnType)
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"))
}
return responses
}
// api ignore
private func isIgnore(endpoint: RouteEndpoint) {
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiVisibilityProvider> {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
}
}
+123
View File
@@ -0,0 +1,123 @@
// 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>()
/**
* @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))
}
}
@@ -0,0 +1,106 @@
/*
* 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 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 参数生成能力。
*/
protected class OpenApiParameterService {
private let _openApiSchemaService: OpenApiSchemaService
public init(openApiSchemaService: OpenApiSchemaService) {
_openApiSchemaService = openApiSchemaService
}
/**
* @brief 为 action 参数生成 OpenAPI parameters。
* @param actionDescriptor 控制器 action 描述。
* @return operation 对应的 parameters。
*/
public func createParameters(actionDescriptor: ControllerActionDescriptor): ArrayList<OpenApiParameter> {
let parameters = ArrayList<OpenApiParameter>()
for (parameter in actionDescriptor.actionFunction.parameters) {
if (let Some(location) <- getParameterLocation(parameter)) {
parameters.add(all: createParameters(parameter, location))
}
}
return parameters
}
private func createParameters(parameter: ParameterInfo, location: ParameterLocation): Array<OpenApiParameter> {
let underlyingType = Nullable.getUnderlyingType(parameter.typeInfo)
let isNullable = underlyingType.isSome()
let typeInfo = underlyingType ?? parameter.typeInfo
if (let classTypeInfo: ClassTypeInfo <- typeInfo) {
let objectSchema = _openApiSchemaService.createInlineSchema(typeInfo)
let parameters = ArrayList<OpenApiParameter>(objectSchema.properties.size)
for ((name, propertySchema) in objectSchema.properties) {
let isPropertyNullable = propertySchema.nullable ?? false
parameters.add(
OpenApiParameter(
name: name,
schema: propertySchema,
required: isRequiredParameter(location, isPropertyNullable),
location: location
)
)
}
return parameters.toArray()
}
let schema = _openApiSchemaService.createSchema(typeInfo)
if (isNullable) {
schema.nullable = true
}
return [
OpenApiParameter(
name: resolveParameterName(parameter, location),
schema: schema,
required: isRequiredParameter(location, isNullable),
location: location
)
]
}
private func getParameterLocation(parameter: ParameterInfo): ?ParameterLocation {
if (parameter.findAnnotation<FromQuery>().isSome()) {
return ParameterLocation.Query
}
if (parameter.findAnnotation<FromRoute>().isSome()) {
return ParameterLocation.Path
}
if (parameter.findAnnotation<FromHeader>().isSome()) {
return ParameterLocation.Header
}
return None
}
private func resolveParameterName(parameter: ParameterInfo, location: ParameterLocation): String {
match (location) {
case ParameterLocation.Header => BindingNameUtilities.resolveHeaderName(parameter)
case ParameterLocation.Path => BindingNameUtilities.resolveRouteName(parameter)
case ParameterLocation.Query => BindingNameUtilities.resolveQueryName(parameter)
case _ => parameter.name
}
}
private func isRequiredParameter(location: ParameterLocation, isNullable: Bool): Bool {
match (location) {
case ParameterLocation.Path => true
case _ => !isNullable
}
}
}
@@ -0,0 +1,335 @@
// 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.time.*
import std.reflect.*
import std.collection.*
import std.math.numeric.*
import stdx.encoding.json.*
import soulsoft_web_http.*
import simcu::simapi.openapi.models.*
import soulsoft_web_mvc.IActionResult
import simcu::simapi.openapi.metadata.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.transformers.*
import simcu::simapi.openapi.infrastructure.*
/**
* @brief 提供 OpenAPI Schema 生成功能。
*/
protected class OpenApiSchemaService {
private static let _primitiveTypes = HashSet<TypeInfo>(
[
TypeInfo.of<Rune>(),
TypeInfo.of<Bool>(),
TypeInfo.of<Int8>(),
TypeInfo.of<UInt8>(),
TypeInfo.of<Int16>(),
TypeInfo.of<UInt16>(),
TypeInfo.of<Int32>(),
TypeInfo.of<UInt32>(),
TypeInfo.of<Int64>(),
TypeInfo.of<UInt64>(),
TypeInfo.of<Float16>(),
TypeInfo.of<Float32>(),
TypeInfo.of<Float64>(),
TypeInfo.of<Any>(),
TypeInfo.of<Object>(),
TypeInfo.of<BigInt>(),
TypeInfo.of<String>(),
TypeInfo.of<Decimal>(),
TypeInfo.of<DateTime>()
]
)
private static let _stringType = TypeInfo.of<String>()
private static let _formFileType = TypeInfo.of<IFormFile>()
private static let _jsonValueType = TypeInfo.of<JsonValue>()
private static let _actionResultType = TypeInfo.of<IActionResult>()
private let _documentName: String
private let _options: OpenApiOptions
private let _services: IServiceProvider
private let _schemas = HashMap<String, OpenApiSchema>()
/**
* @brief 创建 OpenAPI Schema 服务实例。
* @param options 当前文档的 OpenAPI 选项。
* @param services 请求作用域的服务提供器。
*/
public init(options: OpenApiOptions, services: IServiceProvider, documentName: String) {
_options = options
_services = services
_documentName = documentName
}
/**
* @brief 返回当前已缓存的 Schema 集合。
* @return 以类型限定名为键的 Schema 集合。
*/
public func getSchemas(): HashMap<String, OpenApiSchema> {
return _schemas
}
/**
* @brief 为指定类型创建 OpenAPI Schema。
* @param typeInfo 要转换的类型信息。
* @return 对应的 OpenAPI Schema。
*/
public func createSchema(typeInfo: TypeInfo): OpenApiSchema {
return buildSchema(typeInfo, false, true)
}
/**
* @brief 为指定类型创建内联 OpenAPI Schema。
* @param typeInfo 要转换的类型信息。
* @return 对应的 OpenAPI Schema。
*/
public func createInlineSchema(typeInfo: TypeInfo): OpenApiSchema {
return buildSchema(typeInfo, false, false)
}
/*
生成类型的 Schema。
preferReference=true 时,复杂对象优先输出为组件引用。
*/
private func buildSchema(typeInfo: TypeInfo, nullable: Bool, preferReference: Bool): OpenApiSchema {
// 处理基本类型
if (OpenApiSchemaService._primitiveTypes.contains(typeInfo)) {
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
}
// 处理字典类型
if (let Some((keyTypeInfo, valueTypeInfo)) <- resolveMapTypeArguments(typeInfo)) {
return createDictionarySchema(keyTypeInfo, valueTypeInfo)
}
// 处理集合类型
if (let Some(elementTypeInfo) <- resolveCollectionElementType(typeInfo)) {
return createArraySchema(elementTypeInfo, preferReference)
}
// 处理泛型:IActionResult 派生类型解构一层,将泛型参数当作普通类型处理
if (typeInfo.isSubtypeOf(OpenApiSchemaService._actionResultType)) {
if (let Some(resultValueType) <- resolveSingleGenericArgumentType(typeInfo)) {
return buildSchema(resultValueType, false, preferReference)
}
}
if (let _: EnumTypeInfo <- typeInfo) {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING, nullable: nullable)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema
}
// 处理复合类型
if (typeInfo is ClassTypeInfo || typeInfo is StructTypeInfo) {
if (preferReference) {
return createReferencedObjectSchema(typeInfo, nullable)
}
return createInlineObjectSchema(typeInfo, nullable)
}
return OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT, nullable: nullable)
}
/*
生成对象的 Schema,引用 ID 由 createSchemaReferenceId 委托决定;
返回 None 时强制内联,不生成 $ref
*/
private func createReferencedObjectSchema(typeInfo: TypeInfo, nullable: Bool): OpenApiSchema {
let schemaId = _options.createSchemaReferenceId(typeInfo)
if (let Some(schemaId) <- schemaId) {
if (_schemas.get(schemaId).isNone()) {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT)
schema.additionalProperties = false
// 先加入缓存,再处理属性,避免循环引用导致无限递归
_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
}
}
private func createReferenceSchema(schemaId: String, nullable: Bool): OpenApiSchema {
let reference = OpenApiReference(ReferenceType.Schema, schemaId)
if (!nullable) {
return OpenApiSchema(reference: reference)
}
let schema = OpenApiSchema(nullable: true)
schema.allOf.add(OpenApiSchema(reference: reference))
return schema
}
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
}
private func buildObjectProperties(schema: OpenApiSchema, typeInfo: TypeInfo): Unit {
// prop 属性
let properties = getObjectProperties(typeInfo)
for (property in properties) {
let underlyingType = Nullable.getUnderlyingType(property.typeInfo)
let propertyTypeInfo = underlyingType ?? property.typeInfo
let propertySchema = buildSchema(propertyTypeInfo, underlyingType.isSome(), true)
for (annotation in property.annotations) {
if (let meta: IApiDescriptionMetadata <- annotation) {
propertySchema.description = meta.description
}
}
schema.properties.add(property.name, propertySchema)
}
// var 字段(DTO 常以 public var 定义,instanceProperties 不收录,需经 instanceVariables 补充)
for (variable in ReflectUtilities.collectInstanceVariables(typeInfo)) {
if (schema.properties.contains(variable.name)) {
continue
}
let underlyingType = Nullable.getUnderlyingType(variable.typeInfo)
let variableTypeInfo = underlyingType ?? variable.typeInfo
let variableSchema = buildSchema(variableTypeInfo, underlyingType.isSome(), true)
for (annotation in variable.annotations) {
if (let meta: IApiDescriptionMetadata <- annotation) {
variableSchema.description = meta.description
}
}
schema.properties.add(variable.name, variableSchema)
}
}
private func getObjectProperties(typeInfo: TypeInfo): Array<InstancePropertyInfo> {
if (typeInfo is ClassTypeInfo) {
return ReflectUtilities.collectInstanceProperties(typeInfo)
}
return typeInfo.instanceProperties.toArray()
}
/*
生成数组的 Schema
*/
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
}
/*
生成字典的 Schema。
OpenAPI 3.0 的 dictionary 仅能稳定映射到 string key 的 JSON object。
*/
private func createDictionarySchema(keyTypeInfo: TypeInfo, valueTypeInfo: TypeInfo): OpenApiSchema {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT)
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
*/
private func resolveCollectionElementType(typeInfo: TypeInfo): ?TypeInfo {
for (pattern in typeInfo.superInterfaces) {
let interfaceName = pattern.qualifiedName
let baseName = TypeNameParser.getGenericBaseName(interfaceName)
if (baseName != "Collection" && baseName != "std.collection.Collection") {
continue
}
let genericArguments = TypeNameParser.getGenericArguments(interfaceName) ?? continue
if (genericArguments.size == 1) {
return TypeInfo.get(genericArguments[0])
}
}
return None
}
private func resolveMapTypeArguments(typeInfo: TypeInfo): ?(TypeInfo, TypeInfo) {
for (pattern in typeInfo.superInterfaces) {
let interfaceName = pattern.qualifiedName
let baseName = TypeNameParser.getGenericBaseName(interfaceName)
if (!baseName.startsWith("std.collection.Map") && baseName != "Map") {
continue
}
let genericArguments = TypeNameParser.getGenericArguments(interfaceName) ?? continue
if (genericArguments.size == 2) {
return (TypeInfo.get(genericArguments[0]), TypeInfo.get(genericArguments[1]))
}
}
return None
}
/*
解析 ActionResult<T> 中的 T
*/
private func resolveSingleGenericArgumentType(typeInfo: TypeInfo): ?TypeInfo {
let genericArguments = TypeNameParser.getGenericArguments(typeInfo.qualifiedName) ?? return None
if (genericArguments.size == 1) {
return TypeInfo.get(genericArguments[0])
}
return None
}
private static func convertToSchemaFormat(typeInfo: TypeInfo): ?String {
match (typeInfo.name) {
case "Int32" => "int32"
case "Int64" => "int64"
case "DateTime" => "date-time"
case "Float32" => "float"
case "Float64" => "double"
case _ => None
}
}
}
@@ -0,0 +1,36 @@
// 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)
}
}
@@ -0,0 +1,24 @@
// 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)
}
}
@@ -0,0 +1,36 @@
// 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)
}
}
@@ -0,0 +1,24 @@
// 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
}
@@ -0,0 +1,24 @@
// 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
}
@@ -0,0 +1,24 @@
// 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
}
@@ -0,0 +1,36 @@
// 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
}
}
@@ -0,0 +1,43 @@
// 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
}
}
@@ -0,0 +1,42 @@
// 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
}
}
+55 -52
View File
@@ -42,6 +42,8 @@ import simcu::simapi.helpers.*
import simcu::simapi.interfaces.*
import simcu::simapi.logger.*
import simcu::simapi.middlewares.*
import simcu::simapi.openapi.*
import simcu::simapi.openapi.annotations.*
/**
* SimApi 扩展入口。
@@ -160,69 +162,58 @@ public class SimApiExtensions {
host.use<SimApiAuthMiddleware>()
}
// 内置路由:RouteOptions 自定义路径时真实注册(默认路径已由内置控制器特性路由覆盖,
// 因 soulsoft 无约定路由 defaults,用 mapGet/mapPost 委托实现)
// 内置路由:全部动态注册(路径由 SimApiRouteOptions 决定,不写死在控制器特性注解上)
let routeOptions = options.simApiRouteOptions
if (let Some(route) <- routeOptions.userInfoRoute) {
if (route != "/user/info") {
host.mapPost(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiCommonController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiCommonController <- controller) {
SimApiResultWriter.write(context, c.userInfo())
}
})
}
host.mapPost(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiCommonController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiCommonController <- controller) {
SimApiResultWriter.write(context, c.userInfo())
}
}).withOpenApi(SimApiDoc(tags: "认证", summary: "获取登录用户信息"))
logger.info("注册内置Route: UserInfo => ${route}")
}
if (let Some(route) <- routeOptions.logoutRoute) {
if (route != "/auth/logout") {
host.mapPost(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiAuthController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiAuthController <- controller) {
SimApiResultWriter.write(context, c.logout())
}
})
}
host.mapPost(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiAuthController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiAuthController <- controller) {
SimApiResultWriter.write(context, c.logout())
}
}).withOpenApi(SimApiDoc(tags: "认证", summary: "退出登录"))
logger.info("注册内置Route: Logout => ${route}")
}
if (let Some(route) <- routeOptions.webConfigRoute) {
if (route != "/config") {
host.mapGet(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiCommonController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiCommonController <- controller) {
SimApiResultWriter.write(context, c.webConfig())
}
})
host.mapPost(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiCommonController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiCommonController <- controller) {
SimApiResultWriter.write(context, c.webConfigPost())
}
})
}
host.mapGet(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiCommonController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiCommonController <- controller) {
SimApiResultWriter.write(context, c.webConfig())
}
}).withOpenApi(SimApiDoc(tags: "公共", summary: "获取公共系统配置"))
host.mapPost(route, { context =>
let controller = ActivatorUtilities.createInstance(context.services,
TypeInfo.of<SimApiCommonController>())
if (let c: SimApiBaseController <- controller) {
c.bindRequestContext(context)
}
if (let c: SimApiCommonController <- controller) {
SimApiResultWriter.write(context, c.webConfig())
}
}).withOpenApi(SimApiDoc(tags: "公共", summary: "获取公共系统配置"))
logger.info("注册内置Route: WebConfig => ${route}")
}
// SimApiDoc(占位:soulsoft 暂无内置 Swagger 文档页)
if (options.enableSimApiDoc) {
logger.info("开始配置 SimApiDoc...")
}
// 请求日志中间件
if (options.enableRequestLog) {
@@ -257,6 +248,13 @@ public class SimApiExtensions {
if (callSiteFactory.isService<ApplicationPartManager>()) {
host.mapControllers()
}
// SimApiDocOpenAPI 文档 JSON 路由 + Swagger UI 静态资源,最内层挂载)
if (options.enableSimApiDoc) {
logger.info("开始配置 SimApiDoc...")
host.mapOpenApi()
host.useOpenApiUI()
}
}
// ===== 私有辅助 =====
@@ -279,6 +277,11 @@ public class SimApiExtensions {
// 中间件无需注册:挂载时由 ActivatorUtilities 从 DI 解析构造参数创建
// API 文档(OpenAPI
if (options.enableSimApiDoc) {
builder.services.addOpenApi()
}
// 认证(DI 自动注入 SimApiOptions
if (options.enableSimApiAuth) {
builder.services.addSingleton<SimApiAuth, SimApiAuth>()