1use std::collections::HashMap;
31use thiserror::Error;
32use winnow::ascii::{digit1, multispace0, multispace1};
33use winnow::combinator::{alt, delimited, opt, preceded, repeat, separated, terminated};
34use winnow::error::ContextError;
35use winnow::prelude::*;
36use winnow::token::{any, none_of, one_of, take_while};
37
38#[derive(Debug, Clone, PartialEq)]
41pub enum Value {
42 Number(f64),
43 String(String),
44 Bool(bool),
45 Null,
46 Array(Vec<Value>),
47 Object(HashMap<String, Value>),
48 Type(String),
49}
50
51impl Value {
52 pub fn as_bool(&self) -> Result<bool, EvalError> {
53 match self {
54 Value::Bool(b) => Ok(*b),
55 _ => Err(EvalError::TypeError {
56 expected: "bool",
57 got: self.type_name(),
58 }),
59 }
60 }
61
62 pub fn as_number(&self) -> Result<f64, EvalError> {
63 match self {
64 Value::Number(n) => Ok(*n),
65 _ => Err(EvalError::TypeError {
66 expected: "number",
67 got: self.type_name(),
68 }),
69 }
70 }
71
72 pub fn as_string(&self) -> Result<&str, EvalError> {
73 match self {
74 Value::String(s) => Ok(s),
75 _ => Err(EvalError::TypeError {
76 expected: "string",
77 got: self.type_name(),
78 }),
79 }
80 }
81
82 pub fn as_array(&self) -> Result<&[Value], EvalError> {
83 match self {
84 Value::Array(a) => Ok(a),
85 _ => Err(EvalError::TypeError {
86 expected: "array",
87 got: self.type_name(),
88 }),
89 }
90 }
91
92 pub fn as_object(&self) -> Result<&HashMap<String, Value>, EvalError> {
93 match self {
94 Value::Object(o) => Ok(o),
95 _ => Err(EvalError::TypeError {
96 expected: "object",
97 got: self.type_name(),
98 }),
99 }
100 }
101
102 pub fn type_name(&self) -> &'static str {
103 match self {
104 Value::Number(_) => "number",
105 Value::String(_) => "string",
106 Value::Bool(_) => "bool",
107 Value::Null => "null",
108 Value::Array(_) => "array",
109 Value::Object(_) => "object",
110 Value::Type(_) => "type",
111 }
112 }
113
114 pub fn type_value(&self) -> Value {
115 Value::Type(self.type_name().to_string())
116 }
117}
118
119#[derive(Debug, Clone, PartialEq)]
122pub enum Expr {
123 Number(f64),
124 String(String),
125 Bool(bool),
126 Null,
127 Var(String),
128 Array(Vec<Expr>),
129 Object(Vec<(String, Expr)>),
130 TypeLiteral(String),
131 UnaryOp {
132 op: UnaryOp,
133 expr: Box<Expr>,
134 },
135 BinaryOp {
136 op: BinaryOp,
137 left: Box<Expr>,
138 right: Box<Expr>,
139 },
140 FuncCall {
141 name: String,
142 args: Vec<Expr>,
143 },
144 Index {
145 expr: Box<Expr>,
146 index: Box<Expr>,
147 },
148 Property {
149 expr: Box<Expr>,
150 name: String,
151 },
152 ForAll {
153 predicate: Box<Expr>,
154 var: String,
155 iterable: Box<Expr>,
156 },
157}
158
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub enum UnaryOp {
161 Not,
162 Neg,
163}
164
165#[derive(Debug, Clone, Copy, PartialEq)]
166pub enum BinaryOp {
167 Add,
168 Sub,
169 Mul,
170 Div,
171 Mod,
172 Pow,
173 Eq,
174 Ne,
175 Lt,
176 Le,
177 Gt,
178 Ge,
179 And,
180 Or,
181 Contains,
182 NotContains,
183 StartsWith,
184 NotStartsWith,
185 EndsWith,
186 NotEndsWith,
187 Matches,
188 NotMatches,
189}
190
191#[derive(Error, Debug, Clone, PartialEq)]
192pub enum EvalError {
193 #[error("type error: expected {expected}, got {got}")]
194 TypeError {
195 expected: &'static str,
196 got: &'static str,
197 },
198 #[error("undefined variable: {0}")]
199 UndefinedVariable(String),
200 #[error("undefined function: {0}")]
201 UndefinedFunction(String),
202 #[error("invalid regex: {0}")]
203 InvalidRegex(String),
204 #[error("division by zero")]
205 DivisionByZero,
206 #[error("parse error: {0}")]
207 ParseError(String),
208 #[error("wrong number of arguments for {func}: expected {expected}, got {got}")]
209 WrongArgCount {
210 func: String,
211 expected: usize,
212 got: usize,
213 },
214 #[error("index out of bounds: {index} (len: {len})")]
215 IndexOutOfBounds { index: i64, len: usize },
216 #[error("key not found: {0}")]
217 KeyNotFound(String),
218}
219
220fn ws<'a, P, O>(p: P) -> impl Parser<&'a str, O, ContextError>
223where
224 P: Parser<&'a str, O, ContextError>,
225{
226 delimited(multispace0, p, multispace0)
227}
228
229fn number(input: &mut &str) -> ModalResult<Expr> {
230 let neg: Option<char> = opt('-').parse_next(input)?;
231 let int_part: &str = digit1.parse_next(input)?;
232 let frac_part: Option<&str> = opt(preceded('.', digit1)).parse_next(input)?;
233
234 let mut s = String::new();
235 if neg.is_some() {
236 s.push('-');
237 }
238 s.push_str(int_part);
239 if let Some(frac) = frac_part {
240 s.push('.');
241 s.push_str(frac);
242 }
243
244 Ok(Expr::Number(s.parse().unwrap()))
245}
246
247fn string_char(input: &mut &str) -> ModalResult<char> {
248 let c: char = none_of('"').parse_next(input)?;
249 if c == '\\' {
250 let escaped: char = any.parse_next(input)?;
251 Ok(match escaped {
252 'n' => '\n',
253 't' => '\t',
254 'r' => '\r',
255 '"' => '"',
256 '\\' => '\\',
257 c => c,
258 })
259 } else {
260 Ok(c)
261 }
262}
263
264fn string_literal(input: &mut &str) -> ModalResult<Expr> {
265 let chars: String = delimited(
266 '"',
267 repeat(0.., string_char).fold(String::new, |mut s, c| {
268 s.push(c);
269 s
270 }),
271 '"',
272 )
273 .parse_next(input)?;
274 Ok(Expr::String(chars))
275}
276
277fn regex_literal(input: &mut &str) -> ModalResult<Expr> {
278 '/'.parse_next(input)?;
279 let mut s = String::new();
280 loop {
281 let c: char = any.parse_next(input)?;
282 if c == '/' {
283 break;
284 }
285 if c == '\\' {
286 let escaped: char = any.parse_next(input)?;
287 s.push('\\');
288 s.push(escaped);
289 } else {
290 s.push(c);
291 }
292 }
293 Ok(Expr::String(s))
294}
295
296fn ident(input: &mut &str) -> ModalResult<String> {
297 let first: char = one_of(|c: char| c.is_ascii_alphabetic() || c == '_').parse_next(input)?;
298 let rest: &str =
299 take_while(0.., |c: char| c.is_ascii_alphanumeric() || c == '_').parse_next(input)?;
300 Ok(format!("{}{}", first, rest))
301}
302
303fn var_or_bool_or_func(input: &mut &str) -> ModalResult<Expr> {
304 let name = ident.parse_next(input)?;
305
306 let _ = multispace0.parse_next(input)?;
307 if input.starts_with('(') {
308 '('.parse_next(input)?;
309 let _ = multispace0.parse_next(input)?;
310 let args: Vec<Expr> = separated(0.., ws(expr), ws(',')).parse_next(input)?;
311 let _ = multispace0.parse_next(input)?;
312 ')'.parse_next(input)?;
313 return Ok(Expr::FuncCall { name, args });
314 }
315
316 match name.as_str() {
317 "true" => Ok(Expr::Bool(true)),
318 "false" => Ok(Expr::Bool(false)),
319 "null" => Ok(Expr::TypeLiteral(name)),
322 "number" | "string" | "bool" | "array" | "object" => Ok(Expr::TypeLiteral(name)),
324 _ => Ok(Expr::Var(name)),
325 }
326}
327
328fn array(input: &mut &str) -> ModalResult<Expr> {
329 let elements: Vec<Expr> = delimited(
330 ('[', multispace0),
331 separated(0.., ws(expr), ws(',')),
332 (multispace0, ']'),
333 )
334 .parse_next(input)?;
335 Ok(Expr::Array(elements))
336}
337
338fn object_key(input: &mut &str) -> ModalResult<String> {
339 alt((
340 delimited(
342 '"',
343 repeat(0.., string_char).fold(String::new, |mut s, c| {
344 s.push(c);
345 s
346 }),
347 '"',
348 ),
349 ident,
351 ))
352 .parse_next(input)
353}
354
355fn object_entry(input: &mut &str) -> ModalResult<(String, Expr)> {
356 let key = ws(object_key).parse_next(input)?;
357 ws(':').parse_next(input)?;
358 let value = ws(expr).parse_next(input)?;
359 Ok((key, value))
360}
361
362fn object(input: &mut &str) -> ModalResult<Expr> {
363 let entries: Vec<(String, Expr)> = delimited(
364 ('{', multispace0),
365 separated(0.., object_entry, ws(',')),
366 (multispace0, '}'),
367 )
368 .parse_next(input)?;
369 Ok(Expr::Object(entries))
370}
371
372const TYPE_KEYWORDS: &[&str] = &["number", "string", "bool", "null", "array", "object"];
373
374fn type_literal(input: &mut &str) -> ModalResult<Expr> {
375 for &kw in TYPE_KEYWORDS {
376 if input.starts_with(kw) {
377 let after = &(*input)[kw.len()..];
378 let next_char = after.chars().next();
379 if next_char
380 .map(|c| c.is_ascii_alphanumeric() || c == '_')
381 .unwrap_or(false)
382 {
383 continue;
384 }
385 *input = after;
386 return Ok(Expr::TypeLiteral(kw.to_string()));
387 }
388 }
389 Err(winnow::error::ErrMode::Backtrack(ContextError::new()))
390}
391
392fn atom(input: &mut &str) -> ModalResult<Expr> {
393 let _ = multispace0.parse_next(input)?;
394 alt((
395 delimited(('(', multispace0), expr, (multispace0, ')')),
396 array,
397 object,
398 string_literal,
399 regex_literal,
400 number,
401 var_or_bool_or_func,
402 type_literal,
403 ))
404 .parse_next(input)
405}
406
407fn postfix(input: &mut &str) -> ModalResult<Expr> {
408 let mut base = atom.parse_next(input)?;
409 loop {
410 let _ = multispace0.parse_next(input)?;
411 if input.starts_with('[') {
412 '['.parse_next(input)?;
413 let _ = multispace0.parse_next(input)?;
414 let index = expr.parse_next(input)?;
415 let _ = multispace0.parse_next(input)?;
416 ']'.parse_next(input)?;
417 base = Expr::Index {
418 expr: Box::new(base),
419 index: Box::new(index),
420 };
421 } else if input.starts_with('.') {
422 '.'.parse_next(input)?;
423 let name = ident.parse_next(input)?;
424 base = Expr::Property {
425 expr: Box::new(base),
426 name,
427 };
428 } else {
429 break;
430 }
431 }
432 Ok(base)
433}
434
435fn unary(input: &mut &str) -> ModalResult<Expr> {
436 let _ = multispace0.parse_next(input)?;
437 let neg: Option<char> = opt('-').parse_next(input)?;
438 if neg.is_some() {
439 let e = unary.parse_next(input)?;
440 return Ok(Expr::UnaryOp {
441 op: UnaryOp::Neg,
442 expr: Box::new(e),
443 });
444 }
445 postfix(input)
446}
447
448fn pow(input: &mut &str) -> ModalResult<Expr> {
449 let base = unary.parse_next(input)?;
450 let _ = multispace0.parse_next(input)?;
451 let caret: Option<char> = opt('^').parse_next(input)?;
452 if caret.is_some() {
453 let _ = multispace0.parse_next(input)?;
454 let exp = pow.parse_next(input)?;
455 Ok(Expr::BinaryOp {
456 op: BinaryOp::Pow,
457 left: Box::new(base),
458 right: Box::new(exp),
459 })
460 } else {
461 Ok(base)
462 }
463}
464
465fn term(input: &mut &str) -> ModalResult<Expr> {
466 let init = pow.parse_next(input)?;
467
468 repeat(0.., (ws(one_of(['*', '/', '%'])), pow))
469 .fold(
470 move || init.clone(),
471 |acc, (op_char, val): (char, Expr)| {
472 let op = match op_char {
473 '*' => BinaryOp::Mul,
474 '/' => BinaryOp::Div,
475 '%' => BinaryOp::Mod,
476 _ => unreachable!(),
477 };
478 Expr::BinaryOp {
479 op,
480 left: Box::new(acc),
481 right: Box::new(val),
482 }
483 },
484 )
485 .parse_next(input)
486}
487
488fn arith(input: &mut &str) -> ModalResult<Expr> {
489 let init = term.parse_next(input)?;
490
491 repeat(0.., (ws(one_of(['+', '-'])), term))
492 .fold(
493 move || init.clone(),
494 |acc, (op_char, val): (char, Expr)| {
495 let op = if op_char == '+' {
496 BinaryOp::Add
497 } else {
498 BinaryOp::Sub
499 };
500 Expr::BinaryOp {
501 op,
502 left: Box::new(acc),
503 right: Box::new(val),
504 }
505 },
506 )
507 .parse_next(input)
508}
509
510fn peek_non_ident(input: &mut &str) -> ModalResult<()> {
511 let next = input.chars().next();
512 if next
513 .map(|c| c.is_ascii_alphanumeric() || c == '_')
514 .unwrap_or(false)
515 {
516 Err(winnow::error::ErrMode::Backtrack(ContextError::new()))
517 } else {
518 Ok(())
519 }
520}
521
522fn cmp_op(input: &mut &str) -> ModalResult<BinaryOp> {
523 alt((
524 "==".value(BinaryOp::Eq),
525 "!=".value(BinaryOp::Ne),
526 "<=".value(BinaryOp::Le),
527 ">=".value(BinaryOp::Ge),
528 "<".value(BinaryOp::Lt),
529 ">".value(BinaryOp::Gt),
530 (
531 terminated("not", peek_non_ident),
532 multispace1,
533 terminated("contains", peek_non_ident),
534 )
535 .value(BinaryOp::NotContains),
536 (
537 terminated("not", peek_non_ident),
538 multispace1,
539 terminated("startswith", peek_non_ident),
540 )
541 .value(BinaryOp::NotStartsWith),
542 (
543 terminated("not", peek_non_ident),
544 multispace1,
545 terminated("endswith", peek_non_ident),
546 )
547 .value(BinaryOp::NotEndsWith),
548 (
549 terminated("not", peek_non_ident),
550 multispace1,
551 terminated("matches", peek_non_ident),
552 )
553 .value(BinaryOp::NotMatches),
554 terminated("contains", peek_non_ident).value(BinaryOp::Contains),
555 terminated("startswith", peek_non_ident).value(BinaryOp::StartsWith),
556 terminated("endswith", peek_non_ident).value(BinaryOp::EndsWith),
557 terminated("matches", peek_non_ident).value(BinaryOp::Matches),
558 ))
559 .parse_next(input)
560}
561
562fn comparison(input: &mut &str) -> ModalResult<Expr> {
563 let left = arith.parse_next(input)?;
564 let _ = multispace0.parse_next(input)?;
565
566 let op_opt: Option<BinaryOp> = opt(cmp_op).parse_next(input)?;
567 match op_opt {
568 Some(op) => {
569 let _ = multispace0.parse_next(input)?;
570 let right = arith.parse_next(input)?;
571 Ok(Expr::BinaryOp {
572 op,
573 left: Box::new(left),
574 right: Box::new(right),
575 })
576 }
577 None => Ok(left),
578 }
579}
580
581fn not_expr(input: &mut &str) -> ModalResult<Expr> {
582 let _ = multispace0.parse_next(input)?;
583 let not_kw: Option<&str> = opt(terminated("not", peek_non_ident)).parse_next(input)?;
584 if not_kw.is_some() {
585 let _ = multispace0.parse_next(input)?;
586 let e = not_expr.parse_next(input)?;
587 Ok(Expr::UnaryOp {
588 op: UnaryOp::Not,
589 expr: Box::new(e),
590 })
591 } else {
592 comparison(input)
593 }
594}
595
596fn and_expr(input: &mut &str) -> ModalResult<Expr> {
597 let init = not_expr.parse_next(input)?;
598
599 repeat(
600 0..,
601 preceded((multispace0, "and", peek_non_ident, multispace0), not_expr),
602 )
603 .fold(
604 move || init.clone(),
605 |acc, val| Expr::BinaryOp {
606 op: BinaryOp::And,
607 left: Box::new(acc),
608 right: Box::new(val),
609 },
610 )
611 .parse_next(input)
612}
613
614fn or_expr(input: &mut &str) -> ModalResult<Expr> {
615 let init = and_expr.parse_next(input)?;
616
617 repeat(
618 0..,
619 preceded((multispace0, "or", peek_non_ident, multispace0), and_expr),
620 )
621 .fold(
622 move || init.clone(),
623 |acc, val| Expr::BinaryOp {
624 op: BinaryOp::Or,
625 left: Box::new(acc),
626 right: Box::new(val),
627 },
628 )
629 .parse_next(input)
630}
631
632fn forall_expr(input: &mut &str) -> ModalResult<Expr> {
633 let predicate = or_expr.parse_next(input)?;
634 let _ = multispace0.parse_next(input)?;
635
636 let forall_kw: Option<&str> = opt(terminated("forall", peek_non_ident)).parse_next(input)?;
637 if forall_kw.is_some() {
638 let _ = multispace0.parse_next(input)?;
639 let var = ident.parse_next(input)?;
640 let _ = multispace0.parse_next(input)?;
641 terminated("in", peek_non_ident).parse_next(input)?;
642 let _ = multispace0.parse_next(input)?;
643 let iterable = or_expr.parse_next(input)?;
644 Ok(Expr::ForAll {
645 predicate: Box::new(predicate),
646 var,
647 iterable: Box::new(iterable),
648 })
649 } else {
650 Ok(predicate)
651 }
652}
653
654fn expr(input: &mut &str) -> ModalResult<Expr> {
655 forall_expr(input)
656}
657
658pub fn parse(input: &str) -> Result<Expr, EvalError> {
659 let original_input = input.trim();
660 let mut input = original_input;
661 match expr.parse_next(&mut input) {
662 Ok(e) => {
663 let remaining = input.trim();
664 if remaining.is_empty() {
665 Ok(e)
666 } else {
667 Err(EvalError::ParseError(format!(
668 "unexpected trailing input: '{}'",
669 remaining
670 )))
671 }
672 }
673 Err(_) => {
674 if original_input.starts_with('#') {
676 Err(EvalError::ParseError(
677 "comments are not supported (lines starting with '#' are treated as constraints)".to_string()
678 ))
679 } else if original_input.contains("//") {
680 Err(EvalError::ParseError(
681 "comments are not supported ('// ...' is not valid)".to_string(),
682 ))
683 } else if original_input.is_empty() {
684 Err(EvalError::ParseError("empty constraint".to_string()))
685 } else {
686 let first_word = original_input.split_whitespace().next().unwrap_or("");
688 if !first_word
689 .chars()
690 .next()
691 .map(|c| c.is_alphabetic() || c == '_')
692 .unwrap_or(false)
693 && !first_word.starts_with('(')
694 && !first_word.starts_with('-')
695 && !first_word.starts_with('"')
696 && !first_word.starts_with('[')
697 && !first_word.starts_with('{')
698 && !first_word
699 .chars()
700 .next()
701 .map(|c| c.is_numeric())
702 .unwrap_or(false)
703 {
704 Err(EvalError::ParseError(format!(
705 "invalid syntax near '{}' - constraints must be expressions like 'x > 0' or 'len(arr) == 3'",
706 first_word
707 )))
708 } else {
709 Err(EvalError::ParseError(format!(
710 "invalid expression syntax in '{}'",
711 if original_input.len() > 50 {
712 format!("{}...", &original_input[..50])
713 } else {
714 original_input.to_string()
715 }
716 )))
717 }
718 }
719 }
720 }
721}
722
723pub fn evaluate(expr: &Expr, vars: &HashMap<String, Value>) -> Result<Value, EvalError> {
726 match expr {
727 Expr::Number(n) => Ok(Value::Number(*n)),
728 Expr::String(s) => Ok(Value::String(s.clone())),
729 Expr::Bool(b) => Ok(Value::Bool(*b)),
730 Expr::Null => Ok(Value::Null),
731 Expr::TypeLiteral(t) => Ok(Value::Type(t.clone())),
732 Expr::Var(name) => vars
733 .get(name)
734 .cloned()
735 .ok_or_else(|| EvalError::UndefinedVariable(name.clone())),
736 Expr::Array(elements) => {
737 let values: Result<Vec<_>, _> = elements.iter().map(|e| evaluate(e, vars)).collect();
738 Ok(Value::Array(values?))
739 }
740 Expr::Object(entries) => {
741 let mut map = HashMap::new();
742 for (key, val_expr) in entries {
743 map.insert(key.clone(), evaluate(val_expr, vars)?);
744 }
745 Ok(Value::Object(map))
746 }
747 Expr::UnaryOp { op, expr } => {
748 let val = evaluate(expr, vars)?;
749 match op {
750 UnaryOp::Not => Ok(Value::Bool(!val.as_bool()?)),
751 UnaryOp::Neg => Ok(Value::Number(-val.as_number()?)),
752 }
753 }
754 Expr::BinaryOp { op, left, right } => eval_binary_op(*op, left, right, vars),
755 Expr::FuncCall { name, args } => eval_func_call(name, args, vars),
756 Expr::Index { expr, index } => {
757 let base = evaluate(expr, vars)?;
758 let idx = evaluate(index, vars)?;
759 match &base {
760 Value::Array(arr) => {
761 let i = idx.as_number()?;
762 let actual_index = if i < 0.0 {
763 let neg_idx = (-i) as usize;
765 if neg_idx > arr.len() {
766 return Err(EvalError::IndexOutOfBounds {
767 index: i as i64,
768 len: arr.len(),
769 });
770 }
771 arr.len() - neg_idx
772 } else {
773 i as usize
774 };
775 arr.get(actual_index)
776 .cloned()
777 .ok_or(EvalError::IndexOutOfBounds {
778 index: i as i64,
779 len: arr.len(),
780 })
781 }
782 Value::String(s) => {
783 let i = idx.as_number()?;
784 let chars: Vec<char> = s.chars().collect();
785 let actual_index = if i < 0.0 {
786 let neg_idx = (-i) as usize;
787 if neg_idx > chars.len() {
788 return Err(EvalError::IndexOutOfBounds {
789 index: i as i64,
790 len: chars.len(),
791 });
792 }
793 chars.len() - neg_idx
794 } else {
795 i as usize
796 };
797 chars
798 .get(actual_index)
799 .map(|c| Value::String(c.to_string()))
800 .ok_or(EvalError::IndexOutOfBounds {
801 index: i as i64,
802 len: chars.len(),
803 })
804 }
805 Value::Object(obj) => {
806 let key = idx.as_string()?;
807 obj.get(key)
808 .cloned()
809 .ok_or_else(|| EvalError::KeyNotFound(key.to_string()))
810 }
811 _ => Err(EvalError::TypeError {
812 expected: "array, string, or object",
813 got: base.type_name(),
814 }),
815 }
816 }
817 Expr::Property { expr, name } => {
818 let base = evaluate(expr, vars)?;
819 let obj = base.as_object()?;
820 obj.get(name)
821 .cloned()
822 .ok_or_else(|| EvalError::KeyNotFound(name.clone()))
823 }
824 Expr::ForAll {
825 predicate,
826 var,
827 iterable,
828 } => {
829 let iter_val = evaluate(iterable, vars)?;
830 let items = match &iter_val {
831 Value::Array(arr) => arr.clone(),
832 Value::Object(obj) => obj.values().cloned().collect(),
833 _ => {
834 return Err(EvalError::TypeError {
835 expected: "array or object",
836 got: iter_val.type_name(),
837 });
838 }
839 };
840 for item in items {
841 let mut local_vars = vars.clone();
842 local_vars.insert(var.clone(), item);
843 let result = evaluate(predicate, &local_vars)?;
844 if !result.as_bool()? {
845 return Ok(Value::Bool(false));
846 }
847 }
848 Ok(Value::Bool(true))
849 }
850 }
851}
852
853fn eval_func_call(
854 name: &str,
855 args: &[Expr],
856 vars: &HashMap<String, Value>,
857) -> Result<Value, EvalError> {
858 match name {
859 "len" => {
860 if args.len() != 1 {
861 return Err(EvalError::WrongArgCount {
862 func: name.to_string(),
863 expected: 1,
864 got: args.len(),
865 });
866 }
867 let val = evaluate(&args[0], vars)?;
868 match val {
869 Value::String(s) => Ok(Value::Number(s.chars().count() as f64)),
870 Value::Array(a) => Ok(Value::Number(a.len() as f64)),
871 Value::Object(o) => Ok(Value::Number(o.len() as f64)),
872 _ => Err(EvalError::TypeError {
873 expected: "string, array, or object",
874 got: val.type_name(),
875 }),
876 }
877 }
878 "type" => {
879 if args.len() != 1 {
880 return Err(EvalError::WrongArgCount {
881 func: name.to_string(),
882 expected: 1,
883 got: args.len(),
884 });
885 }
886 let val = evaluate(&args[0], vars)?;
887 Ok(Value::Type(val.type_name().to_string()))
888 }
889 "keys" => {
890 if args.len() != 1 {
891 return Err(EvalError::WrongArgCount {
892 func: name.to_string(),
893 expected: 1,
894 got: args.len(),
895 });
896 }
897 let val = evaluate(&args[0], vars)?;
898 let obj = val.as_object()?;
899 let mut keys: Vec<String> = obj.keys().cloned().collect();
900 keys.sort();
901 let keys: Vec<Value> = keys.into_iter().map(Value::String).collect();
902 Ok(Value::Array(keys))
903 }
904 "values" => {
905 if args.len() != 1 {
906 return Err(EvalError::WrongArgCount {
907 func: name.to_string(),
908 expected: 1,
909 got: args.len(),
910 });
911 }
912 let val = evaluate(&args[0], vars)?;
913 let obj = val.as_object()?;
914 let mut pairs: Vec<(&String, &Value)> = obj.iter().collect();
916 pairs.sort_by_key(|(k, _)| *k);
917 let values: Vec<Value> = pairs.into_iter().map(|(_, v)| v.clone()).collect();
918 Ok(Value::Array(values))
919 }
920 "sum" => {
921 if args.len() != 1 {
922 return Err(EvalError::WrongArgCount {
923 func: name.to_string(),
924 expected: 1,
925 got: args.len(),
926 });
927 }
928 let val = evaluate(&args[0], vars)?;
929 let arr = val.as_array()?;
930 let mut total = 0.0;
931 for item in arr {
932 total += item.as_number()?;
933 }
934 Ok(Value::Number(total))
935 }
936 "min" => {
937 if args.len() != 1 {
938 return Err(EvalError::WrongArgCount {
939 func: name.to_string(),
940 expected: 1,
941 got: args.len(),
942 });
943 }
944 let val = evaluate(&args[0], vars)?;
945 let arr = val.as_array()?;
946 if arr.is_empty() {
947 return Err(EvalError::TypeError {
948 expected: "non-empty array",
949 got: "empty array",
950 });
951 }
952 let mut min_val = arr[0].as_number()?;
953 for item in arr.iter().skip(1) {
954 let n = item.as_number()?;
955 if n < min_val {
956 min_val = n;
957 }
958 }
959 Ok(Value::Number(min_val))
960 }
961 "max" => {
962 if args.len() != 1 {
963 return Err(EvalError::WrongArgCount {
964 func: name.to_string(),
965 expected: 1,
966 got: args.len(),
967 });
968 }
969 let val = evaluate(&args[0], vars)?;
970 let arr = val.as_array()?;
971 if arr.is_empty() {
972 return Err(EvalError::TypeError {
973 expected: "non-empty array",
974 got: "empty array",
975 });
976 }
977 let mut max_val = arr[0].as_number()?;
978 for item in arr.iter().skip(1) {
979 let n = item.as_number()?;
980 if n > max_val {
981 max_val = n;
982 }
983 }
984 Ok(Value::Number(max_val))
985 }
986 "abs" => {
987 if args.len() != 1 {
988 return Err(EvalError::WrongArgCount {
989 func: name.to_string(),
990 expected: 1,
991 got: args.len(),
992 });
993 }
994 let val = evaluate(&args[0], vars)?;
995 Ok(Value::Number(val.as_number()?.abs()))
996 }
997 "lower" => {
998 if args.len() != 1 {
999 return Err(EvalError::WrongArgCount {
1000 func: name.to_string(),
1001 expected: 1,
1002 got: args.len(),
1003 });
1004 }
1005 let val = evaluate(&args[0], vars)?;
1006 Ok(Value::String(val.as_string()?.to_lowercase()))
1007 }
1008 "upper" => {
1009 if args.len() != 1 {
1010 return Err(EvalError::WrongArgCount {
1011 func: name.to_string(),
1012 expected: 1,
1013 got: args.len(),
1014 });
1015 }
1016 let val = evaluate(&args[0], vars)?;
1017 Ok(Value::String(val.as_string()?.to_uppercase()))
1018 }
1019 "strip" => {
1020 if args.len() != 1 {
1021 return Err(EvalError::WrongArgCount {
1022 func: name.to_string(),
1023 expected: 1,
1024 got: args.len(),
1025 });
1026 }
1027 let val = evaluate(&args[0], vars)?;
1028 Ok(Value::String(val.as_string()?.trim().to_string()))
1029 }
1030 "unique" => {
1031 if args.len() != 1 {
1032 return Err(EvalError::WrongArgCount {
1033 func: name.to_string(),
1034 expected: 1,
1035 got: args.len(),
1036 });
1037 }
1038 let val = evaluate(&args[0], vars)?;
1039 let arr = val.as_array()?;
1040 let mut result = Vec::new();
1041 for item in arr {
1042 if !result.iter().any(|v| values_equal(v, item)) {
1043 result.push(item.clone());
1044 }
1045 }
1046 Ok(Value::Array(result))
1047 }
1048 "env" => {
1049 if args.len() != 1 {
1050 return Err(EvalError::WrongArgCount {
1051 func: name.to_string(),
1052 expected: 1,
1053 got: args.len(),
1054 });
1055 }
1056 let val = evaluate(&args[0], vars)?;
1057 let var_name = val.as_string()?;
1058 match std::env::var(var_name) {
1059 Ok(value) => Ok(Value::String(value)),
1060 Err(_) => Ok(Value::Null),
1061 }
1062 }
1063 _ => Err(EvalError::UndefinedFunction(name.to_string())),
1064 }
1065}
1066
1067fn eval_binary_op(
1068 op: BinaryOp,
1069 left: &Expr,
1070 right: &Expr,
1071 vars: &HashMap<String, Value>,
1072) -> Result<Value, EvalError> {
1073 if op == BinaryOp::And {
1074 let l = evaluate(left, vars)?.as_bool()?;
1075 if !l {
1076 return Ok(Value::Bool(false));
1077 }
1078 return Ok(Value::Bool(evaluate(right, vars)?.as_bool()?));
1079 }
1080 if op == BinaryOp::Or {
1081 let l = evaluate(left, vars)?.as_bool()?;
1082 if l {
1083 return Ok(Value::Bool(true));
1084 }
1085 return Ok(Value::Bool(evaluate(right, vars)?.as_bool()?));
1086 }
1087
1088 let l = evaluate(left, vars)?;
1089 let r = evaluate(right, vars)?;
1090
1091 match op {
1092 BinaryOp::Add => match (&l, &r) {
1093 (Value::String(ls), Value::String(rs)) => Ok(Value::String(format!("{}{}", ls, rs))),
1094 (Value::Array(la), Value::Array(ra)) => {
1095 let mut result = la.clone();
1096 result.extend(ra.clone());
1097 Ok(Value::Array(result))
1098 }
1099 _ => Ok(Value::Number(l.as_number()? + r.as_number()?)),
1100 },
1101 BinaryOp::Sub => Ok(Value::Number(l.as_number()? - r.as_number()?)),
1102 BinaryOp::Mul => Ok(Value::Number(l.as_number()? * r.as_number()?)),
1103 BinaryOp::Mod => {
1104 let divisor = r.as_number()?;
1105 if divisor == 0.0 {
1106 Err(EvalError::DivisionByZero)
1107 } else {
1108 Ok(Value::Number(l.as_number()? % divisor))
1109 }
1110 }
1111 BinaryOp::Div => {
1112 let divisor = r.as_number()?;
1113 if divisor == 0.0 {
1114 Err(EvalError::DivisionByZero)
1115 } else {
1116 Ok(Value::Number(l.as_number()? / divisor))
1117 }
1118 }
1119 BinaryOp::Pow => Ok(Value::Number(l.as_number()?.powf(r.as_number()?))),
1120 BinaryOp::Eq => Ok(Value::Bool(values_equal(&l, &r))),
1121 BinaryOp::Ne => Ok(Value::Bool(!values_equal(&l, &r))),
1122 BinaryOp::Lt => match (&l, &r) {
1123 (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls < rs)),
1124 _ => Ok(Value::Bool(l.as_number()? < r.as_number()?)),
1125 },
1126 BinaryOp::Le => match (&l, &r) {
1127 (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls <= rs)),
1128 _ => Ok(Value::Bool(l.as_number()? <= r.as_number()?)),
1129 },
1130 BinaryOp::Gt => match (&l, &r) {
1131 (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls > rs)),
1132 _ => Ok(Value::Bool(l.as_number()? > r.as_number()?)),
1133 },
1134 BinaryOp::Ge => match (&l, &r) {
1135 (Value::String(ls), Value::String(rs)) => Ok(Value::Bool(ls >= rs)),
1136 _ => Ok(Value::Bool(l.as_number()? >= r.as_number()?)),
1137 },
1138 BinaryOp::Contains | BinaryOp::NotContains => {
1139 let result = match &l {
1140 Value::String(haystack) => {
1141 let needle = r.as_string()?;
1142 haystack.contains(needle)
1143 }
1144 Value::Array(arr) => arr.iter().any(|v| values_equal(v, &r)),
1145 Value::Object(obj) => {
1146 let key = r.as_string()?;
1147 obj.contains_key(key)
1148 }
1149 _ => {
1150 return Err(EvalError::TypeError {
1151 expected: "string, array, or object",
1152 got: l.type_name(),
1153 })
1154 }
1155 };
1156 Ok(Value::Bool(if op == BinaryOp::NotContains {
1157 !result
1158 } else {
1159 result
1160 }))
1161 }
1162 BinaryOp::StartsWith | BinaryOp::NotStartsWith => {
1163 let s = l.as_string()?;
1164 let prefix = r.as_string()?;
1165 let result = s.starts_with(prefix);
1166 Ok(Value::Bool(if op == BinaryOp::NotStartsWith {
1167 !result
1168 } else {
1169 result
1170 }))
1171 }
1172 BinaryOp::EndsWith | BinaryOp::NotEndsWith => {
1173 let s = l.as_string()?;
1174 let suffix = r.as_string()?;
1175 let result = s.ends_with(suffix);
1176 Ok(Value::Bool(if op == BinaryOp::NotEndsWith {
1177 !result
1178 } else {
1179 result
1180 }))
1181 }
1182 BinaryOp::Matches | BinaryOp::NotMatches => {
1183 let s = l.as_string()?;
1184 let pattern = r.as_string()?;
1185 let re =
1186 regex::Regex::new(pattern).map_err(|e| EvalError::InvalidRegex(e.to_string()))?;
1187 let result = re.is_match(s);
1188 Ok(Value::Bool(if op == BinaryOp::NotMatches {
1189 !result
1190 } else {
1191 result
1192 }))
1193 }
1194 BinaryOp::And | BinaryOp::Or => unreachable!(),
1195 }
1196}
1197
1198fn values_equal(a: &Value, b: &Value) -> bool {
1199 match (a, b) {
1200 (Value::Number(a), Value::Number(b)) => (a - b).abs() < f64::EPSILON,
1201 (Value::String(a), Value::String(b)) => a == b,
1202 (Value::Bool(a), Value::Bool(b)) => a == b,
1203 (Value::Null, Value::Null) => true,
1204 (Value::Null, Value::Type(t)) | (Value::Type(t), Value::Null) => t == "null",
1206 (Value::Array(a), Value::Array(b)) => {
1207 a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| values_equal(x, y))
1208 }
1209 (Value::Object(a), Value::Object(b)) => {
1210 a.len() == b.len()
1211 && a.iter()
1212 .all(|(k, v)| b.get(k).map(|bv| values_equal(v, bv)).unwrap_or(false))
1213 }
1214 (Value::Type(a), Value::Type(b)) => a == b,
1215 _ => false,
1216 }
1217}
1218
1219pub fn eval_bool(expr_str: &str, vars: &HashMap<String, Value>) -> Result<bool, EvalError> {
1222 let ast = parse(expr_str)?;
1223 let result = evaluate(&ast, vars)?;
1224 result.as_bool()
1225}
1226
1227#[cfg(test)]
1228mod tests {
1229 use super::*;
1230
1231 fn vars(pairs: &[(&str, Value)]) -> HashMap<String, Value> {
1232 pairs
1233 .iter()
1234 .map(|(k, v)| (k.to_string(), v.clone()))
1235 .collect()
1236 }
1237
1238 #[test]
1239 fn test_number_parsing() {
1240 assert_eq!(parse("42").unwrap(), Expr::Number(42.0));
1241 assert_eq!(parse("0.5").unwrap(), Expr::Number(0.5));
1242 }
1243
1244 #[test]
1245 fn test_string_parsing() {
1246 assert_eq!(
1247 parse(r#""hello""#).unwrap(),
1248 Expr::String("hello".to_string())
1249 );
1250 }
1251
1252 #[test]
1253 fn test_arithmetic() {
1254 let v = vars(&[]);
1255 assert!(eval_bool("1 + 2 == 3", &v).unwrap());
1256 assert!(eval_bool("10 - 3 == 7", &v).unwrap());
1257 assert!(eval_bool("4 * 5 == 20", &v).unwrap());
1258 assert!(eval_bool("10 / 2 == 5", &v).unwrap());
1259 assert!(eval_bool("2 ^ 3 == 8", &v).unwrap());
1260 assert!(eval_bool("1 + 2 * 3 == 7", &v).unwrap());
1261 assert!(eval_bool("(1 + 2) * 3 == 9", &v).unwrap());
1262 }
1263
1264 #[test]
1265 fn test_comparisons() {
1266 let v = vars(&[("n", Value::Number(42.0))]);
1267 assert!(eval_bool("n > 0", &v).unwrap());
1268 assert!(eval_bool("n < 100", &v).unwrap());
1269 assert!(eval_bool("n >= 42", &v).unwrap());
1270 assert!(eval_bool("n <= 42", &v).unwrap());
1271 assert!(eval_bool("n == 42", &v).unwrap());
1272 assert!(eval_bool("n != 0", &v).unwrap());
1273 }
1274
1275 #[test]
1276 fn test_boolean_logic() {
1277 let v = vars(&[("n", Value::Number(42.0))]);
1278 assert!(eval_bool("n > 0 and n < 100", &v).unwrap());
1279 assert!(eval_bool("n < 0 or n > 0", &v).unwrap());
1280 assert!(eval_bool("not (n < 0)", &v).unwrap());
1281 }
1282
1283 #[test]
1284 fn test_array_contains() {
1285 let v = vars(&[("n", Value::Number(2.0))]);
1286 assert!(eval_bool("[1, 2, 3] contains n", &v).unwrap());
1287 assert!(!eval_bool("[4, 5, 6] contains n", &v).unwrap());
1288 }
1289
1290 #[test]
1291 fn test_array_not_contains() {
1292 let v = vars(&[("n", Value::Number(5.0))]);
1293 assert!(eval_bool("not ([1, 2, 3] contains n)", &v).unwrap());
1294 assert!(!eval_bool("not ([4, 5, 6] contains n)", &v).unwrap());
1295 }
1296
1297 #[test]
1298 fn test_object_contains_key() {
1299 let mut obj = HashMap::new();
1300 obj.insert("name".to_string(), Value::String("alice".to_string()));
1301 obj.insert("age".to_string(), Value::Number(30.0));
1302 let v = vars(&[("o", Value::Object(obj))]);
1303 assert!(eval_bool("o contains \"name\"", &v).unwrap());
1304 assert!(eval_bool("o contains \"age\"", &v).unwrap());
1305 assert!(!eval_bool("o contains \"email\"", &v).unwrap());
1306 }
1307
1308 #[test]
1309 fn test_string_operators() {
1310 let v = vars(&[("s", Value::String("hello world".to_string()))]);
1311 assert!(eval_bool(r#"s contains "world""#, &v).unwrap());
1312 assert!(eval_bool(r#"s startswith "hello""#, &v).unwrap());
1313 assert!(eval_bool(r#"s endswith "world""#, &v).unwrap());
1314 }
1315
1316 #[test]
1317 fn test_negated_string_operators() {
1318 let v = vars(&[("s", Value::String("hello world".to_string()))]);
1319 assert!(eval_bool(r#"s not contains "foo""#, &v).unwrap());
1320 assert!(!eval_bool(r#"s not contains "world""#, &v).unwrap());
1321 assert!(eval_bool(r#"s not startswith "foo""#, &v).unwrap());
1322 assert!(!eval_bool(r#"s not startswith "hello""#, &v).unwrap());
1323 assert!(eval_bool(r#"s not endswith "foo""#, &v).unwrap());
1324 assert!(!eval_bool(r#"s not endswith "world""#, &v).unwrap());
1325 }
1326
1327 #[test]
1328 fn test_regex_matches() {
1329 let v = vars(&[("s", Value::String("hello123".to_string()))]);
1330 assert!(eval_bool(r#"s matches /^hello\d+$/"#, &v).unwrap());
1331 }
1332
1333 #[test]
1334 fn test_negated_regex_matches() {
1335 let v = vars(&[("s", Value::String("hello123".to_string()))]);
1336 assert!(eval_bool(r#"s not matches /^foo/"#, &v).unwrap());
1337 assert!(!eval_bool(r#"s not matches /^hello\d+$/"#, &v).unwrap());
1338 }
1339
1340 #[test]
1341 fn test_negated_array_contains() {
1342 let v = vars(&[("n", Value::Number(5.0))]);
1343 assert!(eval_bool("[1, 2, 3] not contains n", &v).unwrap());
1344 assert!(!eval_bool("[4, 5, 6] not contains n", &v).unwrap());
1345 }
1346
1347 #[test]
1348 fn test_len_function() {
1349 let v = vars(&[("s", Value::String("hello".to_string()))]);
1350 assert!(eval_bool("len(s) == 5", &v).unwrap());
1351 }
1352
1353 #[test]
1354 fn test_backslash_in_string() {
1355 let v = vars(&[("p", Value::String("C:\\Users\\test".to_string()))]);
1357
1358 assert!(eval_bool(r#"p contains "test""#, &v).unwrap());
1360
1361 assert!(eval_bool(r#"p contains "\\""#, &v).unwrap());
1363
1364 assert!(eval_bool(r#"p contains "Users""#, &v).unwrap());
1366 }
1367
1368 #[test]
1369 fn test_array_indexing() {
1370 let v = vars(&[(
1371 "a",
1372 Value::Array(vec![
1373 Value::Number(10.0),
1374 Value::Number(20.0),
1375 Value::Number(30.0),
1376 ]),
1377 )]);
1378 assert!(eval_bool("a[0] == 10", &v).unwrap());
1379 assert!(eval_bool("a[1] == 20", &v).unwrap());
1380 assert!(eval_bool("a[2] == 30", &v).unwrap());
1381 }
1382
1383 #[test]
1384 fn test_object_property_access() {
1385 let mut obj = HashMap::new();
1386 obj.insert("name".to_string(), Value::String("alice".to_string()));
1387 obj.insert("age".to_string(), Value::Number(30.0));
1388 let v = vars(&[("o", Value::Object(obj))]);
1389
1390 assert!(eval_bool(r#"o.name == "alice""#, &v).unwrap());
1391 assert!(eval_bool("o.age == 30", &v).unwrap());
1392 assert!(eval_bool(r#"o["name"] == "alice""#, &v).unwrap());
1393 }
1394
1395 #[test]
1396 fn test_nested_access() {
1397 let inner = Value::Array(vec![Value::Number(1.0), Value::Number(2.0)]);
1398 let mut obj = HashMap::new();
1399 obj.insert("items".to_string(), inner);
1400 let v = vars(&[("o", Value::Object(obj))]);
1401
1402 assert!(eval_bool("o.items[0] == 1", &v).unwrap());
1403 assert!(eval_bool("o.items[1] == 2", &v).unwrap());
1404 assert!(eval_bool("len(o.items) == 2", &v).unwrap());
1405 }
1406
1407 #[test]
1408 fn test_type_function() {
1409 let v = vars(&[
1410 ("n", Value::Number(42.0)),
1411 ("s", Value::String("hello".to_string())),
1412 ("b", Value::Bool(true)),
1413 ("a", Value::Array(vec![])),
1414 ]);
1415
1416 assert!(eval_bool("type(n) == number", &v).unwrap());
1417 assert!(eval_bool("type(s) == string", &v).unwrap());
1418 assert!(eval_bool("type(b) == bool", &v).unwrap());
1419 assert!(eval_bool("type(a) == array", &v).unwrap());
1420 }
1421
1422 #[test]
1423 fn test_keys_function() {
1424 let mut obj = HashMap::new();
1425 obj.insert("a".to_string(), Value::Number(1.0));
1426 obj.insert("b".to_string(), Value::Number(2.0));
1427 let v = vars(&[("o", Value::Object(obj))]);
1428
1429 assert!(eval_bool("len(keys(o)) == 2", &v).unwrap());
1430 }
1431
1432 #[test]
1433 fn test_forall_array() {
1434 let v = vars(&[(
1435 "a",
1436 Value::Array(vec![
1437 Value::Number(1.0),
1438 Value::Number(2.0),
1439 Value::Number(3.0),
1440 ]),
1441 )]);
1442
1443 assert!(eval_bool("x <= 3 forall x in a", &v).unwrap());
1444 assert!(eval_bool("x > 0 forall x in a", &v).unwrap());
1445 assert!(!eval_bool("x > 2 forall x in a", &v).unwrap());
1446 }
1447
1448 #[test]
1449 fn test_forall_object() {
1450 let mut obj = HashMap::new();
1451 obj.insert("a".to_string(), Value::Number(1.0));
1452 obj.insert("b".to_string(), Value::Number(2.0));
1453 obj.insert("c".to_string(), Value::Number(3.0));
1454 let v = vars(&[("o", Value::Object(obj))]);
1455
1456 assert!(eval_bool("x <= 3 forall x in o", &v).unwrap());
1457 assert!(eval_bool("type(x) == number forall x in o", &v).unwrap());
1458 }
1459
1460 #[test]
1461 fn test_object_literal() {
1462 let v = vars(&[]);
1463 assert!(eval_bool(r#"{"a": 1, "b": 2}.a == 1"#, &v).unwrap());
1464 assert!(eval_bool(r#"len({"x": 1, "y": 2}) == 2"#, &v).unwrap());
1465 }
1466
1467 #[test]
1468 fn test_type_literal() {
1469 let v = vars(&[("n", Value::Number(42.0))]);
1470 assert!(eval_bool("type(n) == number", &v).unwrap());
1471 assert!(!eval_bool("type(n) == string", &v).unwrap());
1472 }
1473
1474 #[test]
1475 fn test_len_object() {
1476 let mut obj = HashMap::new();
1477 obj.insert("a".to_string(), Value::Number(1.0));
1478 obj.insert("b".to_string(), Value::Number(2.0));
1479 let v = vars(&[("o", Value::Object(obj))]);
1480
1481 assert!(eval_bool("len(o) == 2", &v).unwrap());
1482 }
1483
1484 #[test]
1485 fn test_bool_comparison() {
1486 let v = vars(&[("b", Value::Bool(true))]);
1487 assert!(eval_bool("b == true", &v).unwrap());
1488 assert!(eval_bool("b != false", &v).unwrap());
1489 assert!(eval_bool("(1 == 1) == true", &v).unwrap());
1490 }
1491
1492 #[test]
1493 fn test_env_function() {
1494 std::env::set_var("CCTR_TEST_VAR", "test_value");
1495 let v = vars(&[]);
1496 assert!(eval_bool(r#"env("CCTR_TEST_VAR") == "test_value""#, &v).unwrap());
1497 assert!(eval_bool(r#"type(env("CCTR_TEST_VAR")) == string"#, &v).unwrap());
1498 assert!(eval_bool(r#"env("CCTR_NONEXISTENT_VAR_12345") == null"#, &v).unwrap());
1500 assert!(eval_bool(r#"type(env("CCTR_NONEXISTENT_VAR_12345")) == null"#, &v).unwrap());
1501 std::env::remove_var("CCTR_TEST_VAR");
1502 }
1503
1504 #[test]
1505 fn test_strip_function() {
1506 let v = vars(&[
1507 ("s", Value::String(" hello ".to_string())),
1508 ("t", Value::String("\t\nworld\r\n".to_string())),
1509 ("clean", Value::String("no whitespace".to_string())),
1510 ]);
1511 assert!(eval_bool(r#"strip(s) == "hello""#, &v).unwrap());
1512 assert!(eval_bool(r#"strip(t) == "world""#, &v).unwrap());
1513 assert!(eval_bool(r#"strip(clean) == "no whitespace""#, &v).unwrap());
1514 assert!(eval_bool(r#"strip(" test ") == "test""#, &v).unwrap());
1515 }
1516}