1use crate::error::{WorkflowError, WorkflowResult};
2use serde_json::Value;
3use std::collections::HashMap;
4use std::sync::LazyLock;
5use swf_core::models::expression::{is_strict_expr, sanitize_expr};
6
7pub trait ExpressionEngine: Send + Sync {
43 fn engine_prefix(&self) -> &str;
45
46 fn evaluate(
48 &self,
49 expression: &str,
50 input: &Value,
51 vars: &HashMap<String, Value>,
52 ) -> WorkflowResult<Value>;
53}
54
55#[derive(Default, Clone)]
57pub struct ExpressionEngineRegistry {
58 engines: std::sync::Arc<HashMap<String, std::sync::Arc<dyn ExpressionEngine>>>,
59}
60
61impl ExpressionEngineRegistry {
62 pub fn new() -> Self {
63 Self::default()
64 }
65
66 pub fn register(&mut self, engine: std::sync::Arc<dyn ExpressionEngine>) {
67 let key = engine.engine_prefix().to_string();
68 std::sync::Arc::make_mut(&mut self.engines).insert(key, engine);
69 }
70
71 pub fn get(&self, prefix: &str) -> Option<std::sync::Arc<dyn ExpressionEngine>> {
72 self.engines.get(prefix).cloned()
73 }
74}
75
76pub fn strip_engine_prefix(expr: &str) -> Option<(&str, &str)> {
79 let expr = expr.trim_start();
81 for sep in &[':'] {
82 if let Some(pos) = expr.find(*sep) {
83 let prefix = &expr[..pos];
84 if prefix.chars().all(|c| c.is_ascii_alphabetic()) && !prefix.is_empty() {
86 let rest = expr[pos + 1..].trim_start();
87 return Some((prefix, rest));
88 }
89 }
90 }
91 None
92}
93
94pub fn evaluate_with_engines(
97 expression: &str,
98 input: &Value,
99 vars: &HashMap<String, Value>,
100 engines: &ExpressionEngineRegistry,
101) -> WorkflowResult<Value> {
102 if let Some((prefix, rest)) = strip_engine_prefix(expression) {
104 if let Some(engine) = engines.get(prefix) {
105 return engine.evaluate(rest, input, vars);
106 }
107 }
109 evaluate_jq(expression, input, vars)
110}
111
112type CacheKey = (String, String);
114
115static FILTER_CACHE: LazyLock<
119 std::sync::RwLock<HashMap<CacheKey, jaq_core::Filter<jaq_core::Native<jaq_json::Val>>>>,
120> = LazyLock::new(|| std::sync::RwLock::new(HashMap::new()));
121
122pub fn evaluate_jq(
125 expression: &str,
126 input: &Value,
127 vars: &HashMap<String, Value>,
128) -> WorkflowResult<Value> {
129 use jaq_core::{load, Compiler, Ctx, RcIter};
130 use jaq_json::Val;
131
132 let mut var_names: Vec<String> = vars.keys().cloned().collect();
134 var_names.sort();
135 let var_name_refs: Vec<&str> = var_names.iter().map(|s| s.as_str()).collect();
136
137 let cache_key = (expression.to_string(), var_names.join("\0"));
139
140 let filter = {
142 let cache = FILTER_CACHE.read().unwrap_or_else(|e| e.into_inner());
143 cache.get(&cache_key).cloned()
144 };
145
146 let filter = match filter {
147 Some(f) => f,
148 None => {
149 let program = load::File {
151 code: expression,
152 path: (),
153 };
154 let loader = load::Loader::new(jaq_std::defs().chain(jaq_json::defs()));
155 let arena = load::Arena::default();
156
157 let modules = loader.load(&arena, program).map_err(|e| {
158 WorkflowError::expression(
159 format!("failed to parse jq expression '{}': {:?}", expression, e),
160 "",
161 )
162 })?;
163
164 let filter = Compiler::default()
166 .with_funs(jaq_std::funs().chain(jaq_json::funs()))
167 .with_global_vars(var_name_refs)
168 .compile(modules)
169 .map_err(|errs| {
170 WorkflowError::expression(
171 format!(
172 "failed to compile jq expression '{}': {:?}",
173 expression, errs
174 ),
175 "",
176 )
177 })?;
178
179 let mut cache = FILTER_CACHE.write().unwrap_or_else(|e| e.into_inner());
181 cache.entry(cache_key).or_insert(filter).clone()
182 }
183 };
184
185 let jaq_input = Val::from(input.clone());
187
188 let var_vals: Vec<Val> = var_names
190 .iter()
191 .map(|k| Val::from(vars[k].clone()))
192 .collect();
193 let inputs = RcIter::new(core::iter::empty());
194
195 let out = filter.run((Ctx::new(var_vals, &inputs), jaq_input));
196
197 let mut results = Vec::new();
198 for item in out {
199 match item {
200 Ok(val) => {
201 let json_val: Value = val.into();
202 results.push(json_val);
203 }
204 Err(e) => {
205 return Err(WorkflowError::expression(
206 format!("jq evaluation error: {:?}", e),
207 "",
208 ));
209 }
210 }
211 }
212
213 match results.len() {
214 0 => Err(WorkflowError::expression(
215 "no result from jq evaluation",
216 "",
217 )),
218 1 => Ok(results.into_iter().next().unwrap_or(Value::Null)),
219 _ => Ok(Value::Array(results)),
220 }
221}
222
223pub fn traverse_and_evaluate(
225 node: &mut Value,
226 input: &Value,
227 vars: &HashMap<String, Value>,
228) -> WorkflowResult<()> {
229 match node {
230 Value::Object(map) => {
231 for (_key, value) in map.iter_mut() {
232 traverse_and_evaluate(value, input, vars)?;
233 }
234 }
235 Value::Array(arr) => {
236 for item in arr.iter_mut() {
237 traverse_and_evaluate(item, input, vars)?;
238 }
239 }
240 Value::String(s) if is_strict_expr(s) => {
241 let expr = sanitize_expr(s);
242 let result = evaluate_jq(&expr, input, vars)?;
243 *node = result;
244 }
245 _ => {}
246 }
247 Ok(())
248}
249
250pub fn traverse_and_evaluate_bool(
252 expr: &str,
253 input: &Value,
254 vars: &HashMap<String, Value>,
255) -> WorkflowResult<bool> {
256 if expr.is_empty() {
257 return Ok(false);
258 }
259
260 let normalized = if is_strict_expr(expr) {
262 expr.to_string()
263 } else {
264 swf_core::models::expression::normalize_expr(expr)
265 };
266
267 let sanitized = sanitize_expr(&normalized);
268 let result = evaluate_jq(&sanitized, input, vars)?;
269
270 match result {
271 Value::Bool(b) => Ok(b),
272 _ => Ok(false),
273 }
274}
275
276pub fn traverse_and_evaluate_obj(
278 obj: Option<&Value>,
279 input: &Value,
280 vars: &HashMap<String, Value>,
281 task_name: &str,
282) -> WorkflowResult<Value> {
283 match obj {
284 None => Ok(input.clone()),
285 Some(value) => {
286 let mut result = value.clone();
287 traverse_and_evaluate(&mut result, input, vars)
288 .map_err(|e| WorkflowError::expression(format!("{}", e), task_name))?;
289 Ok(result)
290 }
291 }
292}
293
294pub fn evaluate_expression_str(
303 expr: &str,
304 input: &Value,
305 vars: &HashMap<String, Value>,
306 task_name: &str,
307) -> WorkflowResult<String> {
308 if is_strict_expr(expr) {
309 let sanitized = sanitize_expr(expr);
311 let result = evaluate_jq(&sanitized, input, vars)
312 .map_err(|e| WorkflowError::expression(format!("{}", e), task_name))?;
313 match result {
314 Value::String(s) => Ok(s),
315 other => Ok(other.to_string()),
316 }
317 } else if expr.contains("${") {
318 evaluate_embedded_expressions(expr, input, vars, task_name)
320 } else {
321 Ok(expr.to_string())
322 }
323}
324
325fn evaluate_embedded_expressions(
327 s: &str,
328 input: &Value,
329 vars: &HashMap<String, Value>,
330 task_name: &str,
331) -> WorkflowResult<String> {
332 let mut result = String::new();
333 let mut chars = s.chars().peekable();
334
335 while let Some(c) = chars.next() {
336 if c == '$' && chars.peek() == Some(&'{') {
337 chars.next(); let mut depth = 1;
340 let mut expr_buf = String::new();
341 #[allow(clippy::while_let_on_iterator)]
342 while let Some(ec) = chars.next() {
343 match ec {
344 '{' => depth += 1,
345 '}' => {
346 depth -= 1;
347 if depth == 0 {
348 break;
349 }
350 }
351 _ => {}
352 }
353 expr_buf.push(ec);
354 }
355 let sanitized = sanitize_expr(&expr_buf);
357 let val = evaluate_jq(&sanitized, input, vars)
358 .map_err(|e| WorkflowError::expression(format!("{}", e), task_name))?;
359 match val {
360 Value::String(vs) => result.push_str(&vs),
361 other => result.push_str(&other.to_string()),
362 }
363 } else {
364 result.push(c);
365 }
366 }
367
368 Ok(result)
369}
370
371pub fn evaluate_value_expr(
376 value: &Value,
377 input: &Value,
378 vars: &HashMap<String, Value>,
379 task_name: &str,
380) -> WorkflowResult<Value> {
381 match value {
382 Value::String(expr) => {
383 let sanitized = prepare_expression(expr);
384 evaluate_jq(&sanitized, input, vars)
385 .map_err(|e| WorkflowError::expression(format!("{}", e), task_name))
386 }
387 _ => traverse_and_evaluate_obj(Some(value), input, vars, task_name),
388 }
389}
390
391pub fn prepare_expression(expr: &str) -> String {
396 if is_strict_expr(expr) {
397 sanitize_expr(expr)
398 } else {
399 let normalized = swf_core::models::expression::normalize_expr(expr);
400 sanitize_expr(&normalized)
401 }
402}
403
404pub fn evaluate_expression_json(
409 expr: &str,
410 input: &Value,
411 vars: &HashMap<String, Value>,
412 task_name: &str,
413) -> WorkflowResult<Value> {
414 if is_strict_expr(expr) {
415 let sanitized = sanitize_expr(expr);
416 evaluate_jq(&sanitized, input, vars)
417 .map_err(|e| WorkflowError::expression(format!("{}", e), task_name))
418 } else {
419 serde_json::from_str(expr).map_err(|e| {
421 WorkflowError::expression(
422 format!("failed to parse non-expression value as JSON: {}", e),
423 task_name,
424 )
425 })
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use serde_json::json;
433
434 #[test]
437 fn test_evaluate_jq_simple_path() {
438 let input = json!({"foo": "bar"});
439 let vars = HashMap::new();
440 let result = evaluate_jq(".foo", &input, &vars).unwrap();
441 assert_eq!(result, json!("bar"));
442 }
443
444 #[test]
445 fn test_evaluate_jq_nested_path() {
446 let input = json!({"foo": {"bar": 42}});
447 let vars = HashMap::new();
448 let result = evaluate_jq(".foo.bar", &input, &vars).unwrap();
449 assert_eq!(result, json!(42));
450 }
451
452 #[test]
453 fn test_evaluate_jq_with_variable() {
454 let input = json!({});
455 let mut vars = HashMap::new();
456 vars.insert("$input".to_string(), json!({"x": 1}));
457 let result = evaluate_jq("$input.x", &input, &vars).unwrap();
458 assert_eq!(result, json!(1));
459 }
460
461 #[test]
462 fn test_evaluate_jq_undefined_variable() {
463 let input = json!({"foo": "bar"});
464 let vars = HashMap::new();
465 let result = evaluate_jq("$undefinedVar", &input, &vars);
466 assert!(result.is_err());
467 }
468
469 #[test]
470 fn test_evaluate_jq_invalid_expression() {
471 let input = json!({"foo": "bar"});
472 let vars = HashMap::new();
473 let result = evaluate_jq(".foo(", &input, &vars);
474 assert!(result.is_err());
475 }
476
477 #[test]
478 fn test_evaluate_jq_array_result() {
479 let input = json!({"items": [1, 2, 3]});
480 let vars = HashMap::new();
481 let result = evaluate_jq(".items[]", &input, &vars).unwrap();
482 assert_eq!(result, json!([1, 2, 3]));
483 }
484
485 #[test]
486 fn test_evaluate_jq_length_function() {
487 let input = json!({"items": [1, 2, 3]});
488 let vars = HashMap::new();
489 let result = evaluate_jq(".items | length", &input, &vars).unwrap();
490 assert_eq!(result, json!(3));
491 }
492
493 #[test]
494 fn test_evaluate_jq_arithmetic() {
495 let input = json!({"a": 10, "b": 3});
496 let vars = HashMap::new();
497 let result = evaluate_jq(".a - .b", &input, &vars).unwrap();
498 assert_eq!(result, json!(7));
499 }
500
501 #[test]
502 fn test_jq_filter_cache_hit() {
503 let input1 = json!({"x": 1});
505 let input2 = json!({"x": 2});
506 let vars = HashMap::new();
507
508 let result1 = evaluate_jq(".x", &input1, &vars).unwrap();
509 assert_eq!(result1, json!(1));
510
511 let result2 = evaluate_jq(".x", &input2, &vars).unwrap();
512 assert_eq!(result2, json!(2));
513
514 let cache = FILTER_CACHE.read().unwrap();
516 assert!(!cache.is_empty());
517 }
518
519 #[test]
522 fn test_traverse_no_expression() {
523 let mut node = json!({
524 "key": "value",
525 "num": 123
526 });
527 let input = json!(null);
528 let vars = HashMap::new();
529 traverse_and_evaluate(&mut node, &input, &vars).unwrap();
530 assert_eq!(node["key"], json!("value"));
531 assert_eq!(node["num"], json!(123));
532 }
533
534 #[test]
535 fn test_traverse_and_evaluate_object() {
536 let mut node = json!({
537 "name": "${.foo}",
538 "count": 42
539 });
540 let input = json!({"foo": "hello"});
541 let vars = HashMap::new();
542 traverse_and_evaluate(&mut node, &input, &vars).unwrap();
543 assert_eq!(node["name"], json!("hello"));
544 assert_eq!(node["count"], json!(42));
545 }
546
547 #[test]
548 fn test_traverse_expression_in_array() {
549 let mut node = json!(["static", "${.foo}"]);
550 let input = json!({"foo": "bar"});
551 let vars = HashMap::new();
552 traverse_and_evaluate(&mut node, &input, &vars).unwrap();
553 assert_eq!(node[0], json!("static"));
554 assert_eq!(node[1], json!("bar"));
555 }
556
557 #[test]
558 fn test_traverse_and_evaluate_nested_expr() {
559 let mut node = json!({
560 "data": {
561 "inner": "${.name}"
562 }
563 });
564 let input = json!({"name": "world"});
565 let vars = HashMap::new();
566 traverse_and_evaluate(&mut node, &input, &vars).unwrap();
567 assert_eq!(node["data"]["inner"], json!("world"));
568 }
569
570 #[test]
571 fn test_traverse_nested_structure_in_array() {
572 let mut node = json!({
573 "level1": [{"expr": "${.foo}"}]
574 });
575 let input = json!({"foo": "nestedValue"});
576 let vars = HashMap::new();
577 traverse_and_evaluate(&mut node, &input, &vars).unwrap();
578 assert_eq!(node["level1"][0]["expr"], json!("nestedValue"));
579 }
580
581 #[test]
582 fn test_traverse_with_vars() {
583 let mut node = json!({"expr": "${$myVar}"});
584 let input = json!({});
585 let mut vars = HashMap::new();
586 vars.insert("$myVar".to_string(), json!("HelloVars"));
587 traverse_and_evaluate(&mut node, &input, &vars).unwrap();
588 assert_eq!(node["expr"], json!("HelloVars"));
589 }
590
591 #[test]
592 fn test_traverse_invalid_jq_expression() {
593 let mut node = json!("${ .foo( }");
594 let input = json!({"foo": "bar"});
595 let vars = HashMap::new();
596 let result = traverse_and_evaluate(&mut node, &input, &vars);
597 assert!(result.is_err());
598 }
599
600 #[test]
603 fn test_traverse_and_evaluate_bool_true() {
604 let input = json!({"x": 1});
605 let vars = HashMap::new();
606 let result = traverse_and_evaluate_bool("${.x == 1}", &input, &vars).unwrap();
607 assert!(result);
608 }
609
610 #[test]
611 fn test_traverse_and_evaluate_bool_false() {
612 let input = json!({"x": 1});
613 let vars = HashMap::new();
614 let result = traverse_and_evaluate_bool("${.x == 2}", &input, &vars).unwrap();
615 assert!(!result);
616 }
617
618 #[test]
619 fn test_traverse_and_evaluate_bool_empty() {
620 let input = json!({});
621 let vars = HashMap::new();
622 let result = traverse_and_evaluate_bool("", &input, &vars).unwrap();
623 assert!(!result);
624 }
625
626 #[test]
629 fn test_traverse_and_evaluate_obj_none() {
630 let input = json!({"key": "value"});
631 let vars = HashMap::new();
632 let result = traverse_and_evaluate_obj(None, &input, &vars, "test").unwrap();
633 assert_eq!(result, input);
634 }
635
636 #[test]
637 fn test_traverse_and_evaluate_obj_with_expression() {
638 let obj = json!({"result": "${.value}"});
639 let input = json!({"value": 42});
640 let vars = HashMap::new();
641 let result = traverse_and_evaluate_obj(Some(&obj), &input, &vars, "test").unwrap();
642 assert_eq!(result["result"], json!(42));
643 }
644
645 #[test]
646 fn test_jq_update_operator() {
647 let input = json!({"incr": [2, 3], "counter": 6});
649 let vars = HashMap::new();
650 let result = evaluate_jq(".incr += [5]", &input, &vars);
651 match result {
653 Ok(val) => {
654 assert_eq!(val["incr"], json!([2, 3, 5]));
656 assert_eq!(val["counter"], json!(6));
657 }
658 Err(_) => {
659 }
662 }
663 }
664
665 #[test]
666 fn test_jq_if_then_else_with_concat() {
667 let input = json!({"incr": [2, 3], "counter": 6});
670 let vars = HashMap::new();
671 let result = evaluate_jq(
673 "if .incr == null then {incr: [5]} else {incr: (.incr + [5])} end",
674 &input,
675 &vars,
676 )
677 .unwrap();
678 assert_eq!(result["incr"], json!([2, 3, 5]));
679 }
680
681 #[test]
682 fn test_jq_if_then_else_null_check() {
683 let input = json!({"counter": 0});
685 let vars = HashMap::new();
686 let result = evaluate_jq(
687 "if .incr == null then {incr: [2]} else {incr: (.incr + [2])} end",
688 &input,
689 &vars,
690 )
691 .unwrap();
692 assert_eq!(result["incr"], json!([2]));
693 }
694}