//! Plugin registry: stores loaded device plugins and provides lookup by name. use std::collections::HashMap; use audiofiles_core::export::profile::{DeviceProfile, DeviceProfileSummary}; use rhai::Engine; use crate::hooks::CompiledHooks; /// A loaded device plugin: profile + optional compiled hooks. pub struct LoadedPlugin { /// The device profile (constraints). pub profile: DeviceProfile, /// Compiled Rhai hooks (if any). pub hooks: CompiledHooks, /// Whether this is a bundled (built-in) plugin. pub bundled: bool, } /// Registry of loaded device plugins. pub struct PluginRegistry { /// Map from device name (lowercase) to loaded plugin. plugins: HashMap, /// Shared Rhai engine for all plugins. engine: Engine, } impl Default for PluginRegistry { fn default() -> Self { Self::new() } } impl PluginRegistry { /// Create an empty registry with a fresh Rhai engine. pub fn new() -> Self { Self { plugins: HashMap::new(), engine: crate::engine::create_engine(), } } /// Insert a plugin into the registry. If a plugin with the same name /// already exists, user plugins override bundled ones. pub fn insert(&mut self, plugin: LoadedPlugin) { let key = plugin.profile.name.to_lowercase(); // User plugins override bundled ones if let Some(existing) = self.plugins.get(&key) { if existing.bundled && !plugin.bundled { // User plugin overrides bundled — expected behavior } else if !existing.bundled && plugin.bundled { // Don't override user plugin with bundled return; } else if !existing.bundled && !plugin.bundled { // User plugin overrides another user plugin — warn about ambiguity tracing::warn!( device = %plugin.profile.name, "Multiple user plugins with the same device name — last one loaded wins" ); } } self.plugins.insert(key, plugin); } /// Look up a plugin by device name (case-insensitive). pub fn get(&self, name: &str) -> Option<&LoadedPlugin> { self.plugins.get(&name.to_lowercase()) } /// Get the shared Rhai engine. pub fn engine(&self) -> &Engine { &self.engine } /// List all loaded plugins as summaries. pub fn list(&self) -> Vec { let mut summaries: Vec = self .plugins .values() .map(|p| DeviceProfileSummary { name: p.profile.name.clone(), manufacturer: p.profile.manufacturer.clone(), bundled: p.bundled, max_file_size_bytes: p.profile.limits.as_ref() .and_then(|l| l.max_file_size_bytes), format_summary: Some(audiofiles_core::export::profile::format_audio_constraints( &p.profile.audio, )), category: p.profile.category.clone(), notes: p.profile.notes.clone(), }) .collect(); summaries.sort_by(|a, b| a.name.cmp(&b.name)); summaries } /// Get the number of loaded plugins. pub fn len(&self) -> usize { self.plugins.len() } /// Check if the registry is empty. pub fn is_empty(&self) -> bool { self.plugins.is_empty() } } #[cfg(test)] mod tests { use super::*; use audiofiles_core::export::profile::{ AudioConstraints, ChannelConstraint, }; use audiofiles_core::export::ExportFormat; fn dummy_profile(name: &str) -> DeviceProfile { DeviceProfile { name: name.to_string(), manufacturer: "Test".to_string(), category: None, notes: None, audio: AudioConstraints { formats: vec![ExportFormat::Wav], sample_rates: vec![44100], bit_depths: vec![16], channels: ChannelConstraint::Both, }, naming: None, limits: None, } } fn dummy_hooks() -> CompiledHooks { CompiledHooks { validate_sample: None, transform_filename: None, pre_export: None, post_export: None, } } #[test] fn insert_and_lookup() { let mut registry = PluginRegistry::new(); registry.insert(LoadedPlugin { profile: dummy_profile("SP-404 MKII"), hooks: dummy_hooks(), bundled: true, }); assert_eq!(registry.len(), 1); assert!(registry.get("SP-404 MKII").is_some()); assert!(registry.get("sp-404 mkii").is_some()); // case-insensitive assert!(registry.get("nonexistent").is_none()); } #[test] fn user_overrides_bundled() { let mut registry = PluginRegistry::new(); // Insert bundled first registry.insert(LoadedPlugin { profile: dummy_profile("SP-404 MKII"), hooks: dummy_hooks(), bundled: true, }); // Insert user plugin with same name — should override let mut user_profile = dummy_profile("SP-404 MKII"); user_profile.manufacturer = "Custom".to_string(); registry.insert(LoadedPlugin { profile: user_profile, hooks: dummy_hooks(), bundled: false, }); assert_eq!(registry.len(), 1); let plugin = registry.get("SP-404 MKII").unwrap(); assert_eq!(plugin.profile.manufacturer, "Custom"); assert!(!plugin.bundled); } #[test] fn bundled_does_not_override_user() { let mut registry = PluginRegistry::new(); // Insert user first let mut user_profile = dummy_profile("SP-404 MKII"); user_profile.manufacturer = "Custom".to_string(); registry.insert(LoadedPlugin { profile: user_profile, hooks: dummy_hooks(), bundled: false, }); // Insert bundled with same name — should NOT override registry.insert(LoadedPlugin { profile: dummy_profile("SP-404 MKII"), hooks: dummy_hooks(), bundled: true, }); let plugin = registry.get("SP-404 MKII").unwrap(); assert_eq!(plugin.profile.manufacturer, "Custom"); } #[test] fn list_sorted() { let mut registry = PluginRegistry::new(); registry.insert(LoadedPlugin { profile: dummy_profile("Zebra"), hooks: dummy_hooks(), bundled: true, }); registry.insert(LoadedPlugin { profile: dummy_profile("Alpha"), hooks: dummy_hooks(), bundled: true, }); let list = registry.list(); assert_eq!(list.len(), 2); assert_eq!(list[0].name, "Alpha"); assert_eq!(list[1].name, "Zebra"); } }