1use rhai::{Engine, EvalAltResult, Scope, module_resolvers::FileModuleResolver};
26use std::fs;
27use std::path::{Path, PathBuf};
28
29#[derive(Debug)]
31pub enum HerodoError {
32 FileNotFound(String),
34 ScriptError(String),
36 InvalidExtension(String),
38 EngineError(String),
40 IoError(std::io::Error),
42}
43
44impl std::fmt::Display for HerodoError {
45 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
46 match self {
47 HerodoError::FileNotFound(path) => write!(f, "File not found: {}", path),
48 HerodoError::ScriptError(msg) => write!(f, "Script error: {}", msg),
49 HerodoError::InvalidExtension(path) => {
50 write!(f, "Invalid extension (must be .rhai): {}", path)
51 }
52 HerodoError::EngineError(msg) => write!(f, "Engine error: {}", msg),
53 HerodoError::IoError(e) => write!(f, "IO error: {}", e),
54 }
55 }
56}
57
58impl std::error::Error for HerodoError {}
59
60impl From<std::io::Error> for HerodoError {
61 fn from(e: std::io::Error) -> Self {
62 HerodoError::IoError(e)
63 }
64}
65
66impl From<Box<EvalAltResult>> for HerodoError {
67 fn from(e: Box<EvalAltResult>) -> Self {
68 HerodoError::ScriptError(e.to_string())
69 }
70}
71
72pub type Result<T> = std::result::Result<T, HerodoError>;
74
75fn create_engine() -> Result<Engine> {
77 create_engine_with_base_path(None)
78}
79
80fn create_engine_with_base_path(base_path: Option<&Path>) -> Result<Engine> {
82 let mut engine = Engine::new();
83
84 herolib_core::rhai::register_core_module(&mut engine)
86 .map_err(|e| HerodoError::EngineError(e.to_string()))?;
87 herolib_crypt::rhai::register(&mut engine)
88 .map_err(|e| HerodoError::EngineError(e.to_string()))?;
89 herolib_os::rhai::register_system_module(&mut engine)
90 .map_err(|e| HerodoError::EngineError(e.to_string()))?;
91 herolib_clients::rhai::register_clients_module(&mut engine)
92 .map_err(|e| HerodoError::EngineError(e.to_string()))?;
93 herolib_virt::rhai::register_virt_module(&mut engine)
94 .map_err(|e| HerodoError::EngineError(e.to_string()))?;
95 herolib_ai::rhai::register(&mut engine).map_err(|e| HerodoError::EngineError(e.to_string()))?;
96 eprintln!("[DEBUG herodo] About to register rust_builder");
97 herolib_code::rust_builder::rhai::register_rust_builder_module(&mut engine)
98 .map_err(|e| HerodoError::EngineError(e.to_string()))?;
99 eprintln!("[DEBUG herodo] Registered rust_builder");
100
101 let resolver = if let Some(path) = base_path {
103 FileModuleResolver::new_with_path(path)
104 } else {
105 FileModuleResolver::new_with_path(
106 std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
107 )
108 };
109 engine.set_module_resolver(resolver);
110
111 engine.on_print(|s| {
113 println!("{}", s);
114 });
115
116 engine.on_debug(|s, source, pos| {
117 let location = match source {
118 Some(src) => format!("[{}:{}] ", src, pos),
119 None if !pos.is_none() => format!("[{}] ", pos),
120 None => String::new(),
121 };
122 println!("[DEBUG] {}{}", location, s);
123 });
124
125 Ok(engine)
126}
127
128pub fn run(path: &str) -> Result<()> {
154 let path = Path::new(path);
155
156 if !path.exists() {
157 return Err(HerodoError::FileNotFound(path.display().to_string()));
158 }
159
160 if path.is_file() {
161 run_script_file(path)
162 } else if path.is_dir() {
163 run_scripts_in_directory(path)
164 } else {
165 Err(HerodoError::FileNotFound(path.display().to_string()))
166 }
167}
168
169fn run_script_file(path: &Path) -> Result<()> {
171 if path.extension().is_some_and(|ext| ext != "rhai") {
173 return Err(HerodoError::InvalidExtension(path.display().to_string()));
174 }
175
176 let base_path = path.parent().unwrap_or_else(|| Path::new("."));
178
179 let script_dir = base_path
181 .canonicalize()
182 .unwrap_or_else(|_| base_path.to_path_buf())
183 .display()
184 .to_string();
185 unsafe {
187 std::env::set_var("HERODO_SCRIPT_DIR", &script_dir);
188 }
189
190 let engine = create_engine_with_base_path(Some(base_path))?;
191 let mut scope = Scope::new();
192
193 let script = fs::read_to_string(path)?;
194
195 let script = if script.starts_with("#!") {
197 script.lines().skip(1).collect::<Vec<_>>().join("\n")
198 } else {
199 script
200 };
201
202 engine
203 .run_with_scope(&mut scope, &script)
204 .map_err(|e| HerodoError::ScriptError(e.to_string()))
205}
206
207fn run_scripts_in_directory(dir: &Path) -> Result<()> {
209 let mut scripts: Vec<PathBuf> = Vec::new();
210
211 collect_rhai_files(dir, &mut scripts)?;
213
214 if scripts.is_empty() {
215 return Ok(());
217 }
218
219 scripts.sort();
221
222 for script_path in scripts {
224 run_script_file(&script_path)?;
225 }
226
227 Ok(())
228}
229
230fn collect_rhai_files(dir: &Path, scripts: &mut Vec<PathBuf>) -> Result<()> {
232 for entry in fs::read_dir(dir)? {
233 let entry = entry?;
234 let path = entry.path();
235
236 if path.is_dir() {
237 collect_rhai_files(&path, scripts)?;
238 } else if path.extension().is_some_and(|ext| ext == "rhai") {
239 scripts.push(path);
240 }
241 }
242 Ok(())
243}
244
245pub fn run_script(script: &str) -> Result<()> {
256 let engine = create_engine()?;
257 let mut scope = Scope::new();
258
259 engine
260 .run_with_scope(&mut scope, script)
261 .map_err(|e| HerodoError::ScriptError(e.to_string()))
262}
263
264pub fn eval(script: &str) -> Result<rhai::Dynamic> {
275 let engine = create_engine()?;
276 let mut scope = Scope::new();
277
278 engine
279 .eval_with_scope::<rhai::Dynamic>(&mut scope, script)
280 .map_err(|e| HerodoError::ScriptError(e.to_string()))
281}
282
283#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn test_run_script_simple() {
289 let result = run_script(r#"let x = 42; x"#);
290 assert!(result.is_ok());
291 }
292
293 #[test]
294 fn test_eval_simple() {
295 let result = eval(r#"let x = 42; x"#);
296 assert!(result.is_ok());
297 assert_eq!(result.unwrap().as_int().unwrap(), 42);
298 }
299
300 #[test]
301 fn test_file_not_found() {
302 let result = run("/nonexistent/path/script.rhai");
303 assert!(matches!(result, Err(HerodoError::FileNotFound(_))));
304 }
305}