use serde_json::Value;
use std::collections::HashMap;
#[derive(Debug, Clone)]
pub struct StepResult {
pub output: Value,
pub confidence: Option<f32>,
}
pub struct ExprContext {
pub input: Value,
pub steps: HashMap<String, StepResult>,
}
impl ExprContext {
pub fn new(input: Value) -> Self {
Self {
input,
steps: HashMap::new(),
}
}
pub fn eval(&self, expr: &str) -> Result<Value, ExprError> {
let trimmed = expr.trim();
if let Some(inner) = trimmed
.strip_prefix("{{")
.and_then(|s| s.strip_suffix("}}"))
{
return self.resolve_path(inner.trim());
}
if !expr.contains("{{") {
return Ok(Value::String(expr.to_string()));
}
let mut result = String::with_capacity(expr.len());
let mut remaining = expr;
while let Some(start) = remaining.find("{{") {
result.push_str(&remaining[..start]);
let after_open = &remaining[start + 2..];
let end = after_open
.find("}}")
.ok_or_else(|| ExprError::Syntax(expr.to_string()))?;
let path = after_open[..end].trim();
let value = self.resolve_path(path)?;
match value {
Value::String(s) => result.push_str(&s),
other => result.push_str(&other.to_string()),
}
remaining = &after_open[end + 2..];
}
result.push_str(remaining);
Ok(Value::String(result))
}
pub fn eval_f32(&self, expr: &str) -> Result<f32, ExprError> {
let value = self.eval(expr)?;
match value {
Value::Number(n) => n.as_f64().map(|f| f as f32).ok_or(ExprError::NotNumeric),
_ => Err(ExprError::NotNumeric),
}
}
fn resolve_path(&self, path: &str) -> Result<Value, ExprError> {
if path == "input" {
return Ok(self.input.clone());
}
if let Some(rest) = path.strip_prefix("input.") {
return json_get(&self.input, rest)
.ok_or_else(|| ExprError::UnknownPath(path.to_string()));
}
let parts: Vec<&str> = path.splitn(3, '.').collect();
match parts.as_slice() {
["steps", name, "output"] => self
.steps
.get(*name)
.map(|r| r.output.clone())
.ok_or_else(|| ExprError::UnknownStep((*name).to_string())),
["steps", name, "confidence"] => {
let result = self
.steps
.get(*name)
.ok_or_else(|| ExprError::UnknownStep((*name).to_string()))?;
let score = result
.confidence
.ok_or_else(|| ExprError::NoConfidence((*name).to_string()))?;
Ok(Value::Number(
serde_json::Number::from_f64(score as f64)
.expect("confidence is always finite"),
))
}
["steps", name, rest] if rest.starts_with("output.") => {
let step = self
.steps
.get(*name)
.ok_or_else(|| ExprError::UnknownStep((*name).to_string()))?;
let field_path = rest.strip_prefix("output.").unwrap();
json_get(&step.output, field_path)
.ok_or_else(|| ExprError::UnknownPath(path.to_string()))
}
_ => Err(ExprError::UnknownPath(path.to_string())),
}
}
}
fn json_get(value: &Value, path: &str) -> Option<Value> {
let mut current = value;
let mut owned;
for key in path.split('.') {
match current.get(key) {
Some(v) => {
owned = v.clone();
current = &owned;
}
None => return None,
}
}
Some(current.clone())
}
#[derive(Debug, thiserror::Error)]
pub enum ExprError {
#[error("syntax error in expression: {0}")]
Syntax(String),
#[error("unknown step: {0}")]
UnknownStep(String),
#[error("unknown path: {0}")]
UnknownPath(String),
#[error("value is not numeric")]
NotNumeric,
#[error("step '{0}' produced no confidence score — use a handler that emits confidence")]
NoConfidence(String),
}