diff --git a/cjpm.lock b/cjpm.lock index f4470f8..b2c270f 100644 --- a/cjpm.lock +++ b/cjpm.lock @@ -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"} diff --git a/src/communications/simapi_login_item.cj b/src/communications/simapi_login_item.cj index dc95b0d..9e33897 100644 --- a/src/communications/simapi_login_item.cj +++ b/src/communications/simapi_login_item.cj @@ -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 = ["user"] + @NotRequired public var meta: HashMap = HashMap() + @NotRequired public var extra: HashMap = HashMap() public init() {} diff --git a/src/controllers/simapi_auth_controller.cj b/src/controllers/simapi_auth_controller.cj index 05556cc..5765b26 100644 --- a/src/controllers/simapi_auth_controller.cj +++ b/src/controllers/simapi_auth_controller.cj @@ -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) diff --git a/src/controllers/simapi_base_controller.cj b/src/controllers/simapi_base_controller.cj index fef9a9b..172628a 100644 --- a/src/controllers/simapi_base_controller.cj +++ b/src/controllers/simapi_base_controller.cj @@ -13,6 +13,7 @@ import simcu::simapi.communications.* import simcu::simapi.helpers.* import simcu::simapi.interfaces.* + /** * 基础控制器:所有控制器均继承本控制器。 */ diff --git a/src/controllers/simapi_common_controller.cj b/src/controllers/simapi_common_controller.cj index aa37f58..417041a 100644 --- a/src/controllers/simapi_common_controller.cj +++ b/src/controllers/simapi_common_controller.cj @@ -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 { webConfigMap() } - @HttpPost["/config"] - public func webConfigPost(): HashMap { - webConfigMap() - } /** * POST /user/info:获取已登录用户信息(需登录)。 - * 返回 SimApiLoginItem 由 SimApiResponseFilter 自动封装(data 为登录信息对象)。 + * 动态注册:路由路径由 SimApiRouteOptions.userInfoRoute 决定(见 simapi_extensions.cj)。 */ - @HttpPost["/user/info"] public func userInfo(): SimApiLoginItem { requireLogin() loginInfo diff --git a/src/openapi/Modules.cj b/src/openapi/Modules.cj new file mode 100644 index 0000000..0218bc3 --- /dev/null +++ b/src/openapi/Modules.cj @@ -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.* \ No newline at end of file diff --git a/src/openapi/OpenApiEndpointConventionBuilder.cj b/src/openapi/OpenApiEndpointConventionBuilder.cj new file mode 100644 index 0000000..68e97b3 --- /dev/null +++ b/src/openapi/OpenApiEndpointConventionBuilder.cj @@ -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 + } +} diff --git a/src/openapi/OpenApiEndpointRouteBuilderExtension.cj b/src/openapi/OpenApiEndpointRouteBuilderExtension.cj new file mode 100644 index 0000000..3d53473 --- /dev/null +++ b/src/openapi/OpenApiEndpointRouteBuilderExtension.cj @@ -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() + 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() + if (!callSiteFactory.isService()) { + 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.") + } + } +} diff --git a/src/openapi/OpenApiServiceCollectionExtensions.cj b/src/openapi/OpenApiServiceCollectionExtensions.cj new file mode 100644 index 0000000..87823c0 --- /dev/null +++ b/src/openapi/OpenApiServiceCollectionExtensions.cj @@ -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(documentName, configureOptions) + this.addSingleton(NamedService(documentName)) + this.addSingleton() + return this + } +} diff --git a/src/openapi/OpenApiUIMiddleware.cj b/src/openapi/OpenApiUIMiddleware.cj new file mode 100644 index 0000000..2d30591 --- /dev/null +++ b/src/openapi/OpenApiUIMiddleware.cj @@ -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 + } + } +} diff --git a/src/openapi/OpenApiUIMiddlewareExtensions.cj b/src/openapi/OpenApiUIMiddlewareExtensions.cj new file mode 100644 index 0000000..736313c --- /dev/null +++ b/src/openapi/OpenApiUIMiddlewareExtensions.cj @@ -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() + } + + private func verifyOpenApiServicesAreRegistered() { + let callSiteFactory = this.services.getOrThrow() + if (!callSiteFactory.isService()) { + 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.") + } + } +} diff --git a/src/openapi/annotations/simapi_doc.cj b/src/openapi/annotations/simapi_doc.cj new file mode 100644 index 0000000..f73d1f5 --- /dev/null +++ b/src/openapi/annotations/simapi_doc.cj @@ -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 + } + } +} diff --git a/src/openapi/infrastructure/Nullable.cj b/src/openapi/infrastructure/Nullable.cj new file mode 100644 index 0000000..264e922 --- /dev/null +++ b/src/openapi/infrastructure/Nullable.cj @@ -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) + } +} diff --git a/src/openapi/infrastructure/OpenApiConstants.cj b/src/openapi/infrastructure/OpenApiConstants.cj new file mode 100644 index 0000000..184a44c --- /dev/null +++ b/src/openapi/infrastructure/OpenApiConstants.cj @@ -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" +} \ No newline at end of file diff --git a/src/openapi/infrastructure/ReflectUtilities.cj b/src/openapi/infrastructure/ReflectUtilities.cj new file mode 100644 index 0000000..297b8c9 --- /dev/null +++ b/src/openapi/infrastructure/ReflectUtilities.cj @@ -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 { + if (let classTypeInfo: ClassTypeInfo <- typeInfo) { + let result = ArrayList(classTypeInfo.instanceProperties.size) + collectInstanceProperties(classTypeInfo, result) + return result.toArray() + } + return [] + } + + /** + * 递归收集继承链上的 public 实例属性。 + * + * 先处理父类,再处理当前类;如果当前类 override 了父类同名属性, + * 则用当前类属性覆盖父类项,只保留最终可见的那一个。 + */ + private static func collectInstanceProperties(classTypeInfo: ClassTypeInfo, result: ArrayList): Unit { + if (let Some(superClass) <- classTypeInfo.superClass && superClass != TypeInfo.of()) { + 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, 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 { + if (let classTypeInfo: ClassTypeInfo <- typeInfo) { + let result = ArrayList(classTypeInfo.instanceVariables.size) + collectInstanceVariables(classTypeInfo, result) + return result.toArray() + } + return [] + } + + /** + * 递归收集继承链上的 public 实例变量(var 字段)。 + * + * 先处理父类,再处理当前类;如果当前类覆盖了父类同名变量, + * 则用当前类变量覆盖父类项,只保留最终可见的那一个。 + */ + private static func collectInstanceVariables(classTypeInfo: ClassTypeInfo, result: ArrayList): Unit { + if (let Some(superClass) <- classTypeInfo.superClass && superClass != TypeInfo.of()) { + if (let superClassTypeInfo: ClassTypeInfo <- superClass) { + collectInstanceVariables(superClassTypeInfo, result) + } + } + + for (pattern in classTypeInfo.instanceVariables) { + addOrReplaceVariable(result, pattern) + } + } + + /** + * 子类同名变量覆盖父类变量,避免 open/override 场景下重复收集。 + */ + private static func addOrReplaceVariable(result: ArrayList, variableInfo: InstanceVariableInfo): Unit { + for ((index, existingVariable) in result |> enumerate) { + if (existingVariable.name == variableInfo.name) { + result[index] = variableInfo + return + } + } + result.add(variableInfo) + } +} diff --git a/src/openapi/infrastructure/TypeNameParser.cj b/src/openapi/infrastructure/TypeNameParser.cj new file mode 100644 index 0000000..ffa4c18 --- /dev/null +++ b/src/openapi/infrastructure/TypeNameParser.cj @@ -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`。 + * @return 顶层泛型参数;若不是合法泛型限定名则返回 `None`。 + */ + public static func getGenericArguments(qualifiedName: String): ?Array { + if (!qualifiedName.endsWith('>')) { + return None + } + let start = qualifiedName.indexOf('<') ?? return None + let payload = qualifiedName[start + 1..qualifiedName.size - 1] + let result = ArrayList() + 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 + } +} diff --git a/src/openapi/interfaces/IOpenApiSerializable.cj b/src/openapi/interfaces/IOpenApiSerializable.cj new file mode 100644 index 0000000..d54f334 --- /dev/null +++ b/src/openapi/interfaces/IOpenApiSerializable.cj @@ -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 +} diff --git a/src/openapi/interfaces/IOpenApiWriter.cj b/src/openapi/interfaces/IOpenApiWriter.cj new file mode 100644 index 0000000..42590f0 --- /dev/null +++ b/src/openapi/interfaces/IOpenApiWriter.cj @@ -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 +} diff --git a/src/openapi/metadata/IApiDescriptionMetadata.cj b/src/openapi/metadata/IApiDescriptionMetadata.cj new file mode 100644 index 0000000..9b801e9 --- /dev/null +++ b/src/openapi/metadata/IApiDescriptionMetadata.cj @@ -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 +} diff --git a/src/openapi/metadata/IApiGroupNameProvider.cj b/src/openapi/metadata/IApiGroupNameProvider.cj new file mode 100644 index 0000000..08c5ff1 --- /dev/null +++ b/src/openapi/metadata/IApiGroupNameProvider.cj @@ -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 +} diff --git a/src/openapi/metadata/IApiNameMetadata.cj b/src/openapi/metadata/IApiNameMetadata.cj new file mode 100644 index 0000000..e0bc44c --- /dev/null +++ b/src/openapi/metadata/IApiNameMetadata.cj @@ -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 +} diff --git a/src/openapi/metadata/IApiResponseMetadataProvider.cj b/src/openapi/metadata/IApiResponseMetadataProvider.cj new file mode 100644 index 0000000..3fe2d44 --- /dev/null +++ b/src/openapi/metadata/IApiResponseMetadataProvider.cj @@ -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 +} diff --git a/src/openapi/metadata/IApiSummaryMetadata.cj b/src/openapi/metadata/IApiSummaryMetadata.cj new file mode 100644 index 0000000..bf298a7 --- /dev/null +++ b/src/openapi/metadata/IApiSummaryMetadata.cj @@ -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 +} diff --git a/src/openapi/metadata/IApiTagsMetadata.cj b/src/openapi/metadata/IApiTagsMetadata.cj new file mode 100644 index 0000000..c21c7ad --- /dev/null +++ b/src/openapi/metadata/IApiTagsMetadata.cj @@ -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 +} diff --git a/src/openapi/metadata/IApiVisibilityProvider.cj b/src/openapi/metadata/IApiVisibilityProvider.cj new file mode 100644 index 0000000..f73e64e --- /dev/null +++ b/src/openapi/metadata/IApiVisibilityProvider.cj @@ -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 +} diff --git a/src/openapi/models/Modules.cj b/src/openapi/models/Modules.cj new file mode 100644 index 0000000..8b32e7c --- /dev/null +++ b/src/openapi/models/Modules.cj @@ -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.* \ No newline at end of file diff --git a/src/openapi/models/OpenApiComponents.cj b/src/openapi/models/OpenApiComponents.cj new file mode 100644 index 0000000..190fd45 --- /dev/null +++ b/src/openapi/models/OpenApiComponents.cj @@ -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() + + /** + * @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() + } +} diff --git a/src/openapi/models/OpenApiContact.cj b/src/openapi/models/OpenApiContact.cj new file mode 100644 index 0000000..49b9da2 --- /dev/null +++ b/src/openapi/models/OpenApiContact.cj @@ -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() + } +} diff --git a/src/openapi/models/OpenApiDocument.cj b/src/openapi/models/OpenApiDocument.cj new file mode 100644 index 0000000..53bf4f8 --- /dev/null +++ b/src/openapi/models/OpenApiDocument.cj @@ -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() + /** + * @brief 表示服务器集合。 + */ + public var servers = ArrayList() + /** + * @brief 表示安全需求集合。 + */ + public var securityRequirements = ArrayList() + + /** + * @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() + } +} diff --git a/src/openapi/models/OpenApiExtensibleDictionary.cj b/src/openapi/models/OpenApiExtensibleDictionary.cj new file mode 100644 index 0000000..705890f --- /dev/null +++ b/src/openapi/models/OpenApiExtensibleDictionary.cj @@ -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 <: IOpenApiSerializable where T <: IOpenApiSerializable { + private let _items: HashMap + + /** + * @brief 创建一个空的 OpenAPI 可扩展字典。 + */ + protected init() { + this(HashMap()) + } + + /** + * @brief 使用已有项创建 OpenAPI 可扩展字典。 + * @param items 初始字典项集合。 + */ + protected init(items: HashMap) { + _items = items + } + + /** + * @brief 返回内部字典项集合。 + * @return 当前字典中的所有项。 + */ + protected prop items: HashMap { + 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() + } +} diff --git a/src/openapi/models/OpenApiInfo.cj b/src/openapi/models/OpenApiInfo.cj new file mode 100644 index 0000000..af80fc5 --- /dev/null +++ b/src/openapi/models/OpenApiInfo.cj @@ -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() + } +} diff --git a/src/openapi/models/OpenApiLicense.cj b/src/openapi/models/OpenApiLicense.cj new file mode 100644 index 0000000..f16e66e --- /dev/null +++ b/src/openapi/models/OpenApiLicense.cj @@ -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() + } +} diff --git a/src/openapi/models/OpenApiMediaType.cj b/src/openapi/models/OpenApiMediaType.cj new file mode 100644 index 0000000..d0ea6d2 --- /dev/null +++ b/src/openapi/models/OpenApiMediaType.cj @@ -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() + } +} diff --git a/src/openapi/models/OpenApiOperation.cj b/src/openapi/models/OpenApiOperation.cj new file mode 100644 index 0000000..4545495 --- /dev/null +++ b/src/openapi/models/OpenApiOperation.cj @@ -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() + /** + * @brief 表示操作服务器集合。 + */ + public var servers = ArrayList() + /** + * @brief 表示操作请求体。 + */ + public var requestBody:? OpenApiRequestBody = None + /** + * @brief 表示操作参数集合。 + */ + public var parameters = ArrayList() + /** + * @brief 表示操作安全需求集合。 + */ + public var security = ArrayList() + + /** + * @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() + } +} diff --git a/src/openapi/models/OpenApiParameter.cj b/src/openapi/models/OpenApiParameter.cj new file mode 100644 index 0000000..de772b7 --- /dev/null +++ b/src/openapi/models/OpenApiParameter.cj @@ -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() + } +} diff --git a/src/openapi/models/OpenApiPathItem.cj b/src/openapi/models/OpenApiPathItem.cj new file mode 100644 index 0000000..415c375 --- /dev/null +++ b/src/openapi/models/OpenApiPathItem.cj @@ -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() + /** + * @brief 表示路径项参数集合。 + */ + public var parameters = ArrayList() + /** + * @brief 表示路径项操作集合。 + */ + public var operations = HashMap() + + /** + * @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() + } +} diff --git a/src/openapi/models/OpenApiPaths.cj b/src/openapi/models/OpenApiPaths.cj new file mode 100644 index 0000000..531d83e --- /dev/null +++ b/src/openapi/models/OpenApiPaths.cj @@ -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 { + /** + * @brief 创建 OpenAPI 路径集合实例。 + */ + public init() { + + } + + private init(paths: HashMap) { + super(paths) + } + +} diff --git a/src/openapi/models/OpenApiReference.cj b/src/openapi/models/OpenApiReference.cj new file mode 100644 index 0000000..ea1d41b --- /dev/null +++ b/src/openapi/models/OpenApiReference.cj @@ -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() + } +} diff --git a/src/openapi/models/OpenApiRequestBody.cj b/src/openapi/models/OpenApiRequestBody.cj new file mode 100644 index 0000000..c2905b9 --- /dev/null +++ b/src/openapi/models/OpenApiRequestBody.cj @@ -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() + + /** + * @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() + } +} diff --git a/src/openapi/models/OpenApiResponse.cj b/src/openapi/models/OpenApiResponse.cj new file mode 100644 index 0000000..4bf3b34 --- /dev/null +++ b/src/openapi/models/OpenApiResponse.cj @@ -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() + + /** + * @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() + } +} diff --git a/src/openapi/models/OpenApiResponses.cj b/src/openapi/models/OpenApiResponses.cj new file mode 100644 index 0000000..83a5cd3 --- /dev/null +++ b/src/openapi/models/OpenApiResponses.cj @@ -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 { + /** + * @brief 创建 OpenAPI 响应集合实例。 + */ + public init() { + } +} diff --git a/src/openapi/models/OpenApiSchema.cj b/src/openapi/models/OpenApiSchema.cj new file mode 100644 index 0000000..6dd6b9c --- /dev/null +++ b/src/openapi/models/OpenApiSchema.cj @@ -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() + /** + * @brief 表示 allOf 组合架构。 + */ + public var allOf = ArrayList() + /** + * @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() + } +} diff --git a/src/openapi/models/OpenApiSchemaTypes.cj b/src/openapi/models/OpenApiSchemaTypes.cj new file mode 100644 index 0000000..ea47915 --- /dev/null +++ b/src/openapi/models/OpenApiSchemaTypes.cj @@ -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 + } +} diff --git a/src/openapi/models/OpenApiSecurityRequirement.cj b/src/openapi/models/OpenApiSecurityRequirement.cj new file mode 100644 index 0000000..2111ef2 --- /dev/null +++ b/src/openapi/models/OpenApiSecurityRequirement.cj @@ -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 {} +} diff --git a/src/openapi/models/OpenApiServer.cj b/src/openapi/models/OpenApiServer.cj new file mode 100644 index 0000000..e83c036 --- /dev/null +++ b/src/openapi/models/OpenApiServer.cj @@ -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() + } +} diff --git a/src/openapi/models/OpenApiTag.cj b/src/openapi/models/OpenApiTag.cj new file mode 100644 index 0000000..8410475 --- /dev/null +++ b/src/openapi/models/OpenApiTag.cj @@ -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) + } + } +} diff --git a/src/openapi/models/OperationType.cj b/src/openapi/models/OperationType.cj new file mode 100644 index 0000000..3d9cb62 --- /dev/null +++ b/src/openapi/models/OperationType.cj @@ -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 & 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 => "*" + } + } +} diff --git a/src/openapi/models/ParameterLocation.cj b/src/openapi/models/ParameterLocation.cj new file mode 100644 index 0000000..ee568e0 --- /dev/null +++ b/src/openapi/models/ParameterLocation.cj @@ -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" + } + } +} diff --git a/src/openapi/models/ReferenceType.cj b/src/openapi/models/ReferenceType.cj new file mode 100644 index 0000000..fab1241 --- /dev/null +++ b/src/openapi/models/ReferenceType.cj @@ -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 & 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" + } + } +} diff --git a/src/openapi/services/IDocumentProvider.cj b/src/openapi/services/IDocumentProvider.cj new file mode 100644 index 0000000..a303469 --- /dev/null +++ b/src/openapi/services/IDocumentProvider.cj @@ -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 + /** + * @brief 按名称创建 OpenAPI 文档。 + * @param documentName 要创建的文档名称。 + * @return 生成后的 OpenAPI 文档。 + */ + func create(documentName: String, services: IServiceProvider): OpenApiDocument +} diff --git a/src/openapi/services/JsonOpenApiWriter.cj b/src/openapi/services/JsonOpenApiWriter.cj new file mode 100644 index 0000000..47b0469 --- /dev/null +++ b/src/openapi/services/JsonOpenApiWriter.cj @@ -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() + } +} diff --git a/src/openapi/services/NamedService.cj b/src/openapi/services/NamedService.cj new file mode 100644 index 0000000..6ef0f93 --- /dev/null +++ b/src/openapi/services/NamedService.cj @@ -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 + } +} diff --git a/src/openapi/services/OpenApiDocumentProvider.cj b/src/openapi/services/OpenApiDocumentProvider.cj new file mode 100644 index 0000000..855994f --- /dev/null +++ b/src/openapi/services/OpenApiDocumentProvider.cj @@ -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 + + /** + * @brief 创建 OpenAPI 文档提供器实例。 + * @param services 服务提供器。 + * @param options OpenAPI 选项监视器。 + * @param provider OpenAPI 文档生成服务。 + */ + public init(services: IServiceProvider, options: IOptionsMonitor) { + _services = services + _options = options + } + + /** + * @brief 返回已注册的文档名称集合。 + * @return 当前可用的文档名称集合。 + */ + public func getDooucmentNames(): Collection { + _services.getAll() |> 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( + 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 + } +} diff --git a/src/openapi/services/OpenApiDocumentService.cj b/src/openapi/services/OpenApiDocumentService.cj new file mode 100644 index 0000000..055fe01 --- /dev/null +++ b/src/openapi/services/OpenApiDocumentService.cj @@ -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>().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()) { + for (httpMethod in metadata.httpMethods) { + let operationType = OperationType.parse(httpMethod) + let actionDescriptor = endpoint.metadata.getMetadata() + 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() |> 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 {f => f.summary.isSome()}) { + if (let Some(summary) <- metadata.summary) { + operation.summary = summary + } + } + + // description + if (let Some(metadata) <- endpoint + .metadata + .getLastMetadata {f => f.description.isSome()}) { + if (let Some(description) <- metadata.description) { + operation.description = description + } + } + + // operationId + if (let Some(metadata) <- endpoint.metadata.getLastMetadata {f => f.name.isSome()}) { + operation.operationId = metadata.name + } + + return operation + } + + private func createOpenApiOperationTags(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) { + let result = ArrayList() + // tags + if (let Some(metadata) <- endpoint.metadata.getLastMetadata {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() + 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().isSome() || !hasBindingSourceMetadata) { + let schema = _openApiSchemaService.createSchema(typeInfo) + requestBody.content.add("application/json", OpenApiMediaType(schema)) + } else if (parameter.findAnnotation().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()) { + 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 {f => f.ignore}) { + return true + } + return false + } +} + +extend EndpointMetadataCollection { + /** + * @brief 返回最后一个满足条件的元数据。 + * @param filter 用于筛选元数据的条件。 + * @return 最后一个满足条件的元数据;如果不存在则返回 `None`。 + */ + public func getLastMetadata(filter: (T) -> Bool): ?T { + let metadatas = this.getOrderedMetadata() + var index = metadatas.size - 1 + while (index >= 0) { + let metadata = metadatas[index] + if (filter(metadata)) { + return metadata + } + index-- + } + + return None + } +} diff --git a/src/openapi/services/OpenApiOptions.cj b/src/openapi/services/OpenApiOptions.cj new file mode 100644 index 0000000..1b78010 --- /dev/null +++ b/src/openapi/services/OpenApiOptions.cj @@ -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() + private let _operationTransformer = ArrayList() + private let _schemaTransformers = ArrayList() + + /** + * @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 { + get() { + _documentTransformers + } + } + + /** + * @brief 返回当前操作转换器集合。 + * @return 已注册的操作转换器集合。 + */ + protected prop operationTransformers: List { + get() { + _operationTransformer + } + } + + /** + * @brief 返回当前架构转换器集合。 + * @return 已注册的架构转换器集合。 + */ + protected prop schemaTransformers: List { + 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)) + } +} diff --git a/src/openapi/services/OpenApiParameterService.cj b/src/openapi/services/OpenApiParameterService.cj new file mode 100644 index 0000000..4081549 --- /dev/null +++ b/src/openapi/services/OpenApiParameterService.cj @@ -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 { + let parameters = ArrayList() + 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 { + 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(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().isSome()) { + return ParameterLocation.Query + } + if (parameter.findAnnotation().isSome()) { + return ParameterLocation.Path + } + if (parameter.findAnnotation().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 + } + } +} diff --git a/src/openapi/services/OpenApiSchemaService.cj b/src/openapi/services/OpenApiSchemaService.cj new file mode 100644 index 0000000..eb4769e --- /dev/null +++ b/src/openapi/services/OpenApiSchemaService.cj @@ -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.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of(), + TypeInfo.of() + ] + ) + private static let _stringType = TypeInfo.of() + private static let _formFileType = TypeInfo.of() + private static let _jsonValueType = TypeInfo.of() + private static let _actionResultType = TypeInfo.of() + private let _documentName: String + private let _options: OpenApiOptions + private let _services: IServiceProvider + private let _schemas = HashMap() + + /** + * @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 { + 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 { + 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 + */ + 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 + */ + 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 + } + } +} diff --git a/src/openapi/transformers/DelegateOpenApiDocumentTransformer.cj b/src/openapi/transformers/DelegateOpenApiDocumentTransformer.cj new file mode 100644 index 0000000..e39a363 --- /dev/null +++ b/src/openapi/transformers/DelegateOpenApiDocumentTransformer.cj @@ -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) + } +} diff --git a/src/openapi/transformers/DelegateOpenApiOperationTransformer.cj b/src/openapi/transformers/DelegateOpenApiOperationTransformer.cj new file mode 100644 index 0000000..ad0d83d --- /dev/null +++ b/src/openapi/transformers/DelegateOpenApiOperationTransformer.cj @@ -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) + } +} diff --git a/src/openapi/transformers/DelegateOpenApiSchemaTransformer.cj b/src/openapi/transformers/DelegateOpenApiSchemaTransformer.cj new file mode 100644 index 0000000..223ec29 --- /dev/null +++ b/src/openapi/transformers/DelegateOpenApiSchemaTransformer.cj @@ -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) + } +} diff --git a/src/openapi/transformers/IOpenApiDocumentTransformer.cj b/src/openapi/transformers/IOpenApiDocumentTransformer.cj new file mode 100644 index 0000000..c10a101 --- /dev/null +++ b/src/openapi/transformers/IOpenApiDocumentTransformer.cj @@ -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 +} diff --git a/src/openapi/transformers/IOpenApiOperationTransformer.cj b/src/openapi/transformers/IOpenApiOperationTransformer.cj new file mode 100644 index 0000000..eeaadf8 --- /dev/null +++ b/src/openapi/transformers/IOpenApiOperationTransformer.cj @@ -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 +} diff --git a/src/openapi/transformers/IOpenApiSchemaTransformer.cj b/src/openapi/transformers/IOpenApiSchemaTransformer.cj new file mode 100644 index 0000000..9975230 --- /dev/null +++ b/src/openapi/transformers/IOpenApiSchemaTransformer.cj @@ -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 +} diff --git a/src/openapi/transformers/OpenApiDocumentTransformerContext.cj b/src/openapi/transformers/OpenApiDocumentTransformerContext.cj new file mode 100644 index 0000000..22bb297 --- /dev/null +++ b/src/openapi/transformers/OpenApiDocumentTransformerContext.cj @@ -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 + } +} diff --git a/src/openapi/transformers/OpenApiOperationTransformerContext.cj b/src/openapi/transformers/OpenApiOperationTransformerContext.cj new file mode 100644 index 0000000..5bdb2bc --- /dev/null +++ b/src/openapi/transformers/OpenApiOperationTransformerContext.cj @@ -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 + } +} diff --git a/src/openapi/transformers/OpenApiSchemaTransformerContext.cj b/src/openapi/transformers/OpenApiSchemaTransformerContext.cj new file mode 100644 index 0000000..ce04007 --- /dev/null +++ b/src/openapi/transformers/OpenApiSchemaTransformerContext.cj @@ -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 + } +} diff --git a/src/simapi_extensions.cj b/src/simapi_extensions.cj index 865b1a3..b71a089 100644 --- a/src/simapi_extensions.cj +++ b/src/simapi_extensions.cj @@ -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() } - // 内置路由: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()) - 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()) + 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()) - 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()) + 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()) - 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()) - 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()) + 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()) + 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()) { host.mapControllers() } + + // SimApiDoc(OpenAPI 文档 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()