精简了openapi相关

This commit is contained in:
2026-09-01 23:36:07 +08:00
parent eed230aab0
commit 06884a920c
73 changed files with 465 additions and 1214 deletions
+10 -10
View File
@@ -3,18 +3,18 @@ version = 0
[requires] [requires]
soulsoft_extensions_hosting = {version = "1.0.20260528"} soulsoft_extensions_hosting = {version = "1.0.20260528"}
soulsoft_extensions_options_configuration = {version = "1.0.20260528"} soulsoft_extensions_options_configuration = {version = "1.0.20260528"}
soulsoft_web_mvc = {version = "1.0.20260528"}
soulsoft_extensions_logging_console = {version = "1.0.20260528"} soulsoft_extensions_logging_console = {version = "1.0.20260528"}
soulsoft_web_http = {version = "1.0.20260528"}
redis = {version = "1.0.20260627"}
soulsoft_web_cors = {version = "1.0.20260528"}
"simcu::serialization" = {version = "1.2.1"}
soulsoft_web_routing = {version = "1.0.20260528"}
soulsoft_extensions_logging = {version = "1.0.20260528"} soulsoft_extensions_logging = {version = "1.0.20260528"}
soulsoft_serialization = {version = "1.0.20260528"} soulsoft_serialization = {version = "1.0.20260528"}
soulsoft_extensions_options = {version = "1.0.20260528"}
soulsoft_web_routing = {version = "1.0.20260528"}
soulsoft_identity_claims = {version = "1.0.20260528"}
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"} soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
soulsoft_web_http = {version = "1.0.20260528"}
soulsoft_extensions_configuration = {version = "1.0.20260528"}
soulsoft_web_cors = {version = "1.0.20260528"}
soulsoft_extensions_injection = {version = "1.0.20260528"}
soulsoft_web_hosting = {version = "1.0.20260528"} soulsoft_web_hosting = {version = "1.0.20260528"}
soulsoft_web_mvc = {version = "1.0.20260528"} soulsoft_extensions_configuration = {version = "1.0.20260528"}
redis = {version = "1.0.20260627"} soulsoft_extensions_injection = {version = "1.0.20260528"}
"simcu::serialization" = {version = "1.2.1"} soulsoft_extensions_options = {version = "1.0.20260528"}
soulsoft_identity_claims = {version = "1.0.20260528"}
+63
View File
@@ -0,0 +1,63 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.annotations
/**
* @brief 为控制器或动作提供 API 文档元数据(对齐 C# [SimApiDoc] 特性)。
*
* 用法:
* @SimApiDoc[tags: "登录", summary: "用户登录相关接口"]
* @SimApiDoc[tags: "认证", summary: "后台登录", groupNames: "admin"]
* @SimApiDoc[tags: "公共", groupNames: "api,admin"] // 同时出现在 api 和 admin 文档
* @SimApiDoc[tags: "公共", groupNames: "*"] // 出现在所有文档
*/
@Annotation[target: [MemberFunction, Type, MemberProperty, MemberVariable, Parameter]]
public class SimApiDoc {
/**
* @brief API 名称。
*/
public let name: ?String
/**
* @brief 当前 API 是否应被忽略。
*/
public let ignore: Bool
/**
* @brief API 摘要信息。
*/
public let summary: ?String
/**
* @brief API 分组名称(逗号分隔,如 "api,admin");"*" 表示出现在所有文档;空串表示未分组(仅进默认文档)。
*/
public let groupNames: String
/**
* @brief API 描述信息。
*/
public let description: ?String
/**
* @brief API 标签字符串。
*/
public let tags: ?String
/**
* @brief 创建 SimApiDoc 注解实例。
* @param name API 名称。
* @param ignore 是否忽略当前 API。
* @param summary API 摘要。
* @param groupNames API 分组名称(逗号分隔,如 "api,admin");"*" 表示出现在所有文档;空串表示未分组(仅进默认文档)。
* @param description API 描述。
* @param tags API 标签字符串。
*/
public const init(name!: ?String = None, ignore!: Bool = false, summary!: ?String = None, groupNames!: String = "",
description!: ?String = None, tags!: ?String = None) {
this.tags = tags
this.name = name
this.ignore = ignore
this.summary = summary
this.groupNames = groupNames
this.description = description
}
}
+1 -2
View File
@@ -11,8 +11,7 @@ import soulsoft_web_mvc.annotations.*
import simcu::simapi.communications.* import simcu::simapi.communications.*
import simcu::simapi.configurations.* import simcu::simapi.configurations.*
import simcu::simapi.helpers.* import simcu::simapi.helpers.*
import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAttribute} import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAttribute, SimApiDoc}
import simcu::simapi.openapi.annotations.*
/** /**
* 通用控制器:错误反馈、WebConfig、用户信息。 * 通用控制器:错误反馈、WebConfig、用户信息。
-9
View File
@@ -1,9 +0,0 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
public import simcu::simapi.openapi.annotations.*
@@ -1,38 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
import soulsoft_web_http.*
/**
* @brief 提供 OpenAPI 端点约定扩展。
*/
public interface OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 OpenAPI 元数据。
* @param metadata 要附加的 OpenAPI 元数据。
* @return 当前端点约定构建器实例。
*/
func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder
}
extend EndpointConventionBuilder <: OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 API 文档元数据。
* @param metadata 要附加的 API 文档元数据。
* @return 当前端点约定构建器实例。
*/
public func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(metadata)
}
return this
}
}
@@ -1,65 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
import soulsoft_web_http.*
import soulsoft_web_routing.*
import simcu::simapi.openapi.services.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.infrastructure.*
/**
* @brief 提供 OpenAPI 文档路由映射扩展。
*/
public interface OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射默认 OpenAPI 文档路由。
* @return 当前端点路由构建器实例。
*/
func mapOpenApi(): EndpointRouteBuilder {
mapOpenApi(OpenApiConstants.DefaultOpenApiRoute)
}
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
func mapOpenApi(pattern: String): EndpointRouteBuilder
}
extend EndpointRouteBuilder <: OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
public func mapOpenApi(pattern: String): EndpointRouteBuilder {
verifyOpenApiServicesAreRegistered()
this.mapGet(pattern) {
context =>
let documentName = context.request.routeValues.get("documentName").flatMap {f => f} ?? "v1"
let documentFactory = context.services.getOrThrow<IDocumentProvider>()
let document = documentFactory.create(documentName, context.services)
context.response.contentType = "application/json; charset=utf-8"
let writer = JsonOpenApiWriter(context.response.body)
document.serializeAsV3(writer)
writer.flush()
}.withOpenApi(SimApiDoc(ignore: true))
return this
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception(
"Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
@@ -1,71 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
import soulsoft_web_routing.*
import soulsoft_extensions_options.*
import simcu::simapi.openapi.services.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.infrastructure.*
/**
* @brief 提供 OpenAPI 服务注册扩展。
*/
public interface OpenApiServiceCollectionExtensions {
/**
* @brief 注册默认名称的 OpenAPI 服务。
* @return 当前服务集合实例。
*/
func addOpenApi(): ServiceCollection {
addOpenApi(OpenApiConstants.DefaultDocumentName, {_ =>})
}
/**
* @brief 注册指定文档名称的 OpenAPI 服务。
* @param documentName OpenAPI 文档名称。
* @return 当前服务集合实例。
*/
func addOpenApi(documentName: String): ServiceCollection {
addOpenApi(documentName, {_ =>})
}
/**
* @brief 注册默认文档名称的 OpenAPI 服务并配置选项。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
addOpenApi(OpenApiConstants.DefaultDocumentName, configureOptions)
}
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection
}
extend ServiceCollection <: OpenApiServiceCollectionExtensions {
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
public func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
this.addRouting()
this.configure<OpenApiOptions>(documentName, configureOptions)
this.addSingleton(NamedService(documentName))
this.addSingleton<IDocumentProvider, OpenApiDocumentProvider>()
return this
}
}
@@ -1,41 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
import soulsoft_web_http.*
import soulsoft_extensions_injection.*
import simcu::simapi.openapi.services.*
/**
* @brief 提供 OpenAPI UI 中间件扩展。
*/
public interface OpenApiUIMiddlewareExtensions {
/**
* @brief 启用 OpenAPI UI 中间件。
*/
func useOpenApiUI(): Unit
}
extend ApplicationBuilder <: OpenApiUIMiddlewareExtensions{
/**
* @brief 启用 OpenAPI UI 中间件。
*/
public func useOpenApiUI(): Unit {
verifyOpenApiServicesAreRegistered()
use<OpenApiUIMiddleware>()
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception("Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
@@ -1,56 +0,0 @@
/*
* Copyright (c) 2025 SimCuTeam. All rights reserved.
* 遵循 MIT 许可证。
*/
package simcu::simapi.openapi
import std.reflect.*
import soulsoft_web_http.*
import simcu::simapi.annotations.*
import simcu::simapi.openapi.metadata.*
/**
* @brief 响应类型元数据实现。
*/
public class ApiResponseTypeMetadata <: IApiResponseTypeMetadata {
private let _responseType: ?TypeInfo
public init(responseType: ?TypeInfo) {
_responseType = responseType
}
public prop responseType: ?TypeInfo {
get() {
_responseType
}
}
}
/**
* @brief 为动态注册的路由提供认证和响应类型元数据扩展。
*/
public interface SimApiEndpointMetadataExtensions {
func withSimApiAuth(auth: SimApiAuth): EndpointConventionBuilder
func withResponseType(typeInfo: TypeInfo): EndpointConventionBuilder
}
extend EndpointConventionBuilder <: SimApiEndpointMetadataExtensions {
/**
* @brief 为端点附加 SimApiAuth 认证元数据(使 OpenAPI 文档显示锁图标)。
*/
public func withSimApiAuth(auth: SimApiAuth): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(auth)
}
return this
}
/**
* @brief 为端点附加响应类型元数据(使动态路由也能生成 response schema)。
*/
public func withResponseType(typeInfo: TypeInfo): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(ApiResponseTypeMetadata(Some(typeInfo)))
}
return this
}
}
-107
View File
@@ -1,107 +0,0 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.annotations
import simcu::simapi.openapi.metadata.*
/**
* @brief 为控制器或动作提供 API 文档元数据(对齐 C# [SimApiDoc] 特性)。
*
* 用法:
* @SimApiDoc[tags: "登录", summary: "用户登录相关接口"]
* @SimApiDoc[tags: "认证", summary: "后台登录", groupNames: "admin"]
* @SimApiDoc[tags: "公共", groupNames: "api,admin"] // 同时出现在 api 和 admin 文档
* @SimApiDoc[tags: "公共", groupNames: "*"] // 出现在所有文档
*/
@Annotation[target: [MemberFunction, Type, MemberProperty, MemberVariable, Parameter]]
public class SimApiDoc <: IApiTagsMetadata & IApiNameMetadata & IApiGroupNamesProvider & IApiDescriptionMetadata & IApiSummaryMetadata & IApiVisibilityProvider {
private let _ignore: Bool
private let _name: ?String
private let _tags: ?String
private let _summary: ?String
private let _groupNames: String
private let _description: ?String
/**
* @brief 创建 SimApiDoc 注解实例。
* @param name API 名称。
* @param ignore 是否忽略当前 API。
* @param summary API 摘要。
* @param groupNames API 分组名称(逗号分隔,如 "api,admin");"*" 表示出现在所有文档;空串表示未分组(仅进默认文档)。
* @param description API 描述。
* @param tags API 标签字符串。
*/
public const init(name!: ?String = None, ignore!: Bool = false, summary!: ?String = None,
groupNames!: String = "", description!: ?String = None, tags!: ?String = None) {
_tags = tags
_name = name
_ignore = ignore
_summary = summary
_groupNames = groupNames
_description = description
}
/**
* @brief 返回 API 标签字符串。
* @return 当前 API 的标签字符串。
*/
public prop tags: ?String {
get() {
_tags
}
}
/**
* @brief 返回 API 名称。
* @return 当前 API 的名称。
*/
public prop name: ?String {
get() {
_name
}
}
/**
* @brief 返回当前 API 是否应被忽略。
* @return 如果当前 API 需要从文档中忽略则返回 `true`。
*/
public prop ignore: Bool {
get() {
_ignore
}
}
/**
* @brief 返回 API 摘要信息。
* @return 当前 API 的摘要信息。
*/
public prop summary: ?String {
get() {
_summary
}
}
/**
* @brief 返回 API 分组名称(逗号分隔,"*" 表示所有文档)。
* @return 逗号分隔的分组名称字符串,空串表示未分组。
*/
public prop groupNames: String {
get() {
_groupNames
}
}
/**
* @brief 返回 API 描述信息。
* @return 当前 API 的描述信息。
*/
public prop description: ?String {
get() {
_description
}
}
}
-28
View File
@@ -1,28 +0,0 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.infrastructure
import std.reflect.*
/*
Option类型操作
*/
protected class Nullable {
/**
* @brief 返回 Option 类型的底层类型。
* @param typeInfo 要解析的类型信息。
* @return 如果是 Option 类型则返回其底层类型,否则返回 `None`。
*/
public static func getUnderlyingType(typeInfo: TypeInfo): ?TypeInfo {
let qualifiedName = typeInfo.qualifiedName
if (!(qualifiedName.startsWith("Option<") && qualifiedName.endsWith('>'))) {
return None
}
let underlyingTypeName = qualifiedName[7..qualifiedName.size - 1]
return TypeInfo.get(underlyingTypeName)
}
}
@@ -1,13 +0,0 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.infrastructure
protected class OpenApiConstants {
public static let DefaultOpenApiName: String = "default"
public static let DefaultDocumentName: String = "v1"
public static let DefaultOpenApiRoute: String = "/openapi/{documentName}.json"
}
+97
View File
@@ -0,0 +1,97 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
import std.reflect.*
import std.collection.*
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
import simcu::simapi.annotations.*
import simcu::simapi.openapi.models.*
/**
* @brief 提供 OpenAPI 端点约定扩展。
*/
public interface OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 OpenAPI 元数据。
* @param metadata 要附加的 OpenAPI 元数据。
* @return 当前端点约定构建器实例。
*/
func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder
}
/**
* @brief 提供 OpenAPI 文档路由映射扩展。
*/
public interface OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
func mapOpenApi(pattern: String): EndpointRouteBuilder
}
/**
* @brief 提供 OpenAPI 服务注册扩展。
*/
public interface OpenApiServiceCollectionExtensions {
/**
* @brief 注册默认文档名称的 OpenAPI 服务并配置选项。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
addOpenApi("v1", configureOptions)
}
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection
}
/**
* @brief 提供 OpenAPI UI 中间件扩展。
*/
public interface OpenApiUIMiddlewareExtensions {
/**
* @brief 启用 OpenAPI UI 中间件。
*/
func useOpenApiUI(): Unit
}
/**
* @brief 为动态注册的路由提供认证和响应类型元数据扩展。
*/
public interface SimApiEndpointMetadataExtensions {
func withSimApiAuth(auth: SimApiAuth): EndpointConventionBuilder
func withResponseType(typeInfo: TypeInfo): EndpointConventionBuilder
}
/**
* @brief 定义 OpenAPI 文档提供器接口。
*/
public interface IDocumentProvider {
/**
* @brief 返回已注册的文档名称集合。
* @return 当前可用的文档名称集合。
*/
func getDooucmentNames(): Collection<String>
/**
* @brief 按名称创建 OpenAPI 文档。
* @param documentName 要创建的文档名称。
* @return 生成后的 OpenAPI 文档。
*/
func create(documentName: String, services: IServiceProvider): OpenApiDocument
}
@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.interfaces
/**
* @brief 定义 OpenAPI 对象序列化接口。
*/
public interface IOpenApiSerializable {
/**
* @brief 按 OpenAPI V3 格式写出当前对象。
* @param writer OpenAPI 写入器。
*/
func serializeAsV3(writer: IOpenApiWriter): Unit
}
@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.metadata
/**
* @brief 提供 API 描述元数据。
*/
public interface IApiDescriptionMetadata {
/**
* @brief 返回 API 描述信息。
* @return 当前 API 的描述信息。
*/
prop description: ?String
}
@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.metadata
/**
* @brief 提供 API 多分组名称元数据(逗号分隔,如 "api,admin")。
*/
public interface IApiGroupNamesProvider {
/**
* @brief 返回 API 所属的多个分组名称(逗号分隔)。
* @return 逗号分隔的分组名称字符串,空串表示未指定。
*/
prop groupNames: String
}
-21
View File
@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.metadata
/**
* @brief 提供 API 名称元数据。
*/
public interface IApiNameMetadata {
/**
* @brief 返回 API 名称。
* @return 当前 API 的名称。
*/
prop name: ?String
}
@@ -1,28 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.metadata
import std.reflect.*
/**
* @brief 提供 API 响应元数据。
*/
public interface IApiResponseMetadataProvider {
/**
* @brief 返回响应状态码。
* @return 当前 API 响应的状态码。
*/
prop status: Int64
/**
* @brief 返回响应类型信息。
* @return 当前 API 响应的数据类型信息。
*/
prop typeInfo: TypeInfo
}
@@ -1,18 +0,0 @@
/*
* Copyright (c) 2025 SimCuTeam. All rights reserved.
* 遵循 MIT 许可证。
*/
package simcu::simapi.openapi.metadata
import std.reflect.*
/**
* @brief 提供 API 响应类型元数据(供动态注册的路由指定返回类型)。
*/
public interface IApiResponseTypeMetadata {
/**
* @brief 返回 API 响应的类型信息。
*/
prop responseType: ?TypeInfo
}
@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.metadata
/**
* @brief 提供 API 摘要元数据。
*/
public interface IApiSummaryMetadata {
/**
* @brief 返回 API 摘要信息。
* @return 当前 API 的摘要信息。
*/
prop summary: ?String
}
-21
View File
@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.metadata
/**
* @brief 提供 API 标签元数据。
*/
public interface IApiTagsMetadata {
/**
* @brief 返回 API 标签字符串。
* @return 当前 API 的标签字符串。
*/
prop tags: ?String
}
@@ -1,21 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.metadata
/**
* @brief 提供 API 可见性元数据。
*/
public interface IApiVisibilityProvider {
/**
* @brief 返回当前 API 是否应被忽略。
* @return 如果当前 API 需要从 OpenAPI 文档中忽略则返回 `true`。
*/
prop ignore: Bool
}
-9
View File
@@ -1,9 +0,0 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.models
protected import simcu::simapi.openapi.interfaces.*
@@ -1,13 +1,21 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/* /*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved. * Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the * This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.interfaces package simcu::simapi.openapi.models
/**
* @brief 定义 OpenAPI 对象序列化接口。
*/
public interface IOpenApiSerializable {
/**
* @brief 按 OpenAPI V3 格式写出当前对象。
* @param writer OpenAPI 写入器。
*/
func serializeAsV3(writer: IOpenApiWriter): Unit
}
/** /**
* @brief 定义 OpenAPI 文档写入接口。 * @brief 定义 OpenAPI 文档写入接口。
@@ -7,13 +7,12 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.services package simcu::simapi.openapi
import std.collection.* import std.collection.*
import simcu::simapi.openapi.models.* import simcu::simapi.openapi.models.*
import soulsoft_extensions_options.* import soulsoft_extensions_options.*
import soulsoft_extensions_injection.* import soulsoft_extensions_injection.*
import simcu::simapi.openapi.transformers.*
/** /**
* @brief 提供 OpenAPI 文档创建能力。 * @brief 提供 OpenAPI 文档创建能力。
@@ -42,10 +41,10 @@ protected class OpenApiDocumentProvider <: IDocumentProvider {
} }
/** /**
* @brief 按名称创建 OpenAPI 文档并依次应用所有转换器 * @brief 按名称创建 OpenAPI 文档。
* @param documentName 要创建的文档名称。 * @param documentName 要创建的文档名称。
* @param services 请求作用域的服务提供器,传递给操作转换器上下文 * @param services 请求作用域的服务提供器。
* @return 经过转换器处理后的 OpenAPI 文档。 * @return OpenAPI 文档。
*/ */
public func create(documentName: String, services: IServiceProvider): OpenApiDocument { public func create(documentName: String, services: IServiceProvider): OpenApiDocument {
let options = _options.get(documentName) let options = _options.get(documentName)
@@ -57,11 +56,6 @@ protected class OpenApiDocumentProvider <: IDocumentProvider {
schemaService, schemaService,
parameterService parameterService
) )
let document = provider.getOpenApiDocument(services) return provider.getOpenApiDocument(services)
let context = OpenApiDocumentTransformerContext(services, documentName)
for (transformer in options.documentTransformers) {
transformer.transform(document, context)
}
return document
} }
} }
@@ -7,7 +7,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.services package simcu::simapi.openapi
import std.reflect.* import std.reflect.*
import std.collection.* import std.collection.*
@@ -20,10 +20,7 @@ import simcu::simapi.openapi.models.*
import soulsoft_extensions_options.* import soulsoft_extensions_options.*
import soulsoft_web_mvc.controllers.* import soulsoft_web_mvc.controllers.*
import soulsoft_extensions_injection.* import soulsoft_extensions_injection.*
import simcu::simapi.openapi.metadata.*
import soulsoft_web_mvc.abstractions.* import soulsoft_web_mvc.abstractions.*
import simcu::simapi.openapi.transformers.*
import simcu::simapi.openapi.infrastructure.*
import simcu::simapi.configurations.* import simcu::simapi.configurations.*
import simcu::simapi.annotations.* import simcu::simapi.annotations.*
@@ -105,7 +102,6 @@ protected class OpenApiDocumentService {
private func createOpenApiPaths(services: IServiceProvider) { private func createOpenApiPaths(services: IServiceProvider) {
let paths = OpenApiPaths() let paths = OpenApiPaths()
let openApiOptions = services.getOrThrow<IOptionsMonitor<OpenApiOptions>>().get(_documentName) let openApiOptions = services.getOrThrow<IOptionsMonitor<OpenApiOptions>>().get(_documentName)
let operationTransformers = openApiOptions.operationTransformers
for (endpoint in _endpointSource.endpoints |> filterMap {f => f as RouteEndpoint} where !isIgnore(endpoint)) { for (endpoint in _endpointSource.endpoints |> filterMap {f => f as RouteEndpoint} where !isIgnore(endpoint)) {
if (!isShouldInclude(endpoint, openApiOptions.includeUnGrouped)) { if (!isShouldInclude(endpoint, openApiOptions.includeUnGrouped)) {
continue continue
@@ -132,10 +128,6 @@ protected class OpenApiDocumentService {
let operationType = OperationType.parse(httpMethod) let operationType = OperationType.parse(httpMethod)
let actionDescriptor = endpoint.metadata.getMetadata<ControllerActionDescriptor>() let actionDescriptor = endpoint.metadata.getMetadata<ControllerActionDescriptor>()
let operation = createOpenApiOperation(endpoint, actionDescriptor) let operation = createOpenApiOperation(endpoint, actionDescriptor)
let context = OpenApiOperationTransformerContext(services, _documentName, actionDescriptor)
for (transformer in operationTransformers) {
transformer.transform(operation, context)
}
pathItem.operations.add(operationType, operation) pathItem.operations.add(operationType, operation)
} }
} }
@@ -144,7 +136,7 @@ protected class OpenApiDocumentService {
} }
private func isShouldInclude(endpoint: RouteEndpoint, includeUnGrouped: Bool) { private func isShouldInclude(endpoint: RouteEndpoint, includeUnGrouped: Bool) {
let rawGroupNames = endpoint.metadata.getOrderedMetadata<IApiGroupNamesProvider>() |> let rawGroupNames = endpoint.metadata.getOrderedMetadata<SimApiDoc>() |>
filterMap { f => f.groupNames } |> collectArray filterMap { f => f.groupNames } |> collectArray
if (rawGroupNames.isEmpty()) { if (rawGroupNames.isEmpty()) {
// 未标注 groupNames 的接口:仅进入默认文档(includeUnGrouped=true 的文档) // 未标注 groupNames 的接口:仅进入默认文档(includeUnGrouped=true 的文档)
@@ -204,7 +196,7 @@ protected class OpenApiDocumentService {
operation.response = createOpenApiOperationResponses(endpoint, actionDescriptor) operation.response = createOpenApiOperationResponses(endpoint, actionDescriptor)
// summary // summary
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiSummaryMetadata> {f => f.summary.isSome()}) { if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.summary.isSome()}) {
if (let Some(summary) <- metadata.summary) { if (let Some(summary) <- metadata.summary) {
operation.summary = summary operation.summary = summary
} }
@@ -213,14 +205,14 @@ protected class OpenApiDocumentService {
// description // description
if (let Some(metadata) <- endpoint if (let Some(metadata) <- endpoint
.metadata .metadata
.getLastMetadata<IApiDescriptionMetadata> {f => f.description.isSome()}) { .getLastMetadata<SimApiDoc> {f => f.description.isSome()}) {
if (let Some(description) <- metadata.description) { if (let Some(description) <- metadata.description) {
operation.description = description operation.description = description
} }
} }
// operationId // operationId
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiNameMetadata> {f => f.name.isSome()}) { if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.name.isSome()}) {
operation.operationId = metadata.name operation.operationId = metadata.name
} }
@@ -237,7 +229,7 @@ protected class OpenApiDocumentService {
private func createOpenApiOperationTags(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) { private func createOpenApiOperationTags(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) {
let result = ArrayList<OpenApiTag>() let result = ArrayList<OpenApiTag>()
// tags // tags
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiTagsMetadata> {f => f.tags.isSome()}) { if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.tags.isSome()}) {
if (let Some(tags) <- metadata.tags) { if (let Some(tags) <- metadata.tags) {
for (name in tags.split(',')) { for (name in tags.split(',')) {
result.add(OpenApiTag(name: name)) result.add(OpenApiTag(name: name))
@@ -251,7 +243,7 @@ protected class OpenApiDocumentService {
if (let Some(actionDescriptor) <- actionDescriptor) { if (let Some(actionDescriptor) <- actionDescriptor) {
result.add(OpenApiTag(name: actionDescriptor.controllerName)) result.add(OpenApiTag(name: actionDescriptor.controllerName))
} else { } else {
result.add(OpenApiTag(name: OpenApiConstants.DefaultOpenApiName)) result.add(OpenApiTag(name: "default"))
} }
return result return result
@@ -299,12 +291,12 @@ protected class OpenApiDocumentService {
private func createOpenApiOperationResponses(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) { private func createOpenApiOperationResponses(endpoint: RouteEndpoint, actionDescriptor: ?ControllerActionDescriptor) {
let responses = OpenApiResponses() let responses = OpenApiResponses()
// 获取action的返回类型:优先从 actionDescriptor,其次从 IApiResponseTypeMetadata(动态路由) // 获取action的返回类型:优先从 actionDescriptor,其次从 ApiResponseTypeMetadata(动态路由)
let returnType: ?TypeInfo = if (let Some(actionDescriptor) <- actionDescriptor) { let returnType: ?TypeInfo = if (let Some(actionDescriptor) <- actionDescriptor) {
Nullable.getUnderlyingType(actionDescriptor.actionFunction.returnType) ?? actionDescriptor Nullable.getUnderlyingType(actionDescriptor.actionFunction.returnType) ?? actionDescriptor
.actionFunction .actionFunction
.returnType .returnType
} else if (let Some(meta) <- endpoint.metadata.getMetadata<IApiResponseTypeMetadata>()) { } else if (let Some(meta) <- endpoint.metadata.getMetadata<ApiResponseTypeMetadata>()) {
meta.responseType meta.responseType
} else { } else {
None None
@@ -357,30 +349,9 @@ protected class OpenApiDocumentService {
// api ignore // api ignore
private func isIgnore(endpoint: RouteEndpoint) { private func isIgnore(endpoint: RouteEndpoint) {
if (let Some(metadata) <- endpoint.metadata.getLastMetadata<IApiVisibilityProvider> {f => f.ignore}) { if (let Some(metadata) <- endpoint.metadata.getLastMetadata<SimApiDoc> {f => f.ignore}) {
return true return true
} }
return false return false
} }
} }
extend EndpointMetadataCollection {
/**
* @brief 返回最后一个满足条件的元数据。
* @param filter 用于筛选元数据的条件。
* @return 最后一个满足条件的元数据;如果不存在则返回 `None`。
*/
public func getLastMetadata<T>(filter: (T) -> Bool): ?T {
let metadatas = this.getOrderedMetadata<T>()
var index = metadatas.size - 1
while (index >= 0) {
let metadata = metadatas[index]
if (filter(metadata)) {
return metadata
}
index--
}
return None
}
}
+152
View File
@@ -0,0 +1,152 @@
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
import std.reflect.*
import soulsoft_web_http.*
import soulsoft_web_routing.*
import soulsoft_extensions_options.*
import soulsoft_extensions_injection.*
import simcu::simapi.annotations.*
/**
* @brief 为端点附加 OpenAPI 元数据。
*/
extend EndpointConventionBuilder <: OpenApiEndpointConventionBuilderExtensions {
/**
* @brief 为端点附加 API 文档元数据。
* @param metadata 要附加的 API 文档元数据。
* @return 当前端点约定构建器实例。
*/
public func withOpenApi(metadata: SimApiDoc): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(metadata)
}
return this
}
}
/**
* @brief 映射默认 OpenAPI 文档路由。
*/
extend EndpointRouteBuilder <: OpenApiEndpointRouteBuilderExtension {
/**
* @brief 映射指定模式的 OpenAPI 文档路由。
* @param pattern OpenAPI 文档路由模式。
* @return 当前端点路由构建器实例。
*/
public func mapOpenApi(pattern: String): EndpointRouteBuilder {
verifyOpenApiServicesAreRegistered()
this.mapGet(pattern) {
context =>
let documentName = context.request.routeValues.get("documentName").flatMap {f => f} ?? "v1"
let documentFactory = context.services.getOrThrow<IDocumentProvider>()
let document = documentFactory.create(documentName, context.services)
context.response.contentType = "application/json; charset=utf-8"
let writer = JsonOpenApiWriter(context.response.body)
document.serializeAsV3(writer)
writer.flush()
}.withOpenApi(SimApiDoc(ignore: true))
return this
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception(
"Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
/**
* @brief 注册默认名称的 OpenAPI 服务。
*/
extend ServiceCollection <: OpenApiServiceCollectionExtensions {
/**
* @brief 注册指定文档名称的 OpenAPI 服务并配置选项。
* @param documentName OpenAPI 文档名称。
* @param configureOptions 用于配置 OpenAPI 选项的回调。
* @return 当前服务集合实例。
*/
public func addOpenApi(documentName: String, configureOptions: (OpenApiOptions) -> Unit): ServiceCollection {
this.addRouting()
this.configure<OpenApiOptions>(documentName, configureOptions)
this.addSingleton(NamedService(documentName))
this.addSingleton<IDocumentProvider, OpenApiDocumentProvider>()
return this
}
}
/**
* @brief 启用 OpenAPI UI 中间件。
*/
extend ApplicationBuilder <: OpenApiUIMiddlewareExtensions{
/**
* @brief 启用 OpenAPI UI 中间件。
*/
public func useOpenApiUI(): Unit {
verifyOpenApiServicesAreRegistered()
use<OpenApiUIMiddleware>()
}
private func verifyOpenApiServicesAreRegistered() {
let callSiteFactory = this.services.getOrThrow<IServiceProviderIsService>()
if (!callSiteFactory.isService<IDocumentProvider>()) {
throw Exception("Unable to find the required services. Please add all the required services by calling 'ServiceCollection.addOpenApi' inside the call to 'configureServices(...)' in the application startup code.")
}
}
}
/**
* @brief 为端点附加认证与响应类型元数据。
*/
extend EndpointConventionBuilder <: SimApiEndpointMetadataExtensions {
/**
* @brief 为端点附加 SimApiAuth 认证元数据(使 OpenAPI 文档显示锁图标)。
*/
public func withSimApiAuth(auth: SimApiAuth): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(auth)
}
return this
}
/**
* @brief 为端点附加响应类型元数据(使动态路由也能生成 response schema)。
*/
public func withResponseType(typeInfo: TypeInfo): EndpointConventionBuilder {
this.add {
builder => builder.metadata.add(ApiResponseTypeMetadata(Some(typeInfo)))
}
return this
}
}
/**
* @brief 提供元数据集合扩展。
*/
extend EndpointMetadataCollection {
/**
* @brief 返回最后一个满足条件的元数据。
* @param filter 用于筛选元数据的条件。
* @return 最后一个满足条件的元数据;如果不存在则返回 `None`。
*/
public func getLastMetadata<T>(filter: (T) -> Bool): ?T {
let metadatas = this.getOrderedMetadata<T>()
var index = metadatas.size - 1
while (index >= 0) {
let metadata = metadatas[index]
if (filter(metadata)) {
return metadata
}
index--
}
return None
}
}
+46
View File
@@ -0,0 +1,46 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi
import std.reflect.*
import std.collection.*
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 文档生成选项。
*/
public class OpenApiOptions {
/**
* 未标注 groupName 的接口是否进入本文档。
* 多文档分组时,仅默认组(第一个 apiGroup 或 isDefault 的组)应设为 true。
*/
public var includeUnGrouped: Bool = false
/**
* @brief 确定指定类型在 components/schemas 中使用的引用 ID。
* 返回 None 时该 Schema 始终内联,不生成 $ref。
*/
public var createSchemaReferenceId: (TypeInfo) -> ?String = {typeInfo =>
let name = typeInfo.name
if (let Some(ltIdx) <- name.indexOf('<') && let Some(gtIdx) <- name.lastIndexOf('>') &&
gtIdx > ltIdx) {
let baseName = name[0..ltIdx]
let argsStr = name[ltIdx + 1..gtIdx]
let simpleName = if (let Some(dotIdx) <- argsStr.lastIndexOf('.')) {
argsStr[dotIdx + 1..]
} else {
argsStr
}
"${baseName}Of${simpleName}"
} else {
name
}
}
}
@@ -4,7 +4,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.services package simcu::simapi.openapi
import std.reflect.* import std.reflect.*
import std.collection.* import std.collection.*
@@ -12,7 +12,6 @@ import soulsoft_web_mvc.*
import soulsoft_web_mvc.utilities.* import soulsoft_web_mvc.utilities.*
import simcu::simapi.openapi.models.* import simcu::simapi.openapi.models.*
import soulsoft_web_mvc.controllers.* import soulsoft_web_mvc.controllers.*
import simcu::simapi.openapi.infrastructure.*
/** /**
* @brief 提供 OpenAPI 参数生成能力。 * @brief 提供 OpenAPI 参数生成能力。
@@ -7,7 +7,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.services package simcu::simapi.openapi
import std.time.* import std.time.*
import std.reflect.* import std.reflect.*
@@ -17,10 +17,8 @@ import stdx.encoding.json.*
import soulsoft_web_http.* import soulsoft_web_http.*
import simcu::simapi.openapi.models.* import simcu::simapi.openapi.models.*
import soulsoft_web_mvc.IActionResult import soulsoft_web_mvc.IActionResult
import simcu::simapi.openapi.metadata.* import simcu::simapi.annotations.*
import soulsoft_extensions_injection.* import soulsoft_extensions_injection.*
import simcu::simapi.openapi.transformers.*
import simcu::simapi.openapi.infrastructure.*
/** /**
* @brief 提供 OpenAPI Schema 生成功能。 * @brief 提供 OpenAPI Schema 生成功能。
@@ -105,21 +103,18 @@ protected class OpenApiSchemaService {
let `type` = OpenApiSchemaTypes.convert(typeInfo) let `type` = OpenApiSchemaTypes.convert(typeInfo)
let format = convertToSchemaFormat(typeInfo) let format = convertToSchemaFormat(typeInfo)
let schema = OpenApiSchema(`type`: `type`, format: format, nullable: nullable) let schema = OpenApiSchema(`type`: `type`, format: format, nullable: nullable)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema return schema
} }
// JsonValue/JsonObject/JsonArray 表示“任意 JSON 值树”,不应展开为固定 object schema。 // JsonValue/JsonObject/JsonArray 表示“任意 JSON 值树”,不应展开为固定 object schema。
if (typeInfo.isSubtypeOf(OpenApiSchemaService._jsonValueType)) { if (typeInfo.isSubtypeOf(OpenApiSchemaService._jsonValueType)) {
let schema = OpenApiSchema(nullable: nullable) let schema = OpenApiSchema(nullable: nullable)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema return schema
} }
// 处理文件上传 // 处理文件上传
if (typeInfo == OpenApiSchemaService._formFileType) { if (typeInfo == OpenApiSchemaService._formFileType) {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING, format: "binary") let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING, format: "binary")
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema return schema
} }
@@ -142,7 +137,6 @@ protected class OpenApiSchemaService {
if (let _: EnumTypeInfo <- typeInfo) { if (let _: EnumTypeInfo <- typeInfo) {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING, nullable: nullable) let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.STRING, nullable: nullable)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema return schema
} }
@@ -170,14 +164,12 @@ protected class OpenApiSchemaService {
// 先加入缓存,再处理属性,避免循环引用导致无限递归 // 先加入缓存,再处理属性,避免循环引用导致无限递归
_schemas.add(schemaId, schema) _schemas.add(schemaId, schema)
buildObjectProperties(schema, typeInfo) buildObjectProperties(schema, typeInfo)
applySchemaTransformers(schema, schemaId)
} }
return createReferenceSchema(schemaId, nullable) return createReferenceSchema(schemaId, nullable)
} else { } else {
// 强制内联:不缓存,不生成 $ref // 强制内联:不缓存,不生成 $ref
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT, nullable: nullable) let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT, nullable: nullable)
buildObjectProperties(schema, typeInfo) buildObjectProperties(schema, typeInfo)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema return schema
} }
} }
@@ -195,7 +187,6 @@ protected class OpenApiSchemaService {
private func createInlineObjectSchema(typeInfo: TypeInfo, nullable: Bool): OpenApiSchema { private func createInlineObjectSchema(typeInfo: TypeInfo, nullable: Bool): OpenApiSchema {
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT, nullable: nullable) let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.OBJECT, nullable: nullable)
buildObjectProperties(schema, typeInfo) buildObjectProperties(schema, typeInfo)
applySchemaTransformers(schema, typeInfo.qualifiedName)
return schema return schema
} }
@@ -207,7 +198,7 @@ protected class OpenApiSchemaService {
let propertyTypeInfo = underlyingType ?? property.typeInfo let propertyTypeInfo = underlyingType ?? property.typeInfo
let propertySchema = buildSchema(propertyTypeInfo, underlyingType.isSome(), true) let propertySchema = buildSchema(propertyTypeInfo, underlyingType.isSome(), true)
for (annotation in property.annotations) { for (annotation in property.annotations) {
if (let meta: IApiDescriptionMetadata <- annotation) { if (let meta: SimApiDoc <- annotation) {
propertySchema.description = meta.description propertySchema.description = meta.description
} }
} }
@@ -223,7 +214,7 @@ protected class OpenApiSchemaService {
let variableTypeInfo = underlyingType ?? variable.typeInfo let variableTypeInfo = underlyingType ?? variable.typeInfo
let variableSchema = buildSchema(variableTypeInfo, underlyingType.isSome(), true) let variableSchema = buildSchema(variableTypeInfo, underlyingType.isSome(), true)
for (annotation in variable.annotations) { for (annotation in variable.annotations) {
if (let meta: IApiDescriptionMetadata <- annotation) { if (let meta: SimApiDoc <- annotation) {
variableSchema.description = meta.description variableSchema.description = meta.description
} }
} }
@@ -244,8 +235,6 @@ protected class OpenApiSchemaService {
private func createArraySchema(elementTypeInfo: TypeInfo, preferReference: Bool): OpenApiSchema { private func createArraySchema(elementTypeInfo: TypeInfo, preferReference: Bool): OpenApiSchema {
let itemSchema = buildSchema(elementTypeInfo, false, preferReference) let itemSchema = buildSchema(elementTypeInfo, false, preferReference)
let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.ARRAY, items: itemSchema) let schema = OpenApiSchema(`type`: OpenApiSchemaTypes.ARRAY, items: itemSchema)
// transformer 面向当前数组 schema,本处使用容器类型名最准确;现阶段退化为元素类型名。
applySchemaTransformers(schema, elementTypeInfo.qualifiedName)
return schema return schema
} }
@@ -258,24 +247,12 @@ protected class OpenApiSchemaService {
if (keyTypeInfo != OpenApiSchemaService._stringType) { if (keyTypeInfo != OpenApiSchemaService._stringType) {
// 非字符串 key 无法直接表达为标准 JSON object key;当前保守输出为封闭 object。 // 非字符串 key 无法直接表达为标准 JSON object key;当前保守输出为封闭 object。
schema.additionalProperties = false schema.additionalProperties = false
applySchemaTransformers(schema, keyTypeInfo.qualifiedName)
return schema return schema
} }
schema.additionalPropertiesSchema = buildSchema(valueTypeInfo, false, true) schema.additionalPropertiesSchema = buildSchema(valueTypeInfo, false, true)
applySchemaTransformers(schema, valueTypeInfo.qualifiedName)
return schema return schema
} }
private func applySchemaTransformers(schema: OpenApiSchema, schemaName: String): Unit {
if (_documentName.isEmpty() || _options.schemaTransformers.isEmpty()) {
return
}
let context = OpenApiSchemaTransformerContext(_services, _documentName, schemaName)
for (transformer in _options.schemaTransformers) {
transformer.transform(schema, context)
}
}
/* /*
解析 Collection<T> 中的 T 解析 Collection<T> 中的 T
*/ */
@@ -1,17 +1,32 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/* /*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved. * Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the * This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.services package simcu::simapi.openapi
import std.io.* import std.io.*
import std.reflect.*
import stdx.encoding.json.stream.* import stdx.encoding.json.stream.*
import simcu::simapi.openapi.interfaces.* import simcu::simapi.openapi.models.*
/**
* @brief 响应类型元数据(供动态注册的路由指定返回类型)。
*/
public class ApiResponseTypeMetadata {
/**
* @brief 响应类型的类型信息。
*/
public let responseType: ?TypeInfo
/**
* @brief 创建响应类型元数据实例。
* @param responseType 响应类型的类型信息。
*/
public init(responseType: ?TypeInfo) {
this.responseType = responseType
}
}
/** /**
* @brief 提供基于 JSON 的 OpenAPI 写入器实现。 * @brief 提供基于 JSON 的 OpenAPI 写入器实现。
@@ -94,3 +109,40 @@ public class JsonOpenApiWriter <: IOpenApiWriter {
_writer.flush() _writer.flush()
} }
} }
/**
* @brief 表示带名称的服务标识。
*/
public class NamedService {
/**
* @brief 表示服务名称。
*/
public let name: String
/**
* @brief 创建带名称的服务标识实例。
* @param name 服务名称。
*/
public init(name: String) {
this.name = name
}
}
/*
Option类型操作
*/
protected class Nullable {
/**
* @brief 返回 Option 类型的底层类型。
* @param typeInfo 要解析的类型信息。
* @return 如果是 Option 类型则返回其底层类型,否则返回 `None`。
*/
public static func getUnderlyingType(typeInfo: TypeInfo): ?TypeInfo {
let qualifiedName = typeInfo.qualifiedName
if (!(qualifiedName.startsWith("Option<") && qualifiedName.endsWith('>'))) {
return None
}
let underlyingTypeName = qualifiedName[7..qualifiedName.size - 1]
return TypeInfo.get(underlyingTypeName)
}
}
@@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.infrastructure package simcu::simapi.openapi
import std.reflect.* import std.reflect.*
import std.collection.* import std.collection.*
-30
View File
@@ -1,30 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.services
import simcu::simapi.openapi.models.*
import soulsoft_extensions_injection.*
/**
* @brief 定义 OpenAPI 文档提供器接口。
*/
public interface IDocumentProvider {
/**
* @brief 返回已注册的文档名称集合。
* @return 当前可用的文档名称集合。
*/
func getDooucmentNames(): Collection<String>
/**
* @brief 按名称创建 OpenAPI 文档。
* @param documentName 要创建的文档名称。
* @return 生成后的 OpenAPI 文档。
*/
func create(documentName: String, services: IServiceProvider): OpenApiDocument
}
-28
View File
@@ -1,28 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.services
/**
* @brief 表示带名称的服务标识。
*/
public class NamedService {
/**
* @brief 表示服务名称。
*/
public let name: String
/**
* @brief 创建带名称的服务标识实例。
* @param name 服务名称。
*/
public init(name: String) {
this.name = name
}
}
-129
View File
@@ -1,129 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.services
import std.reflect.*
import std.collection.*
import simcu::simapi.openapi.models.*
import simcu::simapi.openapi.transformers.*
/**
* @brief 定义 OpenAPI 文档生成选项。
*/
public class OpenApiOptions {
private let _documentTransformers = ArrayList<IOpenApiDocumentTransformer>()
private let _operationTransformer = ArrayList<IOpenApiOperationTransformer>()
private let _schemaTransformers = ArrayList<IOpenApiSchemaTransformer>()
/**
* 未标注 groupName 的接口是否进入本文档。
* 多文档分组时,仅默认组(第一个 apiGroup 或 isDefault 的组)应设为 true。
*/
public var includeUnGrouped: Bool = false
/**
* @brief 确定指定类型在 components/schemas 中使用的引用 ID。
* 返回 None 时该 Schema 始终内联,不生成 $ref。
*/
public var createSchemaReferenceId: (TypeInfo) -> ?String = {typeInfo =>
let name = typeInfo.name
if (let Some(ltIdx) <- name.indexOf('<') && let Some(gtIdx) <- name.lastIndexOf('>') &&
gtIdx > ltIdx) {
let baseName = name[0..ltIdx]
let argsStr = name[ltIdx + 1..gtIdx]
let simpleName = if (let Some(dotIdx) <- argsStr.lastIndexOf('.')) {
argsStr[dotIdx + 1..]
} else {
argsStr
}
"${baseName}Of${simpleName}"
} else {
name
}
}
/**
* @brief 返回当前文档转换器集合。
* @return 已注册的文档转换器集合。
*/
protected prop documentTransformers: List<IOpenApiDocumentTransformer> {
get() {
_documentTransformers
}
}
/**
* @brief 返回当前操作转换器集合。
* @return 已注册的操作转换器集合。
*/
protected prop operationTransformers: List<IOpenApiOperationTransformer> {
get() {
_operationTransformer
}
}
/**
* @brief 返回当前架构转换器集合。
* @return 已注册的架构转换器集合。
*/
protected prop schemaTransformers: List<IOpenApiSchemaTransformer> {
get() {
_schemaTransformers
}
}
/**
* @brief 添加文档转换器实例。
* @param transformer 要添加的 OpenAPI 文档转换器。
*/
public func addDocumentTransformer(transformer: IOpenApiDocumentTransformer): Unit {
_documentTransformers.add(transformer)
}
/**
* @brief 以委托形式添加文档转换器。
* @param transformer 表示文档转换逻辑的委托。
*/
public func addDocumentTransformer(transformer: (OpenApiDocument, OpenApiDocumentTransformerContext) -> Unit): Unit {
_documentTransformers.add(DelegateOpenApiDocumentTransformer(transformer))
}
/**
* @brief 添加操作转换器实例。
* @param transformer 要添加的 OpenAPI 操作转换器。
*/
public func addOperationTransformer(transformer: IOpenApiOperationTransformer): Unit {
_operationTransformer.add(transformer)
}
/**
* @brief 以委托形式添加操作转换器。
* @param transformer 表示操作转换逻辑的委托。
*/
public func addOperationTransformer(transformer: (OpenApiOperation, OpenApiOperationTransformerContext) -> Unit): Unit {
_operationTransformer.add(DelegateOpenApiOperationTransformer(transformer))
}
/**
* @brief 添加架构转换器实例。
* @param transformer 要添加的 OpenAPI 架构转换器。
*/
public func addSchemaTransformer(transformer: IOpenApiSchemaTransformer): Unit {
_schemaTransformers.add(transformer)
}
/**
* @brief 以委托形式添加架构转换器。
* @param transformer 表示架构转换逻辑的委托。
*/
public func addSchemaTransformer(transformer: (OpenApiSchema, OpenApiSchemaTransformerContext) -> Unit): Unit {
_schemaTransformers.add(DelegateOpenApiSchemaTransformer(transformer))
}
}
@@ -1,36 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 将委托包装为 IOpenApiDocumentTransformer 的适配器实现。
*/
protected class DelegateOpenApiDocumentTransformer <: IOpenApiDocumentTransformer {
private let _transformer: (OpenApiDocument, OpenApiDocumentTransformerContext) -> Unit
/**
* @brief 创建委托文档转换器实例。
* @param transformer 表示文档转换逻辑的委托。
*/
public init(transformer: (OpenApiDocument, OpenApiDocumentTransformerContext) -> Unit) {
_transformer = transformer
}
/**
* @brief 调用委托转换指定的 OpenAPI 文档。
* @param document 要转换的 OpenAPI 文档。
* @param context 文档转换器上下文。
*/
public func transform(document: OpenApiDocument, context: OpenApiDocumentTransformerContext): Unit {
_transformer(document, context)
}
}
@@ -1,24 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import simcu::simapi.openapi.models.*
protected class DelegateOpenApiOperationTransformer <: IOpenApiOperationTransformer {
private let _transformer: (OpenApiOperation, OpenApiOperationTransformerContext) -> Unit
public init(transformer: (OpenApiOperation, OpenApiOperationTransformerContext) -> Unit) {
_transformer = transformer
}
public func transform(operation: OpenApiOperation, context: OpenApiOperationTransformerContext): Unit {
_transformer(operation, context)
}
}
@@ -1,36 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 将委托包装为 IOpenApiSchemaTransformer 的适配器实现。
*/
protected class DelegateOpenApiSchemaTransformer <: IOpenApiSchemaTransformer {
private let _transformer: (OpenApiSchema, OpenApiSchemaTransformerContext) -> Unit
/**
* @brief 创建委托架构转换器实例。
* @param transformer 表示架构转换逻辑的委托。
*/
public init(transformer: (OpenApiSchema, OpenApiSchemaTransformerContext) -> Unit) {
_transformer = transformer
}
/**
* @brief 调用委托转换指定的 OpenAPI 架构。
* @param schema 要转换的 OpenAPI 架构。
* @param context 架构转换器上下文。
*/
public func transform(schema: OpenApiSchema, context: OpenApiSchemaTransformerContext): Unit {
_transformer(schema, context)
}
}
@@ -1,24 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 文档转换器接口。
*/
public interface IOpenApiDocumentTransformer {
/**
* @brief 转换指定的 OpenAPI 文档。
* @param document 要转换的 OpenAPI 文档。
* @param context 文档转换器上下文。
*/
func transform(document: OpenApiDocument, context: OpenApiDocumentTransformerContext): Unit
}
@@ -1,24 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 操作转换器接口。
*/
public interface IOpenApiOperationTransformer {
/**
* @brief 转换指定的 OpenAPI 操作对象。
* @param operation 要转换的 OpenAPI 操作对象。
* @param context 操作转换器上下文。
*/
func transform(operation: OpenApiOperation, context: OpenApiOperationTransformerContext): Unit
}
@@ -1,24 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import simcu::simapi.openapi.models.*
/**
* @brief 定义 OpenAPI 架构转换器接口。
*/
public interface IOpenApiSchemaTransformer {
/**
* @brief 转换指定的 OpenAPI 架构。
* @param schema 要转换的 OpenAPI 架构。
* @param context 架构转换器上下文。
*/
func transform(schema: OpenApiSchema, context: OpenApiSchemaTransformerContext): Unit
}
@@ -1,36 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import soulsoft_extensions_injection.*
/**
* @brief 提供文档转换器执行时的上下文信息。
*/
public class OpenApiDocumentTransformerContext {
/**
* @brief 当前正在生成的文档名称。
*/
public let documentName: String
/**
* @brief 请求作用域的服务提供器。
*/
public let services: IServiceProvider
/**
* @brief 创建文档转换器上下文实例。
* @param services 请求作用域的服务提供器。
* @param documentName 当前正在生成的文档名称。
*/
public init(services: IServiceProvider, documentName: String) {
this.services = services
this.documentName = documentName
}
}
@@ -1,43 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import soulsoft_web_mvc.controllers.*
import soulsoft_extensions_injection.*
/**
* @brief 提供操作转换器执行时的上下文信息。
*/
public class OpenApiOperationTransformerContext {
/**
* @brief 当前正在生成的文档名称。
*/
public let documentName: String
/**
* @brief 请求作用域的服务提供器。
*/
public let services: IServiceProvider
/**
* @brief 当前操作关联的控制器动作描述。
*/
public let description: ?ControllerActionDescriptor
/**
* @brief 创建操作转换器上下文实例。
* @param services 请求作用域的服务提供器。
* @param documentName 当前正在生成的文档名称。
* @param description 当前操作关联的控制器动作描述。
*/
public init(services: IServiceProvider, documentName: String, description: ?ControllerActionDescriptor) {
this.services = services
this.documentName = documentName
this.description = description
}
}
@@ -1,42 +0,0 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
/*
* Copyright (c) 杭州颉创科技有限公司 2025. All rights reserved.
* This source file is licensed under the MIT License found in the
* LICENSE file in the root directory of this source tree.
*/
package simcu::simapi.openapi.transformers
import soulsoft_extensions_injection.*
/**
* @brief 提供架构转换器执行时的上下文信息。
*/
public class OpenApiSchemaTransformerContext {
/**
* @brief 当前正在生成的文档名称。
*/
public let documentName: String
/**
* @brief 请求作用域的服务提供器。
*/
public let services: IServiceProvider
/**
* @brief 当前架构在 components/schemas 中的键名(即类型限定名)。
*/
public let schemaName: String
/**
* @brief 创建架构转换器上下文实例。
* @param services 请求作用域的服务提供器。
* @param documentName 当前正在生成的文档名称。
* @param schemaName 当前架构在 components/schemas 中的键名。
*/
public init(services: IServiceProvider, documentName: String, schemaName: String) {
this.services = services
this.documentName = documentName
this.schemaName = schemaName
}
}
@@ -4,7 +4,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
package simcu::simapi.openapi.infrastructure package simcu::simapi.openapi
import std.collection.* import std.collection.*
+1 -2
View File
@@ -44,8 +44,7 @@ import simcu::simapi.interfaces.*
import simcu::simapi.logger.* import simcu::simapi.logger.*
import simcu::simapi.middlewares.* import simcu::simapi.middlewares.*
import simcu::simapi.openapi.* import simcu::simapi.openapi.*
import simcu::simapi.openapi.annotations.* import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAnnotation, SimApiDoc}
import simcu::simapi.annotations.{SimApiAuth as SimApiAuthAnnotation}
/** /**
* SimApi 扩展入口。 * SimApi 扩展入口。
+2 -2
View File
@@ -1,11 +1,11 @@
# 生成 SwaggerUIResources.cj:将 resources/openapi/ 下静态资源以 Base64 内联为 CJ 源常量。 # 生成 swagger_ui_resources.cj:将 resources/openapi/ 下静态资源以 Base64 内联为 CJ 源常量。
# 用法:pwsh tools/gen-swagger-ui-resources.ps1 # 用法:pwsh tools/gen-swagger-ui-resources.ps1
# 资源更新后重新运行此脚本即可。 # 资源更新后重新运行此脚本即可。
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
$root = Split-Path -Parent $PSScriptRoot $root = Split-Path -Parent $PSScriptRoot
$inDir = Join-Path $root "resources\openapi" $inDir = Join-Path $root "resources\openapi"
$outFile = Join-Path $root "src\openapi\SwaggerUIResources.cj" $outFile = Join-Path $root "src\openapi\swagger_ui_resources.cj"
$files = @( $files = @(
@{ Name = "allHtml"; File = "all.html" }, @{ Name = "allHtml"; File = "all.html" },