#![deny(unsafe_code)]
pub use flow_ir_core::{
eval, eval_expr, eval_expr_with_externs, eval_externs, eval_with_storage,
eval_with_storage_externs, is_truthy, read_path, write_path, CtxStorage, Dispatcher, EvalError,
Expr, ExternFn, ExternMap, Externs, JoinMode, MemoryCtx, NoExterns, Node,
};
use serde_json::Value;
use std::sync::Arc;
use async_recursion::async_recursion;
use async_trait::async_trait;
#[async_trait]
pub trait AsyncDispatcher: Send + Sync {
async fn dispatch(&self, ref_: &str, input: Value) -> Result<Value, EvalError>;
}
pub async fn eval_async_with_storage<D>(
node: &Node,
ctx: Arc<dyn CtxStorage>,
dispatcher: &D,
) -> Result<(), EvalError>
where
D: AsyncDispatcher + ?Sized,
{
eval_async_with_storage_externs(node, ctx, dispatcher, &NoExterns).await
}
#[async_recursion]
pub async fn eval_async_with_storage_externs<D>(
node: &Node,
ctx: Arc<dyn CtxStorage>,
dispatcher: &D,
externs: &(dyn Externs + Sync),
) -> Result<(), EvalError>
where
D: AsyncDispatcher + ?Sized,
{
match node {
Node::Step { ref_, in_, out } => {
let snap = ctx.snapshot();
let input = eval_expr_with_externs(in_, &snap, externs)?;
let output =
dispatcher
.dispatch(ref_, input)
.await
.map_err(|e| EvalError::DispatcherError {
ref_: ref_.clone(),
msg: e.to_string(),
})?;
ctx.write(path_str_async(out)?, output)
}
Node::Seq { children } => {
for child in children {
eval_async_with_storage_externs(child, ctx.clone(), dispatcher, externs).await?;
}
Ok(())
}
Node::Branch { cond, then_, else_ } => {
let snap = ctx.snapshot();
match eval_expr_with_externs(cond, &snap, externs)? {
Value::Bool(true) => {
eval_async_with_storage_externs(then_, ctx, dispatcher, externs).await
}
Value::Bool(false) => {
eval_async_with_storage_externs(else_, ctx, dispatcher, externs).await
}
other => Err(EvalError::NonBoolCond(other)),
}
}
Node::Fanout {
items,
bind,
body,
join,
out,
} => fanout_eval(items, bind, body, *join, out, ctx, dispatcher, externs).await,
Node::Loop {
counter,
cond,
body,
max,
} => {
let counter_path = path_str_async(counter)?.to_string();
ctx.write(&counter_path, Value::Number(serde_json::Number::from(0u32)))?;
let mut n: u32 = 0;
loop {
if n >= *max {
break;
}
let snap = ctx.snapshot();
if !is_truthy(&eval_expr_with_externs(cond, &snap, externs)?) {
break;
}
eval_async_with_storage_externs(body, ctx.clone(), dispatcher, externs).await?;
n += 1;
ctx.write(&counter_path, Value::Number(serde_json::Number::from(n)))?;
}
Ok(())
}
Node::Try {
body,
catch,
err_at,
} => {
let snap_before = ctx.snapshot();
match eval_async_with_storage_externs(body, ctx.clone(), dispatcher, externs).await {
Ok(()) => Ok(()),
Err(e) => {
ctx.replace(snap_before);
if let Some(at) = err_at {
ctx.write(path_str_async(at)?, Value::String(e.to_string()))?;
}
eval_async_with_storage_externs(catch, ctx, dispatcher, externs).await
}
}
}
Node::Assign { at, value } => {
let snap = ctx.snapshot();
let v = eval_expr_with_externs(value, &snap, externs)?;
ctx.write(path_str_async(at)?, v)
}
}
}
pub async fn eval_async<D>(node: &Node, ctx: Value, dispatcher: &D) -> Result<Value, EvalError>
where
D: AsyncDispatcher + ?Sized,
{
eval_async_externs(node, ctx, dispatcher, &NoExterns).await
}
pub async fn eval_async_externs<D>(
node: &Node,
ctx: Value,
dispatcher: &D,
externs: &(dyn Externs + Sync),
) -> Result<Value, EvalError>
where
D: AsyncDispatcher + ?Sized,
{
let storage: Arc<dyn CtxStorage> = MemoryCtx::shared(ctx);
eval_async_with_storage_externs(node, storage.clone(), dispatcher, externs).await?;
Ok(storage.snapshot())
}
fn path_str_async(expr: &Expr) -> Result<&str, EvalError> {
match expr {
Expr::Path { at } => Ok(at.as_str()),
_ => Err(EvalError::InvalidPath(
"expected Path expr for write target".into(),
)),
}
}
#[async_recursion]
#[allow(clippy::too_many_arguments)]
async fn fanout_eval<D>(
items: &Expr,
bind: &Expr,
body: &Node,
join: JoinMode,
out: &Expr,
ctx: Arc<dyn CtxStorage>,
dispatcher: &D,
externs: &(dyn Externs + Sync),
) -> Result<(), EvalError>
where
D: AsyncDispatcher + ?Sized,
{
use futures::future::{join_all, select_ok, FutureExt};
let snap = ctx.snapshot();
let items_val = eval_expr_with_externs(items, &snap, externs)?;
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 branches: Vec<Arc<dyn CtxStorage>> = items_arr
.into_iter()
.map(|item| -> Result<Arc<dyn CtxStorage>, EvalError> {
let branch_ctx = write_path(bind, snap.clone(), item)?;
Ok(MemoryCtx::shared(branch_ctx))
})
.collect::<Result<_, _>>()?;
let branch_futs: Vec<_> = branches
.iter()
.map(|b| eval_async_with_storage_externs(body, b.clone(), dispatcher, externs))
.collect();
let joined: Value = match join {
JoinMode::All => {
futures::future::try_join_all(branch_futs).await?;
Value::Array(branches.iter().map(|b| b.snapshot()).collect())
}
JoinMode::Any => {
if branch_futs.is_empty() {
Value::Array(vec![])
} else {
let mapped: Vec<_> = branch_futs
.into_iter()
.enumerate()
.map(|(i, f)| f.map(move |r| r.map(|()| i)).boxed())
.collect();
let (winner_idx, _rest) = select_ok(mapped).await?;
branches[winner_idx].snapshot()
}
}
JoinMode::Race => {
if branch_futs.is_empty() {
Value::Array(vec![])
} else {
let mapped: Vec<_> = branch_futs
.into_iter()
.enumerate()
.map(|(i, f)| f.map(move |r| r.map(|()| i)).boxed())
.collect();
let (first, _idx, _rest) = futures::future::select_all(mapped).await;
let winner_idx = first?;
branches[winner_idx].snapshot()
}
}
JoinMode::AllSettled => {
let results = join_all(branch_futs).await;
let records: Vec<Value> = results
.into_iter()
.zip(branches.iter())
.map(|(r, b)| match r {
Ok(()) => serde_json::json!({"status": "fulfilled", "value": b.snapshot()}),
Err(e) => serde_json::json!({"status": "rejected", "reason": e.to_string()}),
})
.collect();
Value::Array(records)
}
};
ctx.write(path_str_async(out)?, joined)
}
use mlua::LuaSerdeExt;
struct LuaDispatcher<'a> {
lua: &'a mlua::Lua,
func: mlua::Function,
}
impl<'a> Dispatcher for LuaDispatcher<'a> {
fn dispatch(&self, ref_: &str, input: Value) -> Result<Value, EvalError> {
let lua_input = self
.lua
.to_value(&input)
.map_err(|e| EvalError::DispatcherError {
ref_: ref_.into(),
msg: format!("to_value: {}", e),
})?;
let result: mlua::Value = self.func.call((ref_.to_string(), lua_input)).map_err(|e| {
EvalError::DispatcherError {
ref_: ref_.into(),
msg: format!("lua call: {}", e),
}
})?;
let value: Value = self
.lua
.from_value(result)
.map_err(|e| EvalError::DispatcherError {
ref_: ref_.into(),
msg: format!("from_value: {}", e),
})?;
Ok(value)
}
}
struct LuaExterns<'a> {
lua: &'a mlua::Lua,
table: mlua::Table,
}
impl<'a> flow_ir_core::Externs for LuaExterns<'a> {
fn call(&self, ref_: &str, args: &[Value]) -> Result<Value, EvalError> {
let func: mlua::Function = self.table.get(ref_).map_err(|_| EvalError::ExternError {
ref_: ref_.into(),
msg: "not registered in externs (or not a function)".into(),
})?;
let mut lua_args = mlua::MultiValue::new();
for a in args {
lua_args.push_back(self.lua.to_value(a).map_err(|e| EvalError::ExternError {
ref_: ref_.into(),
msg: format!("to_value: {}", e),
})?);
}
let result: mlua::Value = func.call(lua_args).map_err(|e| EvalError::ExternError {
ref_: ref_.into(),
msg: format!("lua call: {}", e),
})?;
self.lua
.from_value(result)
.map_err(|e| EvalError::ExternError {
ref_: ref_.into(),
msg: format!("from_value: {}", e),
})
}
}
pub fn module(lua: &mlua::Lua) -> mlua::Result<mlua::Table> {
let t = lua.create_table()?;
t.set("version", env!("CARGO_PKG_VERSION"))?;
let eval_fn = lua.create_function(
|lua_inner: &mlua::Lua,
(node_val, ctx_val, dispatcher_fn, externs_val): (
mlua::Value,
mlua::Value,
mlua::Function,
Option<mlua::Table>,
)| {
let node: Node = lua_inner
.from_value(node_val)
.map_err(|e| mlua::Error::external(format!("node parse: {}", e)))?;
let ctx: Value = lua_inner
.from_value(ctx_val)
.map_err(|e| mlua::Error::external(format!("ctx parse: {}", e)))?;
let dispatcher = LuaDispatcher {
lua: lua_inner,
func: dispatcher_fn,
};
let result = match externs_val {
Some(table) => {
let externs = LuaExterns {
lua: lua_inner,
table,
};
eval_externs(&node, ctx, &dispatcher, &externs)
}
None => eval(&node, ctx, &dispatcher),
}
.map_err(|e| mlua::Error::external(format!("eval: {}", e)))?;
lua_inner.to_value(&result)
},
)?;
t.set("eval", eval_fn)?;
Ok(t)
}