//! Hook runner: compile and execute Rhai scripts for each hook point. use rhai::{AST, Engine, Scope}; use tracing::instrument; use crate::error::PluginError; use crate::types::{RhaiExportContext, RhaiSampleInfo}; /// Compiled hooks for a single device plugin. #[derive(Debug)] pub struct CompiledHooks { pub validate_sample: Option, pub transform_filename: Option, pub pre_export: Option, pub post_export: Option, } impl CompiledHooks { /// Returns true if no hooks are defined. pub fn is_empty(&self) -> bool { self.validate_sample.is_none() && self.transform_filename.is_none() && self.pre_export.is_none() && self.post_export.is_none() } } /// Compile a Rhai script string into an AST. #[instrument(skip_all)] pub fn compile_hook(engine: &Engine, source: &str) -> Result { engine .compile(source) .map_err(|e| PluginError::ScriptCompile(e.to_string())) } /// Run the `validate_sample` hook. Returns `true` if the sample is accepted /// (or if no hook is defined). #[instrument(skip_all)] pub fn run_validate_sample( engine: &Engine, ast: &AST, info: RhaiSampleInfo, ) -> Result { let mut scope = Scope::new(); scope.push("info", info); engine .eval_ast_with_scope::(&mut scope, ast) .map_err(|e| PluginError::ScriptRuntime(e.to_string())) } /// Run the `transform_filename` hook. Returns the transformed filename. #[instrument(skip_all)] pub fn run_transform_filename( engine: &Engine, ast: &AST, name: String, ctx: RhaiExportContext, ) -> Result { let mut scope = Scope::new(); scope.push("name", name); scope.push("ctx", ctx); engine .eval_ast_with_scope::(&mut scope, ast) .map_err(|e| PluginError::ScriptRuntime(e.to_string())) } /// Run the `pre_export` hook (side-effect only, no return value). #[instrument(skip_all)] pub fn run_pre_export( engine: &Engine, ast: &AST, ctx: RhaiExportContext, ) -> Result<(), PluginError> { let mut scope = Scope::new(); scope.push("ctx", ctx); let _ = engine .eval_ast_with_scope::(&mut scope, ast) .map_err(|e| PluginError::ScriptRuntime(e.to_string()))?; Ok(()) } /// Run the `post_export` hook (side-effect only, no return value). #[instrument(skip_all)] pub fn run_post_export( engine: &Engine, ast: &AST, ctx: RhaiExportContext, ) -> Result<(), PluginError> { let mut scope = Scope::new(); scope.push("ctx", ctx); let _ = engine .eval_ast_with_scope::(&mut scope, ast) .map_err(|e| PluginError::ScriptRuntime(e.to_string()))?; Ok(()) } #[cfg(test)] mod tests { use super::*; use crate::engine::create_engine; fn sample_info() -> RhaiSampleInfo { RhaiSampleInfo { hash: "abc123".to_string(), name: "kick.wav".to_string(), extension: "wav".to_string(), sample_rate: 44100, bit_depth: 16, channels: 1, duration: 0.5, file_size: 44100, } } fn export_ctx() -> RhaiExportContext { RhaiExportContext { device_name: "SP-404 MKII".to_string(), destination: "/tmp/export".to_string(), filename: "kick".to_string(), extension: "wav".to_string(), index: 0, total: 10, } } #[test] fn validate_sample_accepts() { let engine = create_engine(); let ast = compile_hook(&engine, "info.sample_rate == 44100").unwrap(); let result = run_validate_sample(&engine, &ast, sample_info()).unwrap(); assert!(result); } #[test] fn validate_sample_rejects() { let engine = create_engine(); let ast = compile_hook(&engine, "info.sample_rate == 48000").unwrap(); let result = run_validate_sample(&engine, &ast, sample_info()).unwrap(); assert!(!result); } #[test] fn transform_filename_works() { let engine = create_engine(); let ast = compile_hook(&engine, r#"to_upper(name) + "_" + format_index(ctx.index, 3)"#) .unwrap(); let result = run_transform_filename(&engine, &ast, "kick".to_string(), export_ctx()).unwrap(); assert_eq!(result, "KICK_000"); } #[test] fn compile_error_returns_plugin_error() { let engine = create_engine(); let result = compile_hook(&engine, "this is not valid rhai {{{{"); assert!(result.is_err()); assert!(matches!(result.unwrap_err(), PluginError::ScriptCompile(_))); } #[test] fn runtime_error_returns_plugin_error() { let engine = create_engine(); let ast = compile_hook(&engine, "info.nonexistent_field").unwrap(); let result = run_validate_sample(&engine, &ast, sample_info()); assert!(result.is_err()); } }