/* * Copyright (c) 2025 SimcuTeam. All rights reserved. * 移植自 C# 项目 SimApi(E:\simcu\simapi-net),遵循 MIT 许可证。 * 自定义 IRequestDelegateFactory:在结果派发时自动封装响应, * 对齐 C# SimApiResponseFilter(IResultFilter)的行为: * - 返回 SimApiBaseResponse 或其子类 → 原样输出 * - 返回 null/void(Unit)→ SimApiBaseResponse()({code:200, message:成功}) * - 返回 String → SimApiResponse(data 为字符串) * - 返回其他对象 → SimApiResponse(data 为对象) * * 注册方式:必须在 soulsoft addControllers() 之前注册(tryAddSingleton 先到先得)。 */ package simapi.extensions import std.collection.* import std.reflect.* import soulsoft_web_http.* import soulsoft_web_mvc.core.* import soulsoft_web_mvc.routing.* import soulsoft_web_mvc.controllers.* import soulsoft_web_mvc.modelBindings.* import soulsoft_web_mvc.abstractions.* import soulsoft_extensions_options.* import soulsoft_extensions_injection.* import simapi.attributes.{SimApiAuth, OriginResponse} import simapi.communications.* import simapi.configurations.* import simapi.controllers.* import simapi.helpers.{SimApiError} import simapi.interfaces.* /** * 自定义请求委托工厂:接管 soulsoft 的 ControllerRequestDelegateFactory, * 在结果派发时自动封装响应(对齐 C# SimApiResponseFilter)。 */ public class SimApiRequestDelegateFactory <: IRequestDelegateFactory { private let _mvcOptions: MvcOptions private let _modelBinder: IActionModelBinder public init(mvcOptions: IOptions, modelBinder: IActionModelBinder, services: IServiceProvider) { _mvcOptions = mvcOptions.value _modelBinder = modelBinder } public func createRequestDelegate(actionDescriptor: ControllerActionDescriptor): RequestDelegate { return { context => SimApiActionInvoker(context, _modelBinder, actionDescriptor, _mvcOptions).apply() } } } /** * 单次请求的动作执行器(复制 soulsoft ControllerActionInvoker, * dispatchResult 改为自动封装响应)。 */ struct SimApiActionInvoker { SimApiActionInvoker(let context: HttpContext, let modelBinder: IActionModelBinder, let actionDescriptor: ControllerActionDescriptor, let mvcOptions: MvcOptions) { } public func apply(): Unit { let controller = createControllerInstance() checkSimApiAuth() let modelBindingContext = ActionBindingContext(context, actionDescriptor.actionFunction.parameters) let boundParameters = modelBinder.bind(modelBindingContext) if (!modelBindingContext.modelState.isValid) { handleInvalidModelState(modelBindingContext) } else { let actionResult = actionDescriptor.actionFunction.apply(controller, boundParameters) dispatchResult(actionResult) } } /// 检查 @SimApiAuth 注解并执行鉴权(对齐 C# SimApiAuthAttribute.OnActionExecuting): /// 未登录 401 → 类型权限 403 → 遍历执行 ISimApiAuthChecker private func checkSimApiAuth() { var auth: ?SimApiAuth = None for (item in actionDescriptor.endpointMetadata) { if (let a: SimApiAuth <- item) { auth = Some(a) break } } if (let Some(auth) <- auth) { // 1. 未登录 → 401 var loginItem: SimApiLoginItem = SimApiLoginItem("") match (context.items.get("LoginInfo")) { case Some(v) => if (let l: SimApiLoginItem <- v) { loginItem = l } else { SimApiError.error(code: 401, message: "需要登录") } case None => SimApiError.error(code: 401, message: "需要登录") } // 2. 类型权限校验 → 403(对齐 C# Types.Intersect(loginInfo.Type).Any(),支持逗号分隔多类型) if (!auth.`type`.isEmpty()) { let requiredTypes = auth.`type`.split(",") var matched = false for (t in requiredTypes) { if (loginItem._types.contains(t)) { matched = true break } } if (!matched) { SimApiError.error(code: 403, message: "无权访问") } } // 3. 遍历执行 ISimApiAuthChecker(从 DI 按注册类型解析) let token = match (context.items.get("LoginToken")) { case Some(v) => if (let s: String <- v) { s } else { "" } case None => "" } let options = context.services.getOrThrow() for (checkerType in options.authCheckers) { let instance = context.services.getOrThrow(checkerType) if (let checker: ISimApiAuthChecker <- instance) { checker.run(loginItem, token) } } } } /// 模型绑定失败时写入 ProblemDetails 响应 private func handleInvalidModelState(modelBindingContext: ActionBindingContext) { let options = context.services.getOrThrow>() if (let Some(factory) <- options.value.invalidModelStateResponseFactory) { let actionContext = ActionContext(context, modelBindingContext.modelState) factory(actionContext).invoke(context) } else { let details = createValidationProblemDetails(modelBindingContext) if (let Some(status) <- details.status) { context.response.statusCode = UInt16(status) } context.response.writeAsJson(details) } } /// 结果派发 + 自动封装(对齐 C# SimApiResponseFilter) private func dispatchResult(actionResult: Any) { // @OriginResponse:跳过统一封装,原样输出(对齐 C# OnResultExecuting 遇注解直接 return) var originResponse = false for (item in actionDescriptor.endpointMetadata) { if (item is OriginResponse) { originResponse = true break } } if (originResponse) { if (let s: String <- actionResult) { // String 原样输出文本(对齐 C# string 返回直接写入) context.response.contentType = "application/json; charset=utf-8" context.response.write(s) } else { ObjectResult(actionResult).invoke(context) } return } // 统一封装(IActionResult/SimApiBaseResponse/String/Unit/其他对象) SimApiResultWriter.write(context, actionResult) } /// 根据 ModelState 错误构建 ValidationProblemDetails private func createValidationProblemDetails(modelBindingContext: ActionBindingContext) { let details = ValidationProblemDetails() if (hasUnsupportedContentTypeError(modelBindingContext.modelState)) { details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.16" details.title = "Unsupported Media Type" details.status = 415 } else { details.`type` = "https://tools.ietf.org/html/rfc9110#section-15.5.1" details.title = "One or more validation errors occurred." details.status = 400 for ((name, entry) in modelBindingContext.modelState) { details.errors.add(name, entry.errors |> map {f => f.description} |> collectArray) } } return details } /// 检查 ModelState 中是否含有 UnsupportedContentTypeException 错误 private func hasUnsupportedContentTypeError(modelState: ModelStateDictionary) { for ((_, entry) in modelState) { for (error in entry.errors) { if (error.exception.flatMap {f => f as UnsupportedContentTypeException}.isSome()) { return true } } } return false } /// 通过 DI 容器实例化控制器,并注入当前 HttpContext private func createControllerInstance(): Object { let instance = ActivatorUtilities.createInstance(context.services, actionDescriptor.controllerType) if (let controller: SimApiBaseController <- instance) { controller.bindRequestContext(context) } return instance } }