use anyhow::Result;
use rhai::{Dynamic, Engine, Map, Scope};
use std::collections::HashMap;
pub fn evaluate_condition(
condition: &str,
variables: &HashMap<String, String>,
) -> Result<bool> {
let expression = if condition.trim().starts_with("{{") && condition.trim().ends_with("}}") {
condition
.trim()
.strip_prefix("{{")
.unwrap()
.strip_suffix("}}")
.unwrap()
.trim()
} else {
condition.trim()
};
let mut rendered = expression.to_string();
for (key, value) in variables {
rendered = rendered.replace(key, value);
}
fn strip_quotes(s: &str) -> String {
let s = s.trim();
if (s.starts_with('"') && s.ends_with('"')) || (s.starts_with('\'') && s.ends_with('\'')) {
s[1..s.len() - 1].to_string()
} else {
s.to_string()
}
}
if rendered.contains("==") {
let parts: Vec<&str> = rendered.split("==").map(|s| s.trim()).collect();
if parts.len() == 2 {
return Ok(strip_quotes(parts[0]) == strip_quotes(parts[1]));
}
}
if rendered.contains("!=") {
let parts: Vec<&str> = rendered.split("!=").map(|s| s.trim()).collect();
if parts.len() == 2 {
return Ok(strip_quotes(parts[0]) != strip_quotes(parts[1]));
}
}
if rendered.contains(" in ") {
let parts: Vec<&str> = rendered.split(" in ").map(|s| s.trim()).collect();
if parts.len() == 2 {
let needle = strip_quotes(parts[0]);
let haystack = strip_quotes(parts[1]);
return Ok(haystack.contains(&needle));
}
}
let value = strip_quotes(&rendered);
Ok(!value.is_empty() && value != "false" && value != "0")
}
pub fn evaluate_rhai_condition(
code: &str,
variables: &HashMap<String, String>,
) -> Result<bool> {
let mut engine = Engine::new();
let mut scope = Scope::new();
let mut workload_map = Map::new();
for (key, value) in variables {
if key.starts_with("workload.") {
let short_key = key.strip_prefix("workload.").unwrap_or(key);
workload_map.insert(short_key.to_string().into(), Dynamic::from(value.clone()));
}
}
scope.push("workload", workload_map);
let mut vars_map = Map::new();
for (key, value) in variables {
if key.starts_with("vars.") {
let short_key = key.strip_prefix("vars.").unwrap_or(key);
vars_map.insert(short_key.to_string().into(), Dynamic::from(value.clone()));
}
}
scope.push("vars", vars_map);
for (key, value) in variables {
if !key.starts_with("workload.") && !key.starts_with("vars.") && key.contains('.') {
let parts: Vec<&str> = key.splitn(2, '.').collect();
if parts.len() == 2 {
let step_name = parts[0];
let field_name = parts[1];
if !scope.contains(step_name) {
scope.push(step_name.to_string(), Map::new());
}
if let Some(step_map) = scope.get_mut(step_name) {
if let Some(map) = step_map.clone().try_cast::<Map>() {
let mut map = map;
map.insert(field_name.to_string().into(), Dynamic::from(value.clone()));
*step_map = Dynamic::from(map);
}
}
}
}
}
engine.register_fn("eq", |a: &str, b: &str| -> bool { a == b });
engine.register_fn("ne", |a: &str, b: &str| -> bool { a != b });
engine.register_fn("contains", |haystack: &str, needle: &str| -> bool {
haystack.contains(needle)
});
let result = engine
.eval_with_scope::<Dynamic>(&mut scope, code)
.map_err(|e| anyhow::anyhow!("Rhai condition error: {}", e))?;
if result.is_bool() {
Ok(result.as_bool().unwrap_or(false))
} else if result.is_int() {
Ok(result.as_int().unwrap_or(0) != 0)
} else if result.is_string() {
let s = result.into_string().unwrap_or_default();
Ok(!s.is_empty() && s != "false" && s != "0")
} else {
Ok(!result.is_unit())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn vars(pairs: &[(&str, &str)]) -> HashMap<String, String> {
pairs.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn evaluate_condition_equality() {
let v = HashMap::new();
assert!(evaluate_condition("'test' == 'test'", &v).unwrap());
assert!(!evaluate_condition("'test' == 'other'", &v).unwrap());
}
#[test]
fn evaluate_condition_inequality() {
let v = HashMap::new();
assert!(evaluate_condition("'test' != 'other'", &v).unwrap());
assert!(!evaluate_condition("'test' != 'test'", &v).unwrap());
}
#[test]
fn evaluate_condition_in_operator() {
let v = HashMap::new();
assert!(evaluate_condition("'foo' in 'foobar'", &v).unwrap());
assert!(!evaluate_condition("'baz' in 'foobar'", &v).unwrap());
}
#[test]
fn evaluate_condition_substitutes_variables() {
let v = vars(&[("workload.action", "deploy")]);
assert!(evaluate_condition("workload.action == 'deploy'", &v).unwrap());
assert!(!evaluate_condition("workload.action == 'undeploy'", &v).unwrap());
}
#[test]
fn evaluate_rhai_condition_workload_field() {
let v = vars(&[("workload.count", "5")]);
assert!(evaluate_rhai_condition("workload.count == \"5\"", &v).unwrap());
assert!(!evaluate_rhai_condition("workload.count == \"6\"", &v).unwrap());
}
#[test]
fn evaluate_rhai_condition_helpers() {
let v = vars(&[("workload.action", "DEPLOY")]);
assert!(evaluate_rhai_condition("eq(workload.action, \"DEPLOY\")", &v).unwrap());
assert!(evaluate_rhai_condition(
"contains(workload.action, \"DEP\")",
&v
)
.unwrap());
}
}