1use anyhow::Result;
5use regex::Regex;
6use serde_json::Value;
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::rc::Rc;
10
11use crate::engine::AssertionResult;
12use crate::registry::{AssertionTiming, PluginContext, PluginRegistry, PluginResult};
13use apif_ast::assertion_ast::{AssertionExpr, BinaryOp, Expr, Literal, parse_assertion};
14fn normalize_plugin_name(name: &str) -> &str {
15 let trimmed = name.trim();
16 trimmed.strip_prefix('@').unwrap_or(trimmed)
17}
18
19thread_local! {
20 static REGEX_CACHE: RefCell<HashMap<String, std::result::Result<Rc<Regex>, String>>> =
21 RefCell::new(HashMap::new());
22}
23
24fn cached_regex(pattern: &str) -> std::result::Result<Rc<Regex>, String> {
25 if let Some(cached) = REGEX_CACHE.with(|cache| cache.borrow().get(pattern).cloned()) {
26 return cached;
27 }
28
29 let compiled = Regex::new(pattern)
30 .map(Rc::new)
31 .map_err(|err| err.to_string());
32
33 REGEX_CACHE.with(|cache| {
34 cache
35 .borrow_mut()
36 .insert(pattern.to_string(), compiled.clone());
37 });
38
39 compiled
40}
41
42pub fn evaluate_assertion(
46 registry: &dyn PluginRegistry,
47 assertion: &str,
48 response: &Value,
49 headers: Option<&HashMap<String, String>>,
50 trailers: Option<&HashMap<String, String>>,
51 timing: Option<&AssertionTiming>,
52) -> Result<Option<AssertionResult>> {
53 let trimmed = assertion.trim();
54 if trimmed.is_empty() {
55 return Ok(None);
56 }
57
58 let ast = parse_assertion(trimmed);
59 match &ast {
60 AssertionExpr::Raw(_) => Ok(None),
61 _ => evaluate_ast(registry, &ast, response, headers, trailers, timing).map(Some),
62 }
63}
64
65fn evaluate_ast(
66 pm: &dyn PluginRegistry,
67 expr: &AssertionExpr,
68 response: &Value,
69 headers: Option<&HashMap<String, String>>,
70 trailers: Option<&HashMap<String, String>>,
71 timing: Option<&AssertionTiming>,
72) -> Result<AssertionResult> {
73 match expr {
74 AssertionExpr::Not(inner) => {
75 let r = evaluate_ast(pm, inner, response, headers, trailers, timing)?;
76 Ok(negate(r))
77 }
78 AssertionExpr::NotNot(inner) => {
79 evaluate_ast(pm, inner, response, headers, trailers, timing)
80 }
81 AssertionExpr::And { left, right } => {
82 let lr = evaluate_ast(pm, left, response, headers, trailers, timing)?;
83 if !is_pass(&lr) {
84 return Ok(AssertionResult::fail(format!(
85 "Left of 'and' failed: {}",
86 fmt_result_short(&lr)
87 )));
88 }
89 let rr = evaluate_ast(pm, right, response, headers, trailers, timing)?;
90 if !is_pass(&rr) {
91 return Ok(AssertionResult::fail(format!(
92 "Right of 'and' failed: {}",
93 fmt_result_short(&rr)
94 )));
95 }
96 Ok(AssertionResult::Pass)
97 }
98 AssertionExpr::Or { left, right } => {
99 let lr = evaluate_ast(pm, left, response, headers, trailers, timing)?;
100 if is_pass(&lr) {
101 return Ok(AssertionResult::Pass);
102 }
103 let rr = evaluate_ast(pm, right, response, headers, trailers, timing)?;
104 if is_pass(&rr) {
105 return Ok(AssertionResult::Pass);
106 }
107 Ok(AssertionResult::fail(format!(
108 "Both sides of 'or' failed: left={}, right={}",
109 fmt_result_short(&lr),
110 fmt_result_short(&rr)
111 )))
112 }
113 AssertionExpr::Xor { left, right } => {
114 let lr = evaluate_ast(pm, left, response, headers, trailers, timing)?;
115 let rr = evaluate_ast(pm, right, response, headers, trailers, timing)?;
116 let lp = is_pass(&lr);
117 let rp = is_pass(&rr);
118 if lp != rp {
119 Ok(AssertionResult::Pass)
120 } else {
121 Ok(AssertionResult::fail(format!(
122 "Xor expects exactly one true, got left={} right={}",
123 lp, rp
124 )))
125 }
126 }
127 AssertionExpr::Binary { op, left, right } => {
128 let lhs = eval_value(pm, left, response, headers, trailers, timing);
129 let rhs = eval_value(pm, right, response, headers, trailers, timing);
130 compare(lhs, op, rhs, left, right)
131 }
132 AssertionExpr::Paren(inner) => evaluate_ast(pm, inner, response, headers, trailers, timing),
133 AssertionExpr::IfThenElse {
134 condition,
135 then_branch,
136 else_branch,
137 } => {
138 let cond = evaluate_ast(pm, condition, response, headers, trailers, timing)?;
139 if is_pass(&cond) {
140 evaluate_ast(pm, then_branch, response, headers, trailers, timing)
141 } else {
142 evaluate_ast(pm, else_branch, response, headers, trailers, timing)
143 }
144 }
145 AssertionExpr::Atom(_) => {
146 if let AssertionExpr::Atom(Expr::PluginCall { name, args }) = expr {
147 eval_plugin_as_assertion(pm, name, args, response, headers, trailers, timing)
148 } else {
149 let val = eval_value(pm, expr, response, headers, trailers, timing);
150 if is_truthy(&val) {
151 Ok(AssertionResult::Pass)
152 } else {
153 Ok(AssertionResult::fail(format!(
154 "Expression evaluated to falsy: {:?}",
155 val
156 )))
157 }
158 }
159 }
160 AssertionExpr::Raw(_) => Ok(AssertionResult::Error("Unparsed expression".into())),
161 }
162}
163
164fn validate_type_cast(val: &Value, type_name: &str) -> Value {
169 let valid = match type_name {
170 "bool" => val.is_boolean(),
171 "uint" => val.as_u64().is_some(),
172 "number" => val.is_number(),
173 "string" | "uuid" | "email" | "url" | "ip" => val.is_string(),
174 "time" | "timestamp" | "duration" => val.is_string() || val.is_number(),
175 "json" => val.is_object() || val.is_array(),
176 "yaml" => val.is_string(),
177 _ => true,
178 };
179 if valid { val.clone() } else { Value::Null }
180}
181
182fn eval_plugin_as_assertion(
183 pm: &dyn PluginRegistry,
184 name: &str,
185 args: &[AssertionExpr],
186 response: &Value,
187 headers: Option<&HashMap<String, String>>,
188 trailers: Option<&HashMap<String, String>>,
189 timing: Option<&AssertionTiming>,
190) -> Result<AssertionResult> {
191 let func_name = format!("@{}", name);
192 let resolved_name = normalize_plugin_name(&func_name);
193 if let Some(plugin) = pm.get_plugin(resolved_name) {
194 let ctx = PluginContext::new(response)
195 .with_headers(headers)
196 .with_trailers(trailers)
197 .with_timing(timing);
198 let arg_values: Vec<Value> = args
199 .iter()
200 .map(|a| eval_value(pm, a, response, headers, trailers, timing))
201 .collect();
202 match plugin.execute(&arg_values, &ctx) {
203 Ok(PluginResult::Assertion(res)) => Ok(res),
204 Ok(PluginResult::Value(val)) => {
205 if is_truthy(&val) {
206 Ok(AssertionResult::Pass)
207 } else {
208 Ok(AssertionResult::fail(format!(
209 "Plugin {} returned falsy value: {:?}",
210 resolved_name, val
211 )))
212 }
213 }
214 Err(e) => Ok(AssertionResult::Error(format!("Plugin error: {}", e))),
215 }
216 } else {
217 Ok(AssertionResult::Error(format!("Unknown plugin: {}", name)))
218 }
219}
220
221fn eval_value(
222 pm: &dyn PluginRegistry,
223 expr: &AssertionExpr,
224 response: &Value,
225 headers: Option<&HashMap<String, String>>,
226 trailers: Option<&HashMap<String, String>>,
227 timing: Option<&AssertionTiming>,
228) -> Value {
229 match expr {
230 AssertionExpr::Atom(atom) => eval_atom(pm, atom, response, headers, trailers, timing),
231 AssertionExpr::Paren(inner) => eval_value(pm, inner, response, headers, trailers, timing),
232 AssertionExpr::Not(inner) => {
233 let v = eval_value(pm, inner, response, headers, trailers, timing);
234 Value::Bool(!is_truthy(&v))
235 }
236 AssertionExpr::NotNot(inner) => eval_value(pm, inner, response, headers, trailers, timing),
237 AssertionExpr::And { left, right } => {
238 let lv = eval_value(pm, left, response, headers, trailers, timing);
239 if !is_truthy(&lv) {
240 return Value::Bool(false);
241 }
242 let rv = eval_value(pm, right, response, headers, trailers, timing);
243 Value::Bool(is_truthy(&rv))
244 }
245 AssertionExpr::Or { left, right } => {
246 let lv = eval_value(pm, left, response, headers, trailers, timing);
247 if is_truthy(&lv) {
248 return Value::Bool(true);
249 }
250 let rv = eval_value(pm, right, response, headers, trailers, timing);
251 Value::Bool(is_truthy(&rv))
252 }
253 AssertionExpr::Xor { left, right } => {
254 let lv = eval_value(pm, left, response, headers, trailers, timing);
255 let rv = eval_value(pm, right, response, headers, trailers, timing);
256 Value::Bool(is_truthy(&lv) != is_truthy(&rv))
257 }
258 AssertionExpr::Binary { op, left, right } => {
259 let lhs = eval_value(pm, left, response, headers, trailers, timing);
260 let rhs = eval_value(pm, right, response, headers, trailers, timing);
261 eval_binary_value(lhs, op, rhs)
262 }
263 AssertionExpr::IfThenElse {
264 condition,
265 then_branch,
266 else_branch,
267 } => {
268 let cv = eval_value(pm, condition, response, headers, trailers, timing);
269 if is_truthy(&cv) {
270 eval_value(pm, then_branch, response, headers, trailers, timing)
271 } else {
272 eval_value(pm, else_branch, response, headers, trailers, timing)
273 }
274 }
275 AssertionExpr::Raw(s) => resolve_path(s, response),
276 }
277}
278
279fn eval_atom(
280 pm: &dyn PluginRegistry,
281 atom: &Expr,
282 response: &Value,
283 headers: Option<&HashMap<String, String>>,
284 trailers: Option<&HashMap<String, String>>,
285 timing: Option<&AssertionTiming>,
286) -> Value {
287 match atom {
288 Expr::JqPath(p) => resolve_path(p, response),
289 Expr::PluginCall { name, args } => {
290 let func_name = format!("@{}", name);
291 let resolved_name = normalize_plugin_name(&func_name);
292 if let Some(plugin) = pm.get_plugin(resolved_name) {
293 let ctx = PluginContext::new(response)
294 .with_headers(headers)
295 .with_trailers(trailers)
296 .with_timing(timing);
297 let arg_values: Vec<Value> = args
298 .iter()
299 .map(|a| eval_value(pm, a, response, headers, trailers, timing))
300 .collect();
301 match plugin.execute(&arg_values, &ctx) {
302 Ok(PluginResult::Value(v)) => v,
303 Ok(PluginResult::Assertion(AssertionResult::Pass)) => Value::Bool(true),
304 Ok(PluginResult::Assertion(AssertionResult::Fail { .. })) => Value::Bool(false),
305 Ok(PluginResult::Assertion(AssertionResult::Error(e))) => {
306 Value::String(format!("error: {}", e))
307 }
308 Err(_) => Value::Null,
309 }
310 } else {
311 Value::Null
312 }
313 }
314 Expr::Literal(lit) => match lit {
315 Literal::Bool(b) => Value::Bool(*b),
316 Literal::Number(n) => n
317 .parse::<i64>()
318 .map(|i| Value::Number(serde_json::Number::from(i)))
319 .unwrap_or_else(|_| {
320 n.parse::<f64>()
321 .ok()
322 .and_then(serde_json::Number::from_f64)
323 .map(Value::Number)
324 .unwrap_or(Value::Null)
325 }),
326 Literal::Str(s) => Value::String(s.clone()),
327 Literal::Null => Value::Null,
328 },
329 Expr::Variable(name) => Value::String(format!("${}", name)),
330 Expr::RegExp { pattern, flags: _ } => Value::String(pattern.clone()),
331 Expr::Json(s) | Expr::Yaml(s) => serde_json::from_str(s).unwrap_or(Value::Null),
332 Expr::As(inner, type_name) => {
333 let val = eval_atom(pm, inner, response, headers, trailers, timing);
334 validate_type_cast(&val, type_name)
335 }
336 }
337}
338
339fn eval_binary_value(lhs: Value, op: &BinaryOp, rhs: Value) -> Value {
340 let pass = match op {
341 BinaryOp::Eq => lhs == rhs,
342 BinaryOp::Ne => lhs != rhs,
343 BinaryOp::Gt => compare_numeric(&lhs, &rhs, ">").unwrap_or(false),
344 BinaryOp::Lt => compare_numeric(&lhs, &rhs, "<").unwrap_or(false),
345 BinaryOp::Ge => compare_numeric(&lhs, &rhs, ">=").unwrap_or(false),
346 BinaryOp::Le => compare_numeric(&lhs, &rhs, "<=").unwrap_or(false),
347 BinaryOp::Contains => match (&lhs, &rhs) {
348 (Value::String(l), Value::String(r)) => l.contains(r),
349 (Value::Array(l), r) => l.contains(r),
350 (Value::Object(l), Value::String(r)) => l.contains_key(r),
351 _ => false,
352 },
353 BinaryOp::StartsWith => match (&lhs, &rhs) {
354 (Value::String(l), Value::String(r)) => l.starts_with(r),
355 _ => false,
356 },
357 BinaryOp::EndsWith => match (&lhs, &rhs) {
358 (Value::String(l), Value::String(r)) => l.ends_with(r),
359 _ => false,
360 },
361 BinaryOp::Matches => match (&lhs, &rhs) {
362 (Value::String(l), Value::String(r)) => cached_regex(r).is_ok_and(|re| re.is_match(l)),
363 _ => false,
364 },
365 };
366 Value::Bool(pass)
367}
368
369fn compare(
370 lhs: Value,
371 op: &BinaryOp,
372 rhs: Value,
373 left_expr: &AssertionExpr,
374 right_expr: &AssertionExpr,
375) -> Result<AssertionResult> {
376 if let BinaryOp::Matches = op
377 && let (Value::String(_l), Value::String(r)) = (&lhs, &rhs)
378 && cached_regex(r).is_err()
379 {
380 return Ok(AssertionResult::Error(format!("Invalid regex: {}", r)));
381 }
382 let pass = eval_binary_value(lhs.clone(), op, rhs.clone());
383 if pass == Value::Bool(true) {
384 Ok(AssertionResult::Pass)
385 } else {
386 Ok(AssertionResult::Fail {
387 message: format!(
388 "Assertion failed: {} {} {} (Values: {:?} vs {:?})",
389 left_expr,
390 op.as_str(),
391 right_expr,
392 lhs,
393 rhs
394 ),
395 expected: Some(format!("{} {:?}", op.as_str(), rhs)),
396 actual: Some(format!("{:?}", lhs)),
397 })
398 }
399}
400
401fn compare_numeric(lhs: &Value, rhs: &Value, op: &str) -> Option<bool> {
402 let lhs_num = lhs.as_number()?;
403 let rhs_num = rhs.as_number()?;
404
405 let lhs_i = lhs_num
406 .as_i64()
407 .map(i128::from)
408 .or_else(|| lhs_num.as_u64().map(i128::from));
409 let rhs_i = rhs_num
410 .as_i64()
411 .map(i128::from)
412 .or_else(|| rhs_num.as_u64().map(i128::from));
413
414 if let (Some(l), Some(r)) = (lhs_i, rhs_i) {
415 return Some(match op {
416 ">" => l > r,
417 "<" => l < r,
418 ">=" => l >= r,
419 "<=" => l <= r,
420 _ => return None,
421 });
422 }
423
424 let (l, r) = (lhs_num.as_f64()?, rhs_num.as_f64()?);
425 Some(match op {
426 ">" => l > r,
427 "<" => l < r,
428 ">=" => l >= r,
429 "<=" => l <= r,
430 _ => return None,
431 })
432}
433
434fn resolve_path(path: &str, root: &Value) -> Value {
435 if path == "." {
436 return root.clone();
437 }
438 if path.is_empty() {
439 return Value::Null;
440 }
441 if !path.starts_with('.') && !path.starts_with('$') {
442 return Value::String(path.to_string());
443 }
444 eval_jaq_one(path, root).unwrap_or(Value::Null)
445}
446
447fn eval_jaq_one(expr: &str, input: &Value) -> anyhow::Result<Value> {
448 super::engine::AssertionEngine::eval_jaq_one(expr, input)
449}
450
451fn is_truthy(val: &Value) -> bool {
452 !val.is_null() && val != &Value::Bool(false)
453}
454
455fn is_pass(r: &AssertionResult) -> bool {
456 matches!(r, AssertionResult::Pass)
457}
458
459fn negate(r: AssertionResult) -> AssertionResult {
460 r.negate()
461}
462
463fn fmt_result_short(r: &AssertionResult) -> String {
464 match r {
465 AssertionResult::Pass => "pass".into(),
466 AssertionResult::Fail { message, .. } => message.clone(),
467 AssertionResult::Error(e) => format!("error: {}", e),
468 }
469}
470
471#[cfg(test)]
472mod tests {
473 use super::*;
474 use serde_json::json;
475
476 fn pm() -> crate::registry::NoopPluginRegistry {
477 crate::registry::NoopPluginRegistry
478 }
479
480 fn eval(pm: &dyn PluginRegistry, expr: &str, response: &Value) -> AssertionResult {
481 evaluate_assertion(pm, expr, response, None, None, None)
482 .unwrap()
483 .unwrap_or(AssertionResult::Error("AST returned None".into()))
484 }
485
486 #[test]
487 fn test_equality_pass() {
488 let r = eval(
489 &pm(),
490 ".status == \"success\"",
491 &json!({"status": "success"}),
492 );
493 assert!(matches!(r, AssertionResult::Pass));
494 }
495
496 #[test]
497 fn test_equality_fail() {
498 let r = eval(&pm(), ".status == \"error\"", &json!({"status": "success"}));
499 assert!(matches!(r, AssertionResult::Fail { .. }));
500 }
501
502 #[test]
503 fn test_contains() {
504 let r = eval(&pm(), ".name contains \"te\"", &json!({"name": "test"}));
505 assert!(matches!(r, AssertionResult::Pass));
506 }
507
508 #[test]
509 fn test_xor_both_true() {
510 let r = eval(&pm(), ".x == 1 xor .y == 2", &json!({"x": 1, "y": 2}));
511 assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
512 }
513
514 #[test]
515 fn test_xor_both_false() {
516 let r = eval(&pm(), ".x == 9 xor .y == 9", &json!({"x": 1, "y": 2}));
517 assert!(matches!(r, AssertionResult::Fail { .. }), "got: {:?}", r);
518 }
519
520 #[test]
521 fn test_numeric_greater() {
522 let r = eval(&pm(), ".id > 100", &json!({"id": 123}));
523 assert!(matches!(r, AssertionResult::Pass));
524 }
525
526 #[test]
527 fn test_numeric_less() {
528 let r = eval(&pm(), ".id < 200", &json!({"id": 123}));
529 assert!(matches!(r, AssertionResult::Pass));
530 }
531
532 #[test]
533 fn test_matches_regex() {
534 let r = eval(&pm(), ".name matches \"^te.*t$\"", &json!({"name": "test"}));
535 assert!(matches!(r, AssertionResult::Pass));
536 }
537
538 #[test]
539 fn test_matches_regex_fail() {
540 let r = eval(&pm(), ".name matches \"^xyz\"", &json!({"name": "test"}));
541 assert!(matches!(r, AssertionResult::Fail { .. }));
542 }
543
544 #[test]
545 fn test_jq_fallback_via_raw() {
546 let p = pm();
547 let r = evaluate_assertion(
548 &p,
549 ".tags | length",
550 &json!({"tags": [1, 2, 3]}),
551 None,
552 None,
553 None,
554 )
555 .unwrap();
556 assert!(
557 r.is_none(),
558 "JQ pipe should return None to trigger JQ fallback"
559 );
560 }
561
562 #[test]
563 fn test_resolve_path_simple() {
564 let r = resolve_path(".key", &json!({"key": "value"}));
565 assert_eq!(r, json!("value"));
566 }
567
568 #[test]
569 fn test_resolve_path_nested() {
570 let r = resolve_path(".outer.inner", &json!({"outer": {"inner": "value"}}));
571 assert_eq!(r, json!("value"));
572 }
573
574 #[test]
575 fn test_resolve_path_array_index() {
576 let r = resolve_path(".items[0]", &json!({"items": ["first", "second"]}));
577 assert_eq!(r, json!("first"));
578 }
579
580 #[test]
581 fn test_resolve_path_missing_key() {
582 let r = resolve_path(".missing", &json!({"a": 1}));
583 assert!(r.is_null());
584 }
585
586 #[test]
587 fn test_compare_numeric_greater() {
588 assert_eq!(compare_numeric(&json!(5), &json!(3), ">"), Some(true));
589 }
590
591 #[test]
592 fn test_compare_numeric_less() {
593 assert_eq!(compare_numeric(&json!(3), &json!(5), "<"), Some(true));
594 }
595
596 #[test]
597 fn test_compare_numeric_equality() {
598 assert_eq!(compare_numeric(&json!(5), &json!(5), ">="), Some(true));
599 assert_eq!(compare_numeric(&json!(5), &json!(5), "<="), Some(true));
600 }
601
602 #[test]
603 fn test_compare_numeric_mixed_types() {
604 assert_eq!(compare_numeric(&json!(5), &json!("5"), ">"), None);
605 }
606
607 #[test]
608 fn test_cached_regex_valid() {
609 assert!(cached_regex(r"\d+").is_ok());
610 }
611
612 #[test]
613 fn test_cached_regex_invalid() {
614 assert!(cached_regex(r"[").is_err());
615 }
616
617 #[test]
618 fn test_validate_type_cast() {
619 use serde_json::json;
620 assert_eq!(validate_type_cast(&json!(42), "number"), json!(42));
621 assert_eq!(validate_type_cast(&json!("hello"), "number"), Value::Null);
622 assert_eq!(
623 validate_type_cast(&json!("hello"), "string"),
624 json!("hello")
625 );
626 assert_eq!(validate_type_cast(&json!(42), "string"), Value::Null);
627 assert_eq!(validate_type_cast(&json!(true), "bool"), json!(true));
628 assert_eq!(validate_type_cast(&json!("hello"), "bool"), Value::Null);
629 assert_eq!(validate_type_cast(&json!(42u64), "uint"), json!(42u64));
630 assert_eq!(validate_type_cast(&json!(-1), "uint"), Value::Null);
631 assert_eq!(
632 validate_type_cast(&json!("uuid-str"), "uuid"),
633 json!("uuid-str")
634 );
635 assert_eq!(
636 validate_type_cast(&json!("email@x.com"), "email"),
637 json!("email@x.com")
638 );
639 assert_eq!(validate_type_cast(&json!("url"), "url"), json!("url"));
640 assert_eq!(
641 validate_type_cast(&json!("1.2.3.4"), "ip"),
642 json!("1.2.3.4")
643 );
644 assert_eq!(
645 validate_type_cast(&json!("2024-01-01"), "time"),
646 json!("2024-01-01")
647 );
648 assert_eq!(validate_type_cast(&json!(12345), "timestamp"), json!(12345));
649 assert_eq!(
650 validate_type_cast(&json!("100ms"), "duration"),
651 json!("100ms")
652 );
653 assert_eq!(
654 validate_type_cast(&json!({"k": "v"}), "json"),
655 json!({"k": "v"})
656 );
657 assert_eq!(validate_type_cast(&json!([1, 2]), "json"), json!([1, 2]));
658 assert_eq!(validate_type_cast(&json!("hello"), "json"), Value::Null);
659 assert_eq!(
660 validate_type_cast(&json!("yaml:val"), "yaml"),
661 json!("yaml:val")
662 );
663 assert_eq!(
664 validate_type_cast(&json!("any_val"), "unknown_type"),
665 json!("any_val")
666 );
667 }
668
669 #[test]
670 fn test_normalize_plugin_name_assert() {
671 assert_eq!(normalize_plugin_name("@uuid"), "uuid");
672 assert_eq!(normalize_plugin_name("uuid"), "uuid");
673 assert_eq!(normalize_plugin_name(" @uuid "), "uuid");
674 }
675
676 #[test]
677 fn test_is_truthy() {
678 assert!(!is_truthy(&Value::Null));
679 assert!(!is_truthy(&Value::Bool(false)));
680 assert!(is_truthy(&Value::Bool(true)));
681 assert!(is_truthy(&Value::Number(0.into())));
682 assert!(is_truthy(&Value::String("".into())));
683 }
684
685 #[test]
686 fn test_negate() {
687 let pass = AssertionResult::Pass;
688 assert!(matches!(negate(pass), AssertionResult::Fail { .. }));
689
690 let fail = AssertionResult::fail("msg");
691 assert!(matches!(negate(fail), AssertionResult::Pass));
692
693 let err = AssertionResult::Error("err".into());
694 assert!(matches!(negate(err), AssertionResult::Error(_)));
695 }
696
697 #[test]
698 fn test_fmt_result_short() {
699 assert_eq!(fmt_result_short(&AssertionResult::Pass), "pass");
700 assert_eq!(fmt_result_short(&AssertionResult::fail("msg")), "msg");
701 assert_eq!(
702 fmt_result_short(&AssertionResult::Error("err".into())),
703 "error: err"
704 );
705 }
706
707 #[test]
708 fn test_eval_atom_literal() {
709 let pm = crate::registry::NoopPluginRegistry;
710 let ctx = &json!({});
711 use apif_ast::assertion_ast::{Expr, Literal};
712 let result = eval_atom(
713 &pm,
714 &Expr::Literal(Literal::Number("42".into())),
715 ctx,
716 None,
717 None,
718 None,
719 );
720 assert_eq!(result, json!(42));
721 }
722
723 #[test]
724 fn test_eval_binary_value_num() {
725 use apif_ast::assertion_ast::BinaryOp;
726 assert_eq!(
727 eval_binary_value(json!(5), &BinaryOp::Gt, json!(3)),
728 json!(true)
729 );
730 assert_eq!(
731 eval_binary_value(json!(3), &BinaryOp::Gt, json!(5)),
732 json!(false)
733 );
734 }
735}