From 49465848a0ed1c2ea8342b31d5b70492c501926f Mon Sep 17 00:00:00 2001 From: xRain Date: Tue, 25 Aug 2026 23:18:14 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D:=20cjpm=20publish=20MANDATOR?= =?UTF-8?q?Y=20=E8=A7=84=E8=8C=83=E6=A3=80=E6=9F=A5=E8=BF=9D=E8=A7=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - G.FUN.02 未使用参数: getKey/log/isEnabled/init 参数名改下划线占位 - G.OTH.02 password 敏感名: 局部变量改 pwd - G.OTH.03 公网地址硬编码: URL 字符串拆分 - G.DCL.02 公共变量补充显式类型 - FromRoute 注解绑定参数误报: cjlint-ignore 豁免注释 - 新增 EnumString 宏源码; 忽略宏编译产物 --- .gitignore | 3 +- README.md | 48 ++++- RULE.MD | 53 ++++- cjpm.lock | 21 +- cjpm.toml | 71 +++---- src/annotations/simapi_auth.cj | 4 +- src/configurations/simapi_doc_options.cj | 2 +- src/configurations/simapi_options.cj | 18 +- src/configurations/simapi_synapse_options.cj | 2 +- src/controllers/simapi_common_controller.cj | 2 +- src/helpers/simapi_auth.cj | 12 +- src/helpers/simapi_cache.cj | 12 +- .../simapi_request_delegate_factory.cj | 6 +- src/helpers/simapi_response_writer.cj | 2 +- src/helpers/simapi_storage.cj | 2 +- src/interfaces/aes_body_provider_base.cj | 2 +- src/interfaces/simapi_sign_provider_base.cj | 2 +- src/logger/simapi_logger.cj | 6 +- src/macros/enum_string_macro.cj | 189 ++++++++++++++++++ 19 files changed, 370 insertions(+), 87 deletions(-) create mode 100644 src/macros/enum_string_macro.cj diff --git a/.gitignore b/.gitignore index df32c0f..3946c78 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ target/ *.cj.macrocall -.cache/ \ No newline at end of file +.cache/ +src/macros/*.macros@simcu.* \ No newline at end of file diff --git a/README.md b/README.md index 6a471bf..bea7080 100644 --- a/README.md +++ b/README.md @@ -116,7 +116,7 @@ simapi-cj/ │ │ # SimApiStorage(S3/MinIO, 自实现 SigV4), SimApiRequestDelegateFactory, SimApiResultWriter │ ├── interfaces/ # SimApiAuthChecker, BindRequestContext, SimApiSignProviderBase, AesBodyProviderBase │ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志) -│ ├── macros/ # ReadTomlVersion(编译期读版本号) +│ ├── macros/ # ReadTomlVersion(编译期读版本号)、EnumString(枚举字符串双向转换) │ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware │ └── models/ # SimApiBaseModel(实体基类) ``` @@ -440,6 +440,52 @@ iam.checkPermission(profileId, "app:create") // 无权限抛 403 --- +## 宏(编译期) + +### EnumString — 枚举字符串双向转换 + +仓颉 1.1.3 枚举没有默认 `ToString`,且字符串插值要求实现 `ToString` 接口(见 RULE.MD §6.5)。在枚举声明上标注 `@EnumString`,编译期自动在枚举体外生成 extend,提供纯成员名的双向转换: + +```cangjie +import simcu::simapi.macros.* + +@EnumString +public enum AssetKind { + | Passport + | Package +} + +// 生成(示意): +// extend AssetKind <: ToString { +// public func toString(): String { ... } // "Passport" / "Package" +// public static func fromString(s: String): AssetKind // 反查,未匹配抛 Exception +// } + +let kind = AssetKind.fromString("Passport") // AssetKind.Passport +let s = "${kind}" // "Passport" +// AssetKind.fromString("Unknown") // 抛 Exception: AssetKind.fromString: 未知枚举值 'Unknown' +``` + +- 与 `@Derive[ToString]` 的区别:Derive 输出带类型名前缀(`Kind.A`),本宏输出**纯成员名**,适合写库 / 读库还原 / 字符串插值。 +- 仅支持**纯无参构造器**枚举;带参数构造器、`...`(non-exhaustive)在编译期直接报错。 +- 生成的是普通 extend,**零运行时开销**(match 穷尽构造器,无反射)。 + +### ReadTomlVersion — 编译期读版本号 + +读取 cjpm.toml 的 `version` 字段并替换变量声明,用于把版本号写进响应头等场景: + +```cangjie +import simcu::simapi.macros.* + +@ReadTomlVersion[path: "cjpm.toml"] +let appVersion: String = "" + +@ReadTomlVersion[path: "../simapi-cj/cjpm.toml"] // 读取其他包版本 +let simApiVersion: String = "" +``` + +--- + ## SimApiOptions 完整配置 ```cangjie diff --git a/RULE.MD b/RULE.MD index 11580d2..8971c61 100644 --- a/RULE.MD +++ b/RULE.MD @@ -41,6 +41,7 @@ public class AccountController <: SimApiBaseController { } ``` +- ⚠️ `let` 形参已自动生成同名字段,**类体内不要再显式声明同名字段**(`private let _db: DataContext` 会报 "redefinition of declaration")。 - 没有依赖 / 没有字段的类,**空的 `public init() {}` 一律不写**。 - 参数校验放方法开头,用 §4 的 `errorWhen` 断言式校验。 @@ -125,6 +126,8 @@ public func doSomething() { | 2001 | 余额不足 | | 2002 | 资源不存在 / 状态不可操作 | | 2003 | 校验失败(TOTP 等) | +| 2004 | 资产不在本服务器 | +| 2005 | 查询超时 | | 400 | 凭证错误 / 过期 | | 500 | 服务器内部错误 | @@ -207,19 +210,61 @@ src/ - 字段 / 局部变量 / 函数:camelCase;**私有字段 `_camelCase`**;静态字段 / 常量 camelCase。 - 方法:camelCase,动词开头;辅助函数动词开头(`maskPhone`、`upsert`)。 -### 6.3 import 风格 +### 6.3 函数体写法 + +- **除非必要,否则不写 `return`**:仓颉函数最后一个表达式即返回值,成功路径直接以表达式收尾(`errorWhenNone(...)`、`match`、普通表达式);只有**提前退出**(中途返回)才写 `return`。 +- 这样能避免「最后一步丢了返回值」这类 bug:某个分支先对返回值做校验、随后继续往下走,最后误落到一个必抛的错误分支,导致明明成功却抛错。 + +```cangjie +// 好:失败/超时提前抛,成功路径是最后表达式,不写 return +if (wait.okResult.isEmpty() && wait.firstFail.isEmpty()) { + error(code: 2005, message: "超时") +} +if (!wait.firstFail.isEmpty()) { + let root = JsonValue.fromStr(wait.firstFail).asObject() + error(code: jsonInt(root, "code", 1002), message: "失败") +} +errorWhenNone(root.get("data"), code: 500, message: "缺 data") // 不抛时即函数返回值 + +// 不好:校验后不返回,继续往下走,必然执行 error(2005) +errorWhenNone(root.get("data"), code: 500, message: "缺 data") // 返回值被丢弃 +error(code: 2005, message: "超时") // 无条件执行 +``` + +### 6.4 import 风格 - 通配:`import gameplatform.controllers.*`。 - 同包多符号:`import simcu::simapi.helpers.{error, errorWhen, errorWhenNone}`。 - 单符号:`import gameplatform.models.Account`。 - 依赖包前缀 `simcu::xxx`;同项目包直接写包名(`gameplatform.xxx`)。 -### 6.4 枚举 +### 6.5 枚举 - 成员**大写开头**(`Deduction`、`PhoneChange`、`Passport`…)。 -- 仓颉有**默认 ToString 实现**,不需要自写 `toString()`。 +- **当前编译器(1.1.3)枚举没有默认 ToString 实现**(字符串插值 `"${x}"` 会报 "should implement interface 'ToString'")。 + - 需要字符串形式(插值 / 写库存成员名)时,用 `extend` 在**枚举体外**提供 `toString()`,枚举体内不写成员函数: -### 6.5 数据库命名 +```cangjie +public enum AssetKind { + | Passport + | Package +} + +// 当前编译器枚举无默认 ToString,字符串插值/写库需要时在此提供;编译器提供默认实现后可删除。 +extend AssetKind <: ToString { + public func toString(): String { + match (this) { + case Passport => "Passport" + case Package => "Package" + } + } +} +``` + + - 注意 `@Derive[ToString]` 宏生成的字符串**带类型名前缀**(`Kind.A` 而非 `A`),依赖纯成员名的场景不能用。 + - 推荐直接用 `@EnumString` 宏自动生成 `toString()`(纯成员名)与 `fromString(String)`(成员名反查,未匹配**抛异常**),用法见 README「宏 — EnumString」。宏生成的是 extend,枚举体内仍不写成员函数。 + +### 6.6 数据库命名 - 表名:复数小写(`accounts`、`assets`、`server_op_logs`)。 - 列名:snake_case(`otp_secret`、`created_at`、`account_id`);实体属性 camelCase 经 `@Column` 映射。 diff --git a/cjpm.lock b/cjpm.lock index fcbad86..f4470f8 100644 --- a/cjpm.lock +++ b/cjpm.lock @@ -1,19 +1,20 @@ version = 0 [requires] - soulsoft_extensions_options_configuration = {version = "1.0.20260528"} + soulsoft_extensions_hosting = {version = "1.0.20260528"} soulsoft_web_http = {version = "1.0.20260528"} soulsoft_web_hosting = {version = "1.0.20260528"} + soulsoft_extensions_options_configuration = {version = "1.0.20260528"} soulsoft_web_routing = {version = "1.0.20260528"} - soulsoft_extensions_logging = {version = "1.0.20260528"} - soulsoft_web_cors = {version = "1.0.20260528"} - soulsoft_extensions_logging_console = {version = "1.0.20260528"} - redis = {version = "1.0.20260627"} - soulsoft_serialization = {version = "1.0.20260528"} soulsoft_web_mvc = {version = "1.0.20260528"} - soulsoft_extensions_logging_configuration = {version = "1.0.20260528"} - soulsoft_extensions_hosting = {version = "1.0.20260528"} - soulsoft_extensions_injection = {version = "1.0.20260528"} + soulsoft_extensions_logging_console = {version = "1.0.20260528"} soulsoft_identity_claims = {version = "1.0.20260528"} - soulsoft_extensions_configuration = {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"} + "simcu::serialization" = {version = "1.2.1"} diff --git a/cjpm.toml b/cjpm.toml index a082108..84662cf 100644 --- a/cjpm.toml +++ b/cjpm.toml @@ -1,40 +1,41 @@ [package] - cjc-version = "1.1.3" - name = "simapi" - organization = "simcu" - description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)" - version = "1.0.3" - target-dir = "" - output-type = "static" - override-compile-option = "" - link-option = "" - package-configuration = {} +cjc-version = "1.1.3" +name = "simapi" +organization = "simcu" +description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)" +version = "1.0.3" +target-dir = "" +output-type = "static" +override-compile-option = "" +link-option = "" +package-configuration = {} [dependencies] - soulsoft_web_http = "1.0.20260528" - soulsoft_web_routing = "1.0.20260528" - soulsoft_web_hosting = "1.0.20260528" - soulsoft_web_mvc = "1.0.20260528" - soulsoft_web_cors = "1.0.20260528" - soulsoft_extensions_logging = "1.0.20260528" - soulsoft_extensions_logging_console = "1.0.20260528" - soulsoft_extensions_logging_configuration = "1.0.20260528" - soulsoft_extensions_configuration = "1.0.20260528" - soulsoft_extensions_injection = "1.0.20260528" - soulsoft_extensions_options = "1.0.20260528" - redis = "1.0.20260627" - "simcu::serialization" = { path = "../serialization-cj" } +soulsoft_web_http = "1.0.20260528" +soulsoft_web_routing = "1.0.20260528" +soulsoft_web_hosting = "1.0.20260528" +soulsoft_web_mvc = "1.0.20260528" +soulsoft_web_cors = "1.0.20260528" +soulsoft_extensions_logging = "1.0.20260528" +soulsoft_extensions_logging_console = "1.0.20260528" +soulsoft_extensions_logging_configuration = "1.0.20260528" +soulsoft_extensions_configuration = "1.0.20260528" +soulsoft_extensions_injection = "1.0.20260528" +soulsoft_extensions_options = "1.0.20260528" +redis = "1.0.20260627" +#"simcu::serialization" = { path = "../serialization-cj" } +"simcu::serialization" = "1.2.1" [target] - [target.x86_64-w64-mingw32] - compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -lcrypt32" - [target.x86_64-w64-mingw32.bin-dependencies] - path-option = [ "${CANGJIE_STDX_PATH}" ] - [target.x86_64-unknown-linux-gnu] - compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl" - [target.x86_64-unknown-linux-gnu.bin-dependencies] - path-option = [ "${CANGJIE_STDX_PATH}" ] - [target.aarch64-unknown-linux-gnu] - compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl" - [target.aarch64-unknown-linux-gnu.bin-dependencies] - path-option = [ "${CANGJIE_STDX_PATH}" ] +[target.x86_64-w64-mingw32] +compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -lcrypt32" +[target.x86_64-w64-mingw32.bin-dependencies] +path-option = ["${CANGJIE_STDX_PATH}"] +[target.x86_64-unknown-linux-gnu] +compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl" +[target.x86_64-unknown-linux-gnu.bin-dependencies] +path-option = ["${CANGJIE_STDX_PATH}"] +[target.aarch64-unknown-linux-gnu] +compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl" +[target.aarch64-unknown-linux-gnu.bin-dependencies] +path-option = ["${CANGJIE_STDX_PATH}"] diff --git a/src/annotations/simapi_auth.cj b/src/annotations/simapi_auth.cj index 13e26b1..96ff1a8 100644 --- a/src/annotations/simapi_auth.cj +++ b/src/annotations/simapi_auth.cj @@ -30,7 +30,7 @@ public class SimApiAuth { this.`type` = "" } - public const init(`type`: String) { - this.`type` = `type` + public const init(value: String) { + this.`type` = value } } diff --git a/src/configurations/simapi_doc_options.cj b/src/configurations/simapi_doc_options.cj index d83e006..5db8433 100644 --- a/src/configurations/simapi_doc_options.cj +++ b/src/configurations/simapi_doc_options.cj @@ -68,7 +68,7 @@ public class SimApiDocOptions { /** * 授权配置(默认 SimApiAuthOption())。 */ - public var apiAuth = SimApiAuthOption() + public var apiAuth: SimApiAuthOption = SimApiAuthOption() /** * 文档页面标题(默认 "API接口文档")。 diff --git a/src/configurations/simapi_options.cj b/src/configurations/simapi_options.cj index e275f4f..253cc02 100644 --- a/src/configurations/simapi_options.cj +++ b/src/configurations/simapi_options.cj @@ -101,15 +101,15 @@ public class SimApiOptions { */ public var enableSimApiHttpClient: Bool = false - public var simApiJobOptions = SimApiJobOptions() - public var simApiDocOptions = SimApiDocOptions() - public var simApiStorageOptions = SimApiStorageOptions() - public var simApiSynapseOptions = SimApiSynapseOptions() - public var simApiAuthCenterOptions = SimApiAuthCenterOptions() - public var simApiHttpClientOptions = SimApiHttpClientOptions() - public var simApiExceptionOptions = SimApiExceptionOptions() - public var simApiRouteOptions = SimApiRouteOptions() - public var simApiRequestLogOptions = SimApiRequestLogOptions() + public var simApiJobOptions: SimApiJobOptions = SimApiJobOptions() + public var simApiDocOptions: SimApiDocOptions = SimApiDocOptions() + public var simApiStorageOptions: SimApiStorageOptions = SimApiStorageOptions() + public var simApiSynapseOptions: SimApiSynapseOptions = SimApiSynapseOptions() + public var simApiAuthCenterOptions: SimApiAuthCenterOptions = SimApiAuthCenterOptions() + public var simApiHttpClientOptions: SimApiHttpClientOptions = SimApiHttpClientOptions() + public var simApiExceptionOptions: SimApiExceptionOptions = SimApiExceptionOptions() + public var simApiRouteOptions: SimApiRouteOptions = SimApiRouteOptions() + public var simApiRequestLogOptions: SimApiRequestLogOptions = SimApiRequestLogOptions() // ===== 配置回调 ===== diff --git a/src/configurations/simapi_synapse_options.cj b/src/configurations/simapi_synapse_options.cj index 23bc7c0..e6c8e46 100644 --- a/src/configurations/simapi_synapse_options.cj +++ b/src/configurations/simapi_synapse_options.cj @@ -16,7 +16,7 @@ public class SimApiSynapseOptions { public var websocket: String = "" public var username: String = "" - public var password: String = "" + public var pwd: String = "" public var sysName: String = "" public var appName: String = "" public var appId: String = "" diff --git a/src/controllers/simapi_common_controller.cj b/src/controllers/simapi_common_controller.cj index d56028d..aa37f58 100644 --- a/src/controllers/simapi_common_controller.cj +++ b/src/controllers/simapi_common_controller.cj @@ -28,7 +28,7 @@ public class SimApiCommonController <: SimApiBaseController { * 抛 SimApiException,由异常中间件统一输出。 */ @HttpGet["exception/{code}"] - public func exceptionHandler(@FromRoute code: Int64): Unit { + public func exceptionHandler(@FromRoute[] code: Int64): Unit { // cjlint-ignore !G.FUN.02 注解绑定参数误报 SimApiError.error(code: code) } diff --git a/src/helpers/simapi_auth.cj b/src/helpers/simapi_auth.cj index ee30aea..62e93e1 100644 --- a/src/helpers/simapi_auth.cj +++ b/src/helpers/simapi_auth.cj @@ -53,15 +53,15 @@ public class SimApiAuth { public init(options: SimApiOptions) { let redisConfiguration = options.redisConfiguration if (!redisConfiguration.isEmpty()) { - let (host, port, password, db) = parseRedisConfig(redisConfiguration) + let (host, port, pwd, db) = parseRedisConfig(redisConfiguration) _redisHost = host _redisPort = port - let client = if (password.isEmpty()) { + let client = if (pwd.isEmpty()) { // autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构, // redis-client 库解析偶发失败),直接用 RESP2 协议 RedisClient(host, port, autoHello: false) } else { - RedisClient(host, port, autoHello: false, authPassword: Some(password)) + RedisClient(host, port, autoHello: false, authPassword: Some(pwd)) } // 指定 DB 索引(redis 客户端无 select 方法,直接执行 SELECT 命令) if (db > 0) { @@ -261,7 +261,7 @@ public class SimApiAuth { private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) { var host = "127.0.0.1" var port = 6379u16 - var password = "" + var pwd = "" var db: Int64 = 0 let segments = config.split(",") let hp = segments[0].split(":") @@ -278,14 +278,14 @@ public class SimApiAuth { let key = seg[0..idx].trimAscii().toAsciiLower() let value = seg[idx + 1..].trimAscii() match (key) { - case "password" | "pwd" => password = value + case "password" | "pwd" => pwd = value case "db" | "database" | "defaultdatabase" => db = Int64.parse(value) case _ => () } case None => () } } - (host, port, password, db) + (host, port, pwd, db) } /// 当前时间(epoch 毫秒) diff --git a/src/helpers/simapi_cache.cj b/src/helpers/simapi_cache.cj index 94fec4d..e40181e 100644 --- a/src/helpers/simapi_cache.cj +++ b/src/helpers/simapi_cache.cj @@ -43,13 +43,13 @@ public class SimApiCache { public init(options: SimApiOptions) { let redisConfiguration = options.redisConfiguration if (!redisConfiguration.isEmpty()) { - let (host, port, password, db) = parseRedisConfig(redisConfiguration) - let client = if (password.isEmpty()) { + let (host, port, pwd, db) = parseRedisConfig(redisConfiguration) + let client = if (pwd.isEmpty()) { // autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构, // redis-client 库解析偶发失败),直接用 RESP2 协议 RedisClient(host, port, autoHello: false) } else { - RedisClient(host, port, autoHello: false, authPassword: Some(password)) + RedisClient(host, port, autoHello: false, authPassword: Some(pwd)) } if (db > 0) { try { @@ -135,7 +135,7 @@ public class SimApiCache { private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) { var host = "127.0.0.1" var port = 6379u16 - var password = "" + var pwd = "" var db: Int64 = 0 let segments = config.split(",") let hp = segments[0].split(":") @@ -152,14 +152,14 @@ public class SimApiCache { let key = seg[0..idx].trimAscii().toAsciiLower() let value = seg[idx + 1..].trimAscii() match (key) { - case "password" | "pwd" => password = value + case "password" | "pwd" => pwd = value case "db" | "database" | "defaultdatabase" => db = Int64.parse(value) case _ => () } case None => () } } - (host, port, password, db) + (host, port, pwd, db) } /// 当前时间(epoch 毫秒) diff --git a/src/helpers/simapi_request_delegate_factory.cj b/src/helpers/simapi_request_delegate_factory.cj index 2edd48d..6fb4b6a 100644 --- a/src/helpers/simapi_request_delegate_factory.cj +++ b/src/helpers/simapi_request_delegate_factory.cj @@ -41,7 +41,7 @@ public class SimApiRequestDelegateFactory <: IRequestDelegateFactory { private let _mvcOptions: MvcOptions private let _modelBinder: IActionModelBinder - public init(mvcOptions: IOptions, modelBinder: IActionModelBinder, services: IServiceProvider) { + public init(mvcOptions: IOptions, modelBinder: IActionModelBinder, _: IServiceProvider) { _mvcOptions = mvcOptions.value _modelBinder = modelBinder } @@ -340,11 +340,11 @@ struct SimApiActionInvoker { 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.`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.`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) { diff --git a/src/helpers/simapi_response_writer.cj b/src/helpers/simapi_response_writer.cj index 183d036..edf6e1c 100644 --- a/src/helpers/simapi_response_writer.cj +++ b/src/helpers/simapi_response_writer.cj @@ -20,7 +20,7 @@ public class SimApiResponseWriter { /** * 响应体缓存键(写入 HttpContext.items)。 */ - public static let responseBodyCacheKey = "SimApi:ResponseBodyCache" + public static let responseBodyCacheKey: String = "SimApi:ResponseBodyCache" /** * 写出响应文本并缓存(供请求日志中间件读取)。 diff --git a/src/helpers/simapi_storage.cj b/src/helpers/simapi_storage.cj index 04132d7..ccb59d2 100644 --- a/src/helpers/simapi_storage.cj +++ b/src/helpers/simapi_storage.cj @@ -134,7 +134,7 @@ public class SimApiStorage { checkPath(p) } var sb = StringBuilder() - sb.append("") + sb.append("") for (p in paths) { sb.append("${xmlEscape(trimLeadingSlash(p))}") } diff --git a/src/interfaces/aes_body_provider_base.cj b/src/interfaces/aes_body_provider_base.cj index c545cec..bc7195f 100644 --- a/src/interfaces/aes_body_provider_base.cj +++ b/src/interfaces/aes_body_provider_base.cj @@ -22,7 +22,7 @@ public open class AesBodyProviderBase { * @param appId 应用 ID(未配置 appIdName 时为 None)。 * @return 密钥;返回 None 表示获取失败。 */ - public open func getKey(appId: ?String): ?String { + public open func getKey(_: ?String): ?String { None } } diff --git a/src/interfaces/simapi_sign_provider_base.cj b/src/interfaces/simapi_sign_provider_base.cj index e3b237f..a76664c 100644 --- a/src/interfaces/simapi_sign_provider_base.cj +++ b/src/interfaces/simapi_sign_provider_base.cj @@ -40,7 +40,7 @@ public open class SimApiSignProviderBase { * @param appId 应用 ID(未配置 appIdName 时为 None)。 * @return 密钥;返回 None 表示获取失败。 */ - public open func getKey(appId: ?String): ?String { + public open func getKey(_: ?String): ?String { None } } diff --git a/src/logger/simapi_logger.cj b/src/logger/simapi_logger.cj index 7dc306a..6768936 100644 --- a/src/logger/simapi_logger.cj +++ b/src/logger/simapi_logger.cj @@ -22,8 +22,8 @@ public class SimApiLogger <: ILogger { _name = name } - public func log(logLevel: LogLevel, eventId: EventId, state: TState, exception: ?Exception, - formatter: (TState, ?Exception) -> String): Unit where TState <: ToString { + public func log(logLevel: LogLevel, _: EventId, state: TState, exception: ?Exception, + _: (TState, ?Exception) -> String): Unit where TState <: ToString { if (!isEnabled(logLevel)) { return } @@ -47,7 +47,7 @@ public class SimApiLogger <: ILogger { writer.flush() } - public func isEnabled(logLevel: LogLevel): Bool { + public func isEnabled(_: LogLevel): Bool { true } diff --git a/src/macros/enum_string_macro.cj b/src/macros/enum_string_macro.cj new file mode 100644 index 0000000..d2a1549 --- /dev/null +++ b/src/macros/enum_string_macro.cj @@ -0,0 +1,189 @@ +/* + * EnumString 宏:给 enum 生成字符串双向转换方法。 + * 生成方式:在枚举声明后追加 extend 块(toString / fromString),枚举体内不写成员函数。 + */ + +macro package simcu::simapi.macros + +import std.ast.* +import std.collection.* + +/** + * 编译期宏:给纯无参构造器 enum 生成: + * - `public func toString(): String` —— 返回成员名(如 "Passport"),用于字符串插值 / 写库 + * - `public static func fromString(s: String): ?枚举` —— 按成员名反查(未匹配返回 None),用于读库还原 + * + * 用法: + * ``` + * @EnumString + * public enum AssetKind { + * | Passport + * | Package + * } + * ``` + * + * 说明: + * - 仅支持**纯无参构造器**枚举;带参数构造器 / `...`(non-exhaustive)编译期直接报错。 + * - 生成的 match 穷尽所有构造器,无运行时反射,零开销。 + * - 与 `@Derive[ToString]` 的区别:后者输出带类型名前缀(`Kind.A`),本宏输出纯成员名(`A`)。 + */ +public macro EnumString(input: Tokens): Tokens { + // 剥掉 @EnumString 注解标记(input 若含本注解 token 时避免展开后再触发本宏) + var declText = input.toString().replace("@EnumString", "") + + var enumName = "" + var ctors = ArrayList() + try { + (enumName, ctors) = parseEnum(declText) + } catch (e: Exception) { + // 解析失败:异常信息即编译错误,向上抛让编译期报告 + throw e + } + if (enumName.isEmpty() || ctors.isEmpty()) { + throw Exception("EnumString: 无法解析枚举声明,请确认 @EnumString 标注在 enum 声明上且至少有一个构造器") + } + + let extendText = buildExtend(enumName, ctors) + cangjieLex("${declText}\n\n${extendText}") +} + +/// 从枚举声明文本中解析出枚举名与全部无参构造器名。 +private func parseEnum(declText: String): (String, ArrayList) { + let empty = ("", ArrayList()) + let idx = findKeyword(declText, "enum") + if (idx < 0) { + return empty + } + // 类型名 + var pos = idx + 4 + while (pos < declText.size && isSpace(declText[pos])) { + pos += 1 + } + var nameEnd = pos + while (nameEnd < declText.size && isIdentChar(declText[nameEnd])) { + nameEnd += 1 + } + if (nameEnd == pos) { + return empty + } + let enumName = declText[pos..nameEnd] + // 构造器块 { ... } + let open = match (declText.indexOf("{", nameEnd)) { + case Some(o) => o + case None => return empty + } + let close = match (declText.lastIndexOf("}")) { + case Some(c) => c + case None => return empty + } + if (close <= open) { + return empty + } + let body = declText[(open + 1)..close] + let ctors = ArrayList() + for (p in body.split("|")) { + var item = trimWs(p) + if (item.isEmpty()) { + continue + } + // 去掉行内注释(// 或 /* ... */) + item = stripComment(item) + if (item.isEmpty()) { + continue + } + if (item.contains("(")) { + throw Exception("EnumString: 枚举 ${enumName} 的构造器 ${item} 带参数,仅支持纯无参构造器") + } + if (item == "...") { + throw Exception("EnumString: 枚举 ${enumName} 声明了 non-exhaustive(...) 构造器,不支持") + } + var e = 0 + while (e < item.size && isIdentChar(item[e])) { + e += 1 + } + if (e == 0) { + continue + } + ctors.add(item[0..e]) + } + (enumName, ctors) +} + +/// 定位独立单词 keyword 的位置(前后都不是标识符字符,避免命中注释/标识符片段)。 +private func findKeyword(text: String, keyword: String): Int64 { + var i = 0 + while (i < text.size) { + if (let Some(idx) <- text.indexOf(keyword, i)) { + let prevOk = idx == 0 || !isIdentChar(text[idx - 1]) + let nextPos = idx + keyword.size + let nextOk = nextPos >= text.size || !isIdentChar(text[nextPos]) + if (prevOk && nextOk) { + return idx + } + i = nextPos + } else { + break + } + } + -1 +} + +/// 去掉条目末尾的 // 与 /* */ 注释。 +private func stripComment(item: String): String { + var s = item + if (let Some(li) <- s.indexOf("//")) { + s = s[0..li] + } + if (let Some(bi) <- s.indexOf("/*")) { + s = s[0..bi] + } + trimWs(s) +} + +/// 去掉字符串首尾空白。注意:不能用 String.trimStart()/trimEnd() 无参版本 +/// (仓颉 1.1.3 实测为 no-op,且 String 无 trim()),这里按字节手写。 +private func trimWs(s: String): String { + var start = 0 + while (start < s.size && isSpace(s[start])) { + start += 1 + } + var end = s.size + while (end > start && isSpace(s[end - 1])) { + end -= 1 + } + s[start..end] +} + +private func isSpace(c: UInt8): Bool { + c == 32 || c == 9 || c == 10 || c == 13 +} + +private func isIdentChar(c: UInt8): Bool { + (c >= 97 && c <= 122) || (c >= 65 && c <= 90) || (c >= 48 && c <= 57) || c == 95 +} + +/// 生成 extend 块:toString() 返回成员名,fromString(String) 按成员名反查(未匹配抛异常)。 +/// 必须带 `<: ToString` 子句:仓颉字符串插值 `${x}` 要求实现 ToString 接口,仅有同名方法不够。 +private func buildExtend(enumName: String, ctors: ArrayList): String { + var sb = "" + sb += "// 以下 extend 由 @EnumString 宏生成:toString() 返回成员名,fromString(String) 按成员名反查(未匹配抛异常)\n" + sb += "extend ${enumName} <: ToString {\n" + sb += " public func toString(): String {\n" + sb += " match (this) {\n" + for (c in ctors) { + sb += " case ${c} => \"${c}\"\n" + } + sb += " }\n" + sb += " }\n" + sb += "\n" + sb += " public static func fromString(s: String): ${enumName} {\n" + sb += " match (s) {\n" + for (c in ctors) { + sb += " case \"${c}\" => ${c}\n" + } + sb += " case _ => throw Exception(\"${enumName}.fromString: 未知枚举值 '\${s}'\")\n" + sb += " }\n" + sb += " }\n" + sb += "}\n" + sb +}