use std::any::Any;
use std::sync::{Arc, Mutex};
use ciborium::Value as CborValue;
use vantage_core::{Context, Result, error};
use vantage_rhai::rhai::{AST, Dynamic, Engine, EvalAltResult, FnPtr};
use vantage_rhai::{Env, Host};
use super::convert::{cbor_to_dynamic, dynamic_to_cbor};
use super::eval::compile;
use super::read::RhaiResult;
use crate::FilterOp;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum RuleVerb {
Where { column: String, described: String },
Condition { described: String },
Hook { on: HookOn },
RemoveColumn { column: String },
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum HookOn {
BeforeInsert,
BeforeUpdate,
BeforeSave,
BeforeDelete,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum RuleHookInput {
Record(CborValue),
Id(CborValue),
Delete { id: CborValue, row: CborValue },
Update {
record: CborValue,
stored: Option<CborValue>,
},
}
pub type RuleHookFn = Arc<dyn Fn(RuleHookInput) -> std::result::Result<(), String> + Send + Sync>;
pub trait RuleTarget: Send {
fn where_value(&mut self, column: &str, op: FilterOp, value: CborValue) -> Result<String>;
fn native_condition(
&mut self,
condition: Box<dyn Any + Send>,
preview: String,
) -> Result<String>;
fn hook(&mut self, on: HookOn, run: RuleHookFn) -> Result<()>;
fn remove_column(&mut self, column: &str) -> Result<()>;
}
struct RuleRuntime {
engine: Arc<Engine>,
ast: Arc<AST>,
}
#[derive(Clone)]
pub struct RuleHandle {
target: Arc<Mutex<Box<dyn RuleTarget>>>,
verbs: Arc<Mutex<Vec<RuleVerb>>>,
runtime: Arc<Mutex<Option<Arc<RuleRuntime>>>>,
}
impl RuleHandle {
pub fn new(target: Box<dyn RuleTarget>) -> Self {
Self {
target: Arc::new(Mutex::new(target)),
verbs: Arc::new(Mutex::new(Vec::new())),
runtime: Arc::new(Mutex::new(None)),
}
}
pub fn verbs(&self) -> Vec<RuleVerb> {
self.verbs.lock().unwrap().clone()
}
pub fn native_condition(&self, condition: Box<dyn Any + Send>, preview: String) -> Result<()> {
let described = self.target().native_condition(condition, preview)?;
self.record(RuleVerb::Condition { described });
Ok(())
}
fn target(&self) -> std::sync::MutexGuard<'_, Box<dyn RuleTarget>> {
self.target.lock().unwrap()
}
fn record(&self, verb: RuleVerb) {
self.verbs.lock().unwrap().push(verb);
}
fn where_value(&self, column: &str, op: FilterOp, value: Dynamic) -> RhaiResult<RuleHandle> {
let value = dynamic_to_cbor(value)?;
let described = self
.target()
.where_value(column, op, value)
.map_err(|e| e.to_string())?;
self.record(RuleVerb::Where {
column: column.to_string(),
described,
});
Ok(self.clone())
}
fn hook(&self, on: HookOn, closure: FnPtr) -> RhaiResult<RuleHandle> {
let runtime = self
.runtime
.lock()
.unwrap()
.clone()
.ok_or("rule hooks need a script run by eval_rule_script")?;
let takes_row = on != HookOn::BeforeInsert && arity(&closure, &runtime.ast) == Some(2);
let run: RuleHookFn = Arc::new(move |input| {
let (first, row) = match input {
RuleHookInput::Record(value) | RuleHookInput::Id(value) => {
(cbor_to_dynamic(&value), Dynamic::UNIT)
}
RuleHookInput::Delete { id, row } => (cbor_to_dynamic(&id), cbor_to_dynamic(&row)),
RuleHookInput::Update { record, stored } => (
cbor_to_dynamic(&record),
stored.as_ref().map_or(Dynamic::UNIT, cbor_to_dynamic),
),
};
let outcome = if takes_row {
closure.call::<Dynamic>(&runtime.engine, &runtime.ast, (first, row))
} else {
closure.call::<Dynamic>(&runtime.engine, &runtime.ast, (first,))
};
outcome.map(|_| ()).map_err(|e| refusal(*e))
});
self.target().hook(on, run).map_err(|e| e.to_string())?;
self.record(RuleVerb::Hook { on });
Ok(self.clone())
}
fn remove_column(&self, column: &str) -> RhaiResult<RuleHandle> {
self.target()
.remove_column(column)
.map_err(|e| e.to_string())?;
self.record(RuleVerb::RemoveColumn {
column: column.to_string(),
});
Ok(self.clone())
}
}
fn arity(closure: &FnPtr, ast: &AST) -> Option<usize> {
let function = ast.iter_functions().find(|f| f.name == closure.fn_name())?;
function.params.len().checked_sub(closure.curry().len())
}
fn refusal(err: EvalAltResult) -> String {
match err {
EvalAltResult::ErrorRuntime(value, _) => value
.clone()
.into_string()
.unwrap_or_else(|_| value.to_string()),
EvalAltResult::ErrorInFunctionCall(_, _, inner, _) => refusal(*inner),
other => other.to_string(),
}
}
pub fn register_rule_vocab(engine: &mut Engine) {
engine.register_type_with_name::<RuleHandle>("Table");
engine.register_fn(
"where",
|h: &mut RuleHandle, column: &str, value: Dynamic| {
h.where_value(column, FilterOp::Eq, value)
},
);
engine.register_fn(
"where",
|h: &mut RuleHandle, column: &str, op: &str, value: Dynamic| -> RhaiResult<RuleHandle> {
let op = FilterOp::parse(op).ok_or_else(|| {
format!(
"unknown operator \"{op}\" in where (expected eq/ne/gt/gte/lt/lte/in/not_in/like)"
)
})?;
h.where_value(column, op, value)
},
);
for (name, on) in [
("before_insert", HookOn::BeforeInsert),
("before_update", HookOn::BeforeUpdate),
("before_save", HookOn::BeforeSave),
("before_delete", HookOn::BeforeDelete),
] {
engine.register_fn(name, move |h: &mut RuleHandle, closure: FnPtr| {
h.hook(on, closure)
});
}
engine.register_fn("remove_column", |h: &mut RuleHandle, column: &str| {
h.remove_column(column)
});
}
pub fn eval_rule_script(host: &Host, code: &str, env: Env, handle: &RuleHandle) -> Result<()> {
const WHAT: &str = "rhai rule script";
let script = compile(host, WHAT, code)?;
let ast = script
.ast()
.ok_or_else(|| error!("rhai rule script compiled to no single AST", script = WHAT))?;
*handle.runtime.lock().unwrap() = Some(Arc::new(RuleRuntime {
engine: host.engine().clone(),
ast: Arc::new(ast.clone()),
}));
script
.run(&env.var("table", Dynamic::from(handle.clone())))
.context(error!("rhai script failed", script = WHAT))
}