1use exprimo::Evaluator;
9use serde_json::Value;
10use std::collections::{HashMap, HashSet};
11
12use super::{Binding, BindingSource};
13use crate::error::EngineError;
14
15pub fn evaluate_expression(
34 expr: &str,
35 context: &HashMap<String, Value>,
36) -> Result<Value, EngineError> {
37 let evaluator = Evaluator::new(context.clone(), HashMap::new());
38 evaluator
39 .evaluate(expr)
40 .map_err(|e| EngineError::ExpressionError(e.to_string()))
41}
42
43pub fn build_expression_context(state: &Value, item: Option<&Value>) -> HashMap<String, Value> {
50 build_expression_context_with_data_sources(state, item, None)
51}
52
53pub fn build_expression_context_with_data_sources(
55 state: &Value,
56 item: Option<&Value>,
57 data_sources: Option<&indexmap::IndexMap<String, Value>>,
58) -> HashMap<String, Value> {
59 let mut context = HashMap::new();
60
61 context.insert("state".to_string(), state.clone());
63
64 if let Some(item_value) = item {
66 context.insert("item".to_string(), item_value.clone());
67 }
68
69 if let Some(ds_map) = data_sources {
71 for (provider, ds_state) in ds_map {
72 context.insert(provider.clone(), ds_state.clone());
73 }
74 }
75
76 context
77}
78
79pub fn extract_bindings_from_expression(expr: &str) -> Vec<Binding> {
95 let mut bindings = Vec::new();
96 let mut seen_paths: HashSet<String> = HashSet::new();
97
98 for prefix in &["state.", "item."] {
100 let source = if *prefix == "state." {
101 BindingSource::State
102 } else {
103 BindingSource::Item
104 };
105
106 let mut search_pos = 0;
107 while let Some(start) = expr[search_pos..].find(prefix) {
108 let abs_start = search_pos + start;
109
110 if abs_start > 0 {
113 let prev_char = expr.chars().nth(abs_start - 1).unwrap_or(' ');
114 if prev_char.is_ascii_alphanumeric() || prev_char == '_' {
115 search_pos = abs_start + prefix.len();
116 continue;
117 }
118 }
119
120 let path_start = abs_start + prefix.len();
122 let mut path_end = path_start;
123
124 let chars: Vec<char> = expr.chars().collect();
126 while path_end < chars.len() {
127 let c = chars[path_end];
128 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
129 path_end += 1;
130 } else {
131 break;
132 }
133 }
134
135 if path_end > path_start {
136 let path_str: String = chars[path_start..path_end].iter().collect();
137 let path_str = path_str.trim_end_matches('.');
139
140 if !path_str.is_empty() {
141 let full_path = format!("{}{}", prefix, path_str);
142 if !seen_paths.contains(&full_path) {
143 seen_paths.insert(full_path);
144 let path: Vec<String> =
145 path_str.split('.').map(|s| s.to_string()).collect();
146 bindings.push(Binding::new(source.clone(), path));
147 }
148 }
149 }
150
151 search_pos = path_end.max(abs_start + prefix.len());
152 }
153 }
154
155 extract_data_source_bindings_from_expression(expr, &mut bindings, &mut seen_paths);
158
159 bindings
160}
161
162fn extract_data_source_bindings_from_expression(
164 expr: &str,
165 bindings: &mut Vec<Binding>,
166 seen_paths: &mut HashSet<String>,
167) {
168 let chars: Vec<char> = expr.chars().collect();
169 let len = chars.len();
170 let mut pos = 0;
171
172 let reserved = ["state", "item", "true", "false", "null"];
174
175 while pos < len {
176 if !chars[pos].is_ascii_alphabetic() && chars[pos] != '_' {
178 pos += 1;
179 continue;
180 }
181
182 if pos > 0 && (chars[pos - 1].is_ascii_alphanumeric() || chars[pos - 1] == '_') {
184 pos += 1;
185 continue;
186 }
187
188 let ident_start = pos;
190 while pos < len && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
191 pos += 1;
192 }
193 let ident: String = chars[ident_start..pos].iter().collect();
194
195 if pos >= len || chars[pos] != '.' {
197 continue;
198 }
199
200 if reserved.contains(&ident.as_str()) {
202 continue;
203 }
204
205 let path_start = pos + 1; let mut path_end = path_start;
208 while path_end < len
209 && (chars[path_end].is_ascii_alphanumeric()
210 || chars[path_end] == '_'
211 || chars[path_end] == '.')
212 {
213 path_end += 1;
214 }
215
216 if path_end > path_start {
217 let path_str: String = chars[path_start..path_end].iter().collect();
218 let path_str = path_str.trim_end_matches('.');
219 if !path_str.is_empty() {
220 let full_path = format!("{}.{}", ident, path_str);
221 if !seen_paths.contains(&full_path) {
222 seen_paths.insert(full_path);
223 let path: Vec<String> =
224 path_str.split('.').map(|s| s.to_string()).collect();
225 bindings.push(Binding::data_source(&ident, path));
226 }
227 }
228 }
229
230 pos = path_end;
231 }
232}
233
234pub fn is_expression(s: &str) -> bool {
239 s.contains('?')
241 || s.contains("&&")
242 || s.contains("||")
243 || s.contains("==")
244 || s.contains("!=")
245 || s.contains(">=")
246 || s.contains("<=")
247 || s.contains('>') && !s.contains("->") || s.contains('<') && !s.contains("<-")
249 || s.contains('+')
250 || s.contains('-') && !s.starts_with('-') || s.contains('*')
252 || s.contains('/')
253 || s.contains('%')
254 || s.contains('!')
255}
256
257pub fn evaluate_template_string(
265 template: &str,
266 state: &Value,
267 item: Option<&Value>,
268) -> Result<String, EngineError> {
269 evaluate_template_string_full(template, state, item, None)
270}
271
272pub fn evaluate_template_string_full(
274 template: &str,
275 state: &Value,
276 item: Option<&Value>,
277 data_sources: Option<&indexmap::IndexMap<String, Value>>,
278) -> Result<String, EngineError> {
279 let context = build_expression_context_with_data_sources(state, item, data_sources);
280 let mut result = template.to_string();
281 let mut pos = 0;
282
283 while let Some(start) = result[pos..].find("${") {
284 let abs_start = pos + start;
285
286 let mut depth = 1;
288 let mut end_pos = abs_start + 2;
289 let chars: Vec<char> = result.chars().collect();
290
291 while end_pos < chars.len() && depth > 0 {
292 match chars[end_pos] {
293 '{' => depth += 1,
294 '}' => depth -= 1,
295 _ => {}
296 }
297 if depth > 0 {
298 end_pos += 1;
299 }
300 }
301
302 if depth != 0 {
303 return Err(EngineError::ExpressionError(
304 "Unclosed expression in template".to_string(),
305 ));
306 }
307
308 let expr_content: String = chars[abs_start + 2..end_pos].iter().collect();
310
311 let value = evaluate_expression(&expr_content, &context)?;
313
314 let replacement = match &value {
316 Value::String(s) => s.clone(),
317 Value::Number(n) => n.to_string(),
318 Value::Bool(b) => b.to_string(),
319 Value::Null => "null".to_string(),
320 _ => serde_json::to_string(&value).unwrap_or_default(),
321 };
322
323 let pattern: String = chars[abs_start..=end_pos].iter().collect();
325 result = result.replacen(&pattern, &replacement, 1);
326
327 pos = 0;
329 }
330
331 Ok(result)
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337 use serde_json::json;
338
339 #[test]
340 fn test_simple_expression() {
341 let mut context = HashMap::new();
342 context.insert("x".to_string(), json!(5));
343 context.insert("y".to_string(), json!(3));
344
345 let result = evaluate_expression("x + y", &context).unwrap();
346 assert_eq!(result.as_f64().unwrap(), 8.0);
348 }
349
350 #[test]
351 fn test_ternary_expression() {
352 let mut context = HashMap::new();
353 context.insert("selected".to_string(), json!(true));
354
355 let result = evaluate_expression("selected ? 'yes' : 'no'", &context).unwrap();
356 assert_eq!(result, json!("yes"));
357 }
358
359 #[test]
360 fn test_ternary_with_colors() {
361 let mut context = HashMap::new();
362 context.insert("selected".to_string(), json!(true));
363
364 let result = evaluate_expression("selected ? '#FFA7E1' : '#374151'", &context).unwrap();
365 assert_eq!(result, json!("#FFA7E1"));
366 }
367
368 #[test]
369 fn test_comparison_expression() {
370 let mut context = HashMap::new();
371 context.insert("count".to_string(), json!(15));
372
373 let result = evaluate_expression("count > 10", &context).unwrap();
374 assert_eq!(result, json!(true));
375 }
376
377 #[test]
378 fn test_state_object_access() {
379 let context =
380 build_expression_context(&json!({"user": {"name": "Alice", "age": 30}}), None);
381
382 let result = evaluate_expression("state.user.name", &context).unwrap();
383 assert_eq!(result, json!("Alice"));
384 }
385
386 #[test]
387 fn test_item_object_access() {
388 let context = build_expression_context(
389 &json!({}),
390 Some(&json!({"name": "Item 1", "selected": true})),
391 );
392
393 let result = evaluate_expression("item.name", &context).unwrap();
394 assert_eq!(result, json!("Item 1"));
395 }
396
397 #[test]
398 fn test_item_ternary() {
399 let context = build_expression_context(&json!({}), Some(&json!({"selected": true})));
400
401 let result =
402 evaluate_expression("item.selected ? '#FFA7E1' : '#374151'", &context).unwrap();
403 assert_eq!(result, json!("#FFA7E1"));
404 }
405
406 #[test]
407 fn test_template_string_simple() {
408 let state = json!({"user": {"name": "Alice"}});
409 let result = evaluate_template_string("Hello ${state.user.name}!", &state, None).unwrap();
410 assert_eq!(result, "Hello Alice!");
411 }
412
413 #[test]
414 fn test_template_string_with_expression() {
415 let state = json!({"selected": true});
416 let result = evaluate_template_string(
417 "Color: ${state.selected ? '#FFA7E1' : '#374151'}",
418 &state,
419 None,
420 )
421 .unwrap();
422 assert_eq!(result, "Color: #FFA7E1");
423 }
424
425 #[test]
426 fn test_template_string_multiple_expressions() {
427 let state = json!({"name": "Alice", "count": 5});
428 let result =
429 evaluate_template_string("${state.name} has ${state.count} items", &state, None)
430 .unwrap();
431 assert_eq!(result, "Alice has 5 items");
432 }
433
434 #[test]
435 fn test_template_with_item() {
436 let state = json!({});
437 let item = json!({"name": "Product", "price": 99});
438 let result =
439 evaluate_template_string("${item.name}: $${item.price}", &state, Some(&item)).unwrap();
440 assert_eq!(result, "Product: $99");
441 }
442
443 #[test]
444 fn test_is_expression() {
445 assert!(!is_expression("state.user.name"));
447 assert!(!is_expression("item.selected"));
448
449 assert!(is_expression("selected ? 'a' : 'b'"));
451 assert!(is_expression("count > 10"));
452 assert!(is_expression("a && b"));
453 assert!(is_expression("a || b"));
454 assert!(is_expression("a == b"));
455 assert!(is_expression("a + b"));
456 }
457
458 #[test]
459 fn test_string_concatenation() {
460 let mut context = HashMap::new();
461 context.insert("first".to_string(), json!("Hello"));
462 context.insert("second".to_string(), json!("World"));
463
464 let result = evaluate_expression("first + ' ' + second", &context).unwrap();
465 assert_eq!(result, json!("Hello World"));
466 }
467
468 #[test]
469 fn test_logical_and() {
470 let mut context = HashMap::new();
471 context.insert("a".to_string(), json!(true));
472 context.insert("b".to_string(), json!(false));
473
474 let result = evaluate_expression("a && b", &context).unwrap();
475 assert_eq!(result, json!(false));
476 }
477
478 #[test]
479 fn test_logical_or() {
480 let mut context = HashMap::new();
481 context.insert("a".to_string(), json!(false));
482 context.insert("b".to_string(), json!(true));
483
484 let result = evaluate_expression("a || b", &context).unwrap();
485 assert_eq!(result, json!(true));
486 }
487
488 #[test]
489 fn test_complex_expression() {
490 let context = build_expression_context(
491 &json!({
492 "user": {
493 "premium": true,
494 "age": 25
495 }
496 }),
497 None,
498 );
499
500 let result = evaluate_expression(
501 "state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'",
502 &context,
503 )
504 .unwrap();
505 assert_eq!(result, json!("VIP Adult"));
506 }
507}