use exprimo::Evaluator;
use serde_json::Value;
use std::collections::{HashMap, HashSet};
use super::{Binding, BindingSource};
use crate::error::EngineError;
pub fn evaluate_expression(
expr: &str,
context: &HashMap<String, Value>,
) -> Result<Value, EngineError> {
let evaluator = Evaluator::new(context.clone(), HashMap::new());
evaluator
.evaluate(expr)
.map_err(|e| EngineError::ExpressionError(e.to_string()))
}
pub fn build_expression_context(
state: &Value,
item: Option<&Value>,
data_sources: Option<&indexmap::IndexMap<String, Value>>,
) -> HashMap<String, Value> {
let mut context = HashMap::new();
context.insert("state".to_string(), state.clone());
if let Some(item_value) = item {
context.insert("item".to_string(), item_value.clone());
}
if let Some(ds_map) = data_sources {
for (provider, ds_state) in ds_map {
context.insert(provider.clone(), ds_state.clone());
}
}
context
}
pub fn extract_bindings_from_expression(expr: &str) -> Vec<Binding> {
let mut bindings = Vec::new();
let mut seen_paths: HashSet<String> = HashSet::new();
for prefix in &["state.", "item."] {
let source = if *prefix == "state." {
BindingSource::State
} else {
BindingSource::Item
};
let mut search_pos = 0;
while let Some(start) = expr[search_pos..].find(prefix) {
let abs_start = search_pos + start;
if abs_start > 0 {
let prev_char = expr.chars().nth(abs_start - 1).unwrap_or(' ');
if prev_char.is_ascii_alphanumeric() || prev_char == '_' {
search_pos = abs_start + prefix.len();
continue;
}
}
let path_start = abs_start + prefix.len();
let mut path_end = path_start;
let chars: Vec<char> = expr.chars().collect();
while path_end < chars.len() {
let c = chars[path_end];
if c.is_ascii_alphanumeric() || c == '_' || c == '.' {
path_end += 1;
} else {
break;
}
}
if path_end > path_start {
let path_str: String = chars[path_start..path_end].iter().collect();
let path_str = path_str.trim_end_matches('.');
if !path_str.is_empty() {
let full_path = format!("{}{}", prefix, path_str);
if !seen_paths.contains(&full_path) {
seen_paths.insert(full_path);
let path: Vec<String> =
path_str.split('.').map(|s| s.to_string()).collect();
bindings.push(Binding::new(source.clone(), path));
}
}
}
search_pos = path_end.max(abs_start + prefix.len());
}
}
extract_data_source_bindings_from_expression(expr, &mut bindings, &mut seen_paths);
bindings
}
fn extract_data_source_bindings_from_expression(
expr: &str,
bindings: &mut Vec<Binding>,
seen_paths: &mut HashSet<String>,
) {
let chars: Vec<char> = expr.chars().collect();
let len = chars.len();
let mut pos = 0;
let reserved = ["state", "item", "true", "false", "null"];
while pos < len {
if !chars[pos].is_ascii_alphabetic() && chars[pos] != '_' {
pos += 1;
continue;
}
if pos > 0 && (chars[pos - 1].is_ascii_alphanumeric() || chars[pos - 1] == '_') {
pos += 1;
continue;
}
let ident_start = pos;
while pos < len && (chars[pos].is_ascii_alphanumeric() || chars[pos] == '_') {
pos += 1;
}
let ident: String = chars[ident_start..pos].iter().collect();
if pos >= len || chars[pos] != '.' {
continue;
}
if reserved.contains(&ident.as_str()) {
continue;
}
let path_start = pos + 1; let mut path_end = path_start;
while path_end < len
&& (chars[path_end].is_ascii_alphanumeric()
|| chars[path_end] == '_'
|| chars[path_end] == '.')
{
path_end += 1;
}
if path_end > path_start {
let path_str: String = chars[path_start..path_end].iter().collect();
let path_str = path_str.trim_end_matches('.');
if !path_str.is_empty() {
let full_path = format!("{}.{}", ident, path_str);
if !seen_paths.contains(&full_path) {
seen_paths.insert(full_path);
let path: Vec<String> =
path_str.split('.').map(|s| s.to_string()).collect();
bindings.push(Binding::data_source(&ident, path));
}
}
}
pos = path_end;
}
}
pub fn build_evaluator(
state: &Value,
item: Option<&Value>,
data_sources: Option<&indexmap::IndexMap<String, Value>>,
) -> Evaluator {
let context = build_expression_context(state, item, data_sources);
Evaluator::new(context, HashMap::new())
}
pub fn evaluate_template_string(
template: &str,
evaluator: &Evaluator,
) -> Result<String, EngineError> {
let mut result = template.to_string();
let mut pos = 0;
while let Some(start) = result[pos..].find("@{") {
let abs_start = pos + start;
let mut depth = 1;
let mut end_pos = abs_start + 2;
let chars: Vec<char> = result.chars().collect();
while end_pos < chars.len() && depth > 0 {
match chars[end_pos] {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
if depth > 0 {
end_pos += 1;
}
}
if depth != 0 {
return Err(EngineError::ExpressionError(
"Unclosed expression in template".to_string(),
));
}
let expr_content: String = chars[abs_start + 2..end_pos].iter().collect();
let value = evaluator
.evaluate(&expr_content)
.map_err(|e| EngineError::ExpressionError(e.to_string()))?;
let replacement = match &value {
Value::String(s) => s.clone(),
Value::Number(n) => n.to_string(),
Value::Bool(b) => b.to_string(),
Value::Null => "null".to_string(),
_ => serde_json::to_string(&value).unwrap_or_default(),
};
let pattern: String = chars[abs_start..=end_pos].iter().collect();
result = result.replacen(&pattern, &replacement, 1);
pos = 0;
}
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_simple_expression() {
let mut context = HashMap::new();
context.insert("x".to_string(), json!(5));
context.insert("y".to_string(), json!(3));
let result = evaluate_expression("x + y", &context).unwrap();
assert_eq!(result.as_f64().unwrap(), 8.0);
}
#[test]
fn test_ternary_expression() {
let mut context = HashMap::new();
context.insert("selected".to_string(), json!(true));
let result = evaluate_expression("selected ? 'yes' : 'no'", &context).unwrap();
assert_eq!(result, json!("yes"));
}
#[test]
fn test_ternary_with_colors() {
let mut context = HashMap::new();
context.insert("selected".to_string(), json!(true));
let result = evaluate_expression("selected ? '#FFA7E1' : '#374151'", &context).unwrap();
assert_eq!(result, json!("#FFA7E1"));
}
#[test]
fn test_comparison_expression() {
let mut context = HashMap::new();
context.insert("count".to_string(), json!(15));
let result = evaluate_expression("count > 10", &context).unwrap();
assert_eq!(result, json!(true));
}
#[test]
fn test_state_object_access() {
let context =
build_expression_context(&json!({"user": {"name": "Alice", "age": 30}}), None, None);
let result = evaluate_expression("state.user.name", &context).unwrap();
assert_eq!(result, json!("Alice"));
}
#[test]
fn test_item_object_access() {
let context = build_expression_context(
&json!({}),
Some(&json!({"name": "Item 1", "selected": true})),
None,
);
let result = evaluate_expression("item.name", &context).unwrap();
assert_eq!(result, json!("Item 1"));
}
#[test]
fn test_item_ternary() {
let context =
build_expression_context(&json!({}), Some(&json!({"selected": true})), None);
let result =
evaluate_expression("item.selected ? '#FFA7E1' : '#374151'", &context).unwrap();
assert_eq!(result, json!("#FFA7E1"));
}
#[test]
fn test_template_string_simple() {
let state = json!({"user": {"name": "Alice"}});
let evaluator = build_evaluator(&state, None, None);
let result = evaluate_template_string("Hello @{state.user.name}!", &evaluator).unwrap();
assert_eq!(result, "Hello Alice!");
}
#[test]
fn test_template_string_with_expression() {
let state = json!({"selected": true});
let evaluator = build_evaluator(&state, None, None);
let result = evaluate_template_string(
"Color: @{state.selected ? '#FFA7E1' : '#374151'}",
&evaluator,
)
.unwrap();
assert_eq!(result, "Color: #FFA7E1");
}
#[test]
fn test_template_string_multiple_expressions() {
let state = json!({"name": "Alice", "count": 5});
let evaluator = build_evaluator(&state, None, None);
let result =
evaluate_template_string("@{state.name} has @{state.count} items", &evaluator)
.unwrap();
assert_eq!(result, "Alice has 5 items");
}
#[test]
fn test_template_with_item() {
let state = json!({});
let item = json!({"name": "Product", "price": 99});
let evaluator = build_evaluator(&state, Some(&item), None);
let result = evaluate_template_string("@{item.name}: $@{item.price}", &evaluator).unwrap();
assert_eq!(result, "Product: $99");
}
#[test]
fn test_string_concatenation() {
let mut context = HashMap::new();
context.insert("first".to_string(), json!("Hello"));
context.insert("second".to_string(), json!("World"));
let result = evaluate_expression("first + ' ' + second", &context).unwrap();
assert_eq!(result, json!("Hello World"));
}
#[test]
fn test_logical_and() {
let mut context = HashMap::new();
context.insert("a".to_string(), json!(true));
context.insert("b".to_string(), json!(false));
let result = evaluate_expression("a && b", &context).unwrap();
assert_eq!(result, json!(false));
}
#[test]
fn test_logical_or() {
let mut context = HashMap::new();
context.insert("a".to_string(), json!(false));
context.insert("b".to_string(), json!(true));
let result = evaluate_expression("a || b", &context).unwrap();
assert_eq!(result, json!(true));
}
#[test]
fn test_complex_expression() {
let context = build_expression_context(
&json!({
"user": {
"premium": true,
"age": 25
}
}),
None,
None,
);
let result = evaluate_expression(
"state.user.premium && state.user.age >= 18 ? 'VIP Adult' : 'Standard'",
&context,
)
.unwrap();
assert_eq!(result, json!("VIP Adult"));
}
}