//! 条件表达式引擎 — 用于 DAG 边上的条件判断 //! //! 支持的语法(由低到高优先级): //! //! - 逻辑或 `or` //! - 逻辑与 `and` //! - 逻辑非 `not`(一元前缀) //! - 括号分组 `( ... )` //! - 比较 `==` `!=` `>` `>=` `<` `<=` `contains` //! - 操作数: //! - 布尔字面量 `true` / `false` //! - 单引号字符串 `'completed'`(内部 `''` 转义为单引号) //! - 数字字面量 `10` / `-3.5`(支持负数、小数) //! - JSON Path `$.a.b` / `$.list[0]` / `$['key with space']`(从求值 context 取值) //! //! 安全约束: //! - **求值失败保守 false** —— 任何解析错误、JSON Path 找不到、类型不兼容,均返回 `Ok(false)` //! 而非 `Err`。条件分支写错或 context 缺字段时不应静默放行(默认 false = 保守拒绝)。 //! `evaluate` 实现把所有错误路径(tokenize 失败、parse 失败)都吞成 `Ok(false)`, //! 永不返回 `Err`;`anyhow::Result` 签名仅作为未来扩展点保留(若日后需向调用方 //! 区分「表达式非法」与「求值结果为 false」,可在此放开)。当前 executor 无需处理 `Err`。 //! - `contains`:左值为字符串 → 子串匹配;左值为数组 → 元素存在性匹配;其余类型 → false。 //! - `>` `<` `>=` `<=`:仅两端正数/负数/小数(数值)才比较;任一非数值 → false。 //! //! 实现说明:手写递归下降解析器,无第三方依赖(jsonpath crate 引入会拉额外体积, //! 且当前需求仅需点路径 + 下标,手写更可控)。表达式按需惰性求值,短路 and/or。 use serde_json::Value; /// 条件表达式求值器 pub struct ConditionEngine; impl ConditionEngine { /// 求值条件表达式 /// /// 详见模块级文档的语法与安全约束。**求值失败保守 false**:解析错误、JSON Path 缺失、 /// 类型不兼容一律返回 `Ok(false)`,不静默放行(默认 false = 保守拒绝)。 pub fn evaluate(expr: &str, context: &Value) -> anyhow::Result { let trimmed = expr.trim(); if trimmed.is_empty() { return Ok(false); } let toks = match tokenize(trimmed) { Ok(t) => t, // tokenizer 阶段即非法(如未闭合引号):保守 false,不静默放行 Err(_) => return Ok(false), }; let mut parser = Parser { toks, pos: 0, ctx: context }; match parser.parse_or() { Ok(v) => Ok(v), // 解析错误保守 false:与历史行为「未识别表达式默认 false」一致,不破坏调用方 Err(_) => Ok(false), } } } // ============================================================================ // JSON Path 求值 —— 手写,支持点路径 / 下标 / 单引号键 // ============================================================================ /// 按手写 JSON Path 从 context 取值。 /// /// 支持语法: /// - `$.a.b` 点取字段 /// - `$.a[0]` 数组下标(支持负数从尾部,-1 = 末尾) /// - `$['a b']` 单引号键(支持含空格/特殊字符的键名) /// /// `path` 必须以 `$` 起头;解析失败或路径不存在返回 `None`(调用方据此保守 false)。 fn eval_jsonpath(path: &str, context: &Value) -> Option { let p = path.strip_prefix('$')?; let bytes = p.as_bytes(); // 全量 clone 而非按需借用:JSON Path 段数与深度运行时才确定,逐段借用需把 // `&Value` 在循环中重绑(每段 as_object/as_array 返回的借用生命周期互相嵌套), // 借用检查极难通过;改用拥有的 Value 顺序覆盖,代码直观。代价是 context 顶层 // 的一次深拷贝——条件求值在 DAG 边上触发,频率低且 context 通常不深,开销可忽略。 let mut cur: Value = context.clone(); let mut i = 0usize; while i < bytes.len() { match bytes[i] { b'.' => { i += 1; let (key, next) = consume_key(p, i)?; i = next; cur = cur.as_object().and_then(|m| m.get(&key).cloned())?; } b'[' => { i += 1; while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; } if i >= bytes.len() { return None; } if bytes[i] == b'\'' { let (key, next) = consume_quoted(p, i)?; i = next; while i < bytes.len() && bytes[i].is_ascii_whitespace() { i += 1; } if i >= bytes.len() || bytes[i] != b']' { return None; } i += 1; cur = cur.as_object().and_then(|m| m.get(&key).cloned())?; } else { let start = i; while i < bytes.len() && bytes[i] != b']' { i += 1; } let idx_str = p[start..i].trim(); let idx: isize = idx_str.parse().ok()?; i += 1; // 跳过 ] let arr = cur.as_array()?; let len = arr.len() as isize; let real = if idx < 0 { len + idx } else { idx }; if real < 0 || real >= len { return None; } cur = arr.get(real as usize).cloned()?; } } c if c.is_ascii_whitespace() => { i += 1; } _ => return None, } } Some(cur) } /// 取一段点路径的键(到下一个 `.`/`[`/行尾),返回 (key, 下一个待处理位置)。 fn consume_key(s: &str, start: usize) -> Option<(String, usize)> { let bytes = s.as_bytes(); let mut i = start; while i < bytes.len() && bytes[i] != b'.' && bytes[i] != b'[' { i += 1; } let key = s[start..i].trim(); if key.is_empty() { return None; } Some((key.to_string(), i)) } /// 取单引号字符串(起始位置指向开引号 `'`),内部 `''` 转义为单引号。 /// 返回 (内容, 闭引号后位置)。 /// /// 实现要点:先扫描定位闭引号的字节位置,再对原始切片整体取出内容,而非逐字节 /// `bytes[i] as char` 累加。原因:单引号 `'` 是 ASCII 单字节字符,从开引号到闭引号 /// 之间的字节范围天然落在 UTF-8 字符边界上,整体切片能完整保留多字节字符(中文/emoji); /// 逐字节 `as char` 会把每个字节当独立码点,破坏多字节 UTF-8 序列致乱码。 fn consume_quoted(s: &str, start: usize) -> Option<(String, usize)> { let bytes = s.as_bytes(); if start >= bytes.len() || bytes[start] != b'\'' { return None; } // 扫描定位真正的闭引号:遇到 `''` 视为转义(跳过),单个 `'` 才是闭引号 let mut i = start + 1; let content_start = i; while i < bytes.len() { if bytes[i] == b'\'' { // 检测转义 '' if i + 1 < bytes.len() && bytes[i + 1] == b'\'' { i += 2; // 跳过转义的两引号,继续找闭引号 continue; } // 闭引号找到:content_start..i 是引号内的原始字节段(落在 UTF-8 边界) let raw = &s[content_start..i]; // 内部 `''` 转义为单引号:整段替换(无转义时直接 clone 原文,零额外分配开销仅一次) let content = raw.replace("''", "'"); return Some((content, i + 1)); } i += 1; } None // 未闭合引号 } // 历史:曾设计「借用或拥有的 Value 视图」类型以避免逐段 clone,后弃用—— // eval_jsonpath 改为直接 clone(见上方实现内注释)。放弃借用优化的理由:JSON Path // 通常只有少数几段,每段 clone 一个 serde_json::Value 的开销可忽略,换取代码可读性 // 与借用检查通过。此段不再对应任何类型定义,仅作设计取舍的历史说明保留。 // ============================================================================ // Tokenizer —— 把表达式切成 Token 流 // ============================================================================ #[derive(Debug, Clone, PartialEq)] enum Token { Bool(bool), Number(f64), Str(String), Path(String), /// `==` `!=` `>` `>=` `<` `<=` `contains` Cmp(CmpOp), And, Or, Not, LParen, RParen, } #[derive(Debug, Clone, Copy, PartialEq)] enum CmpOp { Eq, Ne, Gt, Ge, Lt, Le, Contains, } fn tokenize(s: &str) -> Result, ()> { let bytes = s.as_bytes(); let mut toks = Vec::new(); let mut i = 0usize; while i < bytes.len() { // 略空白 if bytes[i].is_ascii_whitespace() { i += 1; continue; } match bytes[i] { b'(' => { toks.push(Token::LParen); i += 1; } b')' => { toks.push(Token::RParen); i += 1; } b'\'' => { let (val, next) = consume_quoted(s, i).ok_or(())?; toks.push(Token::Str(val)); i = next; } b'$' => { // JSON Path:到下一个空白或比较符或括号外 let start = i; i += 1; while i < bytes.len() { let c = bytes[i]; // 遇空白 / 逻辑词边界 / 比较符 / 括号 停止 if c.is_ascii_whitespace() || c == b'(' || c == b')' { break; } // 比较运算符起始 if c == b'=' || c == b'!' || c == b'>' || c == b'<' { break; } // [ 段内允许,但 ] 后若跟字母数字属下一个 token(罕见,简化:整体吃掉) i += 1; } toks.push(Token::Path(s[start..i].to_string())); } b'=' => { if i + 1 < bytes.len() && bytes[i + 1] == b'=' { toks.push(Token::Cmp(CmpOp::Eq)); i += 2; } else { return Err(()); // 单 = 非法 } } b'!' => { if i + 1 < bytes.len() && bytes[i + 1] == b'=' { toks.push(Token::Cmp(CmpOp::Ne)); i += 2; } else { return Err(()); // 单 ! 非法(不支持 C 风格!) } } b'>' => { if i + 1 < bytes.len() && bytes[i + 1] == b'=' { toks.push(Token::Cmp(CmpOp::Ge)); i += 2; } else { toks.push(Token::Cmp(CmpOp::Gt)); i += 1; } } b'<' => { if i + 1 < bytes.len() && bytes[i + 1] == b'=' { toks.push(Token::Cmp(CmpOp::Le)); i += 2; } else { toks.push(Token::Cmp(CmpOp::Lt)); i += 1; } } // 数字字面量(含负号起始;正号省略)。注意:`and/or/not/true/false` 不会被 // 数字分支吃掉,因为它们以字母开头走下面的字母分支。 b'-' | b'0'..=b'9' => { let start = i; if bytes[i] == b'-' { i += 1; } let mut saw_digit = false; while i < bytes.len() && (bytes[i].is_ascii_digit() || bytes[i] == b'.') { saw_digit = true; i += 1; } if !saw_digit { return Err(()); // 仅 `-` 非法 } let num: f64 = s[start..i].parse().map_err(|_| ())?; toks.push(Token::Number(num)); } // 字母起始:关键字 or/and/not/true/false 或 contains _ => { let start = i; while i < bytes.len() && (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'_') { i += 1; } let word = &s[start..i]; match word { "true" => toks.push(Token::Bool(true)), "false" => toks.push(Token::Bool(false)), "and" => toks.push(Token::And), "or" => toks.push(Token::Or), "not" => toks.push(Token::Not), "contains" => toks.push(Token::Cmp(CmpOp::Contains)), // 未识别关键字:保守起见视为非法(tokenize 失败 → evaluate false) _ => return Err(()), } } } } Ok(toks) } // ============================================================================ // Parser —— 递归下降,优先级 or < and < not < 比较 < 操作数 // ============================================================================ struct Parser<'a> { toks: Vec, pos: usize, ctx: &'a Value, } impl<'a> Parser<'a> { fn peek(&self) -> Option<&Token> { self.toks.get(self.pos) } fn bump(&mut self) -> Option { let t = self.toks.get(self.pos).cloned(); if t.is_some() { self.pos += 1; } t } /// or := and ( 'or' and )* fn parse_or(&mut self) -> Result { let mut left = self.parse_and()?; while let Some(Token::Or) = self.peek() { self.bump(); // 注意:此处并非真正短路求值。右侧 parse_and() 必须无条件执行—— // 递归下降解析器需消费右侧 token 才能正确推进 pos(否则残留 token 会让 // 上层误判为非法表达式)。`||` 仅做最终布尔合并;本引擎求值无副作用, // 右侧即使结果被丢弃也无害。 let right = self.parse_and()?; left = left || right; } Ok(left) } /// and := not ( 'and' not )* fn parse_and(&mut self) -> Result { let mut left = self.parse_not()?; while let Some(Token::And) = self.peek() { self.bump(); // 同 parse_or:右侧 parse_not() 必须无条件执行以推进 pos,`&&` 仅做 // 最终布尔合并,非短路求值(引擎无副作用)。 let right = self.parse_not()?; left = left && right; } Ok(left) } /// not := 'not' not | comparison fn parse_not(&mut self) -> Result { if let Some(Token::Not) = self.peek() { self.bump(); let v = self.parse_not()?; return Ok(!v); } self.parse_comparison() } /// comparison := operand ( cmpop operand )? /// /// 无比较运算符时:operand 自身需为布尔语义(JSON Path 取到 bool、或布尔字面量); /// 否则视为非布尔(保守 false)。 fn parse_comparison(&mut self) -> Result { let left = self.parse_operand()?; match self.peek() { Some(Token::Cmp(op)) => { let op = *op; self.bump(); let right = self.parse_operand()?; Ok(eval_cmp(op, &left, &right)) } _ => { // 单操作数:仅布尔字面量或 JSON Path 指向 bool 为 true;其余保守 false Ok(left.as_bool().unwrap_or(false)) } } } /// operand := '(' or ')' | bool | number | str | path fn parse_operand(&mut self) -> Result { match self.bump() { Some(Token::LParen) => { let v = self.parse_or()?; // 期待闭括号 match self.bump() { Some(Token::RParen) => Ok(Value::Bool(v)), _ => Err(()), } } Some(Token::Bool(b)) => Ok(Value::Bool(b)), Some(Token::Number(n)) => Ok(serde_json::json!(n)), Some(Token::Str(s)) => Ok(Value::String(s)), Some(Token::Path(p)) => { // JSON Path 求值失败 → Null(后续比较保守 false) Ok(eval_jsonpath(&p, self.ctx).unwrap_or(Value::Null)) } _ => Err(()), } } } /// 比较求值。任一侧类型不兼容 → false(保守)。 fn eval_cmp(op: CmpOp, left: &Value, right: &Value) -> bool { match op { CmpOp::Eq => json_eq(left, right), CmpOp::Ne => !json_eq(left, right), CmpOp::Gt | CmpOp::Ge | CmpOp::Lt | CmpOp::Le => { let cmp = num_cmp(left, right); match (op, cmp) { (CmpOp::Gt, Some(o)) => o.is_gt(), (CmpOp::Ge, Some(o)) => o.is_ge(), (CmpOp::Lt, Some(o)) => o.is_lt(), (CmpOp::Le, Some(o)) => o.is_le(), _ => false, } } CmpOp::Contains => json_contains(left, right), } } /// 相等比较:数值按数值比(string "1" 不等于 number 1,避免隐式转换陷阱), /// 其余按 serde_json Value 相等。布尔与数字需类型对齐(serde_json bool != number)。 fn json_eq(left: &Value, right: &Value) -> bool { // 两端均数值(含 i64/u64/f64):数值比较(1.0 == 1) if let (Some(a), Some(b)) = (as_f64(left), as_f64(right)) { return (a - b).abs() < f64::EPSILON; } left == right } /// 数值比较。两端需均可转 f64(数字字面量、JSON number),否则 None(保守 false)。 fn num_cmp(left: &Value, right: &Value) -> Option { let a = as_f64(left)?; let b = as_f64(right)?; a.partial_cmp(&b) } fn as_f64(v: &Value) -> Option { match v { Value::Number(n) => n.as_f64(), _ => None, } } /// contains: /// - 左值为字符串、右值为字符串 → 子串包含 /// - 左值为数组 → 任一元素 json_eq 右值 /// - 其余 → false fn json_contains(left: &Value, right: &Value) -> bool { match left { Value::String(s) => right .as_str() .map(|r| s.contains(r)) .unwrap_or(false), Value::Array(arr) => arr.iter().any(|e| json_eq(e, right)), _ => false, } } #[cfg(test)] mod tests { use super::*; use serde_json::json; // -------------------------------------------------------------------- // 字面量与向后兼容(原有 7 个用例语义保持) // -------------------------------------------------------------------- #[test] fn test_true_literal_returns_true() { assert_eq!(ConditionEngine::evaluate("true", &json!({})).unwrap(), true); } #[test] fn test_false_literal_returns_false() { assert_eq!(ConditionEngine::evaluate("false", &json!({})).unwrap(), false); } #[test] fn test_unsupported_expression_defaults_to_false() { // 历史用例:非法表达式(单 = 等非法 token)保守 false // 注:增强后 "$.status == 'completed'" 已合法,改用真正非法的表达式 assert_eq!( ConditionEngine::evaluate("= =", &json!({})).unwrap(), false ); } #[test] fn test_whitespace_is_trimmed() { assert_eq!( ConditionEngine::evaluate(" true ", &json!({})).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("\tfalse\n", &json!({})).unwrap(), false ); } #[test] fn test_empty_string_defaults_to_false() { assert_eq!(ConditionEngine::evaluate("", &json!({})).unwrap(), false); assert_eq!(ConditionEngine::evaluate(" ", &json!({})).unwrap(), false); } #[test] fn test_case_sensitive_not_matched() { // True/FALSE 仍非合法布尔字面量(只认小写 true/false) assert_eq!(ConditionEngine::evaluate("True", &json!({})).unwrap(), false); assert_eq!(ConditionEngine::evaluate("FALSE", &json!({})).unwrap(), false); } #[test] fn test_arbitrary_string_defaults_to_false() { // 未识别关键字(yes / completed)→ tokenize 失败 → false assert_eq!(ConditionEngine::evaluate("yes", &json!({})).unwrap(), false); assert_eq!(ConditionEngine::evaluate("completed", &json!({})).unwrap(), false); // 裸数字(非布尔语义)→ 单操作数非 bool → false assert_eq!(ConditionEngine::evaluate("1", &json!({})).unwrap(), false); } // -------------------------------------------------------------------- // 一、JSON Path 求值 // -------------------------------------------------------------------- #[test] fn jsonpath_eq_string_literal() { let ctx = json!({ "status": "completed" }); assert_eq!( ConditionEngine::evaluate("$.status == 'completed'", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.status == 'failed'", &ctx).unwrap(), false ); } #[test] fn jsonpath_nested_dot() { let ctx = json!({ "result": { "code": 200 } }); assert_eq!( ConditionEngine::evaluate("$.result.code == 200", &ctx).unwrap(), true ); } #[test] fn jsonpath_array_index() { let ctx = json!({ "tags": ["ai", "rust"] }); assert_eq!( ConditionEngine::evaluate("$.tags[0] == 'ai'", &ctx).unwrap(), true ); // 负数下标从尾部 assert_eq!( ConditionEngine::evaluate("$.tags[-1] == 'rust'", &ctx).unwrap(), true ); } #[test] fn jsonpath_missing_path_defaults_false() { // 路径不存在 → Null == 'completed' → false(保守) let ctx = json!({ "status": "ok" }); assert_eq!( ConditionEngine::evaluate("$.missing == 'x'", &ctx).unwrap(), false ); } #[test] fn jsonpath_bool_value_as_operand() { // JSON Path 指向 bool,可作单操作数布尔求值 let ctx = json!({ "approved": true }); assert_eq!( ConditionEngine::evaluate("$.approved", &ctx).unwrap(), true ); // 非 bool 字段作单操作数 → false(字符串非布尔语义) let ctx2 = json!({ "status": "ok" }); assert_eq!( ConditionEngine::evaluate("$.status", &ctx2).unwrap(), false ); } // -------------------------------------------------------------------- // 二、比较运算符(==/!=/>/=/<=) // -------------------------------------------------------------------- #[test] fn numeric_gt_lt() { let ctx = json!({ "count": 15 }); assert_eq!( ConditionEngine::evaluate("$.count > 10", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.count < 10", &ctx).unwrap(), false ); assert_eq!( ConditionEngine::evaluate("$.count >= 15", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.count <= 14", &ctx).unwrap(), false ); } #[test] fn numeric_negative_and_decimal() { let ctx = json!({ "temp": -3.5 }); assert_eq!( ConditionEngine::evaluate("$.temp < 0", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.temp == -3.5", &ctx).unwrap(), true ); } #[test] fn ne_operator() { let ctx = json!({ "status": "ok" }); assert_eq!( ConditionEngine::evaluate("$.status != 'failed'", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.status != 'ok'", &ctx).unwrap(), false ); } #[test] fn non_numeric_gt_defaults_false() { // 字符串 > 数字:类型不兼容 → 保守 false let ctx = json!({ "name": "abc" }); assert_eq!( ConditionEngine::evaluate("$.name > 10", &ctx).unwrap(), false ); } // -------------------------------------------------------------------- // 三、contains // -------------------------------------------------------------------- #[test] fn contains_substring() { let ctx = json!({ "msg": "hello world" }); assert_eq!( ConditionEngine::evaluate("$.msg contains 'world'", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.msg contains 'xyz'", &ctx).unwrap(), false ); } #[test] fn contains_array_element() { let ctx = json!({ "tags": ["ai", "rust", "devflow"] }); assert_eq!( ConditionEngine::evaluate("$.tags contains 'rust'", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.tags contains 'go'", &ctx).unwrap(), false ); } #[test] fn contains_numeric_in_array() { // 数组 contains 数字(数值相等判定,1 == 1.0) let ctx = json!({ "nums": [1, 2, 3] }); assert_eq!( ConditionEngine::evaluate("$.nums contains 2", &ctx).unwrap(), true ); } #[test] fn contains_non_string_non_array_defaults_false() { let ctx = json!({ "n": 100 }); // 数字 contains ... 无意义 → false assert_eq!( ConditionEngine::evaluate("$.n contains '1'", &ctx).unwrap(), false ); } // -------------------------------------------------------------------- // 四、逻辑组合 and / or / not // -------------------------------------------------------------------- #[test] fn logical_and() { let ctx = json!({ "a": true, "b": false }); assert_eq!( ConditionEngine::evaluate("$.a and $.b", &ctx).unwrap(), false ); let ctx2 = json!({ "a": true, "b": true }); assert_eq!( ConditionEngine::evaluate("$.a and $.b", &ctx2).unwrap(), true ); } #[test] fn logical_or() { let ctx = json!({ "a": false, "b": true }); assert_eq!( ConditionEngine::evaluate("$.a or $.b", &ctx).unwrap(), true ); } #[test] fn logical_not() { let ctx = json!({ "approved": false }); assert_eq!( ConditionEngine::evaluate("not $.approved", &ctx).unwrap(), true ); let ctx2 = json!({ "approved": true }); assert_eq!( ConditionEngine::evaluate("not $.approved", &ctx2).unwrap(), false ); } #[test] fn and_or_precedence() { // and 优先级高于 or:true or (false and false) → true let ctx = json!({}); assert_eq!( ConditionEngine::evaluate("true or false and false", &ctx).unwrap(), true ); } #[test] fn not_precedence_over_and() { // not 高于 and:not false and not false → true and true → true let ctx = json!({}); assert_eq!( ConditionEngine::evaluate("not false and not false", &ctx).unwrap(), true ); } // -------------------------------------------------------------------- // 五、嵌套括号 + 综合 // -------------------------------------------------------------------- #[test] fn nested_parens() { let ctx = json!({}); // (true or false) and (false or true) → true and true → true assert_eq!( ConditionEngine::evaluate("(true or false) and (false or true)", &ctx).unwrap(), true ); // not (true and true) → false assert_eq!( ConditionEngine::evaluate("not (true and true)", &ctx).unwrap(), false ); } #[test] fn complex_realworld_expression() { // 综合场景:状态完成 且 (分数 >= 80 或 含 vip 标签)且 未取消 let ctx = json!({ "status": "completed", "score": 85, "tags": ["vip", "ai"], "cancelled": false }); let expr = "$.status == 'completed' and ($.score >= 80 or $.tags contains 'vip') and not $.cancelled"; assert_eq!( ConditionEngine::evaluate(expr, &ctx).unwrap(), true ); } #[test] fn unbalanced_parens_defaults_false() { // 括号不闭合 → parse 失败 → 保守 false assert_eq!( ConditionEngine::evaluate("(true and false", &json!({})).unwrap(), false ); } #[test] fn unclosed_quote_defaults_false() { assert_eq!( ConditionEngine::evaluate("$.s == 'abc", &json!({"s":"x"})).unwrap(), false ); } // -------------------------------------------------------------------- // 六、多字节 UTF-8(中文 key/值、emoji)与转义——回归 consume_quoted 逐字节 bug // -------------------------------------------------------------------- #[test] fn jsonpath_chinese_key_and_value() { // 中文 key + 中文值:验证 consume_quoted 整体切片保留 UTF-8(逐字节 as char 会乱码) let ctx = json!({ "状态": "已完成" }); assert_eq!( ConditionEngine::evaluate("$.状态 == '已完成'", &ctx).unwrap(), true ); assert_eq!( ConditionEngine::evaluate("$.状态 == '失败'", &ctx).unwrap(), false ); } #[test] fn jsonpath_bracket_chinese_key() { // $['中文'] 单引号括号键含中文值,直接考验 consume_quoted 多字节处理 let ctx = json!({ "中文": "成功" }); assert_eq!( ConditionEngine::evaluate("$['中文'] == '成功'", &ctx).unwrap(), true ); } #[test] fn jsonpath_emoji_value() { // emoji(4 字节 UTF-8 序列)作字面量值 let ctx = json!({ "flag": "🚀" }); assert_eq!( ConditionEngine::evaluate("$.flag == '🚀'", &ctx).unwrap(), true ); } #[test] fn single_quote_escaped_inside_literal() { // 内部 '' 转义为单引号:it''s → it's let ctx = json!({ "msg": "it's done" }); assert_eq!( ConditionEngine::evaluate("$.msg == 'it''s done'", &ctx).unwrap(), true ); } }