feat: body_rules 支持 condition 条件触发

为每条 body_rule 新增可选 condition 字段,支持 eq/neq/gt/lt/gte/lte/
starts_with/ends_with/contains/matches/exists/not_exists/in/type_is
共 14 种操作符,规则仅在条件满足时执行。

后端: 新增 _evaluate_condition 条件评估器与 _validate_condition 校验逻辑
前端: EndpointFormDialog 增加条件编辑行(IF 面板)与 Filter 按钮切换
测试: 新增 TestConditionalBodyRules 覆盖全部操作符及链式触发场景
This commit is contained in:
fawney19
2026-02-11 00:50:08 +08:00
parent 262bc6e1f7
commit 3d6d4a48a5
5 changed files with 1294 additions and 166 deletions

View File

@@ -42,6 +42,31 @@ _BODY_RULE_ACTIONS: frozenset[str] = frozenset(
# regex_replace 允许的 flags 字符
_REGEX_FLAG_CHARS: frozenset[str] = frozenset({"i", "m", "s"})
# condition 允许的操作符
_CONDITION_OPS: frozenset[str] = frozenset(
{
"eq",
"neq",
"gt",
"lt",
"gte",
"lte",
"starts_with",
"ends_with",
"contains",
"matches",
"exists",
"not_exists",
"in",
"type_is",
}
)
# type_is 允许的类型值
_TYPE_IS_VALUES: frozenset[str] = frozenset(
{"string", "number", "boolean", "array", "object", "null"}
)
def parse_re_flags(flags_str: str) -> int:
"""将 flags 字符串i/m/s转换为 re 标志位。
@@ -59,6 +84,65 @@ def parse_re_flags(flags_str: str) -> int:
return result
def _validate_condition(condition: Any, rule_idx: int) -> None:
"""校验单条规则的 condition 结构"""
if not isinstance(condition, dict):
raise ValueError(f"body_rules[{rule_idx}]: condition 必须是 JSON 对象")
op = condition.get("op")
if not isinstance(op, str) or op not in _CONDITION_OPS:
raise ValueError(
f"body_rules[{rule_idx}]: condition.op 必须是 {sorted(_CONDITION_OPS)} 之一,"
f"当前值: {op!r}"
)
path = condition.get("path")
if not isinstance(path, str) or not path.strip():
raise ValueError(f"body_rules[{rule_idx}]: condition 必须提供非空 path")
# exists / not_exists 不需要 value
if op in ("exists", "not_exists"):
return
value = condition.get("value")
# 数值操作符校验
if op in ("gt", "lt", "gte", "lte"):
if not isinstance(value, (int, float)) or isinstance(value, bool):
raise ValueError(f"body_rules[{rule_idx}]: condition op={op!r} 的 value 必须为数值")
# matches 正则校验
if op == "matches":
if not isinstance(value, str) or not value:
raise ValueError(
f"body_rules[{rule_idx}]: condition op=matches 的 value 必须为非空字符串"
)
try:
re.compile(value)
except re.error as e:
raise ValueError(
f"body_rules[{rule_idx}]: condition op=matches 的 value 不是合法正则: {e}"
)
# in 校验
if op == "in":
if not isinstance(value, list):
raise ValueError(f"body_rules[{rule_idx}]: condition op=in 的 value 必须为数组")
# type_is 校验
if op == "type_is":
if not isinstance(value, str) or value not in _TYPE_IS_VALUES:
raise ValueError(
f"body_rules[{rule_idx}]: condition op=type_is 的 value 必须是 "
f"{sorted(_TYPE_IS_VALUES)} 之一"
)
# starts_with / ends_with / contains 对 value 做字符串校验
if op in ("starts_with", "ends_with"):
if not isinstance(value, str):
raise ValueError(f"body_rules[{rule_idx}]: condition op={op!r} 的 value 必须为字符串")
def _validate_body_rules(rules: list[BodyRule]) -> list[BodyRule]:
"""校验 body_rules 列表的结构和正则合法性。
@@ -138,6 +222,11 @@ def _validate_body_rules(rules: list[BodyRule]) -> list[BodyRule]:
if not isinstance(count, int) or count < 0:
raise ValueError(f"body_rules[{idx}]: regex_replace 的 count 必须为非负整数")
# ---------- condition 校验 ----------
condition = rule.get("condition")
if condition is not None:
_validate_condition(condition, idx)
return rules