Skip to main content

max / audiofiles

4.9 KB · 173 lines History Blame Raw
1 //! Hook runner: compile and execute Rhai scripts for each hook point.
2
3 use rhai::{AST, Engine, Scope};
4
5 use tracing::instrument;
6
7 use crate::error::PluginError;
8 use crate::types::{RhaiExportContext, RhaiSampleInfo};
9
10 /// Compiled hooks for a single device plugin.
11 #[derive(Debug)]
12 pub struct CompiledHooks {
13 pub validate_sample: Option<AST>,
14 pub transform_filename: Option<AST>,
15 pub pre_export: Option<AST>,
16 pub post_export: Option<AST>,
17 }
18
19 impl CompiledHooks {
20 /// Returns true if no hooks are defined.
21 pub fn is_empty(&self) -> bool {
22 self.validate_sample.is_none()
23 && self.transform_filename.is_none()
24 && self.pre_export.is_none()
25 && self.post_export.is_none()
26 }
27 }
28
29 /// Compile a Rhai script string into an AST.
30 #[instrument(skip_all)]
31 pub fn compile_hook(engine: &Engine, source: &str) -> Result<AST, PluginError> {
32 engine
33 .compile(source)
34 .map_err(|e| PluginError::ScriptCompile(e.to_string()))
35 }
36
37 /// Run the `validate_sample` hook. Returns `true` if the sample is accepted
38 /// (or if no hook is defined).
39 #[instrument(skip_all)]
40 pub fn run_validate_sample(
41 engine: &Engine,
42 ast: &AST,
43 info: RhaiSampleInfo,
44 ) -> Result<bool, PluginError> {
45 let mut scope = Scope::new();
46 scope.push("info", info);
47
48 engine
49 .eval_ast_with_scope::<bool>(&mut scope, ast)
50 .map_err(|e| PluginError::ScriptRuntime(e.to_string()))
51 }
52
53 /// Run the `transform_filename` hook. Returns the transformed filename.
54 #[instrument(skip_all)]
55 pub fn run_transform_filename(
56 engine: &Engine,
57 ast: &AST,
58 name: String,
59 ctx: RhaiExportContext,
60 ) -> Result<String, PluginError> {
61 let mut scope = Scope::new();
62 scope.push("name", name);
63 scope.push("ctx", ctx);
64
65 engine
66 .eval_ast_with_scope::<String>(&mut scope, ast)
67 .map_err(|e| PluginError::ScriptRuntime(e.to_string()))
68 }
69
70 /// Run the `pre_export` hook (side-effect only, no return value).
71 #[instrument(skip_all)]
72 pub fn run_pre_export(
73 engine: &Engine,
74 ast: &AST,
75 ctx: RhaiExportContext,
76 ) -> Result<(), PluginError> {
77 let mut scope = Scope::new();
78 scope.push("ctx", ctx);
79
80 let _ = engine
81 .eval_ast_with_scope::<rhai::Dynamic>(&mut scope, ast)
82 .map_err(|e| PluginError::ScriptRuntime(e.to_string()))?;
83 Ok(())
84 }
85
86 /// Run the `post_export` hook (side-effect only, no return value).
87 #[instrument(skip_all)]
88 pub fn run_post_export(
89 engine: &Engine,
90 ast: &AST,
91 ctx: RhaiExportContext,
92 ) -> Result<(), PluginError> {
93 let mut scope = Scope::new();
94 scope.push("ctx", ctx);
95
96 let _ = engine
97 .eval_ast_with_scope::<rhai::Dynamic>(&mut scope, ast)
98 .map_err(|e| PluginError::ScriptRuntime(e.to_string()))?;
99 Ok(())
100 }
101
102 #[cfg(test)]
103 mod tests {
104 use super::*;
105 use crate::engine::create_engine;
106
107 fn sample_info() -> RhaiSampleInfo {
108 RhaiSampleInfo {
109 hash: "abc123".to_string(),
110 name: "kick.wav".to_string(),
111 extension: "wav".to_string(),
112 sample_rate: 44100,
113 bit_depth: 16,
114 channels: 1,
115 duration: 0.5,
116 file_size: 44100,
117 }
118 }
119
120 fn export_ctx() -> RhaiExportContext {
121 RhaiExportContext {
122 device_name: "SP-404 MKII".to_string(),
123 destination: "/tmp/export".to_string(),
124 filename: "kick".to_string(),
125 extension: "wav".to_string(),
126 index: 0,
127 total: 10,
128 }
129 }
130
131 #[test]
132 fn validate_sample_accepts() {
133 let engine = create_engine();
134 let ast = compile_hook(&engine, "info.sample_rate == 44100").unwrap();
135 let result = run_validate_sample(&engine, &ast, sample_info()).unwrap();
136 assert!(result);
137 }
138
139 #[test]
140 fn validate_sample_rejects() {
141 let engine = create_engine();
142 let ast = compile_hook(&engine, "info.sample_rate == 48000").unwrap();
143 let result = run_validate_sample(&engine, &ast, sample_info()).unwrap();
144 assert!(!result);
145 }
146
147 #[test]
148 fn transform_filename_works() {
149 let engine = create_engine();
150 let ast = compile_hook(&engine, r#"to_upper(name) + "_" + format_index(ctx.index, 3)"#)
151 .unwrap();
152 let result =
153 run_transform_filename(&engine, &ast, "kick".to_string(), export_ctx()).unwrap();
154 assert_eq!(result, "KICK_000");
155 }
156
157 #[test]
158 fn compile_error_returns_plugin_error() {
159 let engine = create_engine();
160 let result = compile_hook(&engine, "this is not valid rhai {{{{");
161 assert!(result.is_err());
162 assert!(matches!(result.unwrap_err(), PluginError::ScriptCompile(_)));
163 }
164
165 #[test]
166 fn runtime_error_returns_plugin_error() {
167 let engine = create_engine();
168 let ast = compile_hook(&engine, "info.nonexistent_field").unwrap();
169 let result = run_validate_sample(&engine, &ast, sample_info());
170 assert!(result.is_err());
171 }
172 }
173