use rhai::{AST, Dynamic, Engine, Scope};
const VALIDATOR_MAX_OPERATIONS: u64 = 100_000;
#[derive(Debug, Clone)]
pub struct OutputValidator {
pub path: String,
ast: AST,
}
fn build_engine() -> Engine {
let mut engine = Engine::new();
crate::harden(&mut engine, VALIDATOR_MAX_OPERATIONS);
crate::functions::register_functions(&mut engine);
crate::types::register_types(&mut engine);
engine
}
pub fn compile(path: &str, source: &str) -> crate::Result<OutputValidator> {
let engine = build_engine();
let ast = engine
.compile(source)
.map_err(|e| crate::Error::CompilationFailed(format!("{path}: {e}")))?;
let arity = ast
.iter_functions()
.find(|f| f.name == "validate")
.map(|f| f.params.len());
match arity {
Some(1) => Ok(OutputValidator {
path: path.to_string(),
ast,
}),
Some(n) => Err(crate::Error::ValidationFailed(format!(
"{path}: fn validate must take exactly one parameter (content), found {n}"
))),
None => Err(crate::Error::ValidationFailed(format!(
"{path}: script must define fn validate(content)"
))),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Verdict {
Valid,
Invalid(String),
Unusable(String),
}
pub fn validate(validator: &OutputValidator, content: &str) -> Verdict {
let engine = build_engine();
let result: Result<Dynamic, _> = engine.call_fn(
&mut Scope::new(),
&validator.ast,
"validate",
(content.to_string(),),
);
let value = match result {
Ok(v) => v,
Err(e) => {
return Verdict::Unusable(format!("{}: validate: {e}", validator.path));
}
};
if value.is_unit() {
return Verdict::Valid;
}
match value.into_string() {
Ok(reason) if reason.trim().is_empty() => Verdict::Valid,
Ok(reason) => Verdict::Invalid(reason),
Err(actual) => Verdict::Unusable(format!(
"{}: validate must return () or a string, got {actual}",
validator.path
)),
}
}
#[cfg(test)]
mod tests;