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(format!("{:?}", e)))
41}
42
43pub fn build_expression_context(state: &Value, item: Option<&Value>) -> 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> =
127 path_str.split('.').map(|s| s.to_string()).collect();
128 bindings.push(Binding::new(source.clone(), path));
129 }
130 }
131 }
132
133 search_pos = path_end.max(abs_start + prefix.len());
134 }
135 }
136
137 bindings
138}
139
140pub fn is_expression(s: &str) -> bool {
145 s.contains('?')
147 || s.contains("&&")
148 || s.contains("||")
149 || s.contains("==")
150 || s.contains("!=")
151 || s.contains(">=")
152 || s.contains("<=")
153 || s.contains('>') && !s.contains("->") || s.contains('<') && !s.contains("<-")
155 || s.contains('+')
156 || s.contains('-') && !s.starts_with('-') || s.contains('*')
158 || s.contains('/')
159 || s.contains('%')
160 || s.contains('!')
161}
162
163pub fn evaluate_template_string(
170 template: &str,
171 state: &Value,
172 item: Option<&Value>,
173) -> Result<String, EngineError> {
174 let context = build_expression_context(state, item);
175 let mut result = template.to_string();
176 let mut pos = 0;
177
178 while let Some(start) = result[pos..].find("${") {
179 let abs_start = pos + start;
180
181 let mut depth = 1;
183 let mut end_pos = abs_start + 2;
184 let chars: Vec<char> = result.chars().collect();
185
186 while end_pos < chars.len() && depth > 0 {
187 match chars[end_pos] {
188 '{' => depth += 1,
189 '}' => depth -= 1,
190 _ => {}
191 }
192 if depth > 0 {
193 end_pos += 1;
194 }
195 }
196
197 if depth != 0 {
198 return Err(EngineError::ExpressionError(
199 "Unclosed expression in template".to_string(),
200 ));
201 }
202
203 let expr_content: String = chars[abs_start + 2..end_pos].iter().collect();
205
206 let value = evaluate_expression(&expr_content, &context)?;
208
209 let replacement = match &value {
211 Value::String(s) => s.clone(),
212 Value::Number(n) => n.to_string(),
213 Value::Bool(b) => b.to_string(),
214 Value::Null => "null".to_string(),
215 _ => serde_json::to_string(&value).unwrap_or_default(),
216 };
217
218 let pattern: String = chars[abs_start..=end_pos].iter().collect();
220 result = result.replacen(&pattern, &replacement, 1);
221
222 pos = 0;
224 }
225
226 Ok(result)
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232 use serde_json::json;
233
234 #[test]
235 fn test_simple_expression() {
236 let mut context = HashMap::new();
237 context.insert("x".to_string(), json!(5));
238 context.insert("y".to_string(), json!(3));
239
240 let result = evaluate_expression("x + y", &context).unwrap();
241 assert_eq!(result.as_f64().unwrap(), 8.0);
243 }
244
245 #[test]
246 fn test_ternary_expression() {
247 let mut context = HashMap::new();
248 context.insert("selected".to_string(), json!(true));
249
250 let result = evaluate_expression("selected ? 'yes' : 'no'", &context).unwrap();
251 assert_eq!(result, json!("yes"));
252 }
253
254 #[test]
255 fn test_ternary_with_colors() {
256 let mut context = HashMap::new();
257 context.insert("selected".to_string(), json!(true));
258
259 let result = evaluate_expression("selected ? '#FFA7E1' : '#374151'", &context).unwrap();
260 assert_eq!(result, json!("#FFA7E1"));
261 }
262
263 #[test]
264 fn test_comparison_expression() {
265 let mut context = HashMap::new();
266 context.insert("count".to_string(), json!(15));
267
268 let result = evaluate_expression("count > 10", &context).unwrap();
269 assert_eq!(result, json!(true));
270 }
271
272 #[test]
273 fn test_state_object_access() {
274 let context =
275 build_expression_context(&json!({"user": {"name": "Alice", "age": 30}}), None);
276
277 let result = evaluate_expression("state.user.name", &context).unwrap();
278 assert_eq!(result, json!("Alice"));
279 }
280
281 #[test]
282 fn test_item_object_access() {
283 let context = build_expression_context(
284 &json!({}),
285 Some(&json!({"name": "Item 1", "selected": true})),
286 );
287
288 let result = evaluate_expression("item.name", &context).unwrap();
289 assert_eq!(result, json!("Item 1"));
290 }
291
292 #[test]
293 fn test_item_ternary() {
294 let context = build_expression_context(&json!({}), Some(&json!({"selected": true})));
295
296 let result =
297 evaluate_expression("item.selected ? '#FFA7E1' : '#374151'", &context).unwrap();
298 assert_eq!(result, json!("#FFA7E1"));
299 }
300
301 #[test]
302 fn test_template_string_simple() {
303 let state = json!({"user": {"name": "Alice"}});
304 let result = evaluate_template_string("Hello ${state.user.name}!", &state, None).unwrap();
305 assert_eq!(result, "Hello Alice!");
306 }
307
308 #[test]
309 fn test_template_string_with_expression() {
310 let state = json!({"selected": true});
311 let result = evaluate_template_string(
312 "Color: ${state.selected ? '#FFA7E1' : '#374151'}",
313 &state,
314 None,
315 )
316 .unwrap();
317 assert_eq!(result, "Color: #FFA7E1");
318 }
319
320 #[test]
321 fn test_template_string_multiple_expressions() {
322 let state = json!({"name": "Alice", "count": 5});
323 let result =
324 evaluate_template_string("${state.name} has ${state.count} items", &state, None)
325 .unwrap();
326 assert_eq!(result, "Alice has 5 items");
327 }
328
329 #[test]
330 fn test_template_with_item() {
331 let state = json!({});
332 let item = json!({"name": "Product", "price": 99});
333 let result =
334 evaluate_template_string("${item.name}: $${item.price}", &state, Some(&item)).unwrap();
335 assert_eq!(result, "Product: $99");
336 }
337
338 #[test]
339 fn test_is_expression() {
340 assert!(!is_expression("state.user.name"));
342 assert!(!is_expression("item.selected"));
343
344 assert!(is_expression("selected ? 'a' : 'b'"));
346 assert!(is_expression("count > 10"));
347 assert!(is_expression("a && b"));
348 assert!(is_expression("a || b"));
349 assert!(is_expression("a == b"));
350 assert!(is_expression("a + b"));
351 }
352
353 #[test]
354 fn test_string_concatenation() {
355 let mut context = HashMap::new();
356 context.insert("first".to_string(), json!("Hello"));
357 context.insert("second".to_string(), json!("World"));
358
359 let result = evaluate_expression("first + ' ' + second", &context).unwrap();
360 assert_eq!(result, json!("Hello World"));
361 }
362
363 #[test]
364 fn test_logical_and() {
365 let mut context = HashMap::new();
366 context.insert("a".to_string(), json!(true));
367 context.insert("b".to_string(), json!(false));
368
369 let result = evaluate_expression("a && b", &context).unwrap();
370 assert_eq!(result, json!(false));
371 }
372
373 #[test]
374 fn test_logical_or() {
375 let mut context = HashMap::new();
376 context.insert("a".to_string(), json!(false));
377 context.insert("b".to_string(), json!(true));
378
379 let result = evaluate_expression("a || b", &context).unwrap();
380 assert_eq!(result, json!(true));
381 }
382
383 #[test]
384 fn test_complex_expression() {
385 let context = build_expression_context(
386 &json!({
387 "user": {
388 "premium": true,
389 "age": 25
390 }
391 }),
392 None,
393 );
394
395 let result = evaluate_expression(
396 "state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'",
397 &context,
398 )
399 .unwrap();
400 assert_eq!(result, json!("VIP Adult"));
401 }
402}