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(
50 state: &Value,
51 item: Option<&Value>,
52 data_sources: Option<&indexmap::IndexMap<String, Value>>,
53) -> HashMap<String, Value> {
54 let mut context = HashMap::new();
55
56 context.insert("state".to_string(), state.clone());
58
59 if let Some(item_value) = item {
61 context.insert("item".to_string(), item_value.clone());
62 }
63
64 if let Some(ds_map) = data_sources {
66 for (provider, ds_state) in ds_map {
67 context.insert(provider.clone(), ds_state.clone());
68 }
69 }
70
71 context
72}
73
74pub fn extract_bindings_from_expression(expr: &str) -> Vec<Binding> {
90 let mut bindings = Vec::new();
91 let mut seen_paths: HashSet<String> = HashSet::new();
92
93 for prefix in &["state.", "item."] {
95 let source = if *prefix == "state." {
96 BindingSource::State
97 } else {
98 BindingSource::Item
99 };
100
101 let mut search_pos = 0;
102 while let Some(start) = expr[search_pos..].find(prefix) {
103 let abs_start = search_pos + start;
104
105 if abs_start > 0 {
108 let prev_char = expr.chars().nth(abs_start - 1).unwrap_or(' ');
109 if prev_char.is_ascii_alphanumeric() || prev_char == '_' {
110 search_pos = abs_start + prefix.len();
111 continue;
112 }
113 }
114
115 let path_start = abs_start + prefix.len();
117 let mut path_end = path_start;
118
119 let chars: Vec<char> = expr.chars().collect();
121 while path_end < chars.len() {
122 let c = chars[path_end];
123 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
124 path_end += 1;
125 } else {
126 break;
127 }
128 }
129
130 if path_end > path_start {
131 let path_str: String = chars[path_start..path_end].iter().collect();
132 let path_str = path_str.trim_end_matches('.');
134
135 if !path_str.is_empty() {
136 let full_path = format!("{}{}", prefix, path_str);
137 if !seen_paths.contains(&full_path) {
138 seen_paths.insert(full_path);
139 let path: Vec<String> =
140 path_str.split('.').map(|s| s.to_string()).collect();
141 bindings.push(Binding::new(source.clone(), path));
142 }
143 }
144 }
145
146 search_pos = path_end.max(abs_start + prefix.len());
147 }
148 }
149
150 extract_data_source_bindings_from_expression(expr, &mut bindings, &mut seen_paths);
153
154 bindings
155}
156
157fn extract_data_source_bindings_from_expression(
159 expr: &str,
160 bindings: &mut Vec<Binding>,
161 seen_paths: &mut HashSet<String>,
162) {
163 let chars: Vec<char> = expr.chars().collect();
164 let len = chars.len();
165 let mut pos = 0;
166
167 let reserved = ["state", "item", "true", "false", "null"];
169
170 while pos < len {
171 if !chars[pos].is_ascii_alphabetic() && chars[pos] != '_' {
173 pos += 1;
174 continue;
175 }
176
177 if pos > 0 && (chars[pos - 1].is_ascii_alphanumeric() || chars[pos - 1] == '_') {
179 pos += 1;
180 continue;
181 }
182
183 let ident_start = pos;
185 while pos < len && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
186 pos += 1;
187 }
188 let ident: String = chars[ident_start..pos].iter().collect();
189
190 if pos >= len || chars[pos] != '.' {
192 continue;
193 }
194
195 if reserved.contains(&ident.as_str()) {
197 continue;
198 }
199
200 let path_start = pos + 1; let mut path_end = path_start;
203 while path_end < len
204 && (chars[path_end].is_ascii_alphanumeric()
205 || chars[path_end] == '_'
206 || chars[path_end] == '.')
207 {
208 path_end += 1;
209 }
210
211 if path_end > path_start {
212 let path_str: String = chars[path_start..path_end].iter().collect();
213 let path_str = path_str.trim_end_matches('.');
214 if !path_str.is_empty() {
215 let full_path = format!("{}.{}", ident, path_str);
216 if !seen_paths.contains(&full_path) {
217 seen_paths.insert(full_path);
218 let path: Vec<String> =
219 path_str.split('.').map(|s| s.to_string()).collect();
220 bindings.push(Binding::data_source(&ident, path));
221 }
222 }
223 }
224
225 pos = path_end;
226 }
227}
228
229pub fn build_evaluator(
235 state: &Value,
236 item: Option<&Value>,
237 data_sources: Option<&indexmap::IndexMap<String, Value>>,
238) -> Evaluator {
239 let context = build_expression_context(state, item, data_sources);
240 Evaluator::new(context, HashMap::new())
241}
242
243pub fn evaluate_template_string(
249 template: &str,
250 evaluator: &Evaluator,
251) -> Result<String, EngineError> {
252 let mut result = template.to_string();
253 let mut pos = 0;
254
255 while let Some(start) = result[pos..].find("@{") {
256 let abs_start = pos + start;
257
258 let mut depth = 1;
260 let mut end_pos = abs_start + 2;
261 let chars: Vec<char> = result.chars().collect();
262
263 while end_pos < chars.len() && depth > 0 {
264 match chars[end_pos] {
265 '{' => depth += 1,
266 '}' => depth -= 1,
267 _ => {}
268 }
269 if depth > 0 {
270 end_pos += 1;
271 }
272 }
273
274 if depth != 0 {
275 return Err(EngineError::ExpressionError(
276 "Unclosed expression in template".to_string(),
277 ));
278 }
279
280 let expr_content: String = chars[abs_start + 2..end_pos].iter().collect();
282
283 let value = evaluator
285 .evaluate(&expr_content)
286 .map_err(|e| EngineError::ExpressionError(e.to_string()))?;
287
288 let replacement = match &value {
290 Value::String(s) => s.clone(),
291 Value::Number(n) => n.to_string(),
292 Value::Bool(b) => b.to_string(),
293 Value::Null => "null".to_string(),
294 _ => serde_json::to_string(&value).unwrap_or_default(),
295 };
296
297 let pattern: String = chars[abs_start..=end_pos].iter().collect();
299 result = result.replacen(&pattern, &replacement, 1);
300
301 pos = 0;
303 }
304
305 Ok(result)
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311 use serde_json::json;
312
313 #[test]
314 fn test_simple_expression() {
315 let mut context = HashMap::new();
316 context.insert("x".to_string(), json!(5));
317 context.insert("y".to_string(), json!(3));
318
319 let result = evaluate_expression("x + y", &context).unwrap();
320 assert_eq!(result.as_f64().unwrap(), 8.0);
322 }
323
324 #[test]
325 fn test_ternary_expression() {
326 let mut context = HashMap::new();
327 context.insert("selected".to_string(), json!(true));
328
329 let result = evaluate_expression("selected ? 'yes' : 'no'", &context).unwrap();
330 assert_eq!(result, json!("yes"));
331 }
332
333 #[test]
334 fn test_ternary_with_colors() {
335 let mut context = HashMap::new();
336 context.insert("selected".to_string(), json!(true));
337
338 let result = evaluate_expression("selected ? '#FFA7E1' : '#374151'", &context).unwrap();
339 assert_eq!(result, json!("#FFA7E1"));
340 }
341
342 #[test]
343 fn test_comparison_expression() {
344 let mut context = HashMap::new();
345 context.insert("count".to_string(), json!(15));
346
347 let result = evaluate_expression("count > 10", &context).unwrap();
348 assert_eq!(result, json!(true));
349 }
350
351 #[test]
352 fn test_state_object_access() {
353 let context =
354 build_expression_context(&json!({"user": {"name": "Alice", "age": 30}}), None, None);
355
356 let result = evaluate_expression("state.user.name", &context).unwrap();
357 assert_eq!(result, json!("Alice"));
358 }
359
360 #[test]
361 fn test_item_object_access() {
362 let context = build_expression_context(
363 &json!({}),
364 Some(&json!({"name": "Item 1", "selected": true})),
365 None,
366 );
367
368 let result = evaluate_expression("item.name", &context).unwrap();
369 assert_eq!(result, json!("Item 1"));
370 }
371
372 #[test]
373 fn test_item_ternary() {
374 let context =
375 build_expression_context(&json!({}), Some(&json!({"selected": true})), None);
376
377 let result =
378 evaluate_expression("item.selected ? '#FFA7E1' : '#374151'", &context).unwrap();
379 assert_eq!(result, json!("#FFA7E1"));
380 }
381
382 #[test]
383 fn test_template_string_simple() {
384 let state = json!({"user": {"name": "Alice"}});
385 let evaluator = build_evaluator(&state, None, None);
386 let result = evaluate_template_string("Hello @{state.user.name}!", &evaluator).unwrap();
387 assert_eq!(result, "Hello Alice!");
388 }
389
390 #[test]
391 fn test_template_string_with_expression() {
392 let state = json!({"selected": true});
393 let evaluator = build_evaluator(&state, None, None);
394 let result = evaluate_template_string(
395 "Color: @{state.selected ? '#FFA7E1' : '#374151'}",
396 &evaluator,
397 )
398 .unwrap();
399 assert_eq!(result, "Color: #FFA7E1");
400 }
401
402 #[test]
403 fn test_template_string_multiple_expressions() {
404 let state = json!({"name": "Alice", "count": 5});
405 let evaluator = build_evaluator(&state, None, None);
406 let result =
407 evaluate_template_string("@{state.name} has @{state.count} items", &evaluator)
408 .unwrap();
409 assert_eq!(result, "Alice has 5 items");
410 }
411
412 #[test]
413 fn test_template_with_item() {
414 let state = json!({});
415 let item = json!({"name": "Product", "price": 99});
416 let evaluator = build_evaluator(&state, Some(&item), None);
417 let result = evaluate_template_string("@{item.name}: $@{item.price}", &evaluator).unwrap();
418 assert_eq!(result, "Product: $99");
419 }
420
421 #[test]
422 fn test_string_concatenation() {
423 let mut context = HashMap::new();
424 context.insert("first".to_string(), json!("Hello"));
425 context.insert("second".to_string(), json!("World"));
426
427 let result = evaluate_expression("first + ' ' + second", &context).unwrap();
428 assert_eq!(result, json!("Hello World"));
429 }
430
431 #[test]
432 fn test_logical_and() {
433 let mut context = HashMap::new();
434 context.insert("a".to_string(), json!(true));
435 context.insert("b".to_string(), json!(false));
436
437 let result = evaluate_expression("a && b", &context).unwrap();
438 assert_eq!(result, json!(false));
439 }
440
441 #[test]
442 fn test_logical_or() {
443 let mut context = HashMap::new();
444 context.insert("a".to_string(), json!(false));
445 context.insert("b".to_string(), json!(true));
446
447 let result = evaluate_expression("a || b", &context).unwrap();
448 assert_eq!(result, json!(true));
449 }
450
451 #[test]
452 fn test_complex_expression() {
453 let context = build_expression_context(
454 &json!({
455 "user": {
456 "premium": true,
457 "age": 25
458 }
459 }),
460 None,
461 None,
462 );
463
464 let result = evaluate_expression(
465 "state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'",
466 &context,
467 )
468 .unwrap();
469 assert_eq!(result, json!("VIP Adult"));
470 }
471}