Skip to main content

max / audiofiles

6.9 KB · 229 lines History Blame Raw
1 //! Plugin registry: stores loaded device plugins and provides lookup by name.
2
3 use std::collections::HashMap;
4
5 use audiofiles_core::export::profile::{DeviceProfile, DeviceProfileSummary};
6 use rhai::Engine;
7
8 use crate::hooks::CompiledHooks;
9
10 /// A loaded device plugin: profile + optional compiled hooks.
11 pub struct LoadedPlugin {
12 /// The device profile (constraints).
13 pub profile: DeviceProfile,
14 /// Compiled Rhai hooks (if any).
15 pub hooks: CompiledHooks,
16 /// Whether this is a bundled (built-in) plugin.
17 pub bundled: bool,
18 }
19
20 /// Registry of loaded device plugins.
21 pub struct PluginRegistry {
22 /// Map from device name (lowercase) to loaded plugin.
23 plugins: HashMap<String, LoadedPlugin>,
24 /// Shared Rhai engine for all plugins.
25 engine: Engine,
26 }
27
28 impl Default for PluginRegistry {
29 fn default() -> Self {
30 Self::new()
31 }
32 }
33
34 impl PluginRegistry {
35 /// Create an empty registry with a fresh Rhai engine.
36 pub fn new() -> Self {
37 Self {
38 plugins: HashMap::new(),
39 engine: crate::engine::create_engine(),
40 }
41 }
42
43 /// Insert a plugin into the registry. If a plugin with the same name
44 /// already exists, user plugins override bundled ones.
45 pub fn insert(&mut self, plugin: LoadedPlugin) {
46 let key = plugin.profile.name.to_lowercase();
47
48 // User plugins override bundled ones
49 if let Some(existing) = self.plugins.get(&key) {
50 if existing.bundled && !plugin.bundled {
51 // User plugin overrides bundled — expected behavior
52 } else if !existing.bundled && plugin.bundled {
53 // Don't override user plugin with bundled
54 return;
55 } else if !existing.bundled && !plugin.bundled {
56 // User plugin overrides another user plugin — warn about ambiguity
57 tracing::warn!(
58 device = %plugin.profile.name,
59 "Multiple user plugins with the same device name — last one loaded wins"
60 );
61 }
62 }
63
64 self.plugins.insert(key, plugin);
65 }
66
67 /// Look up a plugin by device name (case-insensitive).
68 pub fn get(&self, name: &str) -> Option<&LoadedPlugin> {
69 self.plugins.get(&name.to_lowercase())
70 }
71
72 /// Get the shared Rhai engine.
73 pub fn engine(&self) -> &Engine {
74 &self.engine
75 }
76
77 /// List all loaded plugins as summaries.
78 pub fn list(&self) -> Vec<DeviceProfileSummary> {
79 let mut summaries: Vec<DeviceProfileSummary> = self
80 .plugins
81 .values()
82 .map(|p| DeviceProfileSummary {
83 name: p.profile.name.clone(),
84 manufacturer: p.profile.manufacturer.clone(),
85 bundled: p.bundled,
86 max_file_size_bytes: p.profile.limits.as_ref()
87 .and_then(|l| l.max_file_size_bytes),
88 format_summary: Some(audiofiles_core::export::profile::format_audio_constraints(
89 &p.profile.audio,
90 )),
91 category: p.profile.category.clone(),
92 notes: p.profile.notes.clone(),
93 })
94 .collect();
95 summaries.sort_by(|a, b| a.name.cmp(&b.name));
96 summaries
97 }
98
99 /// Get the number of loaded plugins.
100 pub fn len(&self) -> usize {
101 self.plugins.len()
102 }
103
104 /// Check if the registry is empty.
105 pub fn is_empty(&self) -> bool {
106 self.plugins.is_empty()
107 }
108 }
109
110 #[cfg(test)]
111 mod tests {
112 use super::*;
113 use audiofiles_core::export::profile::{
114 AudioConstraints, ChannelConstraint,
115 };
116 use audiofiles_core::export::ExportFormat;
117
118 fn dummy_profile(name: &str) -> DeviceProfile {
119 DeviceProfile {
120 name: name.to_string(),
121 manufacturer: "Test".to_string(),
122 category: None,
123 notes: None,
124 audio: AudioConstraints {
125 formats: vec![ExportFormat::Wav],
126 sample_rates: vec![44100],
127 bit_depths: vec![16],
128 channels: ChannelConstraint::Both,
129 },
130 naming: None,
131 limits: None,
132 }
133 }
134
135 fn dummy_hooks() -> CompiledHooks {
136 CompiledHooks {
137 validate_sample: None,
138 transform_filename: None,
139 pre_export: None,
140 post_export: None,
141 }
142 }
143
144 #[test]
145 fn insert_and_lookup() {
146 let mut registry = PluginRegistry::new();
147 registry.insert(LoadedPlugin {
148 profile: dummy_profile("SP-404 MKII"),
149 hooks: dummy_hooks(),
150 bundled: true,
151 });
152
153 assert_eq!(registry.len(), 1);
154 assert!(registry.get("SP-404 MKII").is_some());
155 assert!(registry.get("sp-404 mkii").is_some()); // case-insensitive
156 assert!(registry.get("nonexistent").is_none());
157 }
158
159 #[test]
160 fn user_overrides_bundled() {
161 let mut registry = PluginRegistry::new();
162
163 // Insert bundled first
164 registry.insert(LoadedPlugin {
165 profile: dummy_profile("SP-404 MKII"),
166 hooks: dummy_hooks(),
167 bundled: true,
168 });
169
170 // Insert user plugin with same name — should override
171 let mut user_profile = dummy_profile("SP-404 MKII");
172 user_profile.manufacturer = "Custom".to_string();
173 registry.insert(LoadedPlugin {
174 profile: user_profile,
175 hooks: dummy_hooks(),
176 bundled: false,
177 });
178
179 assert_eq!(registry.len(), 1);
180 let plugin = registry.get("SP-404 MKII").unwrap();
181 assert_eq!(plugin.profile.manufacturer, "Custom");
182 assert!(!plugin.bundled);
183 }
184
185 #[test]
186 fn bundled_does_not_override_user() {
187 let mut registry = PluginRegistry::new();
188
189 // Insert user first
190 let mut user_profile = dummy_profile("SP-404 MKII");
191 user_profile.manufacturer = "Custom".to_string();
192 registry.insert(LoadedPlugin {
193 profile: user_profile,
194 hooks: dummy_hooks(),
195 bundled: false,
196 });
197
198 // Insert bundled with same name — should NOT override
199 registry.insert(LoadedPlugin {
200 profile: dummy_profile("SP-404 MKII"),
201 hooks: dummy_hooks(),
202 bundled: true,
203 });
204
205 let plugin = registry.get("SP-404 MKII").unwrap();
206 assert_eq!(plugin.profile.manufacturer, "Custom");
207 }
208
209 #[test]
210 fn list_sorted() {
211 let mut registry = PluginRegistry::new();
212 registry.insert(LoadedPlugin {
213 profile: dummy_profile("Zebra"),
214 hooks: dummy_hooks(),
215 bundled: true,
216 });
217 registry.insert(LoadedPlugin {
218 profile: dummy_profile("Alpha"),
219 hooks: dummy_hooks(),
220 bundled: true,
221 });
222
223 let list = registry.list();
224 assert_eq!(list.len(), 2);
225 assert_eq!(list[0].name, "Alpha");
226 assert_eq!(list[1].name, "Zebra");
227 }
228 }
229