Skip to main content

cruxx_script/
expr.rs

1/// Minimal expression evaluator for `{{ path }}` references in YAML values.
2///
3/// Supports: `{{ input }}`, `{{ steps.<name>.output }}`, `{{ steps.<name>.confidence }}`.
4use serde_json::Value;
5use std::collections::HashMap;
6
7/// Result of a completed step, used for expression resolution.
8#[derive(Debug, Clone)]
9pub struct StepResult {
10    pub output: Value,
11    pub confidence: f32,
12}
13
14/// Evaluation context holding pipeline state.
15pub struct ExprContext {
16    pub input: Value,
17    pub steps: HashMap<String, StepResult>,
18}
19
20impl ExprContext {
21    pub fn new(input: Value) -> Self {
22        Self {
23            input,
24            steps: HashMap::new(),
25        }
26    }
27
28    /// Evaluate an expression string. Returns the resolved Value.
29    ///
30    /// If the string is `{{ path }}`, resolves it. Otherwise returns
31    /// the string as a JSON string value.
32    pub fn eval(&self, expr: &str) -> Result<Value, ExprError> {
33        let trimmed = expr.trim();
34        if let Some(path) = trimmed.strip_prefix("{{") {
35            let path = path
36                .strip_suffix("}}")
37                .ok_or_else(|| ExprError::Syntax(expr.to_string()))?;
38            self.resolve_path(path.trim())
39        } else {
40            Ok(Value::String(expr.to_string()))
41        }
42    }
43
44    /// Evaluate an expression to f32 (for confidence routing).
45    pub fn eval_f32(&self, expr: &str) -> Result<f32, ExprError> {
46        let value = self.eval(expr)?;
47        match value {
48            Value::Number(n) => n.as_f64().map(|f| f as f32).ok_or(ExprError::NotNumeric),
49            _ => Err(ExprError::NotNumeric),
50        }
51    }
52
53    fn resolve_path(&self, path: &str) -> Result<Value, ExprError> {
54        if path == "input" {
55            return Ok(self.input.clone());
56        }
57
58        let parts: Vec<&str> = path.splitn(3, '.').collect();
59        match parts.as_slice() {
60            ["steps", name, "output"] => self
61                .steps
62                .get(*name)
63                .map(|r| r.output.clone())
64                .ok_or_else(|| ExprError::UnknownStep((*name).to_string())),
65            ["steps", name, "confidence"] => self
66                .steps
67                .get(*name)
68                .map(|r| Value::Number(serde_json::Number::from_f64(r.confidence as f64).unwrap()))
69                .ok_or_else(|| ExprError::UnknownStep((*name).to_string())),
70            _ => Err(ExprError::UnknownPath(path.to_string())),
71        }
72    }
73}
74
75#[derive(Debug, thiserror::Error)]
76pub enum ExprError {
77    #[error("syntax error in expression: {0}")]
78    Syntax(String),
79    #[error("unknown step: {0}")]
80    UnknownStep(String),
81    #[error("unknown path: {0}")]
82    UnknownPath(String),
83    #[error("value is not numeric")]
84    NotNumeric,
85}