1use serde_json::Value as JsonValue;
7use std::collections::{HashMap, HashSet};
8
9use crate::error::{TransformError, TransformErrorKind};
10use crate::model::{Expr, ExprOp, ExprRef};
11use crate::path::{get_path, parse_path};
12use crate::transform::{
13 EvalItem as V1EvalItem, EvalLocals as V1EvalLocals, EvalValue as V1EvalValue,
14 eval_op as eval_v1_op,
15};
16use crate::v2_model::{
17 V2Comparison, V2ComparisonOp, V2Condition, V2Expr, V2IfStep, V2LetStep, V2MapStep, V2OpStep,
18 V2Pipe, V2Ref, V2Start, V2Step,
19};
20
21#[derive(Debug, Clone, PartialEq)]
27pub enum EvalValue {
28 Missing,
29 Value(JsonValue),
30}
31
32impl EvalValue {
33 pub fn is_missing(&self) -> bool {
34 matches!(self, EvalValue::Missing)
35 }
36
37 pub fn into_value(self) -> Option<JsonValue> {
38 match self {
39 EvalValue::Value(v) => Some(v),
40 EvalValue::Missing => None,
41 }
42 }
43
44 pub fn as_value(&self) -> Option<&JsonValue> {
45 match self {
46 EvalValue::Value(v) => Some(v),
47 EvalValue::Missing => None,
48 }
49 }
50}
51
52#[derive(Clone)]
58pub struct EvalItem<'a> {
59 pub value: &'a JsonValue,
60 pub index: usize,
61}
62
63#[derive(Clone)]
65pub struct V2EvalContext<'a> {
66 pipe_value: Option<EvalValue>,
68 let_bindings: HashMap<String, EvalValue>,
70 item: Option<EvalItem<'a>>,
72 acc: Option<&'a JsonValue>,
74}
75
76impl<'a> V2EvalContext<'a> {
77 pub fn new() -> Self {
79 Self {
80 pipe_value: None,
81 let_bindings: HashMap::new(),
82 item: None,
83 acc: None,
84 }
85 }
86
87 pub fn with_pipe_value(mut self, value: EvalValue) -> Self {
89 self.pipe_value = Some(value);
90 self
91 }
92
93 pub fn with_let_binding(mut self, name: String, value: EvalValue) -> Self {
95 self.let_bindings.insert(name, value);
96 self
97 }
98
99 pub fn with_let_bindings(mut self, bindings: Vec<(String, EvalValue)>) -> Self {
101 for (name, value) in bindings {
102 self.let_bindings.insert(name, value);
103 }
104 self
105 }
106
107 pub fn with_item(mut self, item: EvalItem<'a>) -> Self {
109 self.item = Some(item);
110 self
111 }
112
113 pub fn with_acc(mut self, acc: &'a JsonValue) -> Self {
115 self.acc = Some(acc);
116 self
117 }
118
119 pub fn get_pipe_value(&self) -> Option<&EvalValue> {
121 self.pipe_value.as_ref()
122 }
123
124 pub fn resolve_local(&self, name: &str) -> Option<&EvalValue> {
126 self.let_bindings.get(name)
127 }
128
129 pub fn get_item(&self) -> Option<&EvalItem<'a>> {
131 self.item.as_ref()
132 }
133
134 pub fn get_acc(&self) -> Option<&JsonValue> {
136 self.acc
137 }
138
139 pub fn has_item_scope(&self) -> bool {
141 self.item.is_some()
142 }
143
144 pub fn has_acc_scope(&self) -> bool {
146 self.acc.is_some()
147 }
148}
149
150impl<'a> Default for V2EvalContext<'a> {
151 fn default() -> Self {
152 Self::new()
153 }
154}
155
156#[cfg(test)]
161mod v2_eval_context_tests {
162 use super::*;
163 use serde_json::json;
164
165 #[test]
166 fn test_context_new() {
167 let ctx = V2EvalContext::new();
168 assert!(ctx.get_pipe_value().is_none());
169 assert!(ctx.resolve_local("x").is_none());
170 assert!(ctx.get_item().is_none());
171 assert!(ctx.get_acc().is_none());
172 }
173
174 #[test]
175 fn test_context_with_pipe_value() {
176 let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Value(json!(42)));
177 assert!(ctx.get_pipe_value().is_some());
178 assert_eq!(ctx.get_pipe_value(), Some(&EvalValue::Value(json!(42))));
179 }
180
181 #[test]
182 fn test_context_with_let_binding() {
183 let ctx =
184 V2EvalContext::new().with_let_binding("x".to_string(), EvalValue::Value(json!(100)));
185 assert!(ctx.resolve_local("x").is_some());
186 assert_eq!(ctx.resolve_local("x"), Some(&EvalValue::Value(json!(100))));
187 assert!(ctx.resolve_local("y").is_none());
188 }
189
190 #[test]
191 fn test_context_with_multiple_let_bindings() {
192 let ctx = V2EvalContext::new().with_let_bindings(vec![
193 ("a".to_string(), EvalValue::Value(json!(1))),
194 ("b".to_string(), EvalValue::Value(json!(2))),
195 ]);
196 assert!(ctx.resolve_local("a").is_some());
197 assert!(ctx.resolve_local("b").is_some());
198 assert!(ctx.resolve_local("c").is_none());
199 }
200
201 #[test]
202 fn test_context_scope_chain() {
203 let ctx =
204 V2EvalContext::new().with_let_binding("x".to_string(), EvalValue::Value(json!(1)));
205 let inner_ctx = ctx
206 .clone()
207 .with_let_binding("y".to_string(), EvalValue::Value(json!(2)));
208
209 assert!(inner_ctx.resolve_local("x").is_some());
211 assert!(inner_ctx.resolve_local("y").is_some());
212
213 assert!(ctx.resolve_local("x").is_some());
215 assert!(ctx.resolve_local("y").is_none());
216 }
217
218 #[test]
219 fn test_context_with_item() {
220 let item_value = json!({"name": "test"});
221 let ctx = V2EvalContext::new().with_item(EvalItem {
222 value: &item_value,
223 index: 0,
224 });
225 assert!(ctx.has_item_scope());
226 assert!(ctx.get_item().is_some());
227 let item = ctx.get_item().unwrap();
228 assert_eq!(item.value, &json!({"name": "test"}));
229 assert_eq!(item.index, 0);
230 }
231
232 #[test]
233 fn test_context_with_acc() {
234 let acc_value = json!(0);
235 let ctx = V2EvalContext::new().with_acc(&acc_value);
236 assert!(ctx.has_acc_scope());
237 assert!(ctx.get_acc().is_some());
238 assert_eq!(ctx.get_acc(), Some(&json!(0)));
239 }
240
241 #[test]
242 fn test_eval_value_is_missing() {
243 assert!(EvalValue::Missing.is_missing());
244 assert!(!EvalValue::Value(json!(null)).is_missing());
245 }
246
247 #[test]
248 fn test_eval_value_into_value() {
249 assert_eq!(EvalValue::Missing.into_value(), None);
250 assert_eq!(
251 EvalValue::Value(json!("hello")).into_value(),
252 Some(json!("hello"))
253 );
254 }
255
256 #[test]
257 fn test_eval_value_as_value() {
258 let missing = EvalValue::Missing;
259 let val = EvalValue::Value(json!(42));
260 assert!(missing.as_value().is_none());
261 assert_eq!(val.as_value(), Some(&json!(42)));
262 }
263
264 #[test]
265 fn test_context_preserves_pipe_value_after_let() {
266 let ctx = V2EvalContext::new()
267 .with_pipe_value(EvalValue::Value(json!(100)))
268 .with_let_binding("x".to_string(), EvalValue::Value(json!(50)));
269
270 assert_eq!(ctx.get_pipe_value(), Some(&EvalValue::Value(json!(100))));
272 assert_eq!(ctx.resolve_local("x"), Some(&EvalValue::Value(json!(50))));
274 }
275}
276
277fn get_path_str<'a>(
283 value: &'a JsonValue,
284 path_str: &str,
285 error_path: &str,
286) -> Result<EvalValue, TransformError> {
287 let tokens = parse_path(path_str).map_err(|_| {
288 TransformError::new(
289 TransformErrorKind::ExprError,
290 format!("invalid path: {}", path_str),
291 )
292 .with_path(error_path)
293 })?;
294 match get_path(value, &tokens) {
295 Some(v) => Ok(EvalValue::Value(v.clone())),
296 None => Ok(EvalValue::Missing),
297 }
298}
299
300pub fn eval_v2_ref<'a>(
302 v2_ref: &V2Ref,
303 record: &'a JsonValue,
304 context: Option<&'a JsonValue>,
305 out: &'a JsonValue,
306 path: &str,
307 ctx: &V2EvalContext<'a>,
308) -> Result<EvalValue, TransformError> {
309 match v2_ref {
310 V2Ref::Input(ref_path) => {
311 if ref_path.is_empty() {
312 Ok(EvalValue::Value(record.clone()))
313 } else {
314 get_path_str(record, ref_path, path)
315 }
316 }
317 V2Ref::Context(ref_path) => {
318 let ctx_value = match context {
319 Some(value) => value,
320 None => return Ok(EvalValue::Missing),
321 };
322 if ref_path.is_empty() {
323 Ok(EvalValue::Value(ctx_value.clone()))
324 } else {
325 get_path_str(ctx_value, ref_path, path)
326 }
327 }
328 V2Ref::Out(ref_path) => {
329 if ref_path.is_empty() {
330 Ok(EvalValue::Value(out.clone()))
331 } else {
332 get_path_str(out, ref_path, path)
333 }
334 }
335 V2Ref::Item(ref_path) => {
336 let item = ctx.get_item().ok_or_else(|| {
337 TransformError::new(
338 TransformErrorKind::ExprError,
339 "@item is only available in map/filter operations",
340 )
341 .with_path(path)
342 })?;
343 if ref_path.is_empty() {
344 Ok(EvalValue::Value(item.value.clone()))
345 } else if ref_path == "index" {
346 Ok(EvalValue::Value(JsonValue::Number(item.index.into())))
347 } else if let Some(rest) = ref_path.strip_prefix("value.") {
348 get_path_str(item.value, rest, path)
349 } else if ref_path == "value" {
350 Ok(EvalValue::Value(item.value.clone()))
351 } else {
352 get_path_str(item.value, ref_path, path)
354 }
355 }
356 V2Ref::Acc(ref_path) => {
357 let acc = ctx.get_acc().ok_or_else(|| {
358 TransformError::new(
359 TransformErrorKind::ExprError,
360 "@acc is only available in reduce/fold operations",
361 )
362 .with_path(path)
363 })?;
364 if ref_path.is_empty() {
365 Ok(EvalValue::Value(acc.clone()))
366 } else if let Some(rest) = ref_path.strip_prefix("value.") {
367 get_path_str(acc, rest, path)
368 } else if ref_path == "value" {
369 Ok(EvalValue::Value(acc.clone()))
370 } else {
371 get_path_str(acc, ref_path, path)
373 }
374 }
375 V2Ref::Local(var_name) => {
376 let value = ctx.resolve_local(var_name).ok_or_else(|| {
377 TransformError::new(
378 TransformErrorKind::ExprError,
379 format!("undefined variable: @{}", var_name),
380 )
381 .with_path(path)
382 })?;
383 Ok(value.clone())
384 }
385 }
386}
387
388#[cfg(test)]
393mod v2_ref_eval_tests {
394 use super::*;
395 use serde_json::json;
396
397 #[test]
398 fn test_eval_input_ref() {
399 let record = json!({"name": "Alice", "age": 30});
400 let ctx = V2EvalContext::new();
401 let result = eval_v2_ref(
402 &V2Ref::Input("name".to_string()),
403 &record,
404 None,
405 &json!({}),
406 "test",
407 &ctx,
408 );
409 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Alice")));
410 }
411
412 #[test]
413 fn test_eval_input_ref_nested() {
414 let record = json!({"user": {"profile": {"name": "Bob"}}});
415 let ctx = V2EvalContext::new();
416 let result = eval_v2_ref(
417 &V2Ref::Input("user.profile.name".to_string()),
418 &record,
419 None,
420 &json!({}),
421 "test",
422 &ctx,
423 );
424 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Bob")));
425 }
426
427 #[test]
428 fn test_eval_input_ref_missing() {
429 let record = json!({"name": "Alice"});
430 let ctx = V2EvalContext::new();
431 let result = eval_v2_ref(
432 &V2Ref::Input("nonexistent".to_string()),
433 &record,
434 None,
435 &json!({}),
436 "test",
437 &ctx,
438 );
439 assert!(matches!(result, Ok(EvalValue::Missing)));
440 }
441
442 #[test]
443 fn test_eval_context_ref() {
444 let record = json!({});
445 let context = json!({"rate": 1.5, "config": {"enabled": true}});
446 let ctx = V2EvalContext::new();
447 let result = eval_v2_ref(
448 &V2Ref::Context("rate".to_string()),
449 &record,
450 Some(&context),
451 &json!({}),
452 "test",
453 &ctx,
454 );
455 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(1.5)));
456 }
457
458 #[test]
459 fn test_eval_context_ref_nested() {
460 let record = json!({});
461 let context = json!({"config": {"enabled": true}});
462 let ctx = V2EvalContext::new();
463 let result = eval_v2_ref(
464 &V2Ref::Context("config.enabled".to_string()),
465 &record,
466 Some(&context),
467 &json!({}),
468 "test",
469 &ctx,
470 );
471 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(true)));
472 }
473
474 #[test]
475 fn test_eval_context_ref_no_context_missing() {
476 let record = json!({});
477 let ctx = V2EvalContext::new();
478 let result = eval_v2_ref(
479 &V2Ref::Context("rate".to_string()),
480 &record,
481 None,
482 &json!({}),
483 "test",
484 &ctx,
485 );
486 assert!(matches!(result, Ok(EvalValue::Missing)));
487 }
488
489 #[test]
490 fn test_eval_out_ref() {
491 let record = json!({});
492 let out = json!({"computed": 42});
493 let ctx = V2EvalContext::new();
494 let result = eval_v2_ref(
495 &V2Ref::Out("computed".to_string()),
496 &record,
497 None,
498 &out,
499 "test",
500 &ctx,
501 );
502 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
503 }
504
505 #[test]
506 fn test_eval_local_ref() {
507 let ctx = V2EvalContext::new()
508 .with_let_binding("price".to_string(), EvalValue::Value(json!(100)));
509 let result = eval_v2_ref(
510 &V2Ref::Local("price".to_string()),
511 &json!({}),
512 None,
513 &json!({}),
514 "test",
515 &ctx,
516 );
517 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
518 }
519
520 #[test]
521 fn test_eval_local_ref_undefined_error() {
522 let ctx = V2EvalContext::new();
523 let result = eval_v2_ref(
524 &V2Ref::Local("undefined".to_string()),
525 &json!({}),
526 None,
527 &json!({}),
528 "test",
529 &ctx,
530 );
531 assert!(result.is_err());
532 }
533
534 #[test]
535 fn test_eval_item_ref() {
536 let item_value = json!({"name": "item1", "value": 10});
537 let ctx = V2EvalContext::new().with_item(EvalItem {
538 value: &item_value,
539 index: 2,
540 });
541 let result = eval_v2_ref(
542 &V2Ref::Item("name".to_string()),
543 &json!({}),
544 None,
545 &json!({}),
546 "test",
547 &ctx,
548 );
549 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("item1")));
550 }
551
552 #[test]
553 fn test_eval_item_ref_index() {
554 let item_value = json!({"name": "item1"});
555 let ctx = V2EvalContext::new().with_item(EvalItem {
556 value: &item_value,
557 index: 5,
558 });
559 let result = eval_v2_ref(
560 &V2Ref::Item("index".to_string()),
561 &json!({}),
562 None,
563 &json!({}),
564 "test",
565 &ctx,
566 );
567 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(5)));
568 }
569
570 #[test]
571 fn test_eval_item_ref_no_scope_error() {
572 let ctx = V2EvalContext::new();
573 let result = eval_v2_ref(
574 &V2Ref::Item("value".to_string()),
575 &json!({}),
576 None,
577 &json!({}),
578 "test",
579 &ctx,
580 );
581 assert!(result.is_err());
582 }
583
584 #[test]
585 fn test_eval_acc_ref() {
586 let acc_value = json!(100);
587 let ctx = V2EvalContext::new().with_acc(&acc_value);
588 let result = eval_v2_ref(
589 &V2Ref::Acc(String::new()),
590 &json!({}),
591 None,
592 &json!({}),
593 "test",
594 &ctx,
595 );
596 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
597 }
598
599 #[test]
600 fn test_eval_acc_ref_no_scope_error() {
601 let ctx = V2EvalContext::new();
602 let result = eval_v2_ref(
603 &V2Ref::Acc("value".to_string()),
604 &json!({}),
605 None,
606 &json!({}),
607 "test",
608 &ctx,
609 );
610 assert!(result.is_err());
611 }
612
613 #[test]
614 fn test_eval_input_ref_empty_path() {
615 let record = json!({"name": "Alice"});
616 let ctx = V2EvalContext::new();
617 let result = eval_v2_ref(
618 &V2Ref::Input(String::new()),
619 &record,
620 None,
621 &json!({}),
622 "test",
623 &ctx,
624 );
625 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!({"name": "Alice"})));
626 }
627}
628
629pub fn eval_v2_start<'a>(
635 start: &V2Start,
636 record: &'a JsonValue,
637 context: Option<&'a JsonValue>,
638 out: &'a JsonValue,
639 path: &str,
640 ctx: &V2EvalContext<'a>,
641) -> Result<EvalValue, TransformError> {
642 match start {
643 V2Start::Ref(v2_ref) => eval_v2_ref(v2_ref, record, context, out, path, ctx),
644 V2Start::PipeValue => {
645 Ok(ctx.get_pipe_value().cloned().unwrap_or(EvalValue::Missing))
648 }
649 V2Start::Literal(value) => Ok(EvalValue::Value(value.clone())),
650 V2Start::V1Expr(_expr) => {
651 Err(TransformError::new(
654 TransformErrorKind::ExprError,
655 "v1 expression fallback not yet implemented",
656 )
657 .with_path(path))
658 }
659 }
660}
661
662#[cfg(test)]
667mod v2_start_eval_tests {
668 use super::*;
669 use serde_json::json;
670
671 #[test]
672 fn test_eval_start_literal_string() {
673 let ctx = V2EvalContext::new();
674 let result = eval_v2_start(
675 &V2Start::Literal(json!("hello")),
676 &json!({}),
677 None,
678 &json!({}),
679 "test",
680 &ctx,
681 );
682 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello")));
683 }
684
685 #[test]
686 fn test_eval_start_literal_number() {
687 let ctx = V2EvalContext::new();
688 let result = eval_v2_start(
689 &V2Start::Literal(json!(42)),
690 &json!({}),
691 None,
692 &json!({}),
693 "test",
694 &ctx,
695 );
696 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
697 }
698
699 #[test]
700 fn test_eval_start_literal_bool() {
701 let ctx = V2EvalContext::new();
702 let result = eval_v2_start(
703 &V2Start::Literal(json!(true)),
704 &json!({}),
705 None,
706 &json!({}),
707 "test",
708 &ctx,
709 );
710 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(true)));
711 }
712
713 #[test]
714 fn test_eval_start_literal_null() {
715 let ctx = V2EvalContext::new();
716 let result = eval_v2_start(
717 &V2Start::Literal(json!(null)),
718 &json!({}),
719 None,
720 &json!({}),
721 "test",
722 &ctx,
723 );
724 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(null)));
725 }
726
727 #[test]
728 fn test_eval_start_literal_array() {
729 let ctx = V2EvalContext::new();
730 let result = eval_v2_start(
731 &V2Start::Literal(json!([1, 2, 3])),
732 &json!({}),
733 None,
734 &json!({}),
735 "test",
736 &ctx,
737 );
738 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([1, 2, 3])));
739 }
740
741 #[test]
742 fn test_eval_start_literal_object() {
743 let ctx = V2EvalContext::new();
744 let result = eval_v2_start(
745 &V2Start::Literal(json!({"key": "value"})),
746 &json!({}),
747 None,
748 &json!({}),
749 "test",
750 &ctx,
751 );
752 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!({"key": "value"})));
753 }
754
755 #[test]
756 fn test_eval_start_ref() {
757 let ctx = V2EvalContext::new();
758 let result = eval_v2_start(
759 &V2Start::Ref(V2Ref::Input("name".to_string())),
760 &json!({"name": "Bob"}),
761 None,
762 &json!({}),
763 "test",
764 &ctx,
765 );
766 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Bob")));
767 }
768
769 #[test]
770 fn test_eval_start_pipe_value() {
771 let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Value(json!(42)));
772 let result = eval_v2_start(
773 &V2Start::PipeValue,
774 &json!({}),
775 None,
776 &json!({}),
777 "test",
778 &ctx,
779 );
780 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
781 }
782
783 #[test]
784 fn test_eval_start_pipe_value_not_available() {
785 let ctx = V2EvalContext::new();
788 let result = eval_v2_start(
789 &V2Start::PipeValue,
790 &json!({}),
791 None,
792 &json!({}),
793 "test",
794 &ctx,
795 );
796 assert!(result.is_ok());
797 assert_eq!(result.unwrap(), EvalValue::Missing);
798 }
799
800 #[test]
801 fn test_eval_start_pipe_value_missing() {
802 let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Missing);
803 let result = eval_v2_start(
804 &V2Start::PipeValue,
805 &json!({}),
806 None,
807 &json!({}),
808 "test",
809 &ctx,
810 );
811 assert!(matches!(result, Ok(EvalValue::Missing)));
812 }
813}
814
815pub fn eval_v2_pipe<'a>(
821 pipe: &V2Pipe,
822 record: &'a JsonValue,
823 context: Option<&'a JsonValue>,
824 out: &'a JsonValue,
825 path: &str,
826 ctx: &V2EvalContext<'a>,
827) -> Result<EvalValue, TransformError> {
828 let mut current = eval_v2_start(&pipe.start, record, context, out, path, ctx)?;
830 let mut current_ctx = ctx.clone();
831
832 for (i, step) in pipe.steps.iter().enumerate() {
834 let step_path = format!("{}[{}]", path, i + 1);
835 current_ctx = current_ctx.clone().with_pipe_value(current.clone());
837
838 match step {
839 V2Step::Op(op_step) => {
840 current = eval_v2_op_step(
841 op_step,
842 current,
843 record,
844 context,
845 out,
846 &step_path,
847 ¤t_ctx,
848 )?;
849 }
850 V2Step::Let(let_step) => {
851 current_ctx = eval_v2_let_step(
853 let_step,
854 current.clone(),
855 record,
856 context,
857 out,
858 &step_path,
859 ¤t_ctx,
860 )?;
861 }
862 V2Step::If(if_step) => {
863 current = eval_v2_if_step(
864 if_step,
865 current,
866 record,
867 context,
868 out,
869 &step_path,
870 ¤t_ctx,
871 )?;
872 }
873 V2Step::Map(map_step) => {
874 current = eval_v2_map_step(
875 map_step,
876 current,
877 record,
878 context,
879 out,
880 &step_path,
881 ¤t_ctx,
882 )?;
883 }
884 V2Step::Ref(v2_ref) => {
885 current = eval_v2_ref(v2_ref, record, context, out, &step_path, ¤t_ctx)?;
887 }
888 }
889 }
890
891 Ok(current)
892}
893
894pub fn eval_v2_let_step<'a>(
896 let_step: &V2LetStep,
897 pipe_value: EvalValue,
898 record: &'a JsonValue,
899 context: Option<&'a JsonValue>,
900 out: &'a JsonValue,
901 path: &str,
902 ctx: &V2EvalContext<'a>,
903) -> Result<V2EvalContext<'a>, TransformError> {
904 let mut new_ctx = ctx.clone().with_pipe_value(pipe_value);
905
906 for (name, expr) in &let_step.bindings {
907 let binding_path = format!("{}.{}", path, name);
908 let value = eval_v2_expr(expr, record, context, out, &binding_path, &new_ctx)?;
909 new_ctx = new_ctx.with_let_binding(name.clone(), value);
910 }
911
912 Ok(new_ctx)
913}
914
915pub fn eval_v2_if_step<'a>(
917 if_step: &V2IfStep,
918 pipe_value: EvalValue,
919 record: &'a JsonValue,
920 context: Option<&'a JsonValue>,
921 out: &'a JsonValue,
922 path: &str,
923 ctx: &V2EvalContext<'a>,
924) -> Result<EvalValue, TransformError> {
925 let cond_ctx = ctx.clone().with_pipe_value(pipe_value.clone());
927
928 let cond_path = format!("{}.cond", path);
930 let cond_result =
931 eval_v2_condition(&if_step.cond, record, context, out, &cond_path, &cond_ctx)?;
932
933 if cond_result {
934 let then_path = format!("{}.then", path);
936 eval_v2_pipe(
937 &if_step.then_branch,
938 record,
939 context,
940 out,
941 &then_path,
942 &cond_ctx,
943 )
944 } else if let Some(ref else_branch) = if_step.else_branch {
945 let else_path = format!("{}.else", path);
947 eval_v2_pipe(else_branch, record, context, out, &else_path, &cond_ctx)
948 } else {
949 Ok(pipe_value)
951 }
952}
953
954pub fn eval_v2_map_step<'a>(
956 map_step: &V2MapStep,
957 pipe_value: EvalValue,
958 record: &'a JsonValue,
959 context: Option<&'a JsonValue>,
960 out: &'a JsonValue,
961 path: &str,
962 ctx: &V2EvalContext<'a>,
963) -> Result<EvalValue, TransformError> {
964 let arr = match &pipe_value {
966 EvalValue::Missing => {
967 return Ok(EvalValue::Missing);
968 }
969 EvalValue::Value(JsonValue::Array(arr)) => arr,
970 EvalValue::Value(other) => {
971 return Err(TransformError::new(
972 TransformErrorKind::ExprError,
973 format!("map step requires array, got {:?}", other),
974 )
975 .with_path(path));
976 }
977 };
978
979 let mut results = Vec::with_capacity(arr.len());
981 for (index, item_value) in arr.iter().enumerate() {
982 let item_path = format!("{}[{}]", path, index);
983
984 let item_ctx = ctx
986 .clone()
987 .with_pipe_value(EvalValue::Value(item_value.clone()))
988 .with_item(EvalItem {
989 value: item_value,
990 index,
991 });
992
993 let mut current = EvalValue::Value(item_value.clone());
995 let mut step_ctx = item_ctx.clone(); for (step_idx, step) in map_step.steps.iter().enumerate() {
998 let step_path = format!("{}.step[{}]", item_path, step_idx);
999 step_ctx = step_ctx.clone().with_pipe_value(current.clone());
1000
1001 match step {
1002 V2Step::Op(op_step) => {
1003 current = eval_v2_op_step(
1004 op_step, current, record, context, out, &step_path, &step_ctx,
1005 )?;
1006 }
1007 V2Step::Let(let_step) => {
1008 step_ctx = eval_v2_let_step(
1010 let_step,
1011 current.clone(),
1012 record,
1013 context,
1014 out,
1015 &step_path,
1016 &step_ctx,
1017 )?;
1018 current = step_ctx.get_pipe_value().cloned().unwrap_or(current);
1020 }
1021 V2Step::If(if_step) => {
1022 current = eval_v2_if_step(
1023 if_step, current, record, context, out, &step_path, &step_ctx,
1024 )?;
1025 }
1026 V2Step::Map(nested_map) => {
1027 current = eval_v2_map_step(
1028 nested_map, current, record, context, out, &step_path, &step_ctx,
1029 )?;
1030 }
1031 V2Step::Ref(v2_ref) => {
1032 current = eval_v2_ref(v2_ref, record, context, out, &step_path, &step_ctx)?;
1034 }
1035 };
1036 }
1037
1038 if let EvalValue::Value(v) = current {
1040 results.push(v);
1041 }
1042 }
1043
1044 Ok(EvalValue::Value(JsonValue::Array(results)))
1045}
1046
1047pub fn eval_v2_condition<'a>(
1049 condition: &V2Condition,
1050 record: &'a JsonValue,
1051 context: Option<&'a JsonValue>,
1052 out: &'a JsonValue,
1053 path: &str,
1054 ctx: &V2EvalContext<'a>,
1055) -> Result<bool, TransformError> {
1056 match condition {
1057 V2Condition::All(conditions) => {
1058 for (i, cond) in conditions.iter().enumerate() {
1059 let cond_path = format!("{}[{}]", path, i);
1060 if !eval_v2_condition(cond, record, context, out, &cond_path, ctx)? {
1061 return Ok(false);
1062 }
1063 }
1064 Ok(true)
1065 }
1066 V2Condition::Any(conditions) => {
1067 for (i, cond) in conditions.iter().enumerate() {
1068 let cond_path = format!("{}[{}]", path, i);
1069 if eval_v2_condition(cond, record, context, out, &cond_path, ctx)? {
1070 return Ok(true);
1071 }
1072 }
1073 Ok(false)
1074 }
1075 V2Condition::Comparison(comparison) => {
1076 eval_v2_comparison(comparison, record, context, out, path, ctx)
1077 }
1078 V2Condition::Expr(expr) => {
1079 let expr_path = format!("{}.expr", path);
1080 let value = eval_v2_expr(expr, record, context, out, &expr_path, ctx)?;
1081 match value {
1082 EvalValue::Value(JsonValue::Bool(flag)) => Ok(flag),
1083 EvalValue::Missing => Ok(false),
1084 EvalValue::Value(_) => Err(TransformError::new(
1085 TransformErrorKind::ExprError,
1086 "when/record_when must evaluate to boolean",
1087 )
1088 .with_path(&expr_path)),
1089 }
1090 }
1091 }
1092}
1093
1094fn eval_v2_comparison<'a>(
1096 comparison: &V2Comparison,
1097 record: &'a JsonValue,
1098 context: Option<&'a JsonValue>,
1099 out: &'a JsonValue,
1100 path: &str,
1101 ctx: &V2EvalContext<'a>,
1102) -> Result<bool, TransformError> {
1103 if comparison.args.len() != 2 {
1104 return Err(TransformError::new(
1105 TransformErrorKind::ExprError,
1106 format!(
1107 "comparison requires exactly 2 arguments, got {}",
1108 comparison.args.len()
1109 ),
1110 )
1111 .with_path(path));
1112 }
1113
1114 let left_path = format!("{}.args[0]", path);
1115 let right_path = format!("{}.args[1]", path);
1116
1117 let left = eval_v2_expr(&comparison.args[0], record, context, out, &left_path, ctx)?;
1118 let right = eval_v2_expr(&comparison.args[1], record, context, out, &right_path, ctx)?;
1119
1120 match comparison.op {
1121 V2ComparisonOp::Eq => Ok(compare_values_eq(&left, &right)),
1122 V2ComparisonOp::Ne => Ok(!compare_values_eq(&left, &right)),
1123 V2ComparisonOp::Gt => {
1124 compare_values_ord(&left, &right, path).map(|ord| ord == std::cmp::Ordering::Greater)
1125 }
1126 V2ComparisonOp::Gte => {
1127 compare_values_ord(&left, &right, path).map(|ord| ord != std::cmp::Ordering::Less)
1128 }
1129 V2ComparisonOp::Lt => {
1130 compare_values_ord(&left, &right, path).map(|ord| ord == std::cmp::Ordering::Less)
1131 }
1132 V2ComparisonOp::Lte => {
1133 compare_values_ord(&left, &right, path).map(|ord| ord != std::cmp::Ordering::Greater)
1134 }
1135 V2ComparisonOp::Match => compare_values_match(&left, &right, path),
1136 }
1137}
1138
1139fn compare_values_eq(left: &EvalValue, right: &EvalValue) -> bool {
1141 match (left, right) {
1142 (EvalValue::Value(l), EvalValue::Value(r)) => l == r,
1143 (EvalValue::Missing, EvalValue::Missing) => true,
1144 (EvalValue::Missing, EvalValue::Value(r)) => r.is_null(),
1145 (EvalValue::Value(l), EvalValue::Missing) => l.is_null(),
1146 }
1147}
1148
1149fn compare_values_ord(
1151 left: &EvalValue,
1152 right: &EvalValue,
1153 path: &str,
1154) -> Result<std::cmp::Ordering, TransformError> {
1155 match (left, right) {
1156 (EvalValue::Value(l), EvalValue::Value(r)) => {
1157 if let (Some(l_num), Some(r_num)) = (value_as_f64(l), value_as_f64(r)) {
1159 return Ok(l_num
1160 .partial_cmp(&r_num)
1161 .unwrap_or(std::cmp::Ordering::Equal));
1162 }
1163 if let (Some(l_str), Some(r_str)) = (value_as_str(l), value_as_str(r)) {
1165 return Ok(l_str.cmp(r_str));
1166 }
1167 Err(TransformError::new(
1168 TransformErrorKind::ExprError,
1169 "cannot compare values of different types",
1170 )
1171 .with_path(path))
1172 }
1173 _ => Err(TransformError::new(
1174 TransformErrorKind::ExprError,
1175 "cannot compare missing values",
1176 )
1177 .with_path(path)),
1178 }
1179}
1180
1181fn compare_values_match(
1183 left: &EvalValue,
1184 right: &EvalValue,
1185 path: &str,
1186) -> Result<bool, TransformError> {
1187 let text = match left {
1188 EvalValue::Value(JsonValue::String(s)) => s.as_str(),
1189 _ => {
1190 return Err(TransformError::new(
1191 TransformErrorKind::ExprError,
1192 "match operator requires string on left side",
1193 )
1194 .with_path(path));
1195 }
1196 };
1197
1198 let pattern = match right {
1199 EvalValue::Value(JsonValue::String(s)) => s.as_str(),
1200 _ => {
1201 return Err(TransformError::new(
1202 TransformErrorKind::ExprError,
1203 "match operator requires regex pattern string on right side",
1204 )
1205 .with_path(path));
1206 }
1207 };
1208
1209 let re = regex::Regex::new(pattern).map_err(|e| {
1210 TransformError::new(
1211 TransformErrorKind::ExprError,
1212 format!("invalid regex pattern: {}", e),
1213 )
1214 .with_path(path)
1215 })?;
1216
1217 Ok(re.is_match(text))
1218}
1219
1220fn value_as_f64(v: &JsonValue) -> Option<f64> {
1222 match v {
1223 JsonValue::Number(n) => n.as_f64(),
1224 JsonValue::String(s) => s.parse::<f64>().ok(),
1225 _ => None,
1226 }
1227}
1228
1229fn value_as_str(v: &JsonValue) -> Option<&str> {
1231 match v {
1232 JsonValue::String(s) => Some(s.as_str()),
1233 _ => None,
1234 }
1235}
1236
1237pub fn eval_v2_expr<'a>(
1239 expr: &V2Expr,
1240 record: &'a JsonValue,
1241 context: Option<&'a JsonValue>,
1242 out: &'a JsonValue,
1243 path: &str,
1244 ctx: &V2EvalContext<'a>,
1245) -> Result<EvalValue, TransformError> {
1246 match expr {
1247 V2Expr::Pipe(pipe) => eval_v2_pipe(pipe, record, context, out, path, ctx),
1248 V2Expr::V1Fallback(_) => Err(TransformError::new(
1249 TransformErrorKind::ExprError,
1250 "v1 fallback not yet implemented",
1251 )
1252 .with_path(path)),
1253 }
1254}
1255
1256fn eval_value_as_string(value: &EvalValue, path: &str) -> Result<String, TransformError> {
1258 match value {
1259 EvalValue::Missing => Err(TransformError::new(
1260 TransformErrorKind::ExprError,
1261 "expected string, got missing value",
1262 )
1263 .with_path(path)),
1264 EvalValue::Value(v) => match v {
1265 JsonValue::String(s) => Ok(s.clone()),
1266 JsonValue::Number(n) => Ok(n.to_string()),
1267 JsonValue::Bool(b) => Ok(b.to_string()),
1268 _ => Err(TransformError::new(
1269 TransformErrorKind::ExprError,
1270 format!("expected string, got {:?}", v),
1271 )
1272 .with_path(path)),
1273 },
1274 }
1275}
1276
1277fn eval_value_as_number(value: &EvalValue, path: &str) -> Result<f64, TransformError> {
1279 match value {
1280 EvalValue::Missing => Err(TransformError::new(
1281 TransformErrorKind::ExprError,
1282 "expected number, got missing value",
1283 )
1284 .with_path(path)),
1285 EvalValue::Value(v) => match v {
1286 JsonValue::Number(n) => n.as_f64().ok_or_else(|| {
1287 TransformError::new(TransformErrorKind::ExprError, "number conversion failed")
1288 .with_path(path)
1289 }),
1290 JsonValue::String(s) => s.parse::<f64>().map_err(|_| {
1291 TransformError::new(
1292 TransformErrorKind::ExprError,
1293 "failed to parse string as number",
1294 )
1295 .with_path(path)
1296 }),
1297 _ => Err(TransformError::new(
1298 TransformErrorKind::ExprError,
1299 format!("expected number, got {:?}", v),
1300 )
1301 .with_path(path)),
1302 },
1303 }
1304}
1305
1306fn value_as_bool(value: &JsonValue, path: &str) -> Result<bool, TransformError> {
1307 match value {
1308 JsonValue::Bool(flag) => Ok(*flag),
1309 _ => Err(
1310 TransformError::new(TransformErrorKind::ExprError, "value must be a boolean")
1311 .with_path(path),
1312 ),
1313 }
1314}
1315
1316fn value_as_string(value: &JsonValue, path: &str) -> Result<String, TransformError> {
1317 match value {
1318 JsonValue::String(value) => Ok(value.clone()),
1319 _ => Err(
1320 TransformError::new(TransformErrorKind::ExprError, "value must be a string")
1321 .with_path(path),
1322 ),
1323 }
1324}
1325
1326fn value_to_number(value: &JsonValue, path: &str, message: &str) -> Result<f64, TransformError> {
1327 match value {
1328 JsonValue::Number(n) => n.as_f64().filter(|f| f.is_finite()).ok_or_else(|| {
1329 TransformError::new(TransformErrorKind::ExprError, message).with_path(path)
1330 }),
1331 JsonValue::String(s) => s
1332 .parse::<f64>()
1333 .ok()
1334 .filter(|f| f.is_finite())
1335 .ok_or_else(|| {
1336 TransformError::new(TransformErrorKind::ExprError, message).with_path(path)
1337 }),
1338 _ => Err(TransformError::new(TransformErrorKind::ExprError, message).with_path(path)),
1339 }
1340}
1341
1342fn compare_eq_v1(
1343 left: &JsonValue,
1344 right: &JsonValue,
1345 left_path: &str,
1346 right_path: &str,
1347) -> Result<bool, TransformError> {
1348 if left.is_null() || right.is_null() {
1349 return Ok(left.is_null() && right.is_null());
1350 }
1351
1352 let left_value = value_to_string(left, left_path)?;
1353 let right_value = value_to_string(right, right_path)?;
1354 Ok(left_value == right_value)
1355}
1356
1357fn compare_numbers_v1<F>(
1358 left: &JsonValue,
1359 right: &JsonValue,
1360 left_path: &str,
1361 right_path: &str,
1362 compare: F,
1363) -> Result<bool, TransformError>
1364where
1365 F: FnOnce(f64, f64) -> bool,
1366{
1367 let left_value = value_to_number(left, left_path, "comparison operand must be a number")?;
1368 let right_value = value_to_number(right, right_path, "comparison operand must be a number")?;
1369 Ok(compare(left_value, right_value))
1370}
1371
1372fn match_regex_v1(
1373 left: &JsonValue,
1374 right: &JsonValue,
1375 left_path: &str,
1376 right_path: &str,
1377) -> Result<bool, TransformError> {
1378 let value = value_as_string(left, left_path)?;
1379 let pattern = value_as_string(right, right_path)?;
1380 let regex = regex::Regex::new(&pattern).map_err(|e| {
1381 TransformError::new(
1382 TransformErrorKind::ExprError,
1383 format!("invalid regex pattern: {}", e),
1384 )
1385 .with_path(right_path)
1386 })?;
1387 Ok(regex.is_match(&value))
1388}
1389
1390fn eval_v2_expr_or_null<'a>(
1391 expr: &V2Expr,
1392 record: &'a JsonValue,
1393 context: Option<&'a JsonValue>,
1394 out: &'a JsonValue,
1395 path: &str,
1396 ctx: &V2EvalContext<'a>,
1397) -> Result<JsonValue, TransformError> {
1398 match eval_v2_expr(expr, record, context, out, path, ctx)? {
1399 EvalValue::Missing => Ok(JsonValue::Null),
1400 EvalValue::Value(value) => Ok(value),
1401 }
1402}
1403
1404fn eval_v2_predicate_expr<'a>(
1405 expr: &V2Expr,
1406 record: &'a JsonValue,
1407 context: Option<&'a JsonValue>,
1408 out: &'a JsonValue,
1409 path: &str,
1410 ctx: &V2EvalContext<'a>,
1411) -> Result<bool, TransformError> {
1412 match eval_v2_expr(expr, record, context, out, path, ctx)? {
1413 EvalValue::Missing => Ok(false),
1414 EvalValue::Value(value) => {
1415 if value.is_null() {
1416 return Ok(false);
1417 }
1418 value_as_bool(&value, path)
1419 }
1420 }
1421}
1422
1423fn eval_v2_key_expr_string<'a>(
1424 expr: &V2Expr,
1425 record: &'a JsonValue,
1426 context: Option<&'a JsonValue>,
1427 out: &'a JsonValue,
1428 path: &str,
1429 ctx: &V2EvalContext<'a>,
1430) -> Result<String, TransformError> {
1431 let value = match eval_v2_expr(expr, record, context, out, path, ctx)? {
1432 EvalValue::Missing => {
1433 return Err(TransformError::new(
1434 TransformErrorKind::ExprError,
1435 "expr arg must not be missing",
1436 )
1437 .with_path(path));
1438 }
1439 EvalValue::Value(value) => value,
1440 };
1441 if value.is_null() {
1442 return Err(TransformError::new(
1443 TransformErrorKind::ExprError,
1444 "expr arg must not be null",
1445 )
1446 .with_path(path));
1447 }
1448 value_to_string(&value, path)
1449}
1450
1451#[derive(Clone, Copy, PartialEq, Eq)]
1452enum SortKeyKind {
1453 Number,
1454 String,
1455 Bool,
1456}
1457
1458#[derive(Clone)]
1459enum SortKey {
1460 Number(f64),
1461 String(String),
1462 Bool(bool),
1463}
1464
1465impl SortKey {
1466 fn kind(&self) -> SortKeyKind {
1467 match self {
1468 SortKey::Number(_) => SortKeyKind::Number,
1469 SortKey::String(_) => SortKeyKind::String,
1470 SortKey::Bool(_) => SortKeyKind::Bool,
1471 }
1472 }
1473}
1474
1475fn compare_sort_keys(left: &SortKey, right: &SortKey) -> std::cmp::Ordering {
1476 match (left, right) {
1477 (SortKey::Number(l), SortKey::Number(r)) => {
1478 l.partial_cmp(r).unwrap_or(std::cmp::Ordering::Equal)
1479 }
1480 (SortKey::String(l), SortKey::String(r)) => l.cmp(r),
1481 (SortKey::Bool(l), SortKey::Bool(r)) => l.cmp(r),
1482 _ => std::cmp::Ordering::Equal,
1483 }
1484}
1485
1486fn eval_v2_sort_key<'a>(
1487 expr: &V2Expr,
1488 record: &'a JsonValue,
1489 context: Option<&'a JsonValue>,
1490 out: &'a JsonValue,
1491 path: &str,
1492 ctx: &V2EvalContext<'a>,
1493) -> Result<SortKey, TransformError> {
1494 let value = match eval_v2_expr(expr, record, context, out, path, ctx)? {
1495 EvalValue::Missing => {
1496 return Err(TransformError::new(
1497 TransformErrorKind::ExprError,
1498 "expr arg must not be missing",
1499 )
1500 .with_path(path));
1501 }
1502 EvalValue::Value(value) => value,
1503 };
1504 if value.is_null() {
1505 return Err(TransformError::new(
1506 TransformErrorKind::ExprError,
1507 "expr arg must not be null",
1508 )
1509 .with_path(path));
1510 }
1511
1512 match value {
1513 JsonValue::Number(number) => {
1514 let value = number
1515 .as_f64()
1516 .filter(|value| value.is_finite())
1517 .ok_or_else(|| {
1518 TransformError::new(
1519 TransformErrorKind::ExprError,
1520 "sort_by key must be a finite number",
1521 )
1522 .with_path(path)
1523 })?;
1524 Ok(SortKey::Number(value))
1525 }
1526 JsonValue::String(value) => Ok(SortKey::String(value)),
1527 JsonValue::Bool(value) => Ok(SortKey::Bool(value)),
1528 _ => Err(TransformError::new(
1529 TransformErrorKind::ExprError,
1530 "sort_by key must be string/number/bool",
1531 )
1532 .with_path(path)),
1533 }
1534}
1535
1536fn eval_v2_array_from_eval_value(
1537 value: EvalValue,
1538 path: &str,
1539) -> Result<Vec<JsonValue>, TransformError> {
1540 match value {
1541 EvalValue::Missing => Ok(Vec::new()),
1542 EvalValue::Value(value) => {
1543 if value.is_null() {
1544 Ok(Vec::new())
1545 } else if let JsonValue::Array(items) = value {
1546 Ok(items)
1547 } else {
1548 Err(
1549 TransformError::new(TransformErrorKind::ExprError, "expr arg must be an array")
1550 .with_path(path),
1551 )
1552 }
1553 }
1554 }
1555}
1556fn v2_eval_to_v1_eval(value: &EvalValue) -> V1EvalValue {
1557 match value {
1558 EvalValue::Missing => V1EvalValue::Missing,
1559 EvalValue::Value(v) => V1EvalValue::Value(v.clone()),
1560 }
1561}
1562
1563fn v1_eval_to_v2_eval(value: V1EvalValue) -> EvalValue {
1564 match value {
1565 V1EvalValue::Missing => EvalValue::Missing,
1566 V1EvalValue::Value(v) => EvalValue::Value(v),
1567 }
1568}
1569
1570fn map_v2_op_name(op: &str) -> &str {
1571 match op {
1572 "add" => "+",
1573 "subtract" => "-",
1574 "multiply" => "*",
1575 "divide" => "/",
1576 _ => op,
1577 }
1578}
1579
1580fn eval_v2_op_with_v1_fallback<'a>(
1581 op_step: &V2OpStep,
1582 pipe_value: EvalValue,
1583 record: &'a JsonValue,
1584 context: Option<&'a JsonValue>,
1585 out: &'a JsonValue,
1586 path: &str,
1587 ctx: &V2EvalContext<'a>,
1588) -> Result<EvalValue, TransformError> {
1589 let mut v1_locals_map: HashMap<String, V1EvalValue> = ctx
1590 .let_bindings
1591 .iter()
1592 .map(|(k, v)| (k.clone(), v2_eval_to_v1_eval(v)))
1593 .collect();
1594 let mut arg_refs = Vec::with_capacity(op_step.args.len());
1595 for (index, arg) in op_step.args.iter().enumerate() {
1596 let arg_path = format!("{}.args[{}]", path, index);
1597 let value = eval_v2_expr(arg, record, context, out, &arg_path, ctx)?;
1598 let mut key = format!("__v2_arg{}", index);
1599 if v1_locals_map.contains_key(&key) {
1600 let mut suffix = 1usize;
1601 while v1_locals_map.contains_key(&format!("{}{}", key, suffix)) {
1602 suffix += 1;
1603 }
1604 key = format!("{}{}", key, suffix);
1605 }
1606 v1_locals_map.insert(key.clone(), v2_eval_to_v1_eval(&value));
1607 arg_refs.push(Expr::Ref(ExprRef {
1608 ref_path: format!("local.{}", key),
1609 }));
1610 }
1611
1612 let expr_op = ExprOp {
1613 op: map_v2_op_name(&op_step.op).to_string(),
1614 args: arg_refs,
1615 };
1616
1617 let v1_pipe = v2_eval_to_v1_eval(&pipe_value);
1618 let v1_item = ctx.get_item().map(|item| V1EvalItem {
1619 value: item.value,
1620 index: item.index,
1621 });
1622 let v1_locals = V1EvalLocals {
1623 item: v1_item,
1624 acc: ctx.get_acc(),
1625 pipe: Some(&v1_pipe),
1626 locals: Some(&v1_locals_map),
1627 };
1628
1629 let result = eval_v1_op(
1630 &expr_op,
1631 record,
1632 context,
1633 out,
1634 path,
1635 Some(&v1_pipe),
1636 Some(&v1_locals),
1637 )?;
1638
1639 Ok(v1_eval_to_v2_eval(result))
1640}
1641
1642fn number_to_string(number: &serde_json::Number) -> String {
1643 if let Some(i) = number.as_i64() {
1644 return i.to_string();
1645 }
1646 if let Some(u) = number.as_u64() {
1647 return u.to_string();
1648 }
1649 if let Some(f) = number.as_f64() {
1650 let mut s = format!("{}", f);
1651 if s.contains('.') {
1652 while s.ends_with('0') {
1653 s.pop();
1654 }
1655 if s.ends_with('.') {
1656 s.pop();
1657 }
1658 }
1659 return s;
1660 }
1661 number.to_string()
1662}
1663
1664fn value_to_string(value: &JsonValue, path: &str) -> Result<String, TransformError> {
1665 match value {
1666 JsonValue::String(s) => Ok(s.clone()),
1667 JsonValue::Number(n) => Ok(number_to_string(n)),
1668 JsonValue::Bool(b) => Ok(b.to_string()),
1669 _ => Err(TransformError::new(
1670 TransformErrorKind::ExprError,
1671 "value must be string/number/bool",
1672 )
1673 .with_path(path)),
1674 }
1675}
1676
1677fn cast_to_int(value: &JsonValue, path: &str) -> Result<JsonValue, TransformError> {
1678 match value {
1679 JsonValue::Number(n) => {
1680 if let Some(i) = n.as_i64() {
1681 Ok(JsonValue::Number(i.into()))
1682 } else if let Some(f) = n.as_f64() {
1683 if (f.fract()).abs() < f64::EPSILON {
1684 Ok(JsonValue::Number((f as i64).into()))
1685 } else {
1686 Err(type_cast_error("int", path))
1687 }
1688 } else {
1689 Err(type_cast_error("int", path))
1690 }
1691 }
1692 JsonValue::String(s) => s
1693 .parse::<i64>()
1694 .map(|i| JsonValue::Number(i.into()))
1695 .map_err(|_| type_cast_error("int", path)),
1696 _ => Err(type_cast_error("int", path)),
1697 }
1698}
1699
1700fn cast_to_float(value: &JsonValue, path: &str) -> Result<JsonValue, TransformError> {
1701 match value {
1702 JsonValue::Number(n) => n
1703 .as_f64()
1704 .ok_or_else(|| type_cast_error("float", path))
1705 .and_then(|f| {
1706 serde_json::Number::from_f64(f)
1707 .map(JsonValue::Number)
1708 .ok_or_else(|| type_cast_error("float", path))
1709 }),
1710 JsonValue::String(s) => s
1711 .parse::<f64>()
1712 .map_err(|_| type_cast_error("float", path))
1713 .and_then(|f| {
1714 serde_json::Number::from_f64(f)
1715 .map(JsonValue::Number)
1716 .ok_or_else(|| type_cast_error("float", path))
1717 }),
1718 _ => Err(type_cast_error("float", path)),
1719 }
1720}
1721
1722fn cast_to_bool(value: &JsonValue, path: &str) -> Result<JsonValue, TransformError> {
1723 match value {
1724 JsonValue::Bool(b) => Ok(JsonValue::Bool(*b)),
1725 JsonValue::String(s) => match s.to_lowercase().as_str() {
1726 "true" => Ok(JsonValue::Bool(true)),
1727 "false" => Ok(JsonValue::Bool(false)),
1728 _ => Err(type_cast_error("bool", path)),
1729 },
1730 _ => Err(type_cast_error("bool", path)),
1731 }
1732}
1733
1734fn type_cast_error(type_name: &str, path: &str) -> TransformError {
1735 TransformError::new(
1736 TransformErrorKind::ExprError,
1737 format!("failed to cast to {}", type_name),
1738 )
1739 .with_path(path)
1740}
1741
1742fn eval_type_cast(op: &str, value: &EvalValue, path: &str) -> Result<EvalValue, TransformError> {
1743 match value {
1744 EvalValue::Missing => Ok(EvalValue::Missing),
1745 EvalValue::Value(v) => {
1746 let casted = match op {
1747 "string" => JsonValue::String(value_to_string(v, path)?),
1748 "int" => cast_to_int(v, path)?,
1749 "float" => cast_to_float(v, path)?,
1750 "bool" => cast_to_bool(v, path)?,
1751 _ => {
1752 return Err(TransformError::new(
1753 TransformErrorKind::ExprError,
1754 "unknown cast op",
1755 )
1756 .with_path(path));
1757 }
1758 };
1759 Ok(EvalValue::Value(casted))
1760 }
1761 }
1762}
1763
1764pub fn eval_v2_op_step<'a>(
1766 op_step: &V2OpStep,
1767 pipe_value: EvalValue,
1768 record: &'a JsonValue,
1769 context: Option<&'a JsonValue>,
1770 out: &'a JsonValue,
1771 path: &str,
1772 ctx: &V2EvalContext<'a>,
1773) -> Result<EvalValue, TransformError> {
1774 let step_ctx = ctx.clone().with_pipe_value(pipe_value.clone());
1776
1777 if op_step.op.starts_with('@') {
1779 use crate::v2_parser::parse_v2_ref;
1780 if let Some(v2_ref) = parse_v2_ref(&op_step.op) {
1781 return eval_v2_ref(&v2_ref, record, context, out, path, &step_ctx);
1782 }
1783 return Err(TransformError::new(
1784 TransformErrorKind::ExprError,
1785 format!("invalid reference: {}", op_step.op),
1786 )
1787 .with_path(path));
1788 }
1789
1790 match op_step.op.as_str() {
1791 "trim" => {
1793 if matches!(pipe_value, EvalValue::Missing) {
1794 return Ok(EvalValue::Missing);
1795 }
1796 let s = eval_value_as_string(&pipe_value, path)?;
1797 Ok(EvalValue::Value(JsonValue::String(s.trim().to_string())))
1798 }
1799 "lowercase" => {
1800 if matches!(pipe_value, EvalValue::Missing) {
1801 return Ok(EvalValue::Missing);
1802 }
1803 let s = eval_value_as_string(&pipe_value, path)?;
1804 Ok(EvalValue::Value(JsonValue::String(s.to_lowercase())))
1805 }
1806 "uppercase" => {
1807 if matches!(pipe_value, EvalValue::Missing) {
1808 return Ok(EvalValue::Missing);
1809 }
1810 let s = eval_value_as_string(&pipe_value, path)?;
1811 Ok(EvalValue::Value(JsonValue::String(s.to_uppercase())))
1812 }
1813 "to_string" => match &pipe_value {
1814 EvalValue::Missing => Ok(EvalValue::Missing),
1815 EvalValue::Value(v) => {
1816 let s = match v {
1817 JsonValue::String(s) => s.clone(),
1818 JsonValue::Number(n) => n.to_string(),
1819 JsonValue::Bool(b) => b.to_string(),
1820 JsonValue::Null => "null".to_string(),
1821 JsonValue::Array(_) | JsonValue::Object(_) => v.to_string(),
1822 };
1823 Ok(EvalValue::Value(JsonValue::String(s)))
1824 }
1825 },
1826 "concat" => {
1827 let mut parts = Vec::new();
1829 if matches!(pipe_value, EvalValue::Missing) {
1830 return Ok(EvalValue::Missing);
1831 }
1832 parts.push(eval_value_as_string(&pipe_value, path)?);
1833 for (i, arg) in op_step.args.iter().enumerate() {
1834 let arg_path = format!("{}.args[{}]", path, i);
1835 let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1836 if matches!(arg_value, EvalValue::Missing) {
1837 return Ok(EvalValue::Missing);
1838 }
1839 parts.push(eval_value_as_string(&arg_value, &arg_path)?);
1840 }
1841 Ok(EvalValue::Value(JsonValue::String(parts.join(""))))
1842 }
1843 "string" | "int" | "float" | "bool" => {
1844 eval_type_cast(op_step.op.as_str(), &pipe_value, path)
1845 }
1846
1847 "add" | "+" => {
1849 if matches!(pipe_value, EvalValue::Missing) {
1850 return Ok(EvalValue::Missing);
1851 }
1852 let mut result = eval_value_as_number(&pipe_value, path)?;
1853 for (i, arg) in op_step.args.iter().enumerate() {
1854 let arg_path = format!("{}.args[{}]", path, i);
1855 let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1856 if matches!(arg_value, EvalValue::Missing) {
1857 return Ok(EvalValue::Missing);
1858 }
1859 result += eval_value_as_number(&arg_value, &arg_path)?;
1860 }
1861 Ok(EvalValue::Value(serde_json::json!(result)))
1862 }
1863 "subtract" | "-" => {
1864 if op_step.args.is_empty() {
1865 return Err(TransformError::new(
1866 TransformErrorKind::ExprError,
1867 "subtract requires at least one argument",
1868 )
1869 .with_path(path));
1870 }
1871 if matches!(pipe_value, EvalValue::Missing) {
1872 return Ok(EvalValue::Missing);
1873 }
1874 let mut result = eval_value_as_number(&pipe_value, path)?;
1875 for (i, arg) in op_step.args.iter().enumerate() {
1876 let arg_path = format!("{}.args[{}]", path, i);
1877 let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1878 if matches!(arg_value, EvalValue::Missing) {
1879 return Ok(EvalValue::Missing);
1880 }
1881 result -= eval_value_as_number(&arg_value, &arg_path)?;
1882 }
1883 Ok(EvalValue::Value(serde_json::json!(result)))
1884 }
1885 "multiply" | "*" => {
1886 if matches!(pipe_value, EvalValue::Missing) {
1887 return Ok(EvalValue::Missing);
1888 }
1889 let mut result = eval_value_as_number(&pipe_value, path)?;
1890 for (i, arg) in op_step.args.iter().enumerate() {
1891 let arg_path = format!("{}.args[{}]", path, i);
1892 let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1893 if matches!(arg_value, EvalValue::Missing) {
1894 return Ok(EvalValue::Missing);
1895 }
1896 result *= eval_value_as_number(&arg_value, &arg_path)?;
1897 }
1898 Ok(EvalValue::Value(serde_json::json!(result)))
1899 }
1900 "divide" | "/" => {
1901 if op_step.args.is_empty() {
1902 return Err(TransformError::new(
1903 TransformErrorKind::ExprError,
1904 "divide requires at least one argument",
1905 )
1906 .with_path(path));
1907 }
1908 if matches!(pipe_value, EvalValue::Missing) {
1909 return Ok(EvalValue::Missing);
1910 }
1911 let mut result = eval_value_as_number(&pipe_value, path)?;
1912 for (i, arg) in op_step.args.iter().enumerate() {
1913 let arg_path = format!("{}.args[{}]", path, i);
1914 let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
1915 if matches!(arg_value, EvalValue::Missing) {
1916 return Ok(EvalValue::Missing);
1917 }
1918 let divisor = eval_value_as_number(&arg_value, &arg_path)?;
1919 if divisor == 0.0 {
1920 return Err(TransformError::new(
1921 TransformErrorKind::ExprError,
1922 "division by zero",
1923 )
1924 .with_path(&arg_path));
1925 }
1926 result /= divisor;
1927 }
1928 Ok(EvalValue::Value(serde_json::json!(result)))
1929 }
1930 "map" => {
1931 if op_step.args.len() != 1 {
1932 return Err(TransformError::new(
1933 TransformErrorKind::ExprError,
1934 "map requires exactly one argument",
1935 )
1936 .with_path(path));
1937 }
1938 let array = match pipe_value {
1939 EvalValue::Missing => {
1940 return Ok(EvalValue::Missing);
1941 }
1942 EvalValue::Value(JsonValue::Array(items)) => items,
1943 EvalValue::Value(other) => {
1944 return Err(TransformError::new(
1945 TransformErrorKind::ExprError,
1946 format!("expr arg must be an array, got {:?}", other),
1947 )
1948 .with_path(path));
1949 }
1950 };
1951 let arg_path = format!("{}.args[0]", path);
1952 let mut results = Vec::new();
1953 for (index, item) in array.iter().enumerate() {
1954 let item_ctx = step_ctx
1955 .clone()
1956 .with_pipe_value(EvalValue::Value(item.clone()))
1957 .with_item(EvalItem { value: item, index });
1958 let value =
1959 eval_v2_expr(&op_step.args[0], record, context, out, &arg_path, &item_ctx)?;
1960 if let EvalValue::Value(value) = value {
1961 results.push(value);
1962 }
1963 }
1964 Ok(EvalValue::Value(JsonValue::Array(results)))
1965 }
1966 "filter" => {
1967 if op_step.args.len() != 1 {
1968 return Err(TransformError::new(
1969 TransformErrorKind::ExprError,
1970 "filter requires exactly one argument",
1971 )
1972 .with_path(path));
1973 }
1974 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
1975 let arg_path = format!("{}.args[0]", path);
1976 let mut results = Vec::new();
1977 for (index, item) in array.iter().enumerate() {
1978 let item_ctx = step_ctx
1979 .clone()
1980 .with_pipe_value(EvalValue::Value(item.clone()))
1981 .with_item(EvalItem { value: item, index });
1982 if eval_v2_predicate_expr(
1983 &op_step.args[0],
1984 record,
1985 context,
1986 out,
1987 &arg_path,
1988 &item_ctx,
1989 )? {
1990 results.push(item.clone());
1991 }
1992 }
1993 Ok(EvalValue::Value(JsonValue::Array(results)))
1994 }
1995 "flat_map" => {
1996 if op_step.args.len() != 1 {
1997 return Err(TransformError::new(
1998 TransformErrorKind::ExprError,
1999 "flat_map requires exactly one argument",
2000 )
2001 .with_path(path));
2002 }
2003 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2004 let arg_path = format!("{}.args[0]", path);
2005 let mut results = Vec::new();
2006 for (index, item) in array.iter().enumerate() {
2007 let item_ctx = step_ctx
2008 .clone()
2009 .with_pipe_value(EvalValue::Value(item.clone()))
2010 .with_item(EvalItem { value: item, index });
2011 let value = eval_v2_expr_or_null(
2012 &op_step.args[0],
2013 record,
2014 context,
2015 out,
2016 &arg_path,
2017 &item_ctx,
2018 )?;
2019 match value {
2020 JsonValue::Array(items) => results.extend(items),
2021 value => results.push(value),
2022 }
2023 }
2024 Ok(EvalValue::Value(JsonValue::Array(results)))
2025 }
2026 "group_by" => {
2027 if op_step.args.len() != 1 {
2028 return Err(TransformError::new(
2029 TransformErrorKind::ExprError,
2030 "group_by requires exactly one argument",
2031 )
2032 .with_path(path));
2033 }
2034 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2035 let arg_path = format!("{}.args[0]", path);
2036 let mut results = serde_json::Map::new();
2037 for (index, item) in array.iter().enumerate() {
2038 let item_ctx = step_ctx
2039 .clone()
2040 .with_pipe_value(EvalValue::Value(item.clone()))
2041 .with_item(EvalItem { value: item, index });
2042 let key = eval_v2_key_expr_string(
2043 &op_step.args[0],
2044 record,
2045 context,
2046 out,
2047 &arg_path,
2048 &item_ctx,
2049 )?;
2050 let entry = results
2051 .entry(key)
2052 .or_insert_with(|| JsonValue::Array(Vec::new()));
2053 if let JsonValue::Array(items) = entry {
2054 items.push(item.clone());
2055 }
2056 }
2057 Ok(EvalValue::Value(JsonValue::Object(results)))
2058 }
2059 "key_by" => {
2060 if op_step.args.len() != 1 {
2061 return Err(TransformError::new(
2062 TransformErrorKind::ExprError,
2063 "key_by requires exactly one argument",
2064 )
2065 .with_path(path));
2066 }
2067 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2068 let arg_path = format!("{}.args[0]", path);
2069 let mut results = serde_json::Map::new();
2070 for (index, item) in array.iter().enumerate() {
2071 let item_ctx = step_ctx
2072 .clone()
2073 .with_pipe_value(EvalValue::Value(item.clone()))
2074 .with_item(EvalItem { value: item, index });
2075 let key = eval_v2_key_expr_string(
2076 &op_step.args[0],
2077 record,
2078 context,
2079 out,
2080 &arg_path,
2081 &item_ctx,
2082 )?;
2083 results.insert(key, item.clone());
2084 }
2085 Ok(EvalValue::Value(JsonValue::Object(results)))
2086 }
2087 "partition" => {
2088 if op_step.args.len() != 1 {
2089 return Err(TransformError::new(
2090 TransformErrorKind::ExprError,
2091 "partition requires exactly one argument",
2092 )
2093 .with_path(path));
2094 }
2095 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2096 let arg_path = format!("{}.args[0]", path);
2097 let mut matched = Vec::new();
2098 let mut unmatched = Vec::new();
2099 for (index, item) in array.iter().enumerate() {
2100 let item_ctx = step_ctx
2101 .clone()
2102 .with_pipe_value(EvalValue::Value(item.clone()))
2103 .with_item(EvalItem { value: item, index });
2104 if eval_v2_predicate_expr(
2105 &op_step.args[0],
2106 record,
2107 context,
2108 out,
2109 &arg_path,
2110 &item_ctx,
2111 )? {
2112 matched.push(item.clone());
2113 } else {
2114 unmatched.push(item.clone());
2115 }
2116 }
2117 Ok(EvalValue::Value(JsonValue::Array(vec![
2118 JsonValue::Array(matched),
2119 JsonValue::Array(unmatched),
2120 ])))
2121 }
2122 "distinct_by" => {
2123 if op_step.args.len() != 1 {
2124 return Err(TransformError::new(
2125 TransformErrorKind::ExprError,
2126 "distinct_by requires exactly one argument",
2127 )
2128 .with_path(path));
2129 }
2130 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2131 let arg_path = format!("{}.args[0]", path);
2132 let mut results = Vec::new();
2133 let mut seen = HashSet::new();
2134 for (index, item) in array.iter().enumerate() {
2135 let item_ctx = step_ctx
2136 .clone()
2137 .with_pipe_value(EvalValue::Value(item.clone()))
2138 .with_item(EvalItem { value: item, index });
2139 let key = eval_v2_key_expr_string(
2140 &op_step.args[0],
2141 record,
2142 context,
2143 out,
2144 &arg_path,
2145 &item_ctx,
2146 )?;
2147 if seen.insert(key) {
2148 results.push(item.clone());
2149 }
2150 }
2151 Ok(EvalValue::Value(JsonValue::Array(results)))
2152 }
2153 "sort_by" => {
2154 if !(1..=2).contains(&op_step.args.len()) {
2155 return Err(TransformError::new(
2156 TransformErrorKind::ExprError,
2157 "sort_by requires one or two arguments",
2158 )
2159 .with_path(path));
2160 }
2161 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2162 if array.is_empty() {
2163 return Ok(EvalValue::Value(JsonValue::Array(Vec::new())));
2164 }
2165 let expr_path = format!("{}.args[0]", path);
2166 let order = if op_step.args.len() == 2 {
2167 let order_path = format!("{}.args[1]", path);
2168 let order_value = eval_v2_expr(
2169 &op_step.args[1],
2170 record,
2171 context,
2172 out,
2173 &order_path,
2174 &step_ctx,
2175 )?;
2176 let order = match order_value {
2177 EvalValue::Missing => return Ok(EvalValue::Missing),
2178 EvalValue::Value(value) => value_to_string(&value, &order_path)?,
2179 };
2180 if order != "asc" && order != "desc" {
2181 return Err(TransformError::new(
2182 TransformErrorKind::ExprError,
2183 "order must be asc or desc",
2184 )
2185 .with_path(order_path));
2186 }
2187 order
2188 } else {
2189 "asc".to_string()
2190 };
2191
2192 struct SortItem {
2193 key: SortKey,
2194 index: usize,
2195 value: JsonValue,
2196 }
2197
2198 let mut items = Vec::with_capacity(array.len());
2199 let mut key_kind: Option<SortKeyKind> = None;
2200 for (index, item) in array.iter().enumerate() {
2201 let item_ctx = step_ctx
2202 .clone()
2203 .with_pipe_value(EvalValue::Value(item.clone()))
2204 .with_item(EvalItem { value: item, index });
2205 let key = eval_v2_sort_key(
2206 &op_step.args[0],
2207 record,
2208 context,
2209 out,
2210 &expr_path,
2211 &item_ctx,
2212 )?;
2213 let kind = key.kind();
2214 if let Some(existing) = key_kind {
2215 if existing != kind {
2216 return Err(TransformError::new(
2217 TransformErrorKind::ExprError,
2218 "sort_by keys must be all the same type",
2219 )
2220 .with_path(expr_path));
2221 }
2222 } else {
2223 key_kind = Some(kind);
2224 }
2225 items.push(SortItem {
2226 key,
2227 index,
2228 value: item.clone(),
2229 });
2230 }
2231
2232 items.sort_by(|left, right| {
2233 let mut ordering = compare_sort_keys(&left.key, &right.key);
2234 if order == "desc" {
2235 ordering = ordering.reverse();
2236 }
2237 if ordering == std::cmp::Ordering::Equal {
2238 left.index.cmp(&right.index)
2239 } else {
2240 ordering
2241 }
2242 });
2243
2244 let results = items.into_iter().map(|item| item.value).collect::<Vec<_>>();
2245 Ok(EvalValue::Value(JsonValue::Array(results)))
2246 }
2247 "find" => {
2248 if op_step.args.len() != 1 {
2249 return Err(TransformError::new(
2250 TransformErrorKind::ExprError,
2251 "find requires exactly one argument",
2252 )
2253 .with_path(path));
2254 }
2255 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2256 let arg_path = format!("{}.args[0]", path);
2257 for (index, item) in array.iter().enumerate() {
2258 let item_ctx = step_ctx
2259 .clone()
2260 .with_pipe_value(EvalValue::Value(item.clone()))
2261 .with_item(EvalItem { value: item, index });
2262 if eval_v2_predicate_expr(
2263 &op_step.args[0],
2264 record,
2265 context,
2266 out,
2267 &arg_path,
2268 &item_ctx,
2269 )? {
2270 return Ok(EvalValue::Value(item.clone()));
2271 }
2272 }
2273 Ok(EvalValue::Value(JsonValue::Null))
2274 }
2275 "find_index" => {
2276 if op_step.args.len() != 1 {
2277 return Err(TransformError::new(
2278 TransformErrorKind::ExprError,
2279 "find_index requires exactly one argument",
2280 )
2281 .with_path(path));
2282 }
2283 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2284 let arg_path = format!("{}.args[0]", path);
2285 for (index, item) in array.iter().enumerate() {
2286 let item_ctx = step_ctx
2287 .clone()
2288 .with_pipe_value(EvalValue::Value(item.clone()))
2289 .with_item(EvalItem { value: item, index });
2290 if eval_v2_predicate_expr(
2291 &op_step.args[0],
2292 record,
2293 context,
2294 out,
2295 &arg_path,
2296 &item_ctx,
2297 )? {
2298 return Ok(EvalValue::Value(JsonValue::Number((index as i64).into())));
2299 }
2300 }
2301 Ok(EvalValue::Value(JsonValue::Number((-1).into())))
2302 }
2303 "reduce" => {
2304 if op_step.args.len() != 1 {
2305 return Err(TransformError::new(
2306 TransformErrorKind::ExprError,
2307 "reduce requires exactly one argument",
2308 )
2309 .with_path(path));
2310 }
2311 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2312 if array.is_empty() {
2313 return Ok(EvalValue::Value(JsonValue::Null));
2314 }
2315 let expr_path = format!("{}.args[0]", path);
2316 let mut acc = array[0].clone();
2317 for (index, item) in array.iter().enumerate().skip(1) {
2318 let item_ctx = step_ctx
2319 .clone()
2320 .with_pipe_value(EvalValue::Value(item.clone()))
2321 .with_item(EvalItem { value: item, index })
2322 .with_acc(&acc);
2323 let value = eval_v2_expr_or_null(
2324 &op_step.args[0],
2325 record,
2326 context,
2327 out,
2328 &expr_path,
2329 &item_ctx,
2330 )?;
2331 acc = value;
2332 }
2333 Ok(EvalValue::Value(acc))
2334 }
2335 "fold" => {
2336 if op_step.args.len() != 2 {
2337 return Err(TransformError::new(
2338 TransformErrorKind::ExprError,
2339 "fold requires exactly two arguments",
2340 )
2341 .with_path(path));
2342 }
2343 let array = eval_v2_array_from_eval_value(pipe_value.clone(), path)?;
2344 let init_path = format!("{}.args[0]", path);
2345 let initial = match eval_v2_expr(
2346 &op_step.args[0],
2347 record,
2348 context,
2349 out,
2350 &init_path,
2351 &step_ctx,
2352 )? {
2353 EvalValue::Missing => return Ok(EvalValue::Missing),
2354 EvalValue::Value(value) => value,
2355 };
2356 let expr_path = format!("{}.args[1]", path);
2357 let mut acc = initial;
2358 for (index, item) in array.iter().enumerate() {
2359 let item_ctx = step_ctx
2360 .clone()
2361 .with_pipe_value(EvalValue::Value(item.clone()))
2362 .with_item(EvalItem { value: item, index })
2363 .with_acc(&acc);
2364 let value = eval_v2_expr_or_null(
2365 &op_step.args[1],
2366 record,
2367 context,
2368 out,
2369 &expr_path,
2370 &item_ctx,
2371 )?;
2372 acc = value;
2373 }
2374 Ok(EvalValue::Value(acc))
2375 }
2376 "zip_with" => {
2377 if op_step.args.len() < 2 {
2378 return Err(TransformError::new(
2379 TransformErrorKind::ExprError,
2380 "zip_with requires at least two arguments",
2381 )
2382 .with_path(path));
2383 }
2384 let mut arrays = Vec::new();
2385 arrays.push(eval_v2_array_from_eval_value(pipe_value.clone(), path)?);
2386 for (index, arg) in op_step.args.iter().enumerate().take(op_step.args.len() - 1) {
2387 let arg_path = format!("{}.args[{}]", path, index);
2388 let value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
2389 arrays.push(eval_v2_array_from_eval_value(value, &arg_path)?);
2390 }
2391
2392 let min_len = arrays.iter().map(|items| items.len()).min().unwrap_or(0);
2393 let expr_index = op_step.args.len() - 1;
2394 let expr_path = format!("{}.args[{}]", path, expr_index);
2395 let expr = &op_step.args[expr_index];
2396 let mut results = Vec::with_capacity(min_len);
2397 for row_index in 0..min_len {
2398 let mut row = Vec::with_capacity(arrays.len());
2399 for array in &arrays {
2400 row.push(array[row_index].clone());
2401 }
2402 let row_value = JsonValue::Array(row);
2403 let item_ctx = step_ctx
2404 .clone()
2405 .with_pipe_value(EvalValue::Value(row_value.clone()))
2406 .with_item(EvalItem {
2407 value: &row_value,
2408 index: row_index,
2409 });
2410 let value =
2411 eval_v2_expr_or_null(expr, record, context, out, &expr_path, &item_ctx)?;
2412 results.push(value);
2413 }
2414 Ok(EvalValue::Value(JsonValue::Array(results)))
2415 }
2416 "first" => match &pipe_value {
2417 EvalValue::Missing => Ok(EvalValue::Missing),
2418 EvalValue::Value(JsonValue::Array(arr)) => {
2419 if let Some(value) = arr.first() {
2420 Ok(EvalValue::Value(value.clone()))
2421 } else {
2422 Ok(EvalValue::Missing)
2423 }
2424 }
2425 EvalValue::Value(other) => Err(TransformError::new(
2426 TransformErrorKind::ExprError,
2427 format!("first requires array, got {:?}", other),
2428 )
2429 .with_path(path)),
2430 },
2431 "last" => match &pipe_value {
2432 EvalValue::Missing => Ok(EvalValue::Missing),
2433 EvalValue::Value(JsonValue::Array(arr)) => {
2434 if let Some(value) = arr.last() {
2435 Ok(EvalValue::Value(value.clone()))
2436 } else {
2437 Ok(EvalValue::Missing)
2438 }
2439 }
2440 EvalValue::Value(other) => Err(TransformError::new(
2441 TransformErrorKind::ExprError,
2442 format!("last requires array, got {:?}", other),
2443 )
2444 .with_path(path)),
2445 },
2446
2447 "coalesce" => {
2449 if let EvalValue::Value(v) = &pipe_value {
2451 if !v.is_null() {
2452 return Ok(pipe_value);
2453 }
2454 }
2455 for (i, arg) in op_step.args.iter().enumerate() {
2457 let arg_path = format!("{}.args[{}]", path, i);
2458 let arg_value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
2459 if let EvalValue::Value(v) = &arg_value {
2460 if !v.is_null() {
2461 return Ok(arg_value);
2462 }
2463 }
2464 }
2465 Ok(EvalValue::Missing)
2466 }
2467 "and" | "or" => {
2468 let is_and = op_step.op == "and";
2469 let total_len = op_step.args.len() + 1;
2470 if total_len < 2 {
2471 return Err(TransformError::new(
2472 TransformErrorKind::ExprError,
2473 "expr.args must contain at least two items",
2474 )
2475 .with_path(format!("{}.args", path)));
2476 }
2477
2478 let mut saw_missing = false;
2479 match &pipe_value {
2480 EvalValue::Missing => saw_missing = true,
2481 EvalValue::Value(value) => {
2482 let flag = value_as_bool(value, path)?;
2483 if is_and {
2484 if !flag {
2485 return Ok(EvalValue::Value(JsonValue::Bool(false)));
2486 }
2487 } else if flag {
2488 return Ok(EvalValue::Value(JsonValue::Bool(true)));
2489 }
2490 }
2491 }
2492
2493 for (index, arg) in op_step.args.iter().enumerate() {
2494 let arg_path = format!("{}.args[{}]", path, index);
2495 let value = eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)?;
2496 match value {
2497 EvalValue::Missing => {
2498 saw_missing = true;
2499 continue;
2500 }
2501 EvalValue::Value(value) => {
2502 let flag = value_as_bool(&value, &arg_path)?;
2503 if is_and {
2504 if !flag {
2505 return Ok(EvalValue::Value(JsonValue::Bool(false)));
2506 }
2507 } else if flag {
2508 return Ok(EvalValue::Value(JsonValue::Bool(true)));
2509 }
2510 }
2511 }
2512 }
2513
2514 if saw_missing {
2515 Ok(EvalValue::Missing)
2516 } else {
2517 Ok(EvalValue::Value(JsonValue::Bool(is_and)))
2518 }
2519 }
2520 "not" => {
2521 if !op_step.args.is_empty() {
2522 return Err(TransformError::new(
2523 TransformErrorKind::ExprError,
2524 "expr.args must contain exactly one item",
2525 )
2526 .with_path(format!("{}.args", path)));
2527 }
2528 match pipe_value {
2529 EvalValue::Missing => Ok(EvalValue::Missing),
2530 EvalValue::Value(value) => {
2531 let flag = value_as_bool(&value, path)?;
2532 Ok(EvalValue::Value(JsonValue::Bool(!flag)))
2533 }
2534 }
2535 }
2536 "==" | "!=" | "<" | "<=" | ">" | ">=" | "~=" | "eq" | "ne" | "lt" | "lte" | "gt"
2537 | "gte" | "match" => {
2538 if op_step.args.len() != 1 {
2539 return Err(TransformError::new(
2540 TransformErrorKind::ExprError,
2541 "expr.args must contain exactly one item",
2542 )
2543 .with_path(format!("{}.args", path)));
2544 }
2545 let left = match pipe_value {
2546 EvalValue::Missing => JsonValue::Null,
2547 EvalValue::Value(value) => value,
2548 };
2549 let right_path = format!("{}.args[0]", path);
2550 let right = eval_v2_expr_or_null(
2551 &op_step.args[0],
2552 record,
2553 context,
2554 out,
2555 &right_path,
2556 &step_ctx,
2557 )?;
2558 let left_path = path.to_string();
2559 let op = match op_step.op.as_str() {
2560 "eq" => "==",
2561 "ne" => "!=",
2562 "lt" => "<",
2563 "lte" => "<=",
2564 "gt" => ">",
2565 "gte" => ">=",
2566 "match" => "~=",
2567 other => other,
2568 };
2569 let result = match op {
2570 "==" => compare_eq_v1(&left, &right, &left_path, &right_path)?,
2571 "!=" => !compare_eq_v1(&left, &right, &left_path, &right_path)?,
2572 "<" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l < r)?,
2573 "<=" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l <= r)?,
2574 ">" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l > r)?,
2575 ">=" => compare_numbers_v1(&left, &right, &left_path, &right_path, |l, r| l >= r)?,
2576 "~=" => match_regex_v1(&left, &right, &left_path, &right_path)?,
2577 _ => false,
2578 };
2579 Ok(EvalValue::Value(JsonValue::Bool(result)))
2580 }
2581 "pick" | "omit" => {
2582 if op_step.args.is_empty() {
2583 return Err(TransformError::new(
2584 TransformErrorKind::ExprError,
2585 format!("{} requires at least one argument", op_step.op),
2586 )
2587 .with_path(format!("{}.args", path)));
2588 }
2589
2590 let mut path_values = Vec::new();
2591 for (index, arg) in op_step.args.iter().enumerate() {
2592 let arg_path = format!("{}.args[{}]", path, index);
2593 let value = match eval_v2_expr(arg, record, context, out, &arg_path, &step_ctx)? {
2594 EvalValue::Missing => return Ok(EvalValue::Missing),
2595 EvalValue::Value(value) => value,
2596 };
2597 if value.is_null() {
2598 return Err(TransformError::new(
2599 TransformErrorKind::ExprError,
2600 "expr arg must not be null",
2601 )
2602 .with_path(arg_path));
2603 }
2604 match value {
2605 JsonValue::String(path_value) => {
2606 path_values.push(JsonValue::String(path_value));
2607 }
2608 JsonValue::Array(items) => {
2609 for (item_index, item) in items.iter().enumerate() {
2610 let item_path = format!("{}.args[{}][{}]", path, index, item_index);
2611 let path_value = item.as_str().ok_or_else(|| {
2612 TransformError::new(
2613 TransformErrorKind::ExprError,
2614 "paths must be a string or array of strings",
2615 )
2616 .with_path(item_path)
2617 })?;
2618 path_values.push(JsonValue::String(path_value.to_string()));
2619 }
2620 }
2621 _ => {
2622 return Err(TransformError::new(
2623 TransformErrorKind::ExprError,
2624 "paths must be a string or array of strings",
2625 )
2626 .with_path(arg_path));
2627 }
2628 }
2629 }
2630
2631 let normalized_op = V2OpStep {
2632 op: op_step.op.clone(),
2633 args: vec![V2Expr::Pipe(V2Pipe {
2634 start: V2Start::Literal(JsonValue::Array(path_values)),
2635 steps: vec![],
2636 })],
2637 };
2638 eval_v2_op_with_v1_fallback(
2639 &normalized_op,
2640 pipe_value,
2641 record,
2642 context,
2643 out,
2644 path,
2645 &step_ctx,
2646 )
2647 }
2648
2649 "lookup_first" => {
2661 if op_step.args.len() < 2 {
2662 return Err(TransformError::new(
2663 TransformErrorKind::ExprError,
2664 "lookup_first requires at least 2 arguments: match_key, match_value",
2665 )
2666 .with_path(path));
2667 }
2668
2669 let args = &op_step.args;
2670 let from_path = format!("{}.from", path);
2671 let match_key_path = format!("{}.match_key", path);
2672 let get_path = format!("{}.get", path);
2673
2674 let (from_value, match_key_value, match_value, get_field) = match args.len() {
2675 0 | 1 => unreachable!("guarded above"),
2676 2 => {
2677 let match_key_value = eval_v2_expr(
2678 &args[0],
2679 record,
2680 context,
2681 out,
2682 &format!("{}.args[0]", path),
2683 &step_ctx,
2684 )?;
2685 let match_value = eval_v2_expr(
2686 &args[1],
2687 record,
2688 context,
2689 out,
2690 &format!("{}.args[1]", path),
2691 &step_ctx,
2692 )?;
2693 (pipe_value.clone(), match_key_value, match_value, None)
2694 }
2695 3 => {
2696 if matches!(pipe_value, EvalValue::Missing) {
2697 let first_value = eval_v2_expr(
2698 &args[0],
2699 record,
2700 context,
2701 out,
2702 &format!("{}.args[0]", path),
2703 &step_ctx,
2704 )?;
2705 let use_explicit_from =
2706 matches!(first_value, EvalValue::Value(JsonValue::Array(_)));
2707 if !use_explicit_from {
2708 return Ok(EvalValue::Missing);
2709 }
2710 let match_key_value = eval_v2_expr(
2711 &args[1],
2712 record,
2713 context,
2714 out,
2715 &format!("{}.args[1]", path),
2716 &step_ctx,
2717 )?;
2718 let match_value = eval_v2_expr(
2719 &args[2],
2720 record,
2721 context,
2722 out,
2723 &format!("{}.args[2]", path),
2724 &step_ctx,
2725 )?;
2726 (first_value, match_key_value, match_value, None)
2727 } else {
2728 let first_value = eval_v2_expr(
2729 &args[0],
2730 record,
2731 context,
2732 out,
2733 &format!("{}.args[0]", path),
2734 &step_ctx,
2735 )?;
2736 let use_explicit_from = matches!(
2737 first_value,
2738 EvalValue::Value(JsonValue::Array(_)) | EvalValue::Missing
2739 );
2740 if use_explicit_from {
2741 let match_key_value = eval_v2_expr(
2742 &args[1],
2743 record,
2744 context,
2745 out,
2746 &format!("{}.args[1]", path),
2747 &step_ctx,
2748 )?;
2749 let match_value = eval_v2_expr(
2750 &args[2],
2751 record,
2752 context,
2753 out,
2754 &format!("{}.args[2]", path),
2755 &step_ctx,
2756 )?;
2757 (first_value, match_key_value, match_value, None)
2758 } else {
2759 let match_value = eval_v2_expr(
2760 &args[1],
2761 record,
2762 context,
2763 out,
2764 &format!("{}.args[1]", path),
2765 &step_ctx,
2766 )?;
2767 let get_value = eval_v2_expr(
2768 &args[2],
2769 record,
2770 context,
2771 out,
2772 &format!("{}.args[2]", path),
2773 &step_ctx,
2774 )?;
2775 let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
2776 (pipe_value.clone(), first_value, match_value, get_field)
2777 }
2778 }
2779 }
2780 _ => {
2781 let from_value = eval_v2_expr(
2782 &args[0],
2783 record,
2784 context,
2785 out,
2786 &format!("{}.args[0]", path),
2787 &step_ctx,
2788 )?;
2789 let match_key_value = eval_v2_expr(
2790 &args[1],
2791 record,
2792 context,
2793 out,
2794 &format!("{}.args[1]", path),
2795 &step_ctx,
2796 )?;
2797 let match_value = eval_v2_expr(
2798 &args[2],
2799 record,
2800 context,
2801 out,
2802 &format!("{}.args[2]", path),
2803 &step_ctx,
2804 )?;
2805 let get_value = eval_v2_expr(
2806 &args[3],
2807 record,
2808 context,
2809 out,
2810 &format!("{}.args[3]", path),
2811 &step_ctx,
2812 )?;
2813 let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
2814 (from_value, match_key_value, match_value, get_field)
2815 }
2816 };
2817
2818 let arr = match &from_value {
2820 EvalValue::Value(JsonValue::Array(arr)) => arr,
2821 EvalValue::Missing => return Ok(EvalValue::Missing),
2822 _ => {
2823 return Err(TransformError::new(
2824 TransformErrorKind::ExprError,
2825 "lookup_first 'from' must be an array",
2826 )
2827 .with_path(&from_path));
2828 }
2829 };
2830
2831 let match_key = eval_value_as_string(&match_key_value, &match_key_path)?;
2833 if matches!(match_value, EvalValue::Missing) {
2834 return Ok(EvalValue::Missing);
2835 }
2836
2837 for item in arr {
2839 if let JsonValue::Object(obj) = item {
2840 if let Some(field_val) = obj.get(&match_key) {
2841 let item_val = EvalValue::Value(field_val.clone());
2842 if compare_values_eq(&item_val, &match_value) {
2843 if let Some(ref get_key) = get_field {
2845 return match obj.get(get_key) {
2847 Some(v) => Ok(EvalValue::Value(v.clone())),
2848 None => Ok(EvalValue::Missing),
2849 };
2850 } else {
2851 return Ok(EvalValue::Value(item.clone()));
2853 }
2854 }
2855 }
2856 }
2857 }
2858
2859 Ok(EvalValue::Missing)
2860 }
2861
2862 "lookup" => {
2863 if op_step.args.len() < 2 {
2864 return Err(TransformError::new(
2865 TransformErrorKind::ExprError,
2866 "lookup requires at least 2 arguments: match_key, match_value",
2867 )
2868 .with_path(path));
2869 }
2870
2871 let args = &op_step.args;
2872 let from_path = format!("{}.from", path);
2873 let match_key_path = format!("{}.match_key", path);
2874 let get_path = format!("{}.get", path);
2875
2876 let (from_value, match_key_value, match_value, get_field) = match args.len() {
2877 0 | 1 => unreachable!("guarded above"),
2878 2 => {
2879 let match_key_value = eval_v2_expr(
2880 &args[0],
2881 record,
2882 context,
2883 out,
2884 &format!("{}.args[0]", path),
2885 &step_ctx,
2886 )?;
2887 let match_value = eval_v2_expr(
2888 &args[1],
2889 record,
2890 context,
2891 out,
2892 &format!("{}.args[1]", path),
2893 &step_ctx,
2894 )?;
2895 (pipe_value.clone(), match_key_value, match_value, None)
2896 }
2897 3 => {
2898 if matches!(pipe_value, EvalValue::Missing) {
2899 let first_value = eval_v2_expr(
2900 &args[0],
2901 record,
2902 context,
2903 out,
2904 &format!("{}.args[0]", path),
2905 &step_ctx,
2906 )?;
2907 let use_explicit_from =
2908 matches!(first_value, EvalValue::Value(JsonValue::Array(_)));
2909 if !use_explicit_from {
2910 return Ok(EvalValue::Missing);
2911 }
2912 let match_key_value = eval_v2_expr(
2913 &args[1],
2914 record,
2915 context,
2916 out,
2917 &format!("{}.args[1]", path),
2918 &step_ctx,
2919 )?;
2920 let match_value = eval_v2_expr(
2921 &args[2],
2922 record,
2923 context,
2924 out,
2925 &format!("{}.args[2]", path),
2926 &step_ctx,
2927 )?;
2928 (first_value, match_key_value, match_value, None)
2929 } else {
2930 let first_value = eval_v2_expr(
2931 &args[0],
2932 record,
2933 context,
2934 out,
2935 &format!("{}.args[0]", path),
2936 &step_ctx,
2937 )?;
2938 let use_explicit_from = matches!(
2939 first_value,
2940 EvalValue::Value(JsonValue::Array(_)) | EvalValue::Missing
2941 );
2942 if use_explicit_from {
2943 let match_key_value = eval_v2_expr(
2944 &args[1],
2945 record,
2946 context,
2947 out,
2948 &format!("{}.args[1]", path),
2949 &step_ctx,
2950 )?;
2951 let match_value = eval_v2_expr(
2952 &args[2],
2953 record,
2954 context,
2955 out,
2956 &format!("{}.args[2]", path),
2957 &step_ctx,
2958 )?;
2959 (first_value, match_key_value, match_value, None)
2960 } else {
2961 let match_value = eval_v2_expr(
2962 &args[1],
2963 record,
2964 context,
2965 out,
2966 &format!("{}.args[1]", path),
2967 &step_ctx,
2968 )?;
2969 let get_value = eval_v2_expr(
2970 &args[2],
2971 record,
2972 context,
2973 out,
2974 &format!("{}.args[2]", path),
2975 &step_ctx,
2976 )?;
2977 let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
2978 (pipe_value.clone(), first_value, match_value, get_field)
2979 }
2980 }
2981 }
2982 _ => {
2983 let from_value = eval_v2_expr(
2984 &args[0],
2985 record,
2986 context,
2987 out,
2988 &format!("{}.args[0]", path),
2989 &step_ctx,
2990 )?;
2991 let match_key_value = eval_v2_expr(
2992 &args[1],
2993 record,
2994 context,
2995 out,
2996 &format!("{}.args[1]", path),
2997 &step_ctx,
2998 )?;
2999 let match_value = eval_v2_expr(
3000 &args[2],
3001 record,
3002 context,
3003 out,
3004 &format!("{}.args[2]", path),
3005 &step_ctx,
3006 )?;
3007 let get_value = eval_v2_expr(
3008 &args[3],
3009 record,
3010 context,
3011 out,
3012 &format!("{}.args[3]", path),
3013 &step_ctx,
3014 )?;
3015 let get_field = Some(eval_value_as_string(&get_value, &get_path)?);
3016 (from_value, match_key_value, match_value, get_field)
3017 }
3018 };
3019
3020 let arr = match &from_value {
3022 EvalValue::Value(JsonValue::Array(arr)) => arr,
3023 EvalValue::Missing => return Ok(EvalValue::Missing),
3024 _ => {
3025 return Err(TransformError::new(
3026 TransformErrorKind::ExprError,
3027 "lookup 'from' must be an array",
3028 )
3029 .with_path(&from_path));
3030 }
3031 };
3032
3033 let match_key = eval_value_as_string(&match_key_value, &match_key_path)?;
3035 if matches!(match_value, EvalValue::Missing) {
3036 return Ok(EvalValue::Missing);
3037 }
3038
3039 let mut results = Vec::new();
3041 for item in arr {
3042 if let JsonValue::Object(obj) = item {
3043 if let Some(field_val) = obj.get(&match_key) {
3044 let item_val = EvalValue::Value(field_val.clone());
3045 if compare_values_eq(&item_val, &match_value) {
3046 if let Some(ref get_key) = get_field {
3048 if let Some(v) = obj.get(get_key) {
3050 results.push(v.clone());
3051 }
3052 } else {
3053 results.push(item.clone());
3055 }
3056 }
3057 }
3058 }
3059 }
3060
3061 Ok(EvalValue::Value(JsonValue::Array(results)))
3062 }
3063
3064 _ => {
3066 eval_v2_op_with_v1_fallback(op_step, pipe_value, record, context, out, path, &step_ctx)
3067 }
3068 }
3069}
3070
3071#[cfg(test)]
3076mod v2_op_step_eval_tests {
3077 use super::*;
3078 use serde_json::{Value as JsonValue, json};
3079
3080 fn lit(value: JsonValue) -> V2Expr {
3081 V2Expr::Pipe(V2Pipe {
3082 start: V2Start::Literal(value),
3083 steps: vec![],
3084 })
3085 }
3086
3087 #[test]
3088 fn test_eval_op_trim() {
3089 let op = V2OpStep {
3090 op: "trim".to_string(),
3091 args: vec![],
3092 };
3093 let ctx = V2EvalContext::new();
3094 let result = eval_v2_op_step(
3095 &op,
3096 EvalValue::Value(json!(" hello ")),
3097 &json!({}),
3098 None,
3099 &json!({}),
3100 "test",
3101 &ctx,
3102 );
3103 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello")));
3104 }
3105
3106 #[test]
3107 fn test_eval_op_lowercase() {
3108 let op = V2OpStep {
3109 op: "lowercase".to_string(),
3110 args: vec![],
3111 };
3112 let ctx = V2EvalContext::new();
3113 let result = eval_v2_op_step(
3114 &op,
3115 EvalValue::Value(json!("HELLO")),
3116 &json!({}),
3117 None,
3118 &json!({}),
3119 "test",
3120 &ctx,
3121 );
3122 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello")));
3123 }
3124
3125 #[test]
3126 fn test_eval_op_uppercase() {
3127 let op = V2OpStep {
3128 op: "uppercase".to_string(),
3129 args: vec![],
3130 };
3131 let ctx = V2EvalContext::new();
3132 let result = eval_v2_op_step(
3133 &op,
3134 EvalValue::Value(json!("hello")),
3135 &json!({}),
3136 None,
3137 &json!({}),
3138 "test",
3139 &ctx,
3140 );
3141 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HELLO")));
3142 }
3143
3144 #[test]
3145 fn test_eval_op_to_string() {
3146 let op = V2OpStep {
3147 op: "to_string".to_string(),
3148 args: vec![],
3149 };
3150 let ctx = V2EvalContext::new();
3151
3152 let result = eval_v2_op_step(
3154 &op,
3155 EvalValue::Value(json!(42)),
3156 &json!({}),
3157 None,
3158 &json!({}),
3159 "test",
3160 &ctx,
3161 );
3162 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("42")));
3163
3164 let result = eval_v2_op_step(
3166 &op,
3167 EvalValue::Value(json!(true)),
3168 &json!({}),
3169 None,
3170 &json!({}),
3171 "test",
3172 &ctx,
3173 );
3174 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("true")));
3175 }
3176
3177 #[test]
3178 fn test_eval_op_replace() {
3179 let op = V2OpStep {
3180 op: "replace".to_string(),
3181 args: vec![lit(json!("world")), lit(json!("there"))],
3182 };
3183 let ctx = V2EvalContext::new();
3184 let result = eval_v2_op_step(
3185 &op,
3186 EvalValue::Value(json!("hello world")),
3187 &json!({}),
3188 None,
3189 &json!({}),
3190 "test",
3191 &ctx,
3192 );
3193 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("hello there")));
3194 }
3195
3196 #[test]
3197 fn test_eval_op_split_and_pad() {
3198 let split = V2OpStep {
3199 op: "split".to_string(),
3200 args: vec![lit(json!(","))],
3201 };
3202 let pad_start = V2OpStep {
3203 op: "pad_start".to_string(),
3204 args: vec![lit(json!(3)), lit(json!("0"))],
3205 };
3206 let pad_end = V2OpStep {
3207 op: "pad_end".to_string(),
3208 args: vec![lit(json!(3)), lit(json!("0"))],
3209 };
3210 let ctx = V2EvalContext::new();
3211
3212 let split_result = eval_v2_op_step(
3213 &split,
3214 EvalValue::Value(json!("a,b,c")),
3215 &json!({}),
3216 None,
3217 &json!({}),
3218 "test",
3219 &ctx,
3220 );
3221 assert!(matches!(
3222 split_result,
3223 Ok(EvalValue::Value(v)) if v == json!(["a", "b", "c"])
3224 ));
3225
3226 let pad_start_result = eval_v2_op_step(
3227 &pad_start,
3228 EvalValue::Value(json!("7")),
3229 &json!({}),
3230 None,
3231 &json!({}),
3232 "test",
3233 &ctx,
3234 );
3235 assert!(matches!(pad_start_result, Ok(EvalValue::Value(v)) if v == json!("007")));
3236
3237 let pad_end_result = eval_v2_op_step(
3238 &pad_end,
3239 EvalValue::Value(json!("7")),
3240 &json!({}),
3241 None,
3242 &json!({}),
3243 "test",
3244 &ctx,
3245 );
3246 assert!(matches!(pad_end_result, Ok(EvalValue::Value(v)) if v == json!("700")));
3247 }
3248
3249 #[test]
3250 fn test_eval_op_round_and_to_base() {
3251 let round = V2OpStep {
3252 op: "round".to_string(),
3253 args: vec![lit(json!(2))],
3254 };
3255 let to_base = V2OpStep {
3256 op: "to_base".to_string(),
3257 args: vec![lit(json!(2))],
3258 };
3259 let ctx = V2EvalContext::new();
3260
3261 let rounded = eval_v2_op_step(
3262 &round,
3263 EvalValue::Value(json!(1.2345)),
3264 &json!({}),
3265 None,
3266 &json!({}),
3267 "test",
3268 &ctx,
3269 )
3270 .unwrap();
3271 if let EvalValue::Value(v) = rounded {
3272 let value = v.as_f64().unwrap();
3273 assert!((value - 1.23).abs() < 1e-9);
3274 } else {
3275 panic!("expected rounded value");
3276 }
3277
3278 let base = eval_v2_op_step(
3279 &to_base,
3280 EvalValue::Value(json!(10)),
3281 &json!({}),
3282 None,
3283 &json!({}),
3284 "test",
3285 &ctx,
3286 );
3287 assert!(matches!(base, Ok(EvalValue::Value(v)) if v == json!("1010")));
3288 }
3289
3290 #[test]
3291 fn test_eval_op_json_merge() {
3292 let op = V2OpStep {
3293 op: "merge".to_string(),
3294 args: vec![lit(json!({"b": 2}))],
3295 };
3296 let ctx = V2EvalContext::new();
3297 let result = eval_v2_op_step(
3298 &op,
3299 EvalValue::Value(json!({"a": 1})),
3300 &json!({}),
3301 None,
3302 &json!({}),
3303 "test",
3304 &ctx,
3305 );
3306 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!({"a": 1, "b": 2})));
3307 }
3308
3309 #[test]
3310 fn test_eval_op_array_map_and_reduce() {
3311 let map_expr = V2Expr::Pipe(V2Pipe {
3312 start: V2Start::Ref(V2Ref::Item(String::new())),
3313 steps: vec![V2Step::Op(V2OpStep {
3314 op: "add".to_string(),
3315 args: vec![lit(json!(1))],
3316 })],
3317 });
3318 let map = V2OpStep {
3319 op: "map".to_string(),
3320 args: vec![map_expr],
3321 };
3322 let reduce_expr = V2Expr::Pipe(V2Pipe {
3323 start: V2Start::Ref(V2Ref::Acc(String::new())),
3324 steps: vec![V2Step::Op(V2OpStep {
3325 op: "add".to_string(),
3326 args: vec![V2Expr::Pipe(V2Pipe {
3327 start: V2Start::Ref(V2Ref::Item(String::new())),
3328 steps: vec![],
3329 })],
3330 })],
3331 });
3332 let reduce = V2OpStep {
3333 op: "reduce".to_string(),
3334 args: vec![reduce_expr],
3335 };
3336 let ctx = V2EvalContext::new();
3337
3338 let map_result = eval_v2_op_step(
3339 &map,
3340 EvalValue::Value(json!([1, 2, 3])),
3341 &json!({}),
3342 None,
3343 &json!({}),
3344 "test",
3345 &ctx,
3346 );
3347 assert!(matches!(map_result, Ok(EvalValue::Value(v)) if v == json!([2.0, 3.0, 4.0])));
3348
3349 let reduce_result = eval_v2_op_step(
3350 &reduce,
3351 EvalValue::Value(json!([1, 2, 3])),
3352 &json!({}),
3353 None,
3354 &json!({}),
3355 "test",
3356 &ctx,
3357 );
3358 assert!(matches!(reduce_result, Ok(EvalValue::Value(v)) if v == json!(6.0)));
3359 }
3360
3361 #[test]
3362 fn test_eval_op_first_last() {
3363 let first = V2OpStep {
3364 op: "first".to_string(),
3365 args: vec![],
3366 };
3367 let last = V2OpStep {
3368 op: "last".to_string(),
3369 args: vec![],
3370 };
3371 let ctx = V2EvalContext::new();
3372
3373 let first_result = eval_v2_op_step(
3374 &first,
3375 EvalValue::Value(json!([1, 2])),
3376 &json!({}),
3377 None,
3378 &json!({}),
3379 "test",
3380 &ctx,
3381 );
3382 assert!(matches!(first_result, Ok(EvalValue::Value(v)) if v == json!(1)));
3383
3384 let last_result = eval_v2_op_step(
3385 &last,
3386 EvalValue::Value(json!([1, 2])),
3387 &json!({}),
3388 None,
3389 &json!({}),
3390 "test",
3391 &ctx,
3392 );
3393 assert!(matches!(last_result, Ok(EvalValue::Value(v)) if v == json!(2)));
3394 }
3395
3396 #[test]
3397 fn test_eval_op_type_casts() {
3398 let op_int = V2OpStep {
3399 op: "int".to_string(),
3400 args: vec![],
3401 };
3402 let op_float = V2OpStep {
3403 op: "float".to_string(),
3404 args: vec![],
3405 };
3406 let op_bool = V2OpStep {
3407 op: "bool".to_string(),
3408 args: vec![],
3409 };
3410 let op_string = V2OpStep {
3411 op: "string".to_string(),
3412 args: vec![],
3413 };
3414 let ctx = V2EvalContext::new();
3415
3416 let int_result = eval_v2_op_step(
3417 &op_int,
3418 EvalValue::Value(json!("42")),
3419 &json!({}),
3420 None,
3421 &json!({}),
3422 "test",
3423 &ctx,
3424 );
3425 assert!(matches!(int_result, Ok(EvalValue::Value(v)) if v == json!(42)));
3426
3427 let float_result = eval_v2_op_step(
3428 &op_float,
3429 EvalValue::Value(json!("3.14")),
3430 &json!({}),
3431 None,
3432 &json!({}),
3433 "test",
3434 &ctx,
3435 );
3436 if let Ok(EvalValue::Value(v)) = float_result {
3437 let value = v.as_f64().unwrap();
3438 assert!((value - 3.14).abs() < 1e-9);
3439 } else {
3440 panic!("expected float cast");
3441 }
3442
3443 let bool_result = eval_v2_op_step(
3444 &op_bool,
3445 EvalValue::Value(json!("true")),
3446 &json!({}),
3447 None,
3448 &json!({}),
3449 "test",
3450 &ctx,
3451 );
3452 assert!(matches!(bool_result, Ok(EvalValue::Value(v)) if v == json!(true)));
3453
3454 let string_result = eval_v2_op_step(
3455 &op_string,
3456 EvalValue::Value(json!(12)),
3457 &json!({}),
3458 None,
3459 &json!({}),
3460 "test",
3461 &ctx,
3462 );
3463 assert!(matches!(string_result, Ok(EvalValue::Value(v)) if v == json!("12")));
3464 }
3465
3466 #[test]
3467 fn test_eval_op_and_or_short_circuit() {
3468 let or_op = V2OpStep {
3469 op: "or".to_string(),
3470 args: vec![V2Expr::Pipe(V2Pipe {
3471 start: V2Start::Literal(json!(1)),
3472 steps: vec![V2Step::Op(V2OpStep {
3473 op: "divide".to_string(),
3474 args: vec![V2Expr::Pipe(V2Pipe {
3475 start: V2Start::Literal(json!(0)),
3476 steps: vec![],
3477 })],
3478 })],
3479 })],
3480 };
3481 let and_op = V2OpStep {
3482 op: "and".to_string(),
3483 args: vec![V2Expr::Pipe(V2Pipe {
3484 start: V2Start::Literal(json!(1)),
3485 steps: vec![V2Step::Op(V2OpStep {
3486 op: "divide".to_string(),
3487 args: vec![V2Expr::Pipe(V2Pipe {
3488 start: V2Start::Literal(json!(0)),
3489 steps: vec![],
3490 })],
3491 })],
3492 })],
3493 };
3494 let ctx = V2EvalContext::new();
3495
3496 let or_result = eval_v2_op_step(
3497 &or_op,
3498 EvalValue::Value(json!(true)),
3499 &json!({}),
3500 None,
3501 &json!({}),
3502 "test",
3503 &ctx,
3504 );
3505 assert!(matches!(or_result, Ok(EvalValue::Value(v)) if v == json!(true)));
3506
3507 let and_result = eval_v2_op_step(
3508 &and_op,
3509 EvalValue::Value(json!(false)),
3510 &json!({}),
3511 None,
3512 &json!({}),
3513 "test",
3514 &ctx,
3515 );
3516 assert!(matches!(and_result, Ok(EvalValue::Value(v)) if v == json!(false)));
3517 }
3518
3519 #[test]
3520 fn test_eval_op_add() {
3521 let op = V2OpStep {
3522 op: "add".to_string(),
3523 args: vec![V2Expr::Pipe(V2Pipe {
3524 start: V2Start::Literal(json!(10)),
3525 steps: vec![],
3526 })],
3527 };
3528 let ctx = V2EvalContext::new();
3529 let result = eval_v2_op_step(
3530 &op,
3531 EvalValue::Value(json!(5)),
3532 &json!({}),
3533 None,
3534 &json!({}),
3535 "test",
3536 &ctx,
3537 );
3538 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(15.0)));
3539 }
3540
3541 #[test]
3542 fn test_eval_op_subtract() {
3543 let op = V2OpStep {
3544 op: "subtract".to_string(),
3545 args: vec![V2Expr::Pipe(V2Pipe {
3546 start: V2Start::Literal(json!(3)),
3547 steps: vec![],
3548 })],
3549 };
3550 let ctx = V2EvalContext::new();
3551 let result = eval_v2_op_step(
3552 &op,
3553 EvalValue::Value(json!(10)),
3554 &json!({}),
3555 None,
3556 &json!({}),
3557 "test",
3558 &ctx,
3559 );
3560 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(7.0)));
3561 }
3562
3563 #[test]
3564 fn test_eval_op_comparison_aliases() {
3565 let ctx = V2EvalContext::new();
3566 let cases = [
3567 ("eq", json!(1), json!("1"), true),
3568 ("ne", json!(1), json!(2), true),
3569 ("lt", json!(5), json!(10), true),
3570 ("lte", json!(10), json!(10), true),
3571 ("gt", json!(10), json!(5), true),
3572 ("gte", json!(10), json!(10), true),
3573 ("match", json!("apple"), json!("^a.*"), true),
3574 ];
3575
3576 for (op, left, right, expected) in cases {
3577 let op_step = V2OpStep {
3578 op: op.to_string(),
3579 args: vec![lit(right)],
3580 };
3581 let result = eval_v2_op_step(
3582 &op_step,
3583 EvalValue::Value(left),
3584 &json!({}),
3585 None,
3586 &json!({}),
3587 "test",
3588 &ctx,
3589 );
3590 assert!(
3591 matches!(result, Ok(EvalValue::Value(v)) if v == json!(expected)),
3592 "op {}",
3593 op
3594 );
3595 }
3596 }
3597
3598 #[test]
3599 fn test_eval_op_pick_multiple_paths() {
3600 let op = V2OpStep {
3601 op: "pick".to_string(),
3602 args: vec![lit(json!("name")), lit(json!("price"))],
3603 };
3604 let ctx = V2EvalContext::new();
3605 let result = eval_v2_op_step(
3606 &op,
3607 EvalValue::Value(json!({"name": "apple", "price": 100, "category": "fruit"})),
3608 &json!({}),
3609 None,
3610 &json!({}),
3611 "test",
3612 &ctx,
3613 );
3614 assert!(matches!(
3615 result,
3616 Ok(EvalValue::Value(v)) if v == json!({"name": "apple", "price": 100})
3617 ));
3618 }
3619
3620 #[test]
3621 fn test_eval_op_omit_multiple_paths() {
3622 let op = V2OpStep {
3623 op: "omit".to_string(),
3624 args: vec![lit(json!("category")), lit(json!("price"))],
3625 };
3626 let ctx = V2EvalContext::new();
3627 let result = eval_v2_op_step(
3628 &op,
3629 EvalValue::Value(json!({"name": "apple", "price": 100, "category": "fruit"})),
3630 &json!({}),
3631 None,
3632 &json!({}),
3633 "test",
3634 &ctx,
3635 );
3636 assert!(matches!(
3637 result,
3638 Ok(EvalValue::Value(v)) if v == json!({"name": "apple"})
3639 ));
3640 }
3641
3642 #[test]
3643 fn test_eval_op_pick_paths_array_arg() {
3644 let op = V2OpStep {
3645 op: "pick".to_string(),
3646 args: vec![lit(json!(["name", "price"]))],
3647 };
3648 let ctx = V2EvalContext::new();
3649 let result = eval_v2_op_step(
3650 &op,
3651 EvalValue::Value(json!({"name": "apple", "price": 100, "category": "fruit"})),
3652 &json!({}),
3653 None,
3654 &json!({}),
3655 "test",
3656 &ctx,
3657 );
3658 assert!(matches!(
3659 result,
3660 Ok(EvalValue::Value(v)) if v == json!({"name": "apple", "price": 100})
3661 ));
3662 }
3663
3664 #[test]
3665 fn test_eval_op_multiply() {
3666 let op = V2OpStep {
3667 op: "multiply".to_string(),
3668 args: vec![V2Expr::Pipe(V2Pipe {
3669 start: V2Start::Literal(json!(0.9)),
3670 steps: vec![],
3671 })],
3672 };
3673 let ctx = V2EvalContext::new();
3674 let result = eval_v2_op_step(
3675 &op,
3676 EvalValue::Value(json!(100)),
3677 &json!({}),
3678 None,
3679 &json!({}),
3680 "test",
3681 &ctx,
3682 );
3683 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(90.0)));
3684 }
3685
3686 #[test]
3687 fn test_eval_op_divide() {
3688 let op = V2OpStep {
3689 op: "divide".to_string(),
3690 args: vec![V2Expr::Pipe(V2Pipe {
3691 start: V2Start::Literal(json!(2)),
3692 steps: vec![],
3693 })],
3694 };
3695 let ctx = V2EvalContext::new();
3696 let result = eval_v2_op_step(
3697 &op,
3698 EvalValue::Value(json!(10)),
3699 &json!({}),
3700 None,
3701 &json!({}),
3702 "test",
3703 &ctx,
3704 );
3705 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(5.0)));
3706 }
3707
3708 #[test]
3709 fn test_eval_op_divide_by_zero() {
3710 let op = V2OpStep {
3711 op: "divide".to_string(),
3712 args: vec![V2Expr::Pipe(V2Pipe {
3713 start: V2Start::Literal(json!(0)),
3714 steps: vec![],
3715 })],
3716 };
3717 let ctx = V2EvalContext::new();
3718 let result = eval_v2_op_step(
3719 &op,
3720 EvalValue::Value(json!(10)),
3721 &json!({}),
3722 None,
3723 &json!({}),
3724 "test",
3725 &ctx,
3726 );
3727 assert!(result.is_err());
3728 }
3729
3730 #[test]
3731 fn test_eval_op_coalesce() {
3732 let op = V2OpStep {
3733 op: "coalesce".to_string(),
3734 args: vec![V2Expr::Pipe(V2Pipe {
3735 start: V2Start::Literal(json!("default")),
3736 steps: vec![],
3737 })],
3738 };
3739 let ctx = V2EvalContext::new();
3740
3741 let result = eval_v2_op_step(
3743 &op,
3744 EvalValue::Value(json!("value")),
3745 &json!({}),
3746 None,
3747 &json!({}),
3748 "test",
3749 &ctx,
3750 );
3751 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("value")));
3752
3753 let result = eval_v2_op_step(
3755 &op,
3756 EvalValue::Value(json!(null)),
3757 &json!({}),
3758 None,
3759 &json!({}),
3760 "test",
3761 &ctx,
3762 );
3763 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("default")));
3764
3765 let result = eval_v2_op_step(
3767 &op,
3768 EvalValue::Missing,
3769 &json!({}),
3770 None,
3771 &json!({}),
3772 "test",
3773 &ctx,
3774 );
3775 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("default")));
3776 }
3777
3778 #[test]
3779 fn test_eval_op_unknown() {
3780 let op = V2OpStep {
3781 op: "unknown_op".to_string(),
3782 args: vec![],
3783 };
3784 let ctx = V2EvalContext::new();
3785 let result = eval_v2_op_step(
3786 &op,
3787 EvalValue::Value(json!("test")),
3788 &json!({}),
3789 None,
3790 &json!({}),
3791 "test",
3792 &ctx,
3793 );
3794 assert!(result.is_err());
3795 }
3796}
3797
3798#[cfg(test)]
3803mod v2_let_step_eval_tests {
3804 use super::*;
3805 use serde_json::json;
3806
3807 #[test]
3808 fn test_eval_let_single_binding() {
3809 let let_step = V2LetStep {
3810 bindings: vec![(
3811 "x".to_string(),
3812 V2Expr::Pipe(V2Pipe {
3813 start: V2Start::Literal(json!(42)),
3814 steps: vec![],
3815 }),
3816 )],
3817 };
3818 let record = json!({});
3819 let out = json!({});
3820 let ctx = V2EvalContext::new();
3821 let result = eval_v2_let_step(
3822 &let_step,
3823 EvalValue::Value(json!("pipe_value")),
3824 &record,
3825 None,
3826 &out,
3827 "test",
3828 &ctx,
3829 );
3830 assert!(result.is_ok());
3831 let new_ctx = result.unwrap();
3832 assert_eq!(
3833 new_ctx.resolve_local("x"),
3834 Some(&EvalValue::Value(json!(42)))
3835 );
3836 }
3837
3838 #[test]
3839 fn test_eval_let_multiple_bindings() {
3840 let let_step = V2LetStep {
3841 bindings: vec![
3842 (
3843 "a".to_string(),
3844 V2Expr::Pipe(V2Pipe {
3845 start: V2Start::Literal(json!(1)),
3846 steps: vec![],
3847 }),
3848 ),
3849 (
3850 "b".to_string(),
3851 V2Expr::Pipe(V2Pipe {
3852 start: V2Start::Literal(json!(2)),
3853 steps: vec![],
3854 }),
3855 ),
3856 ],
3857 };
3858 let record = json!({});
3859 let out = json!({});
3860 let ctx = V2EvalContext::new();
3861 let result = eval_v2_let_step(
3862 &let_step,
3863 EvalValue::Value(json!("pipe")),
3864 &record,
3865 None,
3866 &out,
3867 "test",
3868 &ctx,
3869 );
3870 assert!(result.is_ok());
3871 let new_ctx = result.unwrap();
3872 assert_eq!(
3873 new_ctx.resolve_local("a"),
3874 Some(&EvalValue::Value(json!(1)))
3875 );
3876 assert_eq!(
3877 new_ctx.resolve_local("b"),
3878 Some(&EvalValue::Value(json!(2)))
3879 );
3880 }
3881
3882 #[test]
3883 fn test_eval_let_binding_uses_pipe_value() {
3884 let let_step = V2LetStep {
3886 bindings: vec![(
3887 "x".to_string(),
3888 V2Expr::Pipe(V2Pipe {
3889 start: V2Start::PipeValue,
3890 steps: vec![],
3891 }),
3892 )],
3893 };
3894 let record = json!({});
3895 let out = json!({});
3896 let ctx = V2EvalContext::new();
3897 let result = eval_v2_let_step(
3898 &let_step,
3899 EvalValue::Value(json!(100)),
3900 &record,
3901 None,
3902 &out,
3903 "test",
3904 &ctx,
3905 );
3906 assert!(result.is_ok());
3907 let new_ctx = result.unwrap();
3908 assert_eq!(
3909 new_ctx.resolve_local("x"),
3910 Some(&EvalValue::Value(json!(100)))
3911 );
3912 }
3913
3914 #[test]
3915 fn test_eval_let_binding_from_input() {
3916 let let_step = V2LetStep {
3917 bindings: vec![(
3918 "name".to_string(),
3919 V2Expr::Pipe(V2Pipe {
3920 start: V2Start::Ref(V2Ref::Input("user.name".to_string())),
3921 steps: vec![],
3922 }),
3923 )],
3924 };
3925 let record = json!({"user": {"name": "Alice"}});
3926 let out = json!({});
3927 let ctx = V2EvalContext::new();
3928 let result = eval_v2_let_step(
3929 &let_step,
3930 EvalValue::Value(json!("ignored")),
3931 &record,
3932 None,
3933 &out,
3934 "test",
3935 &ctx,
3936 );
3937 assert!(result.is_ok());
3938 let new_ctx = result.unwrap();
3939 assert_eq!(
3940 new_ctx.resolve_local("name"),
3941 Some(&EvalValue::Value(json!("Alice")))
3942 );
3943 }
3944
3945 #[test]
3946 fn test_eval_let_binding_chain() {
3947 let let_step = V2LetStep {
3949 bindings: vec![
3950 (
3951 "x".to_string(),
3952 V2Expr::Pipe(V2Pipe {
3953 start: V2Start::Literal(json!(10)),
3954 steps: vec![],
3955 }),
3956 ),
3957 (
3958 "y".to_string(),
3959 V2Expr::Pipe(V2Pipe {
3960 start: V2Start::Ref(V2Ref::Local("x".to_string())),
3961 steps: vec![],
3962 }),
3963 ),
3964 ],
3965 };
3966 let record = json!({});
3967 let out = json!({});
3968 let ctx = V2EvalContext::new();
3969 let result = eval_v2_let_step(
3970 &let_step,
3971 EvalValue::Value(json!("pipe")),
3972 &record,
3973 None,
3974 &out,
3975 "test",
3976 &ctx,
3977 );
3978 assert!(result.is_ok());
3979 let new_ctx = result.unwrap();
3980 assert_eq!(
3981 new_ctx.resolve_local("x"),
3982 Some(&EvalValue::Value(json!(10)))
3983 );
3984 assert_eq!(
3985 new_ctx.resolve_local("y"),
3986 Some(&EvalValue::Value(json!(10)))
3987 );
3988 }
3989
3990 #[test]
3991 fn test_eval_pipe_with_let() {
3992 let pipe = V2Pipe {
3994 start: V2Start::Literal(json!(100)),
3995 steps: vec![V2Step::Let(V2LetStep {
3996 bindings: vec![(
3997 "x".to_string(),
3998 V2Expr::Pipe(V2Pipe {
3999 start: V2Start::PipeValue,
4000 steps: vec![],
4001 }),
4002 )],
4003 })],
4004 };
4005 let record = json!({});
4006 let out = json!({});
4007 let ctx = V2EvalContext::new();
4008 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
4009 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
4011 }
4012
4013 #[test]
4014 fn test_eval_pipe_let_then_op() {
4015 let pipe = V2Pipe {
4017 start: V2Start::Literal(json!(100)),
4018 steps: vec![
4019 V2Step::Let(V2LetStep {
4020 bindings: vec![(
4021 "factor".to_string(),
4022 V2Expr::Pipe(V2Pipe {
4023 start: V2Start::Literal(json!(2)),
4024 steps: vec![],
4025 }),
4026 )],
4027 }),
4028 V2Step::Op(V2OpStep {
4029 op: "multiply".to_string(),
4030 args: vec![V2Expr::Pipe(V2Pipe {
4031 start: V2Start::Ref(V2Ref::Local("factor".to_string())),
4032 steps: vec![],
4033 })],
4034 }),
4035 ],
4036 };
4037 let record = json!({});
4038 let out = json!({});
4039 let ctx = V2EvalContext::new();
4040 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
4041 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(200.0)));
4042 }
4043}
4044
4045#[cfg(test)]
4050mod v2_if_step_eval_tests {
4051 use super::*;
4052 use serde_json::json;
4053
4054 #[test]
4057 fn test_eval_condition_eq_true() {
4058 let cond = V2Condition::Comparison(V2Comparison {
4059 op: V2ComparisonOp::Eq,
4060 args: vec![
4061 V2Expr::Pipe(V2Pipe {
4062 start: V2Start::Literal(json!(10)),
4063 steps: vec![],
4064 }),
4065 V2Expr::Pipe(V2Pipe {
4066 start: V2Start::Literal(json!(10)),
4067 steps: vec![],
4068 }),
4069 ],
4070 });
4071 let record = json!({});
4072 let out = json!({});
4073 let ctx = V2EvalContext::new();
4074 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4075 assert!(matches!(result, Ok(true)));
4076 }
4077
4078 #[test]
4079 fn test_eval_condition_eq_false() {
4080 let cond = V2Condition::Comparison(V2Comparison {
4081 op: V2ComparisonOp::Eq,
4082 args: vec![
4083 V2Expr::Pipe(V2Pipe {
4084 start: V2Start::Literal(json!(10)),
4085 steps: vec![],
4086 }),
4087 V2Expr::Pipe(V2Pipe {
4088 start: V2Start::Literal(json!(20)),
4089 steps: vec![],
4090 }),
4091 ],
4092 });
4093 let record = json!({});
4094 let out = json!({});
4095 let ctx = V2EvalContext::new();
4096 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4097 assert!(matches!(result, Ok(false)));
4098 }
4099
4100 #[test]
4101 fn test_eval_condition_eq_numeric_string_is_false() {
4102 let cond = V2Condition::Comparison(V2Comparison {
4103 op: V2ComparisonOp::Eq,
4104 args: vec![
4105 V2Expr::Pipe(V2Pipe {
4106 start: V2Start::Literal(json!("1")),
4107 steps: vec![],
4108 }),
4109 V2Expr::Pipe(V2Pipe {
4110 start: V2Start::Literal(json!(1)),
4111 steps: vec![],
4112 }),
4113 ],
4114 });
4115 let record = json!({});
4116 let out = json!({});
4117 let ctx = V2EvalContext::new();
4118 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4119 assert!(matches!(result, Ok(false)));
4120 }
4121
4122 #[test]
4123 fn test_eval_condition_eq_missing_as_null() {
4124 let cond = V2Condition::Comparison(V2Comparison {
4125 op: V2ComparisonOp::Eq,
4126 args: vec![
4127 V2Expr::Pipe(V2Pipe {
4128 start: V2Start::Ref(V2Ref::Input("optional".to_string())),
4129 steps: vec![],
4130 }),
4131 V2Expr::Pipe(V2Pipe {
4132 start: V2Start::Literal(json!(null)),
4133 steps: vec![],
4134 }),
4135 ],
4136 });
4137 let record = json!({});
4138 let out = json!({});
4139 let ctx = V2EvalContext::new();
4140 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4141 assert!(matches!(result, Ok(true)));
4142 }
4143
4144 #[test]
4145 fn test_eval_condition_ne() {
4146 let cond = V2Condition::Comparison(V2Comparison {
4147 op: V2ComparisonOp::Ne,
4148 args: vec![
4149 V2Expr::Pipe(V2Pipe {
4150 start: V2Start::Literal(json!("a")),
4151 steps: vec![],
4152 }),
4153 V2Expr::Pipe(V2Pipe {
4154 start: V2Start::Literal(json!("b")),
4155 steps: vec![],
4156 }),
4157 ],
4158 });
4159 let record = json!({});
4160 let out = json!({});
4161 let ctx = V2EvalContext::new();
4162 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4163 assert!(matches!(result, Ok(true)));
4164 }
4165
4166 #[test]
4167 fn test_eval_condition_gt() {
4168 let cond = V2Condition::Comparison(V2Comparison {
4169 op: V2ComparisonOp::Gt,
4170 args: vec![
4171 V2Expr::Pipe(V2Pipe {
4172 start: V2Start::Literal(json!(20)),
4173 steps: vec![],
4174 }),
4175 V2Expr::Pipe(V2Pipe {
4176 start: V2Start::Literal(json!(10)),
4177 steps: vec![],
4178 }),
4179 ],
4180 });
4181 let record = json!({});
4182 let out = json!({});
4183 let ctx = V2EvalContext::new();
4184 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4185 assert!(matches!(result, Ok(true)));
4186 }
4187
4188 #[test]
4189 fn test_eval_condition_gt_non_numeric_string_compares_lexicographically() {
4190 let cond = V2Condition::Comparison(V2Comparison {
4191 op: V2ComparisonOp::Gt,
4192 args: vec![
4193 V2Expr::Pipe(V2Pipe {
4194 start: V2Start::Literal(json!("B")),
4195 steps: vec![],
4196 }),
4197 V2Expr::Pipe(V2Pipe {
4198 start: V2Start::Literal(json!("A")),
4199 steps: vec![],
4200 }),
4201 ],
4202 });
4203 let record = json!({});
4204 let out = json!({});
4205 let ctx = V2EvalContext::new();
4206 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4207 assert!(matches!(result, Ok(true)));
4208 }
4209
4210 #[test]
4211 fn test_eval_condition_lt() {
4212 let cond = V2Condition::Comparison(V2Comparison {
4213 op: V2ComparisonOp::Lt,
4214 args: vec![
4215 V2Expr::Pipe(V2Pipe {
4216 start: V2Start::Literal(json!(5)),
4217 steps: vec![],
4218 }),
4219 V2Expr::Pipe(V2Pipe {
4220 start: V2Start::Literal(json!(10)),
4221 steps: vec![],
4222 }),
4223 ],
4224 });
4225 let record = json!({});
4226 let out = json!({});
4227 let ctx = V2EvalContext::new();
4228 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4229 assert!(matches!(result, Ok(true)));
4230 }
4231
4232 #[test]
4233 fn test_eval_condition_gte_equal() {
4234 let cond = V2Condition::Comparison(V2Comparison {
4235 op: V2ComparisonOp::Gte,
4236 args: vec![
4237 V2Expr::Pipe(V2Pipe {
4238 start: V2Start::Literal(json!(10)),
4239 steps: vec![],
4240 }),
4241 V2Expr::Pipe(V2Pipe {
4242 start: V2Start::Literal(json!(10)),
4243 steps: vec![],
4244 }),
4245 ],
4246 });
4247 let record = json!({});
4248 let out = json!({});
4249 let ctx = V2EvalContext::new();
4250 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4251 assert!(matches!(result, Ok(true)));
4252 }
4253
4254 #[test]
4255 fn test_eval_condition_lte_less() {
4256 let cond = V2Condition::Comparison(V2Comparison {
4257 op: V2ComparisonOp::Lte,
4258 args: vec![
4259 V2Expr::Pipe(V2Pipe {
4260 start: V2Start::Literal(json!(5)),
4261 steps: vec![],
4262 }),
4263 V2Expr::Pipe(V2Pipe {
4264 start: V2Start::Literal(json!(10)),
4265 steps: vec![],
4266 }),
4267 ],
4268 });
4269 let record = json!({});
4270 let out = json!({});
4271 let ctx = V2EvalContext::new();
4272 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4273 assert!(matches!(result, Ok(true)));
4274 }
4275
4276 #[test]
4277 fn test_eval_condition_match() {
4278 let cond = V2Condition::Comparison(V2Comparison {
4279 op: V2ComparisonOp::Match,
4280 args: vec![
4281 V2Expr::Pipe(V2Pipe {
4282 start: V2Start::Literal(json!("hello123")),
4283 steps: vec![],
4284 }),
4285 V2Expr::Pipe(V2Pipe {
4286 start: V2Start::Literal(json!("^hello\\d+")),
4287 steps: vec![],
4288 }),
4289 ],
4290 });
4291 let record = json!({});
4292 let out = json!({});
4293 let ctx = V2EvalContext::new();
4294 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4295 assert!(matches!(result, Ok(true)));
4296 }
4297
4298 #[test]
4299 fn test_eval_condition_all_true() {
4300 let cond = V2Condition::All(vec![
4301 V2Condition::Comparison(V2Comparison {
4302 op: V2ComparisonOp::Gt,
4303 args: vec![
4304 V2Expr::Pipe(V2Pipe {
4305 start: V2Start::Literal(json!(10)),
4306 steps: vec![],
4307 }),
4308 V2Expr::Pipe(V2Pipe {
4309 start: V2Start::Literal(json!(5)),
4310 steps: vec![],
4311 }),
4312 ],
4313 }),
4314 V2Condition::Comparison(V2Comparison {
4315 op: V2ComparisonOp::Lt,
4316 args: vec![
4317 V2Expr::Pipe(V2Pipe {
4318 start: V2Start::Literal(json!(10)),
4319 steps: vec![],
4320 }),
4321 V2Expr::Pipe(V2Pipe {
4322 start: V2Start::Literal(json!(20)),
4323 steps: vec![],
4324 }),
4325 ],
4326 }),
4327 ]);
4328 let record = json!({});
4329 let out = json!({});
4330 let ctx = V2EvalContext::new();
4331 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4332 assert!(matches!(result, Ok(true)));
4333 }
4334
4335 #[test]
4336 fn test_eval_condition_all_false() {
4337 let cond = V2Condition::All(vec![
4338 V2Condition::Comparison(V2Comparison {
4339 op: V2ComparisonOp::Gt,
4340 args: vec![
4341 V2Expr::Pipe(V2Pipe {
4342 start: V2Start::Literal(json!(10)),
4343 steps: vec![],
4344 }),
4345 V2Expr::Pipe(V2Pipe {
4346 start: V2Start::Literal(json!(5)),
4347 steps: vec![],
4348 }),
4349 ],
4350 }),
4351 V2Condition::Comparison(V2Comparison {
4352 op: V2ComparisonOp::Lt, args: vec![
4354 V2Expr::Pipe(V2Pipe {
4355 start: V2Start::Literal(json!(10)),
4356 steps: vec![],
4357 }),
4358 V2Expr::Pipe(V2Pipe {
4359 start: V2Start::Literal(json!(5)),
4360 steps: vec![],
4361 }),
4362 ],
4363 }),
4364 ]);
4365 let record = json!({});
4366 let out = json!({});
4367 let ctx = V2EvalContext::new();
4368 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4369 assert!(matches!(result, Ok(false)));
4370 }
4371
4372 #[test]
4373 fn test_eval_condition_any_true() {
4374 let cond = V2Condition::Any(vec![
4375 V2Condition::Comparison(V2Comparison {
4376 op: V2ComparisonOp::Eq,
4377 args: vec![
4378 V2Expr::Pipe(V2Pipe {
4379 start: V2Start::Literal(json!("admin")),
4380 steps: vec![],
4381 }),
4382 V2Expr::Pipe(V2Pipe {
4383 start: V2Start::Literal(json!("user")),
4384 steps: vec![],
4385 }),
4386 ],
4387 }),
4388 V2Condition::Comparison(V2Comparison {
4389 op: V2ComparisonOp::Gt,
4390 args: vec![
4391 V2Expr::Pipe(V2Pipe {
4392 start: V2Start::Literal(json!(100)),
4393 steps: vec![],
4394 }),
4395 V2Expr::Pipe(V2Pipe {
4396 start: V2Start::Literal(json!(50)),
4397 steps: vec![],
4398 }),
4399 ],
4400 }),
4401 ]);
4402 let record = json!({});
4403 let out = json!({});
4404 let ctx = V2EvalContext::new();
4405 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4406 assert!(matches!(result, Ok(true)));
4407 }
4408
4409 #[test]
4410 fn test_eval_condition_any_false() {
4411 let cond = V2Condition::Any(vec![
4412 V2Condition::Comparison(V2Comparison {
4413 op: V2ComparisonOp::Eq,
4414 args: vec![
4415 V2Expr::Pipe(V2Pipe {
4416 start: V2Start::Literal(json!(1)),
4417 steps: vec![],
4418 }),
4419 V2Expr::Pipe(V2Pipe {
4420 start: V2Start::Literal(json!(2)),
4421 steps: vec![],
4422 }),
4423 ],
4424 }),
4425 V2Condition::Comparison(V2Comparison {
4426 op: V2ComparisonOp::Eq,
4427 args: vec![
4428 V2Expr::Pipe(V2Pipe {
4429 start: V2Start::Literal(json!(3)),
4430 steps: vec![],
4431 }),
4432 V2Expr::Pipe(V2Pipe {
4433 start: V2Start::Literal(json!(4)),
4434 steps: vec![],
4435 }),
4436 ],
4437 }),
4438 ]);
4439 let record = json!({});
4440 let out = json!({});
4441 let ctx = V2EvalContext::new();
4442 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4443 assert!(matches!(result, Ok(false)));
4444 }
4445
4446 #[test]
4447 fn test_eval_condition_expr_truthy() {
4448 let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4449 start: V2Start::Literal(json!(true)),
4450 steps: vec![],
4451 }));
4452 let record = json!({});
4453 let out = json!({});
4454 let ctx = V2EvalContext::new();
4455 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4456 assert!(matches!(result, Ok(true)));
4457 }
4458
4459 #[test]
4460 fn test_eval_condition_expr_falsy() {
4461 let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4462 start: V2Start::Literal(json!(false)),
4463 steps: vec![],
4464 }));
4465 let record = json!({});
4466 let out = json!({});
4467 let ctx = V2EvalContext::new();
4468 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4469 assert!(matches!(result, Ok(false)));
4470 }
4471
4472 #[test]
4473 fn test_eval_condition_expr_non_bool_errors() {
4474 let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4475 start: V2Start::Literal(json!("active")),
4476 steps: vec![],
4477 }));
4478 let record = json!({});
4479 let out = json!({});
4480 let ctx = V2EvalContext::new();
4481 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4482 assert!(matches!(result, Err(err)
4483 if err.kind == TransformErrorKind::ExprError
4484 && err.message == "when/record_when must evaluate to boolean"
4485 && err.path.as_deref() == Some("test.expr")
4486 ));
4487 }
4488
4489 #[test]
4490 fn test_eval_condition_expr_missing_is_false() {
4491 let cond = V2Condition::Expr(V2Expr::Pipe(V2Pipe {
4492 start: V2Start::Ref(V2Ref::Input("active".to_string())),
4493 steps: vec![],
4494 }));
4495 let record = json!({});
4496 let out = json!({});
4497 let ctx = V2EvalContext::new();
4498 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4499 assert!(matches!(result, Ok(false)));
4500 }
4501
4502 #[test]
4503 fn test_eval_condition_with_pipe_value() {
4504 let cond = V2Condition::Comparison(V2Comparison {
4506 op: V2ComparisonOp::Gt,
4507 args: vec![
4508 V2Expr::Pipe(V2Pipe {
4509 start: V2Start::PipeValue,
4510 steps: vec![],
4511 }),
4512 V2Expr::Pipe(V2Pipe {
4513 start: V2Start::Literal(json!(100)),
4514 steps: vec![],
4515 }),
4516 ],
4517 });
4518 let record = json!({});
4519 let out = json!({});
4520 let ctx = V2EvalContext::new().with_pipe_value(EvalValue::Value(json!(150)));
4521 let result = eval_v2_condition(&cond, &record, None, &out, "test", &ctx);
4522 assert!(matches!(result, Ok(true)));
4523 }
4524
4525 #[test]
4528 fn test_eval_if_step_then_branch() {
4529 let if_step = V2IfStep {
4531 cond: V2Condition::Comparison(V2Comparison {
4532 op: V2ComparisonOp::Gt,
4533 args: vec![
4534 V2Expr::Pipe(V2Pipe {
4535 start: V2Start::PipeValue,
4536 steps: vec![],
4537 }),
4538 V2Expr::Pipe(V2Pipe {
4539 start: V2Start::Literal(json!(10)),
4540 steps: vec![],
4541 }),
4542 ],
4543 }),
4544 then_branch: V2Pipe {
4545 start: V2Start::PipeValue,
4546 steps: vec![V2Step::Op(V2OpStep {
4547 op: "multiply".to_string(),
4548 args: vec![V2Expr::Pipe(V2Pipe {
4549 start: V2Start::Literal(json!(2)),
4550 steps: vec![],
4551 })],
4552 })],
4553 },
4554 else_branch: None,
4555 };
4556 let record = json!({});
4557 let out = json!({});
4558 let ctx = V2EvalContext::new();
4559 let result = eval_v2_if_step(
4560 &if_step,
4561 EvalValue::Value(json!(20)),
4562 &record,
4563 None,
4564 &out,
4565 "test",
4566 &ctx,
4567 );
4568 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(40.0)));
4569 }
4570
4571 #[test]
4572 fn test_eval_if_step_else_branch() {
4573 let if_step = V2IfStep {
4575 cond: V2Condition::Comparison(V2Comparison {
4576 op: V2ComparisonOp::Gt,
4577 args: vec![
4578 V2Expr::Pipe(V2Pipe {
4579 start: V2Start::PipeValue,
4580 steps: vec![],
4581 }),
4582 V2Expr::Pipe(V2Pipe {
4583 start: V2Start::Literal(json!(10)),
4584 steps: vec![],
4585 }),
4586 ],
4587 }),
4588 then_branch: V2Pipe {
4589 start: V2Start::PipeValue,
4590 steps: vec![V2Step::Op(V2OpStep {
4591 op: "multiply".to_string(),
4592 args: vec![V2Expr::Pipe(V2Pipe {
4593 start: V2Start::Literal(json!(2)),
4594 steps: vec![],
4595 })],
4596 })],
4597 },
4598 else_branch: Some(V2Pipe {
4599 start: V2Start::PipeValue,
4600 steps: vec![V2Step::Op(V2OpStep {
4601 op: "multiply".to_string(),
4602 args: vec![V2Expr::Pipe(V2Pipe {
4603 start: V2Start::Literal(json!(0.5)),
4604 steps: vec![],
4605 })],
4606 })],
4607 }),
4608 };
4609 let record = json!({});
4610 let out = json!({});
4611 let ctx = V2EvalContext::new();
4612 let result = eval_v2_if_step(
4614 &if_step,
4615 EvalValue::Value(json!(5)),
4616 &record,
4617 None,
4618 &out,
4619 "test",
4620 &ctx,
4621 );
4622 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(2.5)));
4623 }
4624
4625 #[test]
4626 fn test_eval_if_step_no_else_returns_pipe_value() {
4627 let if_step = V2IfStep {
4629 cond: V2Condition::Comparison(V2Comparison {
4630 op: V2ComparisonOp::Gt,
4631 args: vec![
4632 V2Expr::Pipe(V2Pipe {
4633 start: V2Start::PipeValue,
4634 steps: vec![],
4635 }),
4636 V2Expr::Pipe(V2Pipe {
4637 start: V2Start::Literal(json!(10)),
4638 steps: vec![],
4639 }),
4640 ],
4641 }),
4642 then_branch: V2Pipe {
4643 start: V2Start::PipeValue,
4644 steps: vec![V2Step::Op(V2OpStep {
4645 op: "multiply".to_string(),
4646 args: vec![V2Expr::Pipe(V2Pipe {
4647 start: V2Start::Literal(json!(2)),
4648 steps: vec![],
4649 })],
4650 })],
4651 },
4652 else_branch: None,
4653 };
4654 let record = json!({});
4655 let out = json!({});
4656 let ctx = V2EvalContext::new();
4657 let result = eval_v2_if_step(
4659 &if_step,
4660 EvalValue::Value(json!(5)),
4661 &record,
4662 None,
4663 &out,
4664 "test",
4665 &ctx,
4666 );
4667 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(5)));
4668 }
4669
4670 #[test]
4671 fn test_eval_pipe_with_if_step() {
4672 let pipe = V2Pipe {
4674 start: V2Start::Literal(json!(10000)),
4675 steps: vec![V2Step::If(V2IfStep {
4676 cond: V2Condition::Comparison(V2Comparison {
4677 op: V2ComparisonOp::Gt,
4678 args: vec![
4679 V2Expr::Pipe(V2Pipe {
4680 start: V2Start::PipeValue,
4681 steps: vec![],
4682 }),
4683 V2Expr::Pipe(V2Pipe {
4684 start: V2Start::Literal(json!(5000)),
4685 steps: vec![],
4686 }),
4687 ],
4688 }),
4689 then_branch: V2Pipe {
4690 start: V2Start::PipeValue,
4691 steps: vec![V2Step::Op(V2OpStep {
4692 op: "multiply".to_string(),
4693 args: vec![V2Expr::Pipe(V2Pipe {
4694 start: V2Start::Literal(json!(0.9)),
4695 steps: vec![],
4696 })],
4697 })],
4698 },
4699 else_branch: None,
4700 })],
4701 };
4702 let record = json!({});
4703 let out = json!({});
4704 let ctx = V2EvalContext::new();
4705 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
4706 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(9000.0)));
4707 }
4708
4709 #[test]
4710 fn test_eval_if_with_input_condition() {
4711 let if_step = V2IfStep {
4713 cond: V2Condition::Comparison(V2Comparison {
4714 op: V2ComparisonOp::Eq,
4715 args: vec![
4716 V2Expr::Pipe(V2Pipe {
4717 start: V2Start::Ref(V2Ref::Input("role".to_string())),
4718 steps: vec![],
4719 }),
4720 V2Expr::Pipe(V2Pipe {
4721 start: V2Start::Literal(json!("admin")),
4722 steps: vec![],
4723 }),
4724 ],
4725 }),
4726 then_branch: V2Pipe {
4727 start: V2Start::Literal(json!(100)),
4728 steps: vec![],
4729 },
4730 else_branch: Some(V2Pipe {
4731 start: V2Start::Literal(json!(50)),
4732 steps: vec![],
4733 }),
4734 };
4735 let record = json!({"role": "admin"});
4736 let out = json!({});
4737 let ctx = V2EvalContext::new();
4738 let result = eval_v2_if_step(
4739 &if_step,
4740 EvalValue::Value(json!(0)),
4741 &record,
4742 None,
4743 &out,
4744 "test",
4745 &ctx,
4746 );
4747 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100)));
4748
4749 let record2 = json!({"role": "user"});
4751 let result2 = eval_v2_if_step(
4752 &if_step,
4753 EvalValue::Value(json!(0)),
4754 &record2,
4755 None,
4756 &out,
4757 "test",
4758 &ctx,
4759 );
4760 assert!(matches!(result2, Ok(EvalValue::Value(v)) if v == json!(50)));
4761 }
4762
4763 #[test]
4764 fn test_eval_nested_if() {
4765 let if_step = V2IfStep {
4767 cond: V2Condition::Comparison(V2Comparison {
4768 op: V2ComparisonOp::Gt,
4769 args: vec![
4770 V2Expr::Pipe(V2Pipe {
4771 start: V2Start::PipeValue,
4772 steps: vec![],
4773 }),
4774 V2Expr::Pipe(V2Pipe {
4775 start: V2Start::Literal(json!(100)),
4776 steps: vec![],
4777 }),
4778 ],
4779 }),
4780 then_branch: V2Pipe {
4781 start: V2Start::PipeValue,
4782 steps: vec![V2Step::If(V2IfStep {
4783 cond: V2Condition::Comparison(V2Comparison {
4784 op: V2ComparisonOp::Gt,
4785 args: vec![
4786 V2Expr::Pipe(V2Pipe {
4787 start: V2Start::PipeValue,
4788 steps: vec![],
4789 }),
4790 V2Expr::Pipe(V2Pipe {
4791 start: V2Start::Literal(json!(500)),
4792 steps: vec![],
4793 }),
4794 ],
4795 }),
4796 then_branch: V2Pipe {
4797 start: V2Start::Literal(json!("gold")),
4798 steps: vec![],
4799 },
4800 else_branch: Some(V2Pipe {
4801 start: V2Start::Literal(json!("silver")),
4802 steps: vec![],
4803 }),
4804 })],
4805 },
4806 else_branch: Some(V2Pipe {
4807 start: V2Start::Literal(json!("bronze")),
4808 steps: vec![],
4809 }),
4810 };
4811 let record = json!({});
4812 let out = json!({});
4813 let ctx = V2EvalContext::new();
4814
4815 let result = eval_v2_if_step(
4817 &if_step,
4818 EvalValue::Value(json!(50)),
4819 &record,
4820 None,
4821 &out,
4822 "test",
4823 &ctx,
4824 );
4825 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("bronze")));
4826
4827 let result = eval_v2_if_step(
4829 &if_step,
4830 EvalValue::Value(json!(200)),
4831 &record,
4832 None,
4833 &out,
4834 "test",
4835 &ctx,
4836 );
4837 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("silver")));
4838
4839 let result = eval_v2_if_step(
4841 &if_step,
4842 EvalValue::Value(json!(600)),
4843 &record,
4844 None,
4845 &out,
4846 "test",
4847 &ctx,
4848 );
4849 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("gold")));
4850 }
4851}
4852
4853#[cfg(test)]
4858mod v2_map_step_eval_tests {
4859 use super::*;
4860 use serde_json::json;
4861
4862 #[test]
4863 fn test_eval_map_step_simple() {
4864 let map_step = V2MapStep {
4866 steps: vec![V2Step::Op(V2OpStep {
4867 op: "uppercase".to_string(),
4868 args: vec![],
4869 })],
4870 };
4871 let record = json!({});
4872 let out = json!({});
4873 let ctx = V2EvalContext::new();
4874 let result = eval_v2_map_step(
4875 &map_step,
4876 EvalValue::Value(json!(["a", "b", "c"])),
4877 &record,
4878 None,
4879 &out,
4880 "test",
4881 &ctx,
4882 );
4883 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["A", "B", "C"])));
4884 }
4885
4886 #[test]
4887 fn test_eval_map_step_with_multiply() {
4888 let map_step = V2MapStep {
4890 steps: vec![V2Step::Op(V2OpStep {
4891 op: "multiply".to_string(),
4892 args: vec![V2Expr::Pipe(V2Pipe {
4893 start: V2Start::Literal(json!(2)),
4894 steps: vec![],
4895 })],
4896 })],
4897 };
4898 let record = json!({});
4899 let out = json!({});
4900 let ctx = V2EvalContext::new();
4901 let result = eval_v2_map_step(
4902 &map_step,
4903 EvalValue::Value(json!([1, 2, 3])),
4904 &record,
4905 None,
4906 &out,
4907 "test",
4908 &ctx,
4909 );
4910 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([2.0, 4.0, 6.0])));
4911 }
4912
4913 #[test]
4914 fn test_eval_map_step_empty_array() {
4915 let map_step = V2MapStep {
4916 steps: vec![V2Step::Op(V2OpStep {
4917 op: "uppercase".to_string(),
4918 args: vec![],
4919 })],
4920 };
4921 let record = json!({});
4922 let out = json!({});
4923 let ctx = V2EvalContext::new();
4924 let result = eval_v2_map_step(
4925 &map_step,
4926 EvalValue::Value(json!([])),
4927 &record,
4928 None,
4929 &out,
4930 "test",
4931 &ctx,
4932 );
4933 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([])));
4934 }
4935
4936 #[test]
4937 fn test_eval_map_step_missing_returns_missing() {
4938 let map_step = V2MapStep {
4939 steps: vec![V2Step::Op(V2OpStep {
4940 op: "uppercase".to_string(),
4941 args: vec![],
4942 })],
4943 };
4944 let record = json!({});
4945 let out = json!({});
4946 let ctx = V2EvalContext::new();
4947 let result = eval_v2_map_step(
4948 &map_step,
4949 EvalValue::Missing,
4950 &record,
4951 None,
4952 &out,
4953 "test",
4954 &ctx,
4955 );
4956 assert!(matches!(result, Ok(EvalValue::Missing)));
4957 }
4958
4959 #[test]
4960 fn test_eval_map_step_non_array_error() {
4961 let map_step = V2MapStep {
4962 steps: vec![V2Step::Op(V2OpStep {
4963 op: "uppercase".to_string(),
4964 args: vec![],
4965 })],
4966 };
4967 let record = json!({});
4968 let out = json!({});
4969 let ctx = V2EvalContext::new();
4970 let result = eval_v2_map_step(
4971 &map_step,
4972 EvalValue::Value(json!("not an array")),
4973 &record,
4974 None,
4975 &out,
4976 "test",
4977 &ctx,
4978 );
4979 assert!(result.is_err());
4980 }
4981
4982 #[test]
4983 fn test_eval_map_step_with_item_ref() {
4984 let map_step = V2MapStep {
4986 steps: vec![V2Step::Op(V2OpStep {
4987 op: "concat".to_string(),
4988 args: vec![V2Expr::Pipe(V2Pipe {
4989 start: V2Start::Literal(json!("!")),
4990 steps: vec![],
4991 })],
4992 })],
4993 };
4994 let record = json!({});
4995 let out = json!({});
4996 let ctx = V2EvalContext::new();
4997 let result = eval_v2_map_step(
4998 &map_step,
4999 EvalValue::Value(json!(["hello", "world"])),
5000 &record,
5001 None,
5002 &out,
5003 "test",
5004 &ctx,
5005 );
5006 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["hello!", "world!"])));
5007 }
5008
5009 #[test]
5010 fn test_eval_map_step_with_item_index() {
5011 let pipe = V2Pipe {
5014 start: V2Start::Ref(V2Ref::Input("items".to_string())),
5015 steps: vec![V2Step::Map(V2MapStep {
5016 steps: vec![], })],
5018 };
5019 let record = json!({"items": [10, 20, 30]});
5020 let out = json!({});
5021 let ctx = V2EvalContext::new();
5022 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5023 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([10, 20, 30])));
5024 }
5025
5026 #[test]
5027 fn test_eval_map_step_multiple_ops() {
5028 let map_step = V2MapStep {
5030 steps: vec![
5031 V2Step::Op(V2OpStep {
5032 op: "trim".to_string(),
5033 args: vec![],
5034 }),
5035 V2Step::Op(V2OpStep {
5036 op: "uppercase".to_string(),
5037 args: vec![],
5038 }),
5039 ],
5040 };
5041 let record = json!({});
5042 let out = json!({});
5043 let ctx = V2EvalContext::new();
5044 let result = eval_v2_map_step(
5045 &map_step,
5046 EvalValue::Value(json!([" a ", " b "])),
5047 &record,
5048 None,
5049 &out,
5050 "test",
5051 &ctx,
5052 );
5053 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["A", "B"])));
5054 }
5055
5056 #[test]
5057 fn test_eval_pipe_with_map_step() {
5058 let pipe = V2Pipe {
5060 start: V2Start::Ref(V2Ref::Input("names".to_string())),
5061 steps: vec![V2Step::Map(V2MapStep {
5062 steps: vec![V2Step::Op(V2OpStep {
5063 op: "uppercase".to_string(),
5064 args: vec![],
5065 })],
5066 })],
5067 };
5068 let record = json!({"names": ["alice", "bob"]});
5069 let out = json!({});
5070 let ctx = V2EvalContext::new();
5071 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5072 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["ALICE", "BOB"])));
5073 }
5074
5075 #[test]
5076 fn test_eval_map_with_if_step() {
5077 let map_step = V2MapStep {
5079 steps: vec![V2Step::If(V2IfStep {
5080 cond: V2Condition::Comparison(V2Comparison {
5081 op: V2ComparisonOp::Gt,
5082 args: vec![
5083 V2Expr::Pipe(V2Pipe {
5084 start: V2Start::PipeValue,
5085 steps: vec![],
5086 }),
5087 V2Expr::Pipe(V2Pipe {
5088 start: V2Start::Literal(json!(5)),
5089 steps: vec![],
5090 }),
5091 ],
5092 }),
5093 then_branch: V2Pipe {
5094 start: V2Start::PipeValue,
5095 steps: vec![V2Step::Op(V2OpStep {
5096 op: "multiply".to_string(),
5097 args: vec![V2Expr::Pipe(V2Pipe {
5098 start: V2Start::Literal(json!(2)),
5099 steps: vec![],
5100 })],
5101 })],
5102 },
5103 else_branch: None,
5104 })],
5105 };
5106 let record = json!({});
5107 let out = json!({});
5108 let ctx = V2EvalContext::new();
5109 let result = eval_v2_map_step(
5111 &map_step,
5112 EvalValue::Value(json!([3, 7, 2, 10])),
5113 &record,
5114 None,
5115 &out,
5116 "test",
5117 &ctx,
5118 );
5119 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([3, 14.0, 2, 20.0])));
5120 }
5121
5122 #[test]
5123 fn test_eval_nested_map() {
5124 let map_step = V2MapStep {
5126 steps: vec![V2Step::Map(V2MapStep {
5127 steps: vec![V2Step::Op(V2OpStep {
5128 op: "multiply".to_string(),
5129 args: vec![V2Expr::Pipe(V2Pipe {
5130 start: V2Start::Literal(json!(2)),
5131 steps: vec![],
5132 })],
5133 })],
5134 })],
5135 };
5136 let record = json!({});
5137 let out = json!({});
5138 let ctx = V2EvalContext::new();
5139 let result = eval_v2_map_step(
5140 &map_step,
5141 EvalValue::Value(json!([[1, 2], [3, 4]])),
5142 &record,
5143 None,
5144 &out,
5145 "test",
5146 &ctx,
5147 );
5148 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([[2.0, 4.0], [6.0, 8.0]])));
5149 }
5150
5151 #[test]
5152 fn test_eval_map_objects() {
5153 let pipe = V2Pipe {
5156 start: V2Start::Ref(V2Ref::Input("users".to_string())),
5157 steps: vec![V2Step::Map(V2MapStep {
5158 steps: vec![], })],
5160 };
5161 let record = json!({"users": [{"name": "Alice"}, {"name": "Bob"}]});
5162 let out = json!({});
5163 let ctx = V2EvalContext::new();
5164 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5165 assert!(
5166 matches!(result, Ok(EvalValue::Value(v)) if v == json!([{"name": "Alice"}, {"name": "Bob"}]))
5167 );
5168 }
5169}
5170
5171#[cfg(test)]
5176mod v2_pipe_eval_tests {
5177 use super::*;
5178 use serde_json::json;
5179
5180 #[test]
5181 fn test_eval_pipe_simple_ref() {
5182 let pipe = V2Pipe {
5183 start: V2Start::Ref(V2Ref::Input("name".to_string())),
5184 steps: vec![],
5185 };
5186 let record = json!({"name": "Alice"});
5187 let out = json!({});
5188 let ctx = V2EvalContext::new();
5189 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5190 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Alice")));
5191 }
5192
5193 #[test]
5194 fn test_eval_pipe_literal_start() {
5195 let pipe = V2Pipe {
5196 start: V2Start::Literal(json!(42)),
5197 steps: vec![],
5198 };
5199 let record = json!({});
5200 let out = json!({});
5201 let ctx = V2EvalContext::new();
5202 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5203 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(42)));
5204 }
5205
5206 #[test]
5207 fn test_eval_pipe_chain_ops() {
5208 let pipe = V2Pipe {
5210 start: V2Start::Literal(json!(" hello ")),
5211 steps: vec![
5212 V2Step::Op(V2OpStep {
5213 op: "trim".to_string(),
5214 args: vec![],
5215 }),
5216 V2Step::Op(V2OpStep {
5217 op: "uppercase".to_string(),
5218 args: vec![],
5219 }),
5220 ],
5221 };
5222 let record = json!({});
5223 let out = json!({});
5224 let ctx = V2EvalContext::new();
5225 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5226 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HELLO")));
5227 }
5228
5229 #[test]
5230 fn test_eval_pipe_with_context() {
5231 let pipe = V2Pipe {
5233 start: V2Start::Ref(V2Ref::Context("multiplier".to_string())),
5234 steps: vec![V2Step::Op(V2OpStep {
5235 op: "multiply".to_string(),
5236 args: vec![V2Expr::Pipe(V2Pipe {
5237 start: V2Start::Ref(V2Ref::Input("value".to_string())),
5238 steps: vec![],
5239 })],
5240 })],
5241 };
5242 let record = json!({"value": 10});
5243 let context = json!({"multiplier": 5});
5244 let out = json!({});
5245 let ctx = V2EvalContext::new();
5246 let result = eval_v2_pipe(&pipe, &record, Some(&context), &out, "test", &ctx);
5247 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(50.0)));
5248 }
5249
5250 #[test]
5251 fn test_eval_pipe_with_out_ref() {
5252 let pipe = V2Pipe {
5254 start: V2Start::Ref(V2Ref::Out("previous".to_string())),
5255 steps: vec![V2Step::Op(V2OpStep {
5256 op: "add".to_string(),
5257 args: vec![V2Expr::Pipe(V2Pipe {
5258 start: V2Start::Literal(json!(1)),
5259 steps: vec![],
5260 })],
5261 })],
5262 };
5263 let record = json!({});
5264 let out = json!({"previous": 99});
5265 let ctx = V2EvalContext::new();
5266 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5267 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(100.0)));
5268 }
5269
5270 #[test]
5271 fn test_eval_pipe_complex_chain() {
5272 let pipe = V2Pipe {
5275 start: V2Start::Ref(V2Ref::Input("price".to_string())),
5276 steps: vec![
5277 V2Step::Let(V2LetStep {
5278 bindings: vec![(
5279 "original".to_string(),
5280 V2Expr::Pipe(V2Pipe {
5281 start: V2Start::PipeValue,
5282 steps: vec![],
5283 }),
5284 )],
5285 }),
5286 V2Step::Op(V2OpStep {
5287 op: "multiply".to_string(),
5288 args: vec![V2Expr::Pipe(V2Pipe {
5289 start: V2Start::Literal(json!(0.9)),
5290 steps: vec![],
5291 })],
5292 }),
5293 V2Step::If(V2IfStep {
5294 cond: V2Condition::Comparison(V2Comparison {
5295 op: V2ComparisonOp::Gt,
5296 args: vec![
5297 V2Expr::Pipe(V2Pipe {
5298 start: V2Start::PipeValue,
5299 steps: vec![],
5300 }),
5301 V2Expr::Pipe(V2Pipe {
5302 start: V2Start::Literal(json!(1000)),
5303 steps: vec![],
5304 }),
5305 ],
5306 }),
5307 then_branch: V2Pipe {
5308 start: V2Start::PipeValue,
5309 steps: vec![V2Step::Op(V2OpStep {
5310 op: "subtract".to_string(),
5311 args: vec![V2Expr::Pipe(V2Pipe {
5312 start: V2Start::Literal(json!(100)),
5313 steps: vec![],
5314 })],
5315 })],
5316 },
5317 else_branch: None,
5318 }),
5319 ],
5320 };
5321 let record = json!({"price": 2000});
5322 let out = json!({});
5323 let ctx = V2EvalContext::new();
5324 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5325 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(1700.0)));
5327 }
5328
5329 #[test]
5330 fn test_eval_pipe_all_step_types() {
5331 let pipe = V2Pipe {
5333 start: V2Start::Ref(V2Ref::Input("items".to_string())),
5334 steps: vec![
5335 V2Step::Map(V2MapStep {
5337 steps: vec![V2Step::Op(V2OpStep {
5338 op: "multiply".to_string(),
5339 args: vec![V2Expr::Pipe(V2Pipe {
5340 start: V2Start::Literal(json!(2)),
5341 steps: vec![],
5342 })],
5343 })],
5344 }),
5345 ],
5346 };
5347 let record = json!({"items": [1, 2, 3]});
5348 let out = json!({});
5349 let ctx = V2EvalContext::new();
5350 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5351 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([2.0, 4.0, 6.0])));
5352 }
5353
5354 #[test]
5355 fn test_eval_pipe_coalesce_chain() {
5356 let pipe = V2Pipe {
5358 start: V2Start::Ref(V2Ref::Input("primary".to_string())),
5359 steps: vec![
5360 V2Step::Op(V2OpStep {
5361 op: "coalesce".to_string(),
5362 args: vec![V2Expr::Pipe(V2Pipe {
5363 start: V2Start::Ref(V2Ref::Input("secondary".to_string())),
5364 steps: vec![],
5365 })],
5366 }),
5367 V2Step::Op(V2OpStep {
5368 op: "coalesce".to_string(),
5369 args: vec![V2Expr::Pipe(V2Pipe {
5370 start: V2Start::Literal(json!("default")),
5371 steps: vec![],
5372 })],
5373 }),
5374 ],
5375 };
5376
5377 let record = json!({"primary": "first"});
5379 let out = json!({});
5380 let ctx = V2EvalContext::new();
5381 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5382 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("first")));
5383
5384 let record = json!({"primary": null, "secondary": "second"});
5386 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5387 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("second")));
5388
5389 let record = json!({"primary": null, "secondary": null});
5391 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5392 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("default")));
5393 }
5394
5395 #[test]
5396 fn test_eval_expr_with_v2_pipe() {
5397 let expr = V2Expr::Pipe(V2Pipe {
5398 start: V2Start::Literal(json!("hello")),
5399 steps: vec![V2Step::Op(V2OpStep {
5400 op: "uppercase".to_string(),
5401 args: vec![],
5402 })],
5403 });
5404 let record = json!({});
5405 let out = json!({});
5406 let ctx = V2EvalContext::new();
5407 let result = eval_v2_expr(&expr, &record, None, &out, "test", &ctx);
5408 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HELLO")));
5409 }
5410
5411 #[test]
5412 fn test_eval_pipe_deep_nesting() {
5413 let pipe = V2Pipe {
5415 start: V2Start::Ref(V2Ref::Input("scores".to_string())),
5416 steps: vec![V2Step::Map(V2MapStep {
5417 steps: vec![V2Step::If(V2IfStep {
5418 cond: V2Condition::Comparison(V2Comparison {
5419 op: V2ComparisonOp::Gte,
5420 args: vec![
5421 V2Expr::Pipe(V2Pipe {
5422 start: V2Start::PipeValue,
5423 steps: vec![],
5424 }),
5425 V2Expr::Pipe(V2Pipe {
5426 start: V2Start::Literal(json!(60)),
5427 steps: vec![],
5428 }),
5429 ],
5430 }),
5431 then_branch: V2Pipe {
5432 start: V2Start::Literal(json!("pass")),
5433 steps: vec![],
5434 },
5435 else_branch: Some(V2Pipe {
5436 start: V2Start::Literal(json!("fail")),
5437 steps: vec![],
5438 }),
5439 })],
5440 })],
5441 };
5442 let record = json!({"scores": [80, 55, 90, 45]});
5443 let out = json!({});
5444 let ctx = V2EvalContext::new();
5445 let result = eval_v2_pipe(&pipe, &record, None, &out, "test", &ctx);
5446 assert!(
5447 matches!(result, Ok(EvalValue::Value(v)) if v == json!(["pass", "fail", "pass", "fail"]))
5448 );
5449 }
5450}
5451
5452#[cfg(test)]
5457mod v2_lookup_eval_tests {
5458 use super::*;
5459 use serde_json::json;
5460
5461 fn make_departments() -> JsonValue {
5462 json!([
5463 {"id": 1, "name": "Engineering", "budget": 100000},
5464 {"id": 2, "name": "Sales", "budget": 50000},
5465 {"id": 3, "name": "HR", "budget": 30000}
5466 ])
5467 }
5468
5469 #[test]
5470 fn test_lookup_first_basic() {
5471 let op = V2OpStep {
5473 op: "lookup_first".to_string(),
5474 args: vec![
5475 V2Expr::Pipe(V2Pipe {
5476 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5477 steps: vec![],
5478 }),
5479 V2Expr::Pipe(V2Pipe {
5480 start: V2Start::Literal(json!("id")),
5481 steps: vec![],
5482 }),
5483 V2Expr::Pipe(V2Pipe {
5484 start: V2Start::Literal(json!(2)),
5485 steps: vec![],
5486 }),
5487 V2Expr::Pipe(V2Pipe {
5488 start: V2Start::Literal(json!("name")),
5489 steps: vec![],
5490 }),
5491 ],
5492 };
5493 let record = json!({});
5494 let context = json!({"departments": make_departments()});
5495 let out = json!({});
5496 let ctx = V2EvalContext::new();
5497 let result = eval_v2_op_step(
5498 &op,
5499 EvalValue::Value(json!(null)),
5500 &record,
5501 Some(&context),
5502 &out,
5503 "test",
5504 &ctx,
5505 );
5506 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("Sales")));
5507 }
5508
5509 #[test]
5510 fn test_lookup_first_uses_pipe_value_from() {
5511 let op = V2OpStep {
5512 op: "lookup_first".to_string(),
5513 args: vec![
5514 V2Expr::Pipe(V2Pipe {
5515 start: V2Start::Literal(json!("id")),
5516 steps: vec![],
5517 }),
5518 V2Expr::Pipe(V2Pipe {
5519 start: V2Start::Literal(json!(2)),
5520 steps: vec![],
5521 }),
5522 V2Expr::Pipe(V2Pipe {
5523 start: V2Start::Literal(json!("budget")),
5524 steps: vec![],
5525 }),
5526 ],
5527 };
5528 let record = json!({});
5529 let out = json!({});
5530 let ctx = V2EvalContext::new();
5531 let result = eval_v2_op_step(
5532 &op,
5533 EvalValue::Value(make_departments()),
5534 &record,
5535 None,
5536 &out,
5537 "test",
5538 &ctx,
5539 );
5540 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(50000)));
5541 }
5542
5543 #[test]
5544 fn test_lookup_first_no_match() {
5545 let op = V2OpStep {
5546 op: "lookup_first".to_string(),
5547 args: vec![
5548 V2Expr::Pipe(V2Pipe {
5549 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5550 steps: vec![],
5551 }),
5552 V2Expr::Pipe(V2Pipe {
5553 start: V2Start::Literal(json!("id")),
5554 steps: vec![],
5555 }),
5556 V2Expr::Pipe(V2Pipe {
5557 start: V2Start::Literal(json!(999)), steps: vec![],
5559 }),
5560 V2Expr::Pipe(V2Pipe {
5561 start: V2Start::Literal(json!("name")),
5562 steps: vec![],
5563 }),
5564 ],
5565 };
5566 let record = json!({});
5567 let context = json!({"departments": make_departments()});
5568 let out = json!({});
5569 let ctx = V2EvalContext::new();
5570 let result = eval_v2_op_step(
5571 &op,
5572 EvalValue::Value(json!(null)),
5573 &record,
5574 Some(&context),
5575 &out,
5576 "test",
5577 &ctx,
5578 );
5579 assert!(matches!(result, Ok(EvalValue::Missing)));
5580 }
5581
5582 #[test]
5583 fn test_lookup_first_return_whole_object() {
5584 let op = V2OpStep {
5586 op: "lookup_first".to_string(),
5587 args: vec![
5588 V2Expr::Pipe(V2Pipe {
5589 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5590 steps: vec![],
5591 }),
5592 V2Expr::Pipe(V2Pipe {
5593 start: V2Start::Literal(json!("id")),
5594 steps: vec![],
5595 }),
5596 V2Expr::Pipe(V2Pipe {
5597 start: V2Start::Literal(json!(1)),
5598 steps: vec![],
5599 }),
5600 ],
5601 };
5602 let record = json!({});
5603 let context = json!({"departments": make_departments()});
5604 let out = json!({});
5605 let ctx = V2EvalContext::new();
5606 let result = eval_v2_op_step(
5607 &op,
5608 EvalValue::Value(json!(null)),
5609 &record,
5610 Some(&context),
5611 &out,
5612 "test",
5613 &ctx,
5614 );
5615 assert!(
5616 matches!(result, Ok(EvalValue::Value(v)) if v == json!({"id": 1, "name": "Engineering", "budget": 100000}))
5617 );
5618 }
5619
5620 #[test]
5621 fn test_lookup_first_with_input_match_value() {
5622 let op = V2OpStep {
5624 op: "lookup_first".to_string(),
5625 args: vec![
5626 V2Expr::Pipe(V2Pipe {
5627 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5628 steps: vec![],
5629 }),
5630 V2Expr::Pipe(V2Pipe {
5631 start: V2Start::Literal(json!("id")),
5632 steps: vec![],
5633 }),
5634 V2Expr::Pipe(V2Pipe {
5635 start: V2Start::Ref(V2Ref::Input("dept_id".to_string())),
5636 steps: vec![],
5637 }),
5638 V2Expr::Pipe(V2Pipe {
5639 start: V2Start::Literal(json!("name")),
5640 steps: vec![],
5641 }),
5642 ],
5643 };
5644 let record = json!({"dept_id": 3});
5645 let context = json!({"departments": make_departments()});
5646 let out = json!({});
5647 let ctx = V2EvalContext::new();
5648 let result = eval_v2_op_step(
5649 &op,
5650 EvalValue::Value(json!(null)),
5651 &record,
5652 Some(&context),
5653 &out,
5654 "test",
5655 &ctx,
5656 );
5657 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!("HR")));
5658 }
5659
5660 #[test]
5661 fn test_lookup_first_missing_match_value_does_not_match_null() {
5662 let users = json!([
5663 {"id": null, "name": "MissingUser"},
5664 {"id": 1, "name": "Alice"}
5665 ]);
5666 let op = V2OpStep {
5667 op: "lookup_first".to_string(),
5668 args: vec![
5669 V2Expr::Pipe(V2Pipe {
5670 start: V2Start::Ref(V2Ref::Context("users".to_string())),
5671 steps: vec![],
5672 }),
5673 V2Expr::Pipe(V2Pipe {
5674 start: V2Start::Literal(json!("id")),
5675 steps: vec![],
5676 }),
5677 V2Expr::Pipe(V2Pipe {
5678 start: V2Start::Ref(V2Ref::Input("user_id".to_string())),
5679 steps: vec![],
5680 }),
5681 V2Expr::Pipe(V2Pipe {
5682 start: V2Start::Literal(json!("name")),
5683 steps: vec![],
5684 }),
5685 ],
5686 };
5687 let record = json!({});
5688 let context = json!({"users": users});
5689 let out = json!({});
5690 let ctx = V2EvalContext::new();
5691 let result = eval_v2_op_step(
5692 &op,
5693 EvalValue::Value(json!(null)),
5694 &record,
5695 Some(&context),
5696 &out,
5697 "test",
5698 &ctx,
5699 );
5700 assert!(matches!(result, Ok(EvalValue::Missing)));
5701 }
5702
5703 #[test]
5704 fn test_lookup_all_matches() {
5705 let employees = json!([
5707 {"name": "Alice", "dept": "Engineering"},
5708 {"name": "Bob", "dept": "Sales"},
5709 {"name": "Charlie", "dept": "Engineering"},
5710 {"name": "Diana", "dept": "HR"}
5711 ]);
5712 let op = V2OpStep {
5713 op: "lookup".to_string(),
5714 args: vec![
5715 V2Expr::Pipe(V2Pipe {
5716 start: V2Start::Ref(V2Ref::Context("employees".to_string())),
5717 steps: vec![],
5718 }),
5719 V2Expr::Pipe(V2Pipe {
5720 start: V2Start::Literal(json!("dept")),
5721 steps: vec![],
5722 }),
5723 V2Expr::Pipe(V2Pipe {
5724 start: V2Start::Literal(json!("Engineering")),
5725 steps: vec![],
5726 }),
5727 V2Expr::Pipe(V2Pipe {
5728 start: V2Start::Literal(json!("name")),
5729 steps: vec![],
5730 }),
5731 ],
5732 };
5733 let record = json!({});
5734 let context = json!({"employees": employees});
5735 let out = json!({});
5736 let ctx = V2EvalContext::new();
5737 let result = eval_v2_op_step(
5738 &op,
5739 EvalValue::Value(json!(null)),
5740 &record,
5741 Some(&context),
5742 &out,
5743 "test",
5744 &ctx,
5745 );
5746 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(["Alice", "Charlie"])));
5747 }
5748
5749 #[test]
5750 fn test_lookup_no_matches() {
5751 let op = V2OpStep {
5752 op: "lookup".to_string(),
5753 args: vec![
5754 V2Expr::Pipe(V2Pipe {
5755 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5756 steps: vec![],
5757 }),
5758 V2Expr::Pipe(V2Pipe {
5759 start: V2Start::Literal(json!("id")),
5760 steps: vec![],
5761 }),
5762 V2Expr::Pipe(V2Pipe {
5763 start: V2Start::Literal(json!(999)),
5764 steps: vec![],
5765 }),
5766 ],
5767 };
5768 let record = json!({});
5769 let context = json!({"departments": make_departments()});
5770 let out = json!({});
5771 let ctx = V2EvalContext::new();
5772 let result = eval_v2_op_step(
5773 &op,
5774 EvalValue::Value(json!(null)),
5775 &record,
5776 Some(&context),
5777 &out,
5778 "test",
5779 &ctx,
5780 );
5781 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!([])));
5782 }
5783
5784 #[test]
5785 fn test_lookup_missing_match_value_does_not_match_null() {
5786 let users = json!([
5787 {"id": null, "name": "MissingUser"},
5788 {"id": 1, "name": "Alice"}
5789 ]);
5790 let op = V2OpStep {
5791 op: "lookup".to_string(),
5792 args: vec![
5793 V2Expr::Pipe(V2Pipe {
5794 start: V2Start::Ref(V2Ref::Context("users".to_string())),
5795 steps: vec![],
5796 }),
5797 V2Expr::Pipe(V2Pipe {
5798 start: V2Start::Literal(json!("id")),
5799 steps: vec![],
5800 }),
5801 V2Expr::Pipe(V2Pipe {
5802 start: V2Start::Ref(V2Ref::Input("user_id".to_string())),
5803 steps: vec![],
5804 }),
5805 V2Expr::Pipe(V2Pipe {
5806 start: V2Start::Literal(json!("name")),
5807 steps: vec![],
5808 }),
5809 ],
5810 };
5811 let record = json!({});
5812 let context = json!({"users": users});
5813 let out = json!({});
5814 let ctx = V2EvalContext::new();
5815 let result = eval_v2_op_step(
5816 &op,
5817 EvalValue::Value(json!(null)),
5818 &record,
5819 Some(&context),
5820 &out,
5821 "test",
5822 &ctx,
5823 );
5824 assert!(matches!(result, Ok(EvalValue::Missing)));
5825 }
5826
5827 #[test]
5828 fn test_lookup_first_missing_from() {
5829 let op = V2OpStep {
5830 op: "lookup_first".to_string(),
5831 args: vec![
5832 V2Expr::Pipe(V2Pipe {
5833 start: V2Start::Ref(V2Ref::Context("nonexistent".to_string())),
5834 steps: vec![],
5835 }),
5836 V2Expr::Pipe(V2Pipe {
5837 start: V2Start::Literal(json!("id")),
5838 steps: vec![],
5839 }),
5840 V2Expr::Pipe(V2Pipe {
5841 start: V2Start::Literal(json!(1)),
5842 steps: vec![],
5843 }),
5844 ],
5845 };
5846 let record = json!({});
5847 let context = json!({});
5848 let out = json!({});
5849 let ctx = V2EvalContext::new();
5850 let result = eval_v2_op_step(
5851 &op,
5852 EvalValue::Value(json!(null)),
5853 &record,
5854 Some(&context),
5855 &out,
5856 "test",
5857 &ctx,
5858 );
5859 assert!(matches!(result, Ok(EvalValue::Missing)));
5861 }
5862
5863 #[test]
5864 fn test_lookup_first_insufficient_args() {
5865 let op = V2OpStep {
5866 op: "lookup_first".to_string(),
5867 args: vec![V2Expr::Pipe(V2Pipe {
5868 start: V2Start::Literal(json!([])),
5869 steps: vec![],
5870 })],
5871 };
5872 let record = json!({});
5873 let out = json!({});
5874 let ctx = V2EvalContext::new();
5875 let result = eval_v2_op_step(
5876 &op,
5877 EvalValue::Value(json!(null)),
5878 &record,
5879 None,
5880 &out,
5881 "test",
5882 &ctx,
5883 );
5884 assert!(result.is_err());
5885 }
5886
5887 #[test]
5888 fn test_lookup_in_pipe() {
5889 let pipe = V2Pipe {
5892 start: V2Start::Literal(json!(null)),
5893 steps: vec![V2Step::Op(V2OpStep {
5894 op: "lookup_first".to_string(),
5895 args: vec![
5896 V2Expr::Pipe(V2Pipe {
5897 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5898 steps: vec![],
5899 }),
5900 V2Expr::Pipe(V2Pipe {
5901 start: V2Start::Literal(json!("id")),
5902 steps: vec![],
5903 }),
5904 V2Expr::Pipe(V2Pipe {
5905 start: V2Start::Ref(V2Ref::Input("dept_id".to_string())),
5906 steps: vec![],
5907 }),
5908 V2Expr::Pipe(V2Pipe {
5909 start: V2Start::Literal(json!("budget")),
5910 steps: vec![],
5911 }),
5912 ],
5913 })],
5914 };
5915 let record = json!({"dept_id": 2}); let context = json!({"departments": make_departments()});
5917 let out = json!({});
5918 let ctx = V2EvalContext::new();
5919 let result = eval_v2_pipe(&pipe, &record, Some(&context), &out, "test", &ctx);
5920 assert!(matches!(result, Ok(EvalValue::Value(v)) if v == json!(50000)));
5922 }
5923
5924 #[test]
5925 fn test_lookup_then_multiply() {
5926 let pipe = V2Pipe {
5928 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5929 steps: vec![],
5930 };
5931 let record = json!({"dept_id": 2});
5932 let context = json!({"departments": make_departments()});
5933 let out = json!({});
5934 let ctx = V2EvalContext::new();
5935
5936 let result = eval_v2_pipe(&pipe, &record, Some(&context), &out, "test", &ctx);
5938 assert!(result.is_ok());
5939
5940 let op = V2OpStep {
5942 op: "lookup_first".to_string(),
5943 args: vec![
5944 V2Expr::Pipe(V2Pipe {
5945 start: V2Start::Ref(V2Ref::Context("departments".to_string())),
5946 steps: vec![],
5947 }),
5948 V2Expr::Pipe(V2Pipe {
5949 start: V2Start::Literal(json!("id")),
5950 steps: vec![],
5951 }),
5952 V2Expr::Pipe(V2Pipe {
5953 start: V2Start::Literal(json!(2)),
5954 steps: vec![],
5955 }),
5956 V2Expr::Pipe(V2Pipe {
5957 start: V2Start::Literal(json!("budget")),
5958 steps: vec![],
5959 }),
5960 ],
5961 };
5962 let result = eval_v2_op_step(
5963 &op,
5964 EvalValue::Value(json!(null)),
5965 &record,
5966 Some(&context),
5967 &out,
5968 "test",
5969 &ctx,
5970 );
5971 assert!(matches!(result, Ok(EvalValue::Value(ref v)) if *v == json!(50000)));
5972
5973 let multiply_op = V2OpStep {
5975 op: "multiply".to_string(),
5976 args: vec![V2Expr::Pipe(V2Pipe {
5977 start: V2Start::Literal(json!(1.1)),
5978 steps: vec![],
5979 })],
5980 };
5981 let budget = result.unwrap();
5982 let result2 = eval_v2_op_step(&multiply_op, budget, &record, None, &out, "test", &ctx);
5983 match result2 {
5985 Ok(EvalValue::Value(v)) => {
5986 let num = v.as_f64().expect("should be number");
5987 assert!(
5988 (num - 55000.0).abs() < 0.001,
5989 "expected 55000.0, got {}",
5990 num
5991 );
5992 }
5993 other => panic!("expected Ok(EvalValue::Value), got {:?}", other),
5994 }
5995 }
5996}