use rhai::{Engine, EvalAltResult, Scope, module_resolvers::FileModuleResolver};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug)]
pub enum HerodoError {
FileNotFound(String),
ScriptError(String),
InvalidExtension(String),
EngineError(String),
IoError(std::io::Error),
}
impl std::fmt::Display for HerodoError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HerodoError::FileNotFound(path) => write!(f, "File not found: {}", path),
HerodoError::ScriptError(msg) => write!(f, "Script error: {}", msg),
HerodoError::InvalidExtension(path) => {
write!(f, "Invalid extension (must be .rhai): {}", path)
}
HerodoError::EngineError(msg) => write!(f, "Engine error: {}", msg),
HerodoError::IoError(e) => write!(f, "IO error: {}", e),
}
}
}
impl std::error::Error for HerodoError {}
impl From<std::io::Error> for HerodoError {
fn from(e: std::io::Error) -> Self {
HerodoError::IoError(e)
}
}
impl From<Box<EvalAltResult>> for HerodoError {
fn from(e: Box<EvalAltResult>) -> Self {
HerodoError::ScriptError(e.to_string())
}
}
pub type Result<T> = std::result::Result<T, HerodoError>;
fn create_engine() -> Result<Engine> {
create_engine_with_base_path(None)
}
fn create_engine_with_base_path(base_path: Option<&Path>) -> Result<Engine> {
let mut engine = Engine::new();
herolib_core::rhai::register_core_module(&mut engine)
.map_err(|e| HerodoError::EngineError(e.to_string()))?;
herolib_crypt::rhai::register_crypt_module(&mut engine)
.map_err(|e| HerodoError::EngineError(e.to_string()))?;
herolib_os::rhai::register_system_module(&mut engine)
.map_err(|e| HerodoError::EngineError(e.to_string()))?;
herolib_clients::rhai::register_clients_module(&mut engine)
.map_err(|e| HerodoError::EngineError(e.to_string()))?;
herolib_virt::rhai::register_kubernetes_module(&mut engine)
.map_err(|e| HerodoError::EngineError(e.to_string()))?;
let resolver = if let Some(path) = base_path {
FileModuleResolver::new_with_path(path)
} else {
FileModuleResolver::new_with_path(
std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
)
};
engine.set_module_resolver(resolver);
engine.on_print(|s| {
println!("{}", s);
});
engine.on_debug(|s, source, pos| {
let location = match source {
Some(src) => format!("[{}:{}] ", src, pos),
None if !pos.is_none() => format!("[{}] ", pos),
None => String::new(),
};
println!("[DEBUG] {}{}", location, s);
});
Ok(engine)
}
pub fn run(path: &str) -> Result<()> {
let path = Path::new(path);
if !path.exists() {
return Err(HerodoError::FileNotFound(path.display().to_string()));
}
if path.is_file() {
run_script_file(path)
} else if path.is_dir() {
run_scripts_in_directory(path)
} else {
Err(HerodoError::FileNotFound(path.display().to_string()))
}
}
fn run_script_file(path: &Path) -> Result<()> {
if path.extension().is_some_and(|ext| ext != "rhai") {
return Err(HerodoError::InvalidExtension(path.display().to_string()));
}
let base_path = path.parent().unwrap_or_else(|| Path::new("."));
let engine = create_engine_with_base_path(Some(base_path))?;
let mut scope = Scope::new();
let script = fs::read_to_string(path)?;
engine
.run_with_scope(&mut scope, &script)
.map_err(|e| HerodoError::ScriptError(e.to_string()))
}
fn run_scripts_in_directory(dir: &Path) -> Result<()> {
let mut scripts: Vec<PathBuf> = Vec::new();
collect_rhai_files(dir, &mut scripts)?;
if scripts.is_empty() {
return Ok(());
}
scripts.sort();
for script_path in scripts {
run_script_file(&script_path)?;
}
Ok(())
}
fn collect_rhai_files(dir: &Path, scripts: &mut Vec<PathBuf>) -> Result<()> {
for entry in fs::read_dir(dir)? {
let entry = entry?;
let path = entry.path();
if path.is_dir() {
collect_rhai_files(&path, scripts)?;
} else if path.extension().is_some_and(|ext| ext == "rhai") {
scripts.push(path);
}
}
Ok(())
}
pub fn run_script(script: &str) -> Result<()> {
let engine = create_engine()?;
let mut scope = Scope::new();
engine
.run_with_scope(&mut scope, script)
.map_err(|e| HerodoError::ScriptError(e.to_string()))
}
pub fn eval(script: &str) -> Result<rhai::Dynamic> {
let engine = create_engine()?;
let mut scope = Scope::new();
engine
.eval_with_scope::<rhai::Dynamic>(&mut scope, script)
.map_err(|e| HerodoError::ScriptError(e.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_run_script_simple() {
let result = run_script(r#"let x = 42; x"#);
assert!(result.is_ok());
}
#[test]
fn test_eval_simple() {
let result = eval(r#"let x = 42; x"#);
assert!(result.is_ok());
assert_eq!(result.unwrap().as_int().unwrap(), 42);
}
#[test]
fn test_file_not_found() {
let result = run("/nonexistent/path/script.rhai");
assert!(matches!(result, Err(HerodoError::FileNotFound(_))));
}
}