#![deny(unsafe_code)]
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "kind", deny_unknown_fields, rename_all = "snake_case")]
pub enum Node {
Step {
#[serde(rename = "ref")]
ref_: String,
#[serde(rename = "in")]
in_: Expr,
out: Expr,
},
Seq { children: Vec<Node> },
Branch {
cond: Expr,
#[serde(rename = "then")]
then_: Box<Node>,
#[serde(rename = "else")]
else_: Box<Node>,
},
Fanout {
items: Expr,
bind: Expr,
body: Box<Node>,
join: JoinMode,
out: Expr,
},
Loop {
counter: Expr,
cond: Expr,
body: Box<Node>,
max: u32,
},
Try {
body: Box<Node>,
catch: Box<Node>,
#[serde(default)]
err_at: Option<Expr>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum JoinMode {
All,
Any,
Race,
AllSettled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "op", deny_unknown_fields, rename_all = "snake_case")]
pub enum Expr {
Path { at: String },
Lit { value: Value },
Eq { lhs: Box<Expr>, rhs: Box<Expr> },
}
pub trait Dispatcher {
fn dispatch(&self, ref_: &str, input: Value) -> Result<Value, EvalError>;
}
impl<F> Dispatcher for F
where
F: Fn(&str, Value) -> Result<Value, EvalError>,
{
fn dispatch(&self, ref_: &str, input: Value) -> Result<Value, EvalError> {
self(ref_, input)
}
}
#[derive(Debug, Error)]
pub enum EvalError {
#[error("path not found: {0}")]
PathNotFound(String),
#[error("invalid path syntax: {0}")]
InvalidPath(String),
#[error("branch cond must be boolean, got: {0}")]
NonBoolCond(Value),
#[error("dispatcher error for ref '{ref_}': {msg}")]
DispatcherError { ref_: String, msg: String },
}
pub fn eval<D: Dispatcher>(node: &Node, ctx: Value, dispatcher: &D) -> Result<Value, EvalError> {
match node {
Node::Step { ref_, in_, out } => {
let input = eval_expr(in_, &ctx)?;
let output =
dispatcher
.dispatch(ref_, input)
.map_err(|e| EvalError::DispatcherError {
ref_: ref_.clone(),
msg: e.to_string(),
})?;
write_path(out, ctx, output)
}
Node::Seq { children } => {
let mut cur = ctx;
for child in children {
cur = eval(child, cur, dispatcher)?;
}
Ok(cur)
}
Node::Branch { cond, then_, else_ } => match eval_expr(cond, &ctx)? {
Value::Bool(true) => eval(then_, ctx, dispatcher),
Value::Bool(false) => eval(else_, ctx, dispatcher),
other => Err(EvalError::NonBoolCond(other)),
},
Node::Fanout {
items,
bind,
body,
join,
out,
} => {
let items_val = eval_expr(items, &ctx)?;
let items_arr = match items_val {
Value::Array(a) => a,
other => {
return Err(EvalError::DispatcherError {
ref_: "fanout.items".into(),
msg: format!("expected array, got {other:?}"),
})
}
};
let joined: Value = match join {
JoinMode::All => {
let mut results = Vec::with_capacity(items_arr.len());
for item in items_arr {
let branch_ctx = write_path(bind, ctx.clone(), item)?;
results.push(eval(body, branch_ctx, dispatcher)?);
}
Value::Array(results)
}
JoinMode::Any => {
let mut winner: Option<Value> = None;
let mut last_err: Option<EvalError> = None;
for item in items_arr {
let branch_ctx = write_path(bind, ctx.clone(), item)?;
match eval(body, branch_ctx, dispatcher) {
Ok(v) => {
winner = Some(v);
last_err = None;
break;
}
Err(e) => last_err = Some(e),
}
}
if let Some(e) = last_err {
return Err(e);
}
winner.unwrap_or(Value::Array(vec![]))
}
JoinMode::Race => {
if let Some(first) = items_arr.into_iter().next() {
let branch_ctx = write_path(bind, ctx.clone(), first)?;
eval(body, branch_ctx, dispatcher)?
} else {
Value::Array(vec![])
}
}
JoinMode::AllSettled => {
let mut records = Vec::with_capacity(items_arr.len());
for item in items_arr {
let branch_ctx = write_path(bind, ctx.clone(), item)?;
match eval(body, branch_ctx, dispatcher) {
Ok(v) => {
records.push(serde_json::json!({"status": "fulfilled", "value": v}))
}
Err(e) => records.push(
serde_json::json!({"status": "rejected", "reason": e.to_string()}),
),
}
}
Value::Array(records)
}
};
write_path(out, ctx, joined)
}
Node::Loop {
counter,
cond,
body,
max,
} => {
let mut cur = write_path(counter, ctx, Value::Number(serde_json::Number::from(0u32)))?;
let mut n: u32 = 0;
while n < *max && is_truthy(&eval_expr(cond, &cur)?) {
cur = eval(body, cur, dispatcher)?;
n += 1;
cur = write_path(counter, cur, Value::Number(serde_json::Number::from(n)))?;
}
Ok(cur)
}
Node::Try {
body,
catch,
err_at,
} => match eval(body, ctx.clone(), dispatcher) {
Ok(v) => Ok(v),
Err(e) => {
let cur = match err_at {
Some(at) => write_path(at, ctx, Value::String(e.to_string()))?,
None => ctx,
};
eval(catch, cur, dispatcher)
}
},
}
}
pub fn is_truthy(v: &Value) -> bool {
match v {
Value::Null => false,
Value::Bool(b) => *b,
_ => true,
}
}
pub fn eval_expr(expr: &Expr, ctx: &Value) -> Result<Value, EvalError> {
match expr {
Expr::Lit { value } => Ok(value.clone()),
Expr::Path { at } => read_path(at, ctx),
Expr::Eq { lhs, rhs } => {
let lv = eval_expr(lhs, ctx)?;
let rv = eval_expr(rhs, ctx)?;
Ok(Value::Bool(lv == rv))
}
}
}
pub fn read_path(path: &str, ctx: &Value) -> Result<Value, EvalError> {
let trimmed = strip_path_prefix(path)?;
if trimmed.is_empty() {
return Ok(ctx.clone());
}
let mut cur = ctx;
for key in trimmed.split('.') {
cur = cur
.get(key)
.ok_or_else(|| EvalError::PathNotFound(path.to_string()))?;
}
Ok(cur.clone())
}
pub fn write_path(out: &Expr, ctx: Value, value: Value) -> Result<Value, EvalError> {
let path = match out {
Expr::Path { at } => at,
_ => {
return Err(EvalError::InvalidPath(
"Step.out must be a Path expr".into(),
))
}
};
let trimmed = strip_path_prefix(path)?;
let keys: Vec<&str> = trimmed.split('.').filter(|s| !s.is_empty()).collect();
if keys.is_empty() {
return Ok(value);
}
let mut root = ctx;
write_path_recursive(&mut root, &keys, value);
Ok(root)
}
fn strip_path_prefix(path: &str) -> Result<&str, EvalError> {
path.strip_prefix("$.")
.or_else(|| path.strip_prefix('$'))
.ok_or_else(|| EvalError::InvalidPath(format!("path must start with $ or $.: {}", path)))
}
fn write_path_recursive(node: &mut Value, keys: &[&str], value: Value) {
if keys.is_empty() {
*node = value;
return;
}
if !node.is_object() {
*node = Value::Object(serde_json::Map::new());
}
let obj = node.as_object_mut().expect("just initialised as object");
let key = keys[0];
if keys.len() == 1 {
obj.insert(key.to_string(), value);
} else {
let entry = obj
.entry(key.to_string())
.or_insert(Value::Object(serde_json::Map::new()));
write_path_recursive(entry, &keys[1..], value);
}
}