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