Go 中的 Rule Cascade
模块:go get rulescascade.com/go(Go 1.26+,除标准库和 gopkg.in/yaml.v3 外无额外依赖)。规则包通过 rcas compile --all 生成到 build/rules/ 目录中。绝不要在生产运行时解析 YAML,也不要把规则复制到代码中。
启动时加载,每次请求评估
import rulecascade "rulescascade.com/go"
// 在启动时执行。任何错误都阻止启动。
data, err := os.ReadFile("build/rules/<ruleset-id>.bundle.json")
doc, err := rulecascade.ParseJSON(data)
ruleSet, err := rulecascade.FromBundle(doc)
推荐使用中间件(自 1.0.0-alpha.7 起):rulescascade.com/go/rulehttp 从 JSON 请求体(保持可读)构建请求、强制执行,并以一致的 422 问题详情响应被拒绝的请求,该格式在所有 Rule Cascade 运行时中保持一致。
mux.Handle("POST /orders", rulehttp.Enforce(rules, rulehttp.Options{Entity: "Order", Operation: "create",
Actor: func(r *http.Request) map[string]any { return actorFromToken(r) }})(createOrder))
// 在 handler 中:rulehttp.FromContext(r.Context()) 是允许的结果
在其他场景中,rules.Enforce(request, "server", nil) 返回一个 *rulecascade.RuleViolation 错误(Problem() 是响应体),rulehttp.WriteError(w, err) 将其写入响应。底层实现中,rules.Evaluate(request, "server", nil) 返回 *rulecascade.Result(包含 Decision、Findings、Effects、Commands;result.Allowed() 判断是否允许)。*RuleSet 可安全并发使用。每个状态变更处理程序遵循以下四个步骤:
- 在服务端评估,使用
map[string]any{"entity", "operation", "data", "original", "actor", "resolutions"}。actor 来自认证中间件,绝不能来自调用方控制的请求头。使用rulecascade.ParseJSON(或json.Decoder.UseNumber())解码请求体,确保数字精确无误。 - 拒绝时返回 422
application/problem+json(type: urn:rule-cascade:rule-violation),携带评估结果;对于 gRPC,返回codes.FailedPrecondition,并将 findings 放在详情中。 - 持久化:在应用
compute效果(Type == "value")后,存储服务端的值。 - 执行
result.Commands:在提交之后执行,每个IdempotencyKey执行一次(通过 outbox 表或具有去重能力的消息代理)。
布局建议:每个实体一个函数,用于构建请求并执行评估,该函数在所有 handler(net/http、chi、gin、echo 均可)中被调用。rulecascade.NewHolder 按时间间隔或 cron 计划重新加载规则包,并保留最后一次有效的规则集。
替换现有验证逻辑
rcas analyze --module <module> 会扫描代码中的 validate: 和 binding: 结构体标签、ozzo-validation、规则引擎、手写的 errors.New 检查、命名限制(如 const MaxQuantity = 10)、数值比较以及 switch 分支。如果某个候选规则出现 drift,意味着项目中另一种语言(通常是前端)以不同的值定义了相同的名称:必须由一个规则集来为两者共同决定。保持解码中的形状检查;将业务决策移至规则。对旧检查和新引擎在相同输入上添加表驱动的一致性测试,然后删除旧检查。
测试
对 Decision 和 finding 的 Code(而非消息文本)编写表驱动测试。规则集的金丝雀测试(rcas check)测试规则本身;CI 在 go test 之前运行 rcas check 和 rcas compile --all。