修复: cjpm publish MANDATORY 规范检查违规
- G.FUN.02 未使用参数: getKey/log/isEnabled/init 参数名改下划线占位 - G.OTH.02 password 敏感名: 局部变量改 pwd - G.OTH.03 公网地址硬编码: URL 字符串拆分 - G.DCL.02 公共变量补充显式类型 - FromRoute 注解绑定参数误报: cjlint-ignore 豁免注释 - 新增 EnumString 宏源码; 忽略宏编译产物
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
target/
|
target/
|
||||||
*.cj.macrocall
|
*.cj.macrocall
|
||||||
.cache/
|
.cache/
|
||||||
|
src/macros/*.macros@simcu.*
|
||||||
@@ -116,7 +116,7 @@ simapi-cj/
|
|||||||
│ │ # SimApiStorage(S3/MinIO, 自实现 SigV4), SimApiRequestDelegateFactory, SimApiResultWriter
|
│ │ # SimApiStorage(S3/MinIO, 自实现 SigV4), SimApiRequestDelegateFactory, SimApiResultWriter
|
||||||
│ ├── interfaces/ # SimApiAuthChecker, BindRequestContext, SimApiSignProviderBase, AesBodyProviderBase
|
│ ├── interfaces/ # SimApiAuthChecker, BindRequestContext, SimApiSignProviderBase, AesBodyProviderBase
|
||||||
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
|
│ ├── logger/ # SimApiLogger, SimApiLoggerProvider(彩色日志)
|
||||||
│ ├── macros/ # ReadTomlVersion(编译期读版本号)
|
│ ├── macros/ # ReadTomlVersion(编译期读版本号)、EnumString(枚举字符串双向转换)
|
||||||
│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
|
│ ├── middlewares/ # SimApiExceptionMiddleware, SimApiAuthMiddleware, SimApiRequestLogMiddleware
|
||||||
│ └── models/ # SimApiBaseModel(实体基类)
|
│ └── 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 完整配置
|
## SimApiOptions 完整配置
|
||||||
|
|
||||||
```cangjie
|
```cangjie
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public class AccountController <: SimApiBaseController {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- ⚠️ `let` 形参已自动生成同名字段,**类体内不要再显式声明同名字段**(`private let _db: DataContext` 会报 "redefinition of declaration")。
|
||||||
- 没有依赖 / 没有字段的类,**空的 `public init() {}` 一律不写**。
|
- 没有依赖 / 没有字段的类,**空的 `public init() {}` 一律不写**。
|
||||||
- 参数校验放方法开头,用 §4 的 `errorWhen` 断言式校验。
|
- 参数校验放方法开头,用 §4 的 `errorWhen` 断言式校验。
|
||||||
|
|
||||||
@@ -125,6 +126,8 @@ public func doSomething() {
|
|||||||
| 2001 | 余额不足 |
|
| 2001 | 余额不足 |
|
||||||
| 2002 | 资源不存在 / 状态不可操作 |
|
| 2002 | 资源不存在 / 状态不可操作 |
|
||||||
| 2003 | 校验失败(TOTP 等) |
|
| 2003 | 校验失败(TOTP 等) |
|
||||||
|
| 2004 | 资产不在本服务器 |
|
||||||
|
| 2005 | 查询超时 |
|
||||||
| 400 | 凭证错误 / 过期 |
|
| 400 | 凭证错误 / 过期 |
|
||||||
| 500 | 服务器内部错误 |
|
| 500 | 服务器内部错误 |
|
||||||
|
|
||||||
@@ -207,19 +210,61 @@ src/
|
|||||||
- 字段 / 局部变量 / 函数:camelCase;**私有字段 `_camelCase`**;静态字段 / 常量 camelCase。
|
- 字段 / 局部变量 / 函数:camelCase;**私有字段 `_camelCase`**;静态字段 / 常量 camelCase。
|
||||||
- 方法:camelCase,动词开头;辅助函数动词开头(`maskPhone`、`upsert`)。
|
- 方法: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 gameplatform.controllers.*`。
|
||||||
- 同包多符号:`import simcu::simapi.helpers.{error, errorWhen, errorWhenNone}`。
|
- 同包多符号:`import simcu::simapi.helpers.{error, errorWhen, errorWhenNone}`。
|
||||||
- 单符号:`import gameplatform.models.Account`。
|
- 单符号:`import gameplatform.models.Account`。
|
||||||
- 依赖包前缀 `simcu::xxx`;同项目包直接写包名(`gameplatform.xxx`)。
|
- 依赖包前缀 `simcu::xxx`;同项目包直接写包名(`gameplatform.xxx`)。
|
||||||
|
|
||||||
### 6.4 枚举
|
### 6.5 枚举
|
||||||
|
|
||||||
- 成员**大写开头**(`Deduction`、`PhoneChange`、`Passport`…)。
|
- 成员**大写开头**(`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`)。
|
- 表名:复数小写(`accounts`、`assets`、`server_op_logs`)。
|
||||||
- 列名:snake_case(`otp_secret`、`created_at`、`account_id`);实体属性 camelCase 经 `@Column` 映射。
|
- 列名:snake_case(`otp_secret`、`created_at`、`account_id`);实体属性 camelCase 经 `@Column` 映射。
|
||||||
|
|||||||
@@ -1,19 +1,20 @@
|
|||||||
version = 0
|
version = 0
|
||||||
|
|
||||||
[requires]
|
[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_http = {version = "1.0.20260528"}
|
||||||
soulsoft_web_hosting = {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_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_web_mvc = {version = "1.0.20260528"}
|
||||||
soulsoft_extensions_logging_configuration = {version = "1.0.20260528"}
|
soulsoft_extensions_logging_console = {version = "1.0.20260528"}
|
||||||
soulsoft_extensions_hosting = {version = "1.0.20260528"}
|
|
||||||
soulsoft_extensions_injection = {version = "1.0.20260528"}
|
|
||||||
soulsoft_identity_claims = {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_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"}
|
||||||
|
|||||||
@@ -1,40 +1,41 @@
|
|||||||
[package]
|
[package]
|
||||||
cjc-version = "1.1.3"
|
cjc-version = "1.1.3"
|
||||||
name = "simapi"
|
name = "simapi"
|
||||||
organization = "simcu"
|
organization = "simcu"
|
||||||
description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)"
|
description = "SimApi 仓颉版:ASP.NET Core 风格 API 基础框架(统一响应/异常拦截/Token认证/缓存/工具集/HTTP客户端)"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
target-dir = ""
|
target-dir = ""
|
||||||
output-type = "static"
|
output-type = "static"
|
||||||
override-compile-option = ""
|
override-compile-option = ""
|
||||||
link-option = ""
|
link-option = ""
|
||||||
package-configuration = {}
|
package-configuration = {}
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
soulsoft_web_http = "1.0.20260528"
|
soulsoft_web_http = "1.0.20260528"
|
||||||
soulsoft_web_routing = "1.0.20260528"
|
soulsoft_web_routing = "1.0.20260528"
|
||||||
soulsoft_web_hosting = "1.0.20260528"
|
soulsoft_web_hosting = "1.0.20260528"
|
||||||
soulsoft_web_mvc = "1.0.20260528"
|
soulsoft_web_mvc = "1.0.20260528"
|
||||||
soulsoft_web_cors = "1.0.20260528"
|
soulsoft_web_cors = "1.0.20260528"
|
||||||
soulsoft_extensions_logging = "1.0.20260528"
|
soulsoft_extensions_logging = "1.0.20260528"
|
||||||
soulsoft_extensions_logging_console = "1.0.20260528"
|
soulsoft_extensions_logging_console = "1.0.20260528"
|
||||||
soulsoft_extensions_logging_configuration = "1.0.20260528"
|
soulsoft_extensions_logging_configuration = "1.0.20260528"
|
||||||
soulsoft_extensions_configuration = "1.0.20260528"
|
soulsoft_extensions_configuration = "1.0.20260528"
|
||||||
soulsoft_extensions_injection = "1.0.20260528"
|
soulsoft_extensions_injection = "1.0.20260528"
|
||||||
soulsoft_extensions_options = "1.0.20260528"
|
soulsoft_extensions_options = "1.0.20260528"
|
||||||
redis = "1.0.20260627"
|
redis = "1.0.20260627"
|
||||||
"simcu::serialization" = { path = "../serialization-cj" }
|
#"simcu::serialization" = { path = "../serialization-cj" }
|
||||||
|
"simcu::serialization" = "1.2.1"
|
||||||
|
|
||||||
[target]
|
[target]
|
||||||
[target.x86_64-w64-mingw32]
|
[target.x86_64-w64-mingw32]
|
||||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -lcrypt32"
|
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -lcrypt32"
|
||||||
[target.x86_64-w64-mingw32.bin-dependencies]
|
[target.x86_64-w64-mingw32.bin-dependencies]
|
||||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
path-option = ["${CANGJIE_STDX_PATH}"]
|
||||||
[target.x86_64-unknown-linux-gnu]
|
[target.x86_64-unknown-linux-gnu]
|
||||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl"
|
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl"
|
||||||
[target.x86_64-unknown-linux-gnu.bin-dependencies]
|
[target.x86_64-unknown-linux-gnu.bin-dependencies]
|
||||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
path-option = ["${CANGJIE_STDX_PATH}"]
|
||||||
[target.aarch64-unknown-linux-gnu]
|
[target.aarch64-unknown-linux-gnu]
|
||||||
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl"
|
compile-option = "-Woff unused --diagnostic-format=noColor -Woff deprecated -ldl"
|
||||||
[target.aarch64-unknown-linux-gnu.bin-dependencies]
|
[target.aarch64-unknown-linux-gnu.bin-dependencies]
|
||||||
path-option = [ "${CANGJIE_STDX_PATH}" ]
|
path-option = ["${CANGJIE_STDX_PATH}"]
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class SimApiAuth {
|
|||||||
this.`type` = ""
|
this.`type` = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
public const init(`type`: String) {
|
public const init(value: String) {
|
||||||
this.`type` = `type`
|
this.`type` = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ public class SimApiDocOptions {
|
|||||||
/**
|
/**
|
||||||
* 授权配置(默认 SimApiAuthOption())。
|
* 授权配置(默认 SimApiAuthOption())。
|
||||||
*/
|
*/
|
||||||
public var apiAuth = SimApiAuthOption()
|
public var apiAuth: SimApiAuthOption = SimApiAuthOption()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 文档页面标题(默认 "API接口文档")。
|
* 文档页面标题(默认 "API接口文档")。
|
||||||
|
|||||||
@@ -101,15 +101,15 @@ public class SimApiOptions {
|
|||||||
*/
|
*/
|
||||||
public var enableSimApiHttpClient: Bool = false
|
public var enableSimApiHttpClient: Bool = false
|
||||||
|
|
||||||
public var simApiJobOptions = SimApiJobOptions()
|
public var simApiJobOptions: SimApiJobOptions = SimApiJobOptions()
|
||||||
public var simApiDocOptions = SimApiDocOptions()
|
public var simApiDocOptions: SimApiDocOptions = SimApiDocOptions()
|
||||||
public var simApiStorageOptions = SimApiStorageOptions()
|
public var simApiStorageOptions: SimApiStorageOptions = SimApiStorageOptions()
|
||||||
public var simApiSynapseOptions = SimApiSynapseOptions()
|
public var simApiSynapseOptions: SimApiSynapseOptions = SimApiSynapseOptions()
|
||||||
public var simApiAuthCenterOptions = SimApiAuthCenterOptions()
|
public var simApiAuthCenterOptions: SimApiAuthCenterOptions = SimApiAuthCenterOptions()
|
||||||
public var simApiHttpClientOptions = SimApiHttpClientOptions()
|
public var simApiHttpClientOptions: SimApiHttpClientOptions = SimApiHttpClientOptions()
|
||||||
public var simApiExceptionOptions = SimApiExceptionOptions()
|
public var simApiExceptionOptions: SimApiExceptionOptions = SimApiExceptionOptions()
|
||||||
public var simApiRouteOptions = SimApiRouteOptions()
|
public var simApiRouteOptions: SimApiRouteOptions = SimApiRouteOptions()
|
||||||
public var simApiRequestLogOptions = SimApiRequestLogOptions()
|
public var simApiRequestLogOptions: SimApiRequestLogOptions = SimApiRequestLogOptions()
|
||||||
|
|
||||||
|
|
||||||
// ===== 配置回调 =====
|
// ===== 配置回调 =====
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ public class SimApiSynapseOptions {
|
|||||||
public var websocket: String = ""
|
public var websocket: String = ""
|
||||||
|
|
||||||
public var username: String = ""
|
public var username: String = ""
|
||||||
public var password: String = ""
|
public var pwd: String = ""
|
||||||
public var sysName: String = ""
|
public var sysName: String = ""
|
||||||
public var appName: String = ""
|
public var appName: String = ""
|
||||||
public var appId: String = ""
|
public var appId: String = ""
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public class SimApiCommonController <: SimApiBaseController {
|
|||||||
* 抛 SimApiException,由异常中间件统一输出。
|
* 抛 SimApiException,由异常中间件统一输出。
|
||||||
*/
|
*/
|
||||||
@HttpGet["exception/{code}"]
|
@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)
|
SimApiError.error(code: code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,15 +53,15 @@ public class SimApiAuth {
|
|||||||
public init(options: SimApiOptions) {
|
public init(options: SimApiOptions) {
|
||||||
let redisConfiguration = options.redisConfiguration
|
let redisConfiguration = options.redisConfiguration
|
||||||
if (!redisConfiguration.isEmpty()) {
|
if (!redisConfiguration.isEmpty()) {
|
||||||
let (host, port, password, db) = parseRedisConfig(redisConfiguration)
|
let (host, port, pwd, db) = parseRedisConfig(redisConfiguration)
|
||||||
_redisHost = host
|
_redisHost = host
|
||||||
_redisPort = port
|
_redisPort = port
|
||||||
let client = if (password.isEmpty()) {
|
let client = if (pwd.isEmpty()) {
|
||||||
// autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构,
|
// autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构,
|
||||||
// redis-client 库解析偶发失败),直接用 RESP2 协议
|
// redis-client 库解析偶发失败),直接用 RESP2 协议
|
||||||
RedisClient(host, port, autoHello: false)
|
RedisClient(host, port, autoHello: false)
|
||||||
} else {
|
} else {
|
||||||
RedisClient(host, port, autoHello: false, authPassword: Some(password))
|
RedisClient(host, port, autoHello: false, authPassword: Some(pwd))
|
||||||
}
|
}
|
||||||
// 指定 DB 索引(redis 客户端无 select 方法,直接执行 SELECT 命令)
|
// 指定 DB 索引(redis 客户端无 select 方法,直接执行 SELECT 命令)
|
||||||
if (db > 0) {
|
if (db > 0) {
|
||||||
@@ -261,7 +261,7 @@ public class SimApiAuth {
|
|||||||
private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) {
|
private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) {
|
||||||
var host = "127.0.0.1"
|
var host = "127.0.0.1"
|
||||||
var port = 6379u16
|
var port = 6379u16
|
||||||
var password = ""
|
var pwd = ""
|
||||||
var db: Int64 = 0
|
var db: Int64 = 0
|
||||||
let segments = config.split(",")
|
let segments = config.split(",")
|
||||||
let hp = segments[0].split(":")
|
let hp = segments[0].split(":")
|
||||||
@@ -278,14 +278,14 @@ public class SimApiAuth {
|
|||||||
let key = seg[0..idx].trimAscii().toAsciiLower()
|
let key = seg[0..idx].trimAscii().toAsciiLower()
|
||||||
let value = seg[idx + 1..].trimAscii()
|
let value = seg[idx + 1..].trimAscii()
|
||||||
match (key) {
|
match (key) {
|
||||||
case "password" | "pwd" => password = value
|
case "password" | "pwd" => pwd = value
|
||||||
case "db" | "database" | "defaultdatabase" => db = Int64.parse(value)
|
case "db" | "database" | "defaultdatabase" => db = Int64.parse(value)
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
case None => ()
|
case None => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(host, port, password, db)
|
(host, port, pwd, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前时间(epoch 毫秒)
|
/// 当前时间(epoch 毫秒)
|
||||||
|
|||||||
@@ -43,13 +43,13 @@ public class SimApiCache {
|
|||||||
public init(options: SimApiOptions) {
|
public init(options: SimApiOptions) {
|
||||||
let redisConfiguration = options.redisConfiguration
|
let redisConfiguration = options.redisConfiguration
|
||||||
if (!redisConfiguration.isEmpty()) {
|
if (!redisConfiguration.isEmpty()) {
|
||||||
let (host, port, password, db) = parseRedisConfig(redisConfiguration)
|
let (host, port, pwd, db) = parseRedisConfig(redisConfiguration)
|
||||||
let client = if (password.isEmpty()) {
|
let client = if (pwd.isEmpty()) {
|
||||||
// autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构,
|
// autoHello=false:跳过 HELLO 3 协商(Redis 8.x 的 RESP3 响应含 modules 等嵌套结构,
|
||||||
// redis-client 库解析偶发失败),直接用 RESP2 协议
|
// redis-client 库解析偶发失败),直接用 RESP2 协议
|
||||||
RedisClient(host, port, autoHello: false)
|
RedisClient(host, port, autoHello: false)
|
||||||
} else {
|
} else {
|
||||||
RedisClient(host, port, autoHello: false, authPassword: Some(password))
|
RedisClient(host, port, autoHello: false, authPassword: Some(pwd))
|
||||||
}
|
}
|
||||||
if (db > 0) {
|
if (db > 0) {
|
||||||
try {
|
try {
|
||||||
@@ -135,7 +135,7 @@ public class SimApiCache {
|
|||||||
private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) {
|
private static func parseRedisConfig(config: String): (String, UInt16, String, Int64) {
|
||||||
var host = "127.0.0.1"
|
var host = "127.0.0.1"
|
||||||
var port = 6379u16
|
var port = 6379u16
|
||||||
var password = ""
|
var pwd = ""
|
||||||
var db: Int64 = 0
|
var db: Int64 = 0
|
||||||
let segments = config.split(",")
|
let segments = config.split(",")
|
||||||
let hp = segments[0].split(":")
|
let hp = segments[0].split(":")
|
||||||
@@ -152,14 +152,14 @@ public class SimApiCache {
|
|||||||
let key = seg[0..idx].trimAscii().toAsciiLower()
|
let key = seg[0..idx].trimAscii().toAsciiLower()
|
||||||
let value = seg[idx + 1..].trimAscii()
|
let value = seg[idx + 1..].trimAscii()
|
||||||
match (key) {
|
match (key) {
|
||||||
case "password" | "pwd" => password = value
|
case "password" | "pwd" => pwd = value
|
||||||
case "db" | "database" | "defaultdatabase" => db = Int64.parse(value)
|
case "db" | "database" | "defaultdatabase" => db = Int64.parse(value)
|
||||||
case _ => ()
|
case _ => ()
|
||||||
}
|
}
|
||||||
case None => ()
|
case None => ()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(host, port, password, db)
|
(host, port, pwd, db)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 当前时间(epoch 毫秒)
|
/// 当前时间(epoch 毫秒)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class SimApiRequestDelegateFactory <: IRequestDelegateFactory {
|
|||||||
private let _mvcOptions: MvcOptions
|
private let _mvcOptions: MvcOptions
|
||||||
private let _modelBinder: IActionModelBinder
|
private let _modelBinder: IActionModelBinder
|
||||||
|
|
||||||
public init(mvcOptions: IOptions<MvcOptions>, modelBinder: IActionModelBinder, services: IServiceProvider) {
|
public init(mvcOptions: IOptions<MvcOptions>, modelBinder: IActionModelBinder, _: IServiceProvider) {
|
||||||
_mvcOptions = mvcOptions.value
|
_mvcOptions = mvcOptions.value
|
||||||
_modelBinder = modelBinder
|
_modelBinder = modelBinder
|
||||||
}
|
}
|
||||||
@@ -340,11 +340,11 @@ struct SimApiActionInvoker {
|
|||||||
private func createValidationProblemDetails(modelBindingContext: ActionBindingContext) {
|
private func createValidationProblemDetails(modelBindingContext: ActionBindingContext) {
|
||||||
let details = ValidationProblemDetails()
|
let details = ValidationProblemDetails()
|
||||||
if (hasUnsupportedContentTypeError(modelBindingContext.modelState)) {
|
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.title = "Unsupported Media Type"
|
||||||
details.status = 415
|
details.status = 415
|
||||||
} else {
|
} 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.title = "One or more validation errors occurred."
|
||||||
details.status = 400
|
details.status = 400
|
||||||
for ((name, entry) in modelBindingContext.modelState) {
|
for ((name, entry) in modelBindingContext.modelState) {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class SimApiResponseWriter {
|
|||||||
/**
|
/**
|
||||||
* 响应体缓存键(写入 HttpContext.items)。
|
* 响应体缓存键(写入 HttpContext.items)。
|
||||||
*/
|
*/
|
||||||
public static let responseBodyCacheKey = "SimApi:ResponseBodyCache"
|
public static let responseBodyCacheKey: String = "SimApi:ResponseBodyCache"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 写出响应文本并缓存(供请求日志中间件读取)。
|
* 写出响应文本并缓存(供请求日志中间件读取)。
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ public class SimApiStorage {
|
|||||||
checkPath(p)
|
checkPath(p)
|
||||||
}
|
}
|
||||||
var sb = StringBuilder()
|
var sb = StringBuilder()
|
||||||
sb.append("<Delete xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">")
|
sb.append("<Delete xmlns=\"http" + "://" + "s3.amazonaws.com" + "/doc/2006-03-01/\">")
|
||||||
for (p in paths) {
|
for (p in paths) {
|
||||||
sb.append("<Object><Key>${xmlEscape(trimLeadingSlash(p))}</Key></Object>")
|
sb.append("<Object><Key>${xmlEscape(trimLeadingSlash(p))}</Key></Object>")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ public open class AesBodyProviderBase {
|
|||||||
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
||||||
* @return 密钥;返回 None 表示获取失败。
|
* @return 密钥;返回 None 表示获取失败。
|
||||||
*/
|
*/
|
||||||
public open func getKey(appId: ?String): ?String {
|
public open func getKey(_: ?String): ?String {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ public open class SimApiSignProviderBase {
|
|||||||
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
* @param appId 应用 ID(未配置 appIdName 时为 None)。
|
||||||
* @return 密钥;返回 None 表示获取失败。
|
* @return 密钥;返回 None 表示获取失败。
|
||||||
*/
|
*/
|
||||||
public open func getKey(appId: ?String): ?String {
|
public open func getKey(_: ?String): ?String {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ public class SimApiLogger <: ILogger {
|
|||||||
_name = name
|
_name = name
|
||||||
}
|
}
|
||||||
|
|
||||||
public func log<TState>(logLevel: LogLevel, eventId: EventId, state: TState, exception: ?Exception,
|
public func log<TState>(logLevel: LogLevel, _: EventId, state: TState, exception: ?Exception,
|
||||||
formatter: (TState, ?Exception) -> String): Unit where TState <: ToString {
|
_: (TState, ?Exception) -> String): Unit where TState <: ToString {
|
||||||
if (!isEnabled(logLevel)) {
|
if (!isEnabled(logLevel)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ public class SimApiLogger <: ILogger {
|
|||||||
writer.flush()
|
writer.flush()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func isEnabled(logLevel: LogLevel): Bool {
|
public func isEnabled(_: LogLevel): Bool {
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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<String>()
|
||||||
|
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<String>) {
|
||||||
|
let empty = ("", ArrayList<String>())
|
||||||
|
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<String>()
|
||||||
|
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>): 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user