1use exprimo::{CustomFuncError, CustomFunction, Evaluator};
9use serde_json::Value;
10use std::collections::{HashMap, HashSet};
11use std::sync::Arc;
12
13use super::{Binding, BindingSource};
14use crate::error::EngineError;
15
16#[derive(Debug)]
27struct LengthFn;
28
29impl CustomFunction for LengthFn {
30 fn call(&self, args: &[Value]) -> Result<Value, CustomFuncError> {
31 if args.len() != 1 {
32 return Err(CustomFuncError::ArityError {
33 expected: 1,
34 got: args.len(),
35 });
36 }
37 let n = match &args[0] {
38 Value::String(s) => s.chars().count(),
43 Value::Array(a) => a.len(),
44 Value::Object(o) => o.len(),
45 Value::Null => 0,
46 other => {
47 return Err(CustomFuncError::ArgumentError(format!(
48 "length() expects string, array, object, or null; got {}",
49 other
50 )));
51 }
52 };
53 Ok(Value::Number(serde_json::Number::from(n as u64)))
56 }
57}
58
59fn builtin_functions() -> HashMap<String, Arc<dyn CustomFunction>> {
64 let mut funcs: HashMap<String, Arc<dyn CustomFunction>> = HashMap::new();
65 funcs.insert("length".to_string(), Arc::new(LengthFn));
66 funcs
67}
68
69pub fn evaluate_expression(
88 expr: &str,
89 context: &HashMap<String, Value>,
90) -> Result<Value, EngineError> {
91 let evaluator = Evaluator::new(context.clone(), builtin_functions());
92 evaluator
93 .evaluate(expr)
94 .map_err(|e| EngineError::ExpressionError(e.to_string()))
95}
96
97pub fn build_expression_context(
104 state: &Value,
105 item: Option<&Value>,
106 data_sources: Option<&indexmap::IndexMap<String, Value>>,
107) -> HashMap<String, Value> {
108 let mut context = HashMap::new();
109
110 context.insert("state".to_string(), state.clone());
112
113 if let Some(item_value) = item {
115 context.insert("item".to_string(), item_value.clone());
116 }
117
118 if let Some(ds_map) = data_sources {
120 for (provider, ds_state) in ds_map {
121 context.insert(provider.clone(), ds_state.clone());
122 }
123 }
124
125 context
126}
127
128pub fn extract_bindings_from_expression(expr: &str) -> Vec<Binding> {
144 let mut bindings = Vec::new();
145 let mut seen_paths: HashSet<String> = HashSet::new();
146
147 for prefix in &["state.", "item."] {
149 let source = if *prefix == "state." {
150 BindingSource::State
151 } else {
152 BindingSource::Item
153 };
154
155 let mut search_pos = 0;
156 while let Some(start) = expr[search_pos..].find(prefix) {
157 let abs_start = search_pos + start;
158
159 if abs_start > 0 {
162 let prev_char = expr.chars().nth(abs_start - 1).unwrap_or(' ');
163 if prev_char.is_ascii_alphanumeric() || prev_char == '_' {
164 search_pos = abs_start + prefix.len();
165 continue;
166 }
167 }
168
169 let path_start = abs_start + prefix.len();
171 let mut path_end = path_start;
172
173 let chars: Vec<char> = expr.chars().collect();
175 while path_end < chars.len() {
176 let c = chars[path_end];
177 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
178 path_end += 1;
179 } else {
180 break;
181 }
182 }
183
184 if path_end > path_start {
185 let path_str: String = chars[path_start..path_end].iter().collect();
186 let path_str = path_str.trim_end_matches('.');
188
189 if !path_str.is_empty() {
190 let full_path = format!("{}{}", prefix, path_str);
191 if !seen_paths.contains(&full_path) {
192 seen_paths.insert(full_path);
193 let path: Vec<String> =
194 path_str.split('.').map(|s| s.to_string()).collect();
195 bindings.push(Binding::new(source.clone(), path));
196 }
197 }
198 }
199
200 search_pos = path_end.max(abs_start + prefix.len());
201 }
202 }
203
204 extract_data_source_bindings_from_expression(expr, &mut bindings, &mut seen_paths);
207
208 bindings
209}
210
211fn extract_data_source_bindings_from_expression(
213 expr: &str,
214 bindings: &mut Vec<Binding>,
215 seen_paths: &mut HashSet<String>,
216) {
217 let chars: Vec<char> = expr.chars().collect();
218 let len = chars.len();
219 let mut pos = 0;
220
221 let reserved = ["state", "item", "true", "false", "null"];
223
224 while pos < len {
225 if !chars[pos].is_ascii_alphabetic() && chars[pos] != '_' {
227 pos += 1;
228 continue;
229 }
230
231 if pos > 0 && (chars[pos - 1].is_ascii_alphanumeric() || chars[pos - 1] == '_') {
233 pos += 1;
234 continue;
235 }
236
237 let ident_start = pos;
239 while pos < len && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
240 pos += 1;
241 }
242 let ident: String = chars[ident_start..pos].iter().collect();
243
244 if pos >= len || chars[pos] != '.' {
246 continue;
247 }
248
249 if reserved.contains(&ident.as_str()) {
251 continue;
252 }
253
254 let path_start = pos + 1; let mut path_end = path_start;
257 while path_end < len
258 && (chars[path_end].is_ascii_alphanumeric()
259 || chars[path_end] == '_'
260 || chars[path_end] == '.')
261 {
262 path_end += 1;
263 }
264
265 if path_end > path_start {
266 let path_str: String = chars[path_start..path_end].iter().collect();
267 let path_str = path_str.trim_end_matches('.');
268 if !path_str.is_empty() {
269 let full_path = format!("{}.{}", ident, path_str);
270 if !seen_paths.contains(&full_path) {
271 seen_paths.insert(full_path);
272 let path: Vec<String> =
273 path_str.split('.').map(|s| s.to_string()).collect();
274 bindings.push(Binding::data_source(&ident, path));
275 }
276 }
277 }
278
279 pos = path_end;
280 }
281}
282
283pub fn build_evaluator(
289 state: &Value,
290 item: Option<&Value>,
291 data_sources: Option<&indexmap::IndexMap<String, Value>>,
292) -> Evaluator {
293 let context = build_expression_context(state, item, data_sources);
294 Evaluator::new(context, builtin_functions())
295}
296
297pub fn evaluate_template_string(
303 template: &str,
304 evaluator: &Evaluator,
305) -> Result<String, EngineError> {
306 let mut result = template.to_string();
307 let mut pos = 0;
308
309 while let Some(rel_start) = result[pos..].find("@{") {
310 let abs_start = pos + rel_start;
311 let body_start = abs_start + 2;
312
313 let mut depth: i32 = 1;
319 let mut close_byte: Option<usize> = None;
320 for (off, ch) in result[body_start..].char_indices() {
321 match ch {
322 '{' => depth += 1,
323 '}' => {
324 depth -= 1;
325 if depth == 0 {
326 close_byte = Some(body_start + off);
327 break;
328 }
329 }
330 _ => {}
331 }
332 }
333
334 let close_byte = match close_byte {
335 Some(b) => b,
336 None => {
337 return Err(EngineError::ExpressionError(
338 "Unclosed expression in template".to_string(),
339 ));
340 }
341 };
342
343 let expr_content = result[body_start..close_byte].to_string();
345
346 let value = evaluator
348 .evaluate(&expr_content)
349 .map_err(|e| EngineError::ExpressionError(e.to_string()))?;
350
351 let replacement = match &value {
353 Value::String(s) => s.clone(),
354 Value::Number(n) => n.to_string(),
355 Value::Bool(b) => b.to_string(),
356 Value::Null => "null".to_string(),
357 _ => serde_json::to_string(&value).unwrap_or_default(),
358 };
359
360 let end_byte = close_byte + 1;
362 result.replace_range(abs_start..end_byte, &replacement);
363
364 pos = abs_start + replacement.len();
366 }
367
368 Ok(result)
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374 use serde_json::json;
375
376 #[test]
377 fn test_simple_expression() {
378 let mut context = HashMap::new();
379 context.insert("x".to_string(), json!(5));
380 context.insert("y".to_string(), json!(3));
381
382 let result = evaluate_expression("x + y", &context).unwrap();
383 assert_eq!(result.as_f64().unwrap(), 8.0);
385 }
386
387 #[test]
388 fn test_ternary_expression() {
389 let mut context = HashMap::new();
390 context.insert("selected".to_string(), json!(true));
391
392 let result = evaluate_expression("selected ? 'yes' : 'no'", &context).unwrap();
393 assert_eq!(result, json!("yes"));
394 }
395
396 #[test]
397 fn test_ternary_with_colors() {
398 let mut context = HashMap::new();
399 context.insert("selected".to_string(), json!(true));
400
401 let result = evaluate_expression("selected ? '#FFA7E1' : '#374151'", &context).unwrap();
402 assert_eq!(result, json!("#FFA7E1"));
403 }
404
405 #[test]
406 fn test_comparison_expression() {
407 let mut context = HashMap::new();
408 context.insert("count".to_string(), json!(15));
409
410 let result = evaluate_expression("count > 10", &context).unwrap();
411 assert_eq!(result, json!(true));
412 }
413
414 #[test]
415 fn test_state_object_access() {
416 let context =
417 build_expression_context(&json!({"user": {"name": "Alice", "age": 30}}), None, None);
418
419 let result = evaluate_expression("state.user.name", &context).unwrap();
420 assert_eq!(result, json!("Alice"));
421 }
422
423 #[test]
424 fn test_item_object_access() {
425 let context = build_expression_context(
426 &json!({}),
427 Some(&json!({"name": "Item 1", "selected": true})),
428 None,
429 );
430
431 let result = evaluate_expression("item.name", &context).unwrap();
432 assert_eq!(result, json!("Item 1"));
433 }
434
435 #[test]
436 fn test_item_ternary() {
437 let context =
438 build_expression_context(&json!({}), Some(&json!({"selected": true})), None);
439
440 let result =
441 evaluate_expression("item.selected ? '#FFA7E1' : '#374151'", &context).unwrap();
442 assert_eq!(result, json!("#FFA7E1"));
443 }
444
445 #[test]
446 fn test_template_string_simple() {
447 let state = json!({"user": {"name": "Alice"}});
448 let evaluator = build_evaluator(&state, None, None);
449 let result = evaluate_template_string("Hello @{state.user.name}!", &evaluator).unwrap();
450 assert_eq!(result, "Hello Alice!");
451 }
452
453 #[test]
454 fn test_template_string_with_expression() {
455 let state = json!({"selected": true});
456 let evaluator = build_evaluator(&state, None, None);
457 let result = evaluate_template_string(
458 "Color: @{state.selected ? '#FFA7E1' : '#374151'}",
459 &evaluator,
460 )
461 .unwrap();
462 assert_eq!(result, "Color: #FFA7E1");
463 }
464
465 #[test]
466 fn test_template_string_multiple_expressions() {
467 let state = json!({"name": "Alice", "count": 5});
468 let evaluator = build_evaluator(&state, None, None);
469 let result =
470 evaluate_template_string("@{state.name} has @{state.count} items", &evaluator)
471 .unwrap();
472 assert_eq!(result, "Alice has 5 items");
473 }
474
475 #[test]
476 fn test_template_with_item() {
477 let state = json!({});
478 let item = json!({"name": "Product", "price": 99});
479 let evaluator = build_evaluator(&state, Some(&item), None);
480 let result = evaluate_template_string("@{item.name}: $@{item.price}", &evaluator).unwrap();
481 assert_eq!(result, "Product: $99");
482 }
483
484 #[test]
485 fn test_string_concatenation() {
486 let mut context = HashMap::new();
487 context.insert("first".to_string(), json!("Hello"));
488 context.insert("second".to_string(), json!("World"));
489
490 let result = evaluate_expression("first + ' ' + second", &context).unwrap();
491 assert_eq!(result, json!("Hello World"));
492 }
493
494 #[test]
495 fn test_logical_and() {
496 let mut context = HashMap::new();
497 context.insert("a".to_string(), json!(true));
498 context.insert("b".to_string(), json!(false));
499
500 let result = evaluate_expression("a && b", &context).unwrap();
501 assert_eq!(result, json!(false));
502 }
503
504 #[test]
505 fn test_logical_or() {
506 let mut context = HashMap::new();
507 context.insert("a".to_string(), json!(false));
508 context.insert("b".to_string(), json!(true));
509
510 let result = evaluate_expression("a || b", &context).unwrap();
511 assert_eq!(result, json!(true));
512 }
513
514 #[test]
515 fn test_template_string_multibyte_before_expression() {
516 let state = json!({"a": "ALPHA", "b": "BETA"});
521 let evaluator = build_evaluator(&state, None, None);
522
523 let result =
525 evaluate_template_string("@{state.a} · @{state.b}", &evaluator).unwrap();
526 assert_eq!(result, "ALPHA · BETA");
527
528 let result =
529 evaluate_template_string("prefix · @{state.a}", &evaluator).unwrap();
530 assert_eq!(result, "prefix · ALPHA");
531
532 let result = evaluate_template_string("@{state.a} — @{state.b}", &evaluator).unwrap();
534 assert_eq!(result, "ALPHA — BETA");
535
536 let result = evaluate_template_string("🍕 @{state.a}", &evaluator).unwrap();
538 assert_eq!(result, "🍕 ALPHA");
539
540 let result = evaluate_template_string("你好 @{state.a}", &evaluator).unwrap();
542 assert_eq!(result, "你好 ALPHA");
543
544 let result =
546 evaluate_template_string("café @{state.a}", &evaluator).unwrap();
547 assert_eq!(result, "café ALPHA");
548 }
549
550 #[test]
551 fn test_template_string_multibyte_inside_replacement() {
552 let state = json!({"a": "café", "b": "naïve"});
555 let evaluator = build_evaluator(&state, None, None);
556
557 let result =
558 evaluate_template_string("@{state.a} · @{state.b}", &evaluator).unwrap();
559 assert_eq!(result, "café · naïve");
560 }
561
562 #[test]
563 fn test_template_string_realistic_restaurant_example() {
564 let state = json!({
568 "restaurant": {
569 "cuisine": "Italian",
570 "description": "Wood-fired pizza"
571 }
572 });
573 let evaluator = build_evaluator(&state, None, None);
574 let result = evaluate_template_string(
575 "@{state.restaurant.cuisine} · @{state.restaurant.description}",
576 &evaluator,
577 )
578 .unwrap();
579 assert_eq!(result, "Italian · Wood-fired pizza");
580 }
581
582 #[test]
583 fn test_length_function_strings_arrays_objects() {
584 let state = json!({
589 "empty": "",
590 "hello": "hello",
591 "unicode": "café", "items_empty": [],
593 "items": [1, 2, 3],
594 "obj": { "a": 1, "b": 2 },
595 });
596 let ev = build_evaluator(&state, None, None);
597
598 assert_eq!(
600 evaluate_template_string("@{length(state.empty)}", &ev).unwrap(),
601 "0"
602 );
603 assert_eq!(
604 evaluate_template_string("@{length(state.hello)}", &ev).unwrap(),
605 "5"
606 );
607 assert_eq!(
609 evaluate_template_string("@{length(state.unicode)}", &ev).unwrap(),
610 "4"
611 );
612
613 assert_eq!(
615 evaluate_template_string("@{length(state.items_empty)}", &ev).unwrap(),
616 "0"
617 );
618 assert_eq!(
619 evaluate_template_string("@{length(state.items)}", &ev).unwrap(),
620 "3"
621 );
622
623 assert_eq!(
625 evaluate_template_string("@{length(state.obj)}", &ev).unwrap(),
626 "2"
627 );
628 }
629
630 #[test]
631 fn test_length_function_empty_search_query_condition() {
632 let state = json!({ "searchQuery": "" });
637 let ev = build_evaluator(&state, None, None);
638
639 let mut ctx = HashMap::new();
640 ctx.insert("state".to_string(), state.clone());
641
642 let result = evaluate_expression("length(state.searchQuery) == 0", &ctx).unwrap();
643 assert_eq!(result, json!(true));
644
645 let populated = json!({ "searchQuery": "pizza" });
646 let mut ctx = HashMap::new();
647 ctx.insert("state".to_string(), populated);
648 let result = evaluate_expression("length(state.searchQuery) == 0", &ctx).unwrap();
649 assert_eq!(result, json!(false));
650
651 let _ = ev; }
656
657 #[test]
658 fn test_length_function_null_and_errors() {
659 let state = json!({ "missing": null });
660 let ev = build_evaluator(&state, None, None);
661
662 assert_eq!(
665 evaluate_template_string("@{length(state.missing)}", &ev).unwrap(),
666 "0"
667 );
668
669 let err = evaluate_template_string("@{length()}", &ev).unwrap_err();
671 match err {
672 EngineError::ExpressionError(msg) => {
673 assert!(
674 msg.contains("expected 1") || msg.contains("arg"),
675 "expected arity error, got: {}",
676 msg
677 );
678 }
679 _ => panic!("expected ExpressionError"),
680 }
681 }
682
683 #[test]
684 fn test_template_string_unclosed_expression() {
685 let state = json!({});
686 let evaluator = build_evaluator(&state, None, None);
687 let err = evaluate_template_string("prefix @{state.a", &evaluator).unwrap_err();
688 match err {
689 EngineError::ExpressionError(msg) => {
690 assert!(msg.contains("Unclosed"));
691 }
692 _ => panic!("expected ExpressionError"),
693 }
694 }
695
696 #[test]
697 fn test_complex_expression() {
698 let context = build_expression_context(
699 &json!({
700 "user": {
701 "premium": true,
702 "age": 25
703 }
704 }),
705 None,
706 None,
707 );
708
709 let result = evaluate_expression(
710 "state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'",
711 &context,
712 )
713 .unwrap();
714 assert_eq!(result, json!("VIP Adult"));
715 }
716}