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(expr: &str, context: &HashMap<String, Value>) -> Result<Value, EngineError> {
34 let evaluator = Evaluator::new(context.clone(), HashMap::new());
35 evaluator
36 .evaluate(expr)
37 .map_err(|e| EngineError::ExpressionError(format!("{:?}", e)))
38}
39
40pub fn build_expression_context(
47 state: &Value,
48 item: Option<&Value>,
49) -> HashMap<String, Value> {
50 let mut context = HashMap::new();
51
52 context.insert("state".to_string(), state.clone());
54
55 if let Some(item_value) = item {
57 context.insert("item".to_string(), item_value.clone());
58 }
59
60 context
61}
62
63pub fn extract_bindings_from_expression(expr: &str) -> Vec<Binding> {
77 let mut bindings = Vec::new();
78 let mut seen_paths: HashSet<String> = HashSet::new();
79
80 for prefix in &["state.", "item."] {
82 let source = if *prefix == "state." {
83 BindingSource::State
84 } else {
85 BindingSource::Item
86 };
87
88 let mut search_pos = 0;
89 while let Some(start) = expr[search_pos..].find(prefix) {
90 let abs_start = search_pos + start;
91
92 if abs_start > 0 {
95 let prev_char = expr.chars().nth(abs_start - 1).unwrap_or(' ');
96 if prev_char.is_ascii_alphanumeric() || prev_char == '_' {
97 search_pos = abs_start + prefix.len();
98 continue;
99 }
100 }
101
102 let path_start = abs_start + prefix.len();
104 let mut path_end = path_start;
105
106 let chars: Vec<char> = expr.chars().collect();
108 while path_end < chars.len() {
109 let c = chars[path_end];
110 if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
111 path_end += 1;
112 } else {
113 break;
114 }
115 }
116
117 if path_end > path_start {
118 let path_str: String = chars[path_start..path_end].iter().collect();
119 let path_str = path_str.trim_end_matches('.');
121
122 if !path_str.is_empty() {
123 let full_path = format!("{}{}", prefix, path_str);
124 if !seen_paths.contains(&full_path) {
125 seen_paths.insert(full_path);
126 let path: Vec<String> = path_str.split('.').map(|s| s.to_string()).collect();
127 bindings.push(Binding::new(source.clone(), path));
128 }
129 }
130 }
131
132 search_pos = path_end.max(abs_start + prefix.len());
133 }
134 }
135
136 bindings
137}
138
139pub fn is_expression(s: &str) -> bool {
144 s.contains('?')
146 || s.contains("&&")
147 || s.contains("||")
148 || s.contains("==")
149 || s.contains("!=")
150 || s.contains(">=")
151 || s.contains("<=")
152 || s.contains('>') && !s.contains("->") || s.contains('<') && !s.contains("<-")
154 || s.contains('+')
155 || s.contains('-') && !s.starts_with('-') || s.contains('*')
157 || s.contains('/')
158 || s.contains('%')
159 || s.contains('!')
160}
161
162pub fn evaluate_template_string(
169 template: &str,
170 state: &Value,
171 item: Option<&Value>,
172) -> Result<String, EngineError> {
173 let context = build_expression_context(state, item);
174 let mut result = template.to_string();
175 let mut pos = 0;
176
177 while let Some(start) = result[pos..].find("${") {
178 let abs_start = pos + start;
179
180 let mut depth = 1;
182 let mut end_pos = abs_start + 2;
183 let chars: Vec<char> = result.chars().collect();
184
185 while end_pos < chars.len() && depth > 0 {
186 match chars[end_pos] {
187 '{' => depth += 1,
188 '}' => depth -= 1,
189 _ => {}
190 }
191 if depth > 0 {
192 end_pos += 1;
193 }
194 }
195
196 if depth != 0 {
197 return Err(EngineError::ExpressionError(
198 "Unclosed expression in template".to_string(),
199 ));
200 }
201
202 let expr_content: String = chars[abs_start + 2..end_pos].iter().collect();
204
205 let value = evaluate_expression(&expr_content, &context)?;
207
208 let replacement = match &value {
210 Value::String(s) => s.clone(),
211 Value::Number(n) => n.to_string(),
212 Value::Bool(b) => b.to_string(),
213 Value::Null => "null".to_string(),
214 _ => serde_json::to_string(&value).unwrap_or_default(),
215 };
216
217 let pattern: String = chars[abs_start..=end_pos].iter().collect();
219 result = result.replacen(&pattern, &replacement, 1);
220
221 pos = 0;
223 }
224
225 Ok(result)
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use serde_json::json;
232
233 #[test]
234 fn test_simple_expression() {
235 let mut context = HashMap::new();
236 context.insert("x".to_string(), json!(5));
237 context.insert("y".to_string(), json!(3));
238
239 let result = evaluate_expression("x + y", &context).unwrap();
240 assert_eq!(result.as_f64().unwrap(), 8.0);
242 }
243
244 #[test]
245 fn test_ternary_expression() {
246 let mut context = HashMap::new();
247 context.insert("selected".to_string(), json!(true));
248
249 let result = evaluate_expression("selected ? 'yes' : 'no'", &context).unwrap();
250 assert_eq!(result, json!("yes"));
251 }
252
253 #[test]
254 fn test_ternary_with_colors() {
255 let mut context = HashMap::new();
256 context.insert("selected".to_string(), json!(true));
257
258 let result = evaluate_expression("selected ? '#FFA7E1' : '#374151'", &context).unwrap();
259 assert_eq!(result, json!("#FFA7E1"));
260 }
261
262 #[test]
263 fn test_comparison_expression() {
264 let mut context = HashMap::new();
265 context.insert("count".to_string(), json!(15));
266
267 let result = evaluate_expression("count > 10", &context).unwrap();
268 assert_eq!(result, json!(true));
269 }
270
271 #[test]
272 fn test_state_object_access() {
273 let context = build_expression_context(
274 &json!({"user": {"name": "Alice", "age": 30}}),
275 None,
276 );
277
278 let result = evaluate_expression("state.user.name", &context).unwrap();
279 assert_eq!(result, json!("Alice"));
280 }
281
282 #[test]
283 fn test_item_object_access() {
284 let context = build_expression_context(
285 &json!({}),
286 Some(&json!({"name": "Item 1", "selected": true})),
287 );
288
289 let result = evaluate_expression("item.name", &context).unwrap();
290 assert_eq!(result, json!("Item 1"));
291 }
292
293 #[test]
294 fn test_item_ternary() {
295 let context = build_expression_context(
296 &json!({}),
297 Some(&json!({"selected": true})),
298 );
299
300 let result = evaluate_expression("item.selected ? '#FFA7E1' : '#374151'", &context).unwrap();
301 assert_eq!(result, json!("#FFA7E1"));
302 }
303
304 #[test]
305 fn test_template_string_simple() {
306 let state = json!({"user": {"name": "Alice"}});
307 let result = evaluate_template_string("Hello ${state.user.name}!", &state, None).unwrap();
308 assert_eq!(result, "Hello Alice!");
309 }
310
311 #[test]
312 fn test_template_string_with_expression() {
313 let state = json!({"selected": true});
314 let result = evaluate_template_string(
315 "Color: ${state.selected ? '#FFA7E1' : '#374151'}",
316 &state,
317 None,
318 )
319 .unwrap();
320 assert_eq!(result, "Color: #FFA7E1");
321 }
322
323 #[test]
324 fn test_template_string_multiple_expressions() {
325 let state = json!({"name": "Alice", "count": 5});
326 let result = evaluate_template_string(
327 "${state.name} has ${state.count} items",
328 &state,
329 None,
330 )
331 .unwrap();
332 assert_eq!(result, "Alice has 5 items");
333 }
334
335 #[test]
336 fn test_template_with_item() {
337 let state = json!({});
338 let item = json!({"name": "Product", "price": 99});
339 let result = evaluate_template_string(
340 "${item.name}: $${item.price}",
341 &state,
342 Some(&item),
343 )
344 .unwrap();
345 assert_eq!(result, "Product: $99");
346 }
347
348 #[test]
349 fn test_is_expression() {
350 assert!(!is_expression("state.user.name"));
352 assert!(!is_expression("item.selected"));
353
354 assert!(is_expression("selected ? 'a' : 'b'"));
356 assert!(is_expression("count > 10"));
357 assert!(is_expression("a && b"));
358 assert!(is_expression("a || b"));
359 assert!(is_expression("a == b"));
360 assert!(is_expression("a + b"));
361 }
362
363 #[test]
364 fn test_string_concatenation() {
365 let mut context = HashMap::new();
366 context.insert("first".to_string(), json!("Hello"));
367 context.insert("second".to_string(), json!("World"));
368
369 let result = evaluate_expression("first + ' ' + second", &context).unwrap();
370 assert_eq!(result, json!("Hello World"));
371 }
372
373 #[test]
374 fn test_logical_and() {
375 let mut context = HashMap::new();
376 context.insert("a".to_string(), json!(true));
377 context.insert("b".to_string(), json!(false));
378
379 let result = evaluate_expression("a && b", &context).unwrap();
380 assert_eq!(result, json!(false));
381 }
382
383 #[test]
384 fn test_logical_or() {
385 let mut context = HashMap::new();
386 context.insert("a".to_string(), json!(false));
387 context.insert("b".to_string(), json!(true));
388
389 let result = evaluate_expression("a || b", &context).unwrap();
390 assert_eq!(result, json!(true));
391 }
392
393 #[test]
394 fn test_complex_expression() {
395 let context = build_expression_context(
396 &json!({
397 "user": {
398 "premium": true,
399 "age": 25
400 }
401 }),
402 None,
403 );
404
405 let result = evaluate_expression(
406 "state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'",
407 &context,
408 )
409 .unwrap();
410 assert_eq!(result, json!("VIP Adult"));
411 }
412}