1use serde_json::Value;
5use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
13pub struct StepResult {
14 pub output: Value,
15 pub confidence: Option<f32>,
16}
17
18pub struct ExprContext {
20 pub input: Value,
21 pub steps: HashMap<String, StepResult>,
22}
23
24impl ExprContext {
25 pub fn new(input: Value) -> Self {
26 Self {
27 input,
28 steps: HashMap::new(),
29 }
30 }
31
32 pub fn eval(&self, expr: &str) -> Result<Value, ExprError> {
39 let trimmed = expr.trim();
40
41 if let Some(inner) = trimmed
43 .strip_prefix("{{")
44 .and_then(|s| s.strip_suffix("}}"))
45 {
46 return self.resolve_path(inner.trim());
47 }
48
49 if !expr.contains("{{") {
51 return Ok(Value::String(expr.to_string()));
52 }
53
54 let mut result = String::with_capacity(expr.len());
56 let mut remaining = expr;
57 while let Some(start) = remaining.find("{{") {
58 result.push_str(&remaining[..start]);
59 let after_open = &remaining[start + 2..];
60 let end = after_open
61 .find("}}")
62 .ok_or_else(|| ExprError::Syntax(expr.to_string()))?;
63 let path = after_open[..end].trim();
64 let value = self.resolve_path(path)?;
65 match value {
66 Value::String(s) => result.push_str(&s),
67 other => result.push_str(&other.to_string()),
68 }
69 remaining = &after_open[end + 2..];
70 }
71 result.push_str(remaining);
72 Ok(Value::String(result))
73 }
74
75 pub fn eval_f32(&self, expr: &str) -> Result<f32, ExprError> {
77 let value = self.eval(expr)?;
78 match value {
79 Value::Number(n) => n.as_f64().map(|f| f as f32).ok_or(ExprError::NotNumeric),
80 _ => Err(ExprError::NotNumeric),
81 }
82 }
83
84 fn resolve_path(&self, path: &str) -> Result<Value, ExprError> {
85 if path == "input" {
86 return Ok(self.input.clone());
87 }
88
89 if let Some(rest) = path.strip_prefix("input.") {
91 return json_get(&self.input, rest)
92 .ok_or_else(|| ExprError::UnknownPath(path.to_string()));
93 }
94
95 let parts: Vec<&str> = path.splitn(3, '.').collect();
96 match parts.as_slice() {
97 ["steps", name, "output"] => self
99 .steps
100 .get(*name)
101 .map(|r| r.output.clone())
102 .ok_or_else(|| ExprError::UnknownStep((*name).to_string())),
103 ["steps", name, "confidence"] => {
104 let result = self
105 .steps
106 .get(*name)
107 .ok_or_else(|| ExprError::UnknownStep((*name).to_string()))?;
108 let score = result
109 .confidence
110 .ok_or_else(|| ExprError::NoConfidence((*name).to_string()))?;
111 Ok(Value::Number(
112 serde_json::Number::from_f64(score as f64)
114 .expect("confidence is always finite"),
115 ))
116 }
117 ["steps", name, rest] if rest.starts_with("output.") => {
119 let step = self
120 .steps
121 .get(*name)
122 .ok_or_else(|| ExprError::UnknownStep((*name).to_string()))?;
123 let field_path = rest.strip_prefix("output.").unwrap();
124 json_get(&step.output, field_path)
125 .ok_or_else(|| ExprError::UnknownPath(path.to_string()))
126 }
127 _ => Err(ExprError::UnknownPath(path.to_string())),
128 }
129 }
130}
131
132fn json_get(value: &Value, path: &str) -> Option<Value> {
134 let mut current = value;
135 let mut owned;
136 for key in path.split('.') {
137 match current.get(key) {
138 Some(v) => {
139 owned = v.clone();
140 current = &owned;
141 }
142 None => return None,
143 }
144 }
145 Some(current.clone())
146}
147
148#[derive(Debug, thiserror::Error)]
149pub enum ExprError {
150 #[error("syntax error in expression: {0}")]
151 Syntax(String),
152 #[error("unknown step: {0}")]
153 UnknownStep(String),
154 #[error("unknown path: {0}")]
155 UnknownPath(String),
156 #[error("value is not numeric")]
157 NotNumeric,
158 #[error("step '{0}' produced no confidence score — use a handler that emits confidence")]
161 NoConfidence(String),
162}