Skip to main content

batuta/
plugin.rs

1/// Plugin Architecture for Custom Transpilers
2///
3/// Provides extensible plugin system for custom transpiler implementations.
4/// Plugins can register as pipeline stages and integrate seamlessly with Batuta.
5///
6/// # Architecture
7///
8/// - **TranspilerPlugin**: Core trait for custom transpiler plugins
9/// - **PluginRegistry**: Central registry for discovering and managing plugins
10/// - **PluginMetadata**: Plugin information (name, version, supported languages)
11/// - **PluginLoader**: Dynamic plugin loading mechanism
12///
13/// # Example
14///
15/// ```rust,no_run
16/// use batuta::plugin::{TranspilerPlugin, PluginMetadata, PluginRegistry};
17/// use batuta::pipeline::{PipelineContext, PipelineStage};
18/// use batuta::types::Language;
19/// use anyhow::Result;
20///
21/// // Define a custom transpiler plugin
22/// struct MyCustomTranspiler;
23///
24/// impl TranspilerPlugin for MyCustomTranspiler {
25///     fn metadata(&self) -> PluginMetadata {
26///         PluginMetadata {
27///             name: "my-custom-transpiler".to_string(),
28///             version: "0.1.0".to_string(),
29///             description: "Custom transpiler for my language".to_string(),
30///             author: "Your Name".to_string(),
31///             supported_languages: vec![Language::Python],
32///         }
33///     }
34///
35///     fn initialize(&mut self) -> Result<()> {
36///         println!("Initializing custom transpiler");
37///         Ok(())
38///     }
39///
40///     fn transpile(&self, source: &str, language: Language) -> Result<String> {
41///         // Custom transpilation logic
42///         Ok(format!("// Transpiled from {:?}\n{}", language, source))
43///     }
44/// }
45///
46/// // Register and use the plugin
47/// let mut registry = PluginRegistry::new();
48/// registry.register(Box::new(MyCustomTranspiler))?;
49///
50/// // Get plugins for a language
51/// let plugins = registry.get_for_language(&Language::Python);
52/// if let Some(plugin) = plugins.first() {
53///     let output = plugin.transpile("print('hello')", Language::Python)?;
54///     println!("{}", output);
55/// }
56/// # Ok::<(), anyhow::Error>(())
57/// ```
58use anyhow::{anyhow, Result};
59use async_trait::async_trait;
60use serde::{Deserialize, Serialize};
61use std::collections::HashMap;
62use std::path::Path;
63
64use crate::pipeline::{PipelineContext, PipelineStage, ValidationResult};
65use crate::types::Language;
66
67/// Metadata describing a transpiler plugin
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PluginMetadata {
70    /// Unique plugin identifier (kebab-case recommended)
71    pub name: String,
72
73    /// Semantic version (e.g., "1.0.0")
74    pub version: String,
75
76    /// Human-readable description
77    pub description: String,
78
79    /// Plugin author or organization
80    pub author: String,
81
82    /// Languages this plugin can transpile
83    pub supported_languages: Vec<Language>,
84}
85
86impl PluginMetadata {
87    /// Check if this plugin supports a given language
88    pub fn supports_language(&self, lang: &Language) -> bool {
89        self.supported_languages.contains(lang)
90    }
91}
92
93/// Core trait for transpiler plugins
94///
95/// Implement this trait to create custom transpilers that integrate with Batuta's pipeline.
96pub trait TranspilerPlugin: Send + Sync {
97    /// Get plugin metadata
98    fn metadata(&self) -> PluginMetadata;
99
100    /// Initialize the plugin
101    ///
102    /// Called once when the plugin is loaded. Use this for setup tasks like:
103    /// - Loading configuration
104    /// - Initializing caches
105    /// - Validating dependencies
106    fn initialize(&mut self) -> Result<()> {
107        Ok(())
108    }
109
110    /// Transpile source code to Rust
111    ///
112    /// # Arguments
113    ///
114    /// * `source` - Source code to transpile
115    /// * `language` - Source language
116    ///
117    /// # Returns
118    ///
119    /// Rust code as a String
120    fn transpile(&self, source: &str, language: Language) -> Result<String>;
121
122    /// Transpile a file
123    ///
124    /// Default implementation reads the file and calls `transpile()`.
125    /// Override for custom file handling (e.g., imports, multi-file projects).
126    fn transpile_file(&self, path: &Path, language: Language) -> Result<String> {
127        let source = std::fs::read_to_string(path)?;
128        self.transpile(&source, language)
129    }
130
131    /// Validate transpiled output
132    ///
133    /// Optional validation hook. Override to add custom validation logic.
134    fn validate(&self, _original: &str, _transpiled: &str) -> Result<()> {
135        Ok(())
136    }
137
138    /// Cleanup resources
139    ///
140    /// Called when the plugin is unloaded or the pipeline completes.
141    fn cleanup(&mut self) -> Result<()> {
142        Ok(())
143    }
144}
145
146/// Wrapper to integrate a plugin as a pipeline stage
147pub struct PluginStage {
148    plugin: Box<dyn TranspilerPlugin>,
149    name: String,
150}
151
152impl PluginStage {
153    pub fn new(plugin: Box<dyn TranspilerPlugin>) -> Self {
154        let name = plugin.metadata().name.clone();
155        Self { plugin, name }
156    }
157}
158
159#[async_trait]
160impl PipelineStage for PluginStage {
161    fn name(&self) -> &str {
162        &self.name
163    }
164
165    async fn execute(&self, mut ctx: PipelineContext) -> Result<PipelineContext> {
166        let metadata = self.plugin.metadata();
167
168        // Check if we have a language to transpile
169        let language =
170            ctx.primary_language.clone().ok_or_else(|| anyhow!("No primary language detected"))?;
171
172        // Check if plugin supports this language
173        if !metadata.supports_language(&language) {
174            return Err(anyhow!("Plugin '{}' does not support {:?}", metadata.name, language));
175        }
176
177        // Transpile files
178        for (source_path, output_path) in &ctx.file_mappings.clone() {
179            let transpiled = self.plugin.transpile_file(source_path, language.clone())?;
180
181            // Write output
182            if let Some(parent) = output_path.parent() {
183                std::fs::create_dir_all(parent)?;
184            }
185            std::fs::write(output_path, transpiled)?;
186        }
187
188        // Record plugin execution in metadata
189        ctx.metadata.insert(
190            format!("plugin_{}", metadata.name),
191            serde_json::json!({
192                "version": metadata.version,
193                "files_processed": ctx.file_mappings.len(),
194            }),
195        );
196
197        Ok(ctx)
198    }
199
200    fn validate(&self, ctx: &PipelineContext) -> Result<ValidationResult> {
201        let metadata = self.plugin.metadata();
202
203        // Validate all transpiled files
204        for (source_path, output_path) in &ctx.file_mappings {
205            let original = std::fs::read_to_string(source_path)?;
206            let transpiled = std::fs::read_to_string(output_path)?;
207
208            self.plugin.validate(&original, &transpiled)?;
209        }
210
211        Ok(ValidationResult {
212            stage: metadata.name.clone(),
213            passed: true,
214            message: format!(
215                "Plugin '{}' validation passed for {} files",
216                metadata.name,
217                ctx.file_mappings.len()
218            ),
219            details: None,
220        })
221    }
222}
223
224/// Plugin registry for managing transpiler plugins
225pub struct PluginRegistry {
226    plugins: Vec<Box<dyn TranspilerPlugin>>,
227    language_map: HashMap<Language, Vec<String>>, // Language -> plugin names
228}
229
230impl PluginRegistry {
231    /// Create a new empty plugin registry
232    pub fn new() -> Self {
233        Self { plugins: Vec::new(), language_map: HashMap::new() }
234    }
235
236    /// Register a transpiler plugin
237    pub fn register(&mut self, mut plugin: Box<dyn TranspilerPlugin>) -> Result<()> {
238        // Initialize the plugin
239        plugin.initialize()?;
240
241        let metadata = plugin.metadata();
242
243        // Update language map
244        for lang in &metadata.supported_languages {
245            self.language_map.entry(lang.clone()).or_default().push(metadata.name.clone());
246        }
247
248        // Store plugin
249        self.plugins.push(plugin);
250
251        Ok(())
252    }
253
254    /// Get plugin by name
255    pub fn get(&self, name: &str) -> Option<&dyn TranspilerPlugin> {
256        self.plugins.iter().find(|p| p.metadata().name == name).map(|p| &**p)
257    }
258
259    /// Get mutable reference to plugin by name
260    pub fn get_mut(&mut self, name: &str) -> Option<&mut dyn TranspilerPlugin> {
261        for plugin in &mut self.plugins {
262            if plugin.metadata().name == name {
263                return Some(&mut **plugin);
264            }
265        }
266        None
267    }
268
269    /// Get all plugins that support a language
270    pub fn get_for_language(&self, language: &Language) -> Vec<&dyn TranspilerPlugin> {
271        self.plugins
272            .iter()
273            .filter(|p| p.metadata().supports_language(language))
274            .map(|p| &**p as &dyn TranspilerPlugin)
275            .collect()
276    }
277
278    /// Get all registered plugin names
279    pub fn list_plugins(&self) -> Vec<String> {
280        self.plugins.iter().map(|p| p.metadata().name.clone()).collect()
281    }
282
283    /// Get languages supported by all plugins
284    pub fn supported_languages(&self) -> Vec<Language> {
285        self.language_map.keys().cloned().collect()
286    }
287
288    /// Unregister a plugin and cleanup
289    pub fn unregister(&mut self, name: &str) -> Result<()> {
290        if let Some(pos) = self.plugins.iter().position(|p| p.metadata().name == name) {
291            let mut plugin = self.plugins.remove(pos);
292            plugin.cleanup()?;
293
294            // Update language map
295            let metadata = plugin.metadata();
296            for lang in &metadata.supported_languages {
297                if let Some(names) = self.language_map.get_mut(lang) {
298                    names.retain(|n| n != &metadata.name);
299                    if names.is_empty() {
300                        self.language_map.remove(lang);
301                    }
302                }
303            }
304        }
305
306        Ok(())
307    }
308
309    /// Cleanup all plugins
310    pub fn cleanup_all(&mut self) -> Result<()> {
311        for plugin in &mut self.plugins {
312            plugin.cleanup()?;
313        }
314        self.plugins.clear();
315        self.language_map.clear();
316        Ok(())
317    }
318
319    /// Get number of registered plugins
320    pub fn len(&self) -> usize {
321        self.plugins.len()
322    }
323
324    /// Check if registry is empty
325    pub fn is_empty(&self) -> bool {
326        self.plugins.is_empty()
327    }
328}
329
330impl Default for PluginRegistry {
331    fn default() -> Self {
332        Self::new()
333    }
334}
335
336impl Drop for PluginRegistry {
337    fn drop(&mut self) {
338        // Best-effort cleanup on drop
339        let _ = self.cleanup_all();
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    struct TestPlugin {
348        name: String,
349        languages: Vec<Language>,
350    }
351
352    impl TranspilerPlugin for TestPlugin {
353        fn metadata(&self) -> PluginMetadata {
354            PluginMetadata {
355                name: self.name.clone(),
356                version: "1.0.0".to_string(),
357                description: "Test plugin".to_string(),
358                author: "Test Author".to_string(),
359                supported_languages: self.languages.clone(),
360            }
361        }
362
363        fn transpile(&self, source: &str, _language: Language) -> Result<String> {
364            Ok(format!("// Transpiled by {}\n{}", self.name, source))
365        }
366    }
367
368    #[test]
369    fn test_plugin_registration() {
370        let mut registry = PluginRegistry::new();
371
372        let plugin = Box::new(TestPlugin {
373            name: "test-plugin".to_string(),
374            languages: vec![Language::Python],
375        });
376
377        assert!(registry.register(plugin).is_ok());
378        assert_eq!(registry.len(), 1);
379        assert!(registry.get("test-plugin").is_some());
380    }
381
382    #[test]
383    fn test_language_lookup() {
384        let mut registry = PluginRegistry::new();
385
386        let plugin = Box::new(TestPlugin {
387            name: "python-plugin".to_string(),
388            languages: vec![Language::Python],
389        });
390
391        registry.register(plugin).expect("registration failed");
392
393        let plugins = registry.get_for_language(&Language::Python);
394        assert_eq!(plugins.len(), 1);
395
396        let plugins = registry.get_for_language(&Language::C);
397        assert_eq!(plugins.len(), 0);
398    }
399
400    #[test]
401    fn test_plugin_unregister() {
402        let mut registry = PluginRegistry::new();
403
404        let plugin = Box::new(TestPlugin {
405            name: "test-plugin".to_string(),
406            languages: vec![Language::Python],
407        });
408
409        registry.register(plugin).expect("registration failed");
410        assert_eq!(registry.len(), 1);
411
412        registry.unregister("test-plugin").expect("unregistration failed");
413        assert_eq!(registry.len(), 0);
414        assert!(registry.get("test-plugin").is_none());
415    }
416
417    // ============================================================================
418    // PLUGIN METADATA TESTS
419    // ============================================================================
420
421    #[test]
422    fn test_plugin_metadata_construction() {
423        let metadata = PluginMetadata {
424            name: "my-plugin".to_string(),
425            version: "1.2.3".to_string(),
426            description: "A test plugin".to_string(),
427            author: "Test Author".to_string(),
428            supported_languages: vec![Language::Python, Language::Rust],
429        };
430
431        assert_eq!(metadata.name, "my-plugin");
432        assert_eq!(metadata.version, "1.2.3");
433        assert_eq!(metadata.description, "A test plugin");
434        assert_eq!(metadata.author, "Test Author");
435        assert_eq!(metadata.supported_languages.len(), 2);
436    }
437
438    #[test]
439    fn test_plugin_metadata_supports_language() {
440        let metadata = PluginMetadata {
441            name: "test".to_string(),
442            version: "1.0.0".to_string(),
443            description: "Test".to_string(),
444            author: "Author".to_string(),
445            supported_languages: vec![Language::Python, Language::C],
446        };
447
448        assert!(metadata.supports_language(&Language::Python));
449        assert!(metadata.supports_language(&Language::C));
450        assert!(!metadata.supports_language(&Language::Rust));
451        assert!(!metadata.supports_language(&Language::Shell));
452    }
453
454    #[test]
455    fn test_plugin_metadata_serialization() {
456        let metadata = PluginMetadata {
457            name: "serialize-test".to_string(),
458            version: "0.1.0".to_string(),
459            description: "Serialization test".to_string(),
460            author: "Tester".to_string(),
461            supported_languages: vec![Language::Python],
462        };
463
464        let json = serde_json::to_string(&metadata).expect("json serialize failed");
465        let deserialized: PluginMetadata =
466            serde_json::from_str(&json).expect("json deserialize failed");
467
468        assert_eq!(metadata.name, deserialized.name);
469        assert_eq!(metadata.version, deserialized.version);
470        assert_eq!(metadata.description, deserialized.description);
471        assert_eq!(metadata.author, deserialized.author);
472        assert_eq!(metadata.supported_languages, deserialized.supported_languages);
473    }
474
475    #[test]
476    fn test_plugin_metadata_empty_languages() {
477        let metadata = PluginMetadata {
478            name: "no-lang".to_string(),
479            version: "1.0.0".to_string(),
480            description: "No languages".to_string(),
481            author: "Test".to_string(),
482            supported_languages: vec![],
483        };
484
485        assert!(!metadata.supports_language(&Language::Python));
486        assert_eq!(metadata.supported_languages.len(), 0);
487    }
488
489    // ============================================================================
490    // TRANSPILER PLUGIN TRAIT TESTS
491    // ============================================================================
492
493    struct MinimalPlugin;
494
495    impl TranspilerPlugin for MinimalPlugin {
496        fn metadata(&self) -> PluginMetadata {
497            PluginMetadata {
498                name: "minimal".to_string(),
499                version: "1.0.0".to_string(),
500                description: "Minimal plugin".to_string(),
501                author: "Test".to_string(),
502                supported_languages: vec![Language::Python],
503            }
504        }
505
506        fn transpile(&self, source: &str, _language: Language) -> Result<String> {
507            Ok(format!("fn main() {{\n    // {}\n}}", source))
508        }
509    }
510
511    #[test]
512    fn test_plugin_default_initialize() {
513        let mut plugin = MinimalPlugin;
514        assert!(plugin.initialize().is_ok());
515    }
516
517    #[test]
518    fn test_plugin_transpile() {
519        let plugin = MinimalPlugin;
520        let result =
521            plugin.transpile("print('hello')", Language::Python).expect("unexpected failure");
522        assert!(result.contains("fn main()"));
523        assert!(result.contains("print('hello')"));
524    }
525
526    #[test]
527    fn test_plugin_transpile_file() {
528        use std::io::Write;
529        use tempfile::NamedTempFile;
530
531        let plugin = MinimalPlugin;
532
533        let mut temp_file = NamedTempFile::new().expect("tempfile creation failed");
534        temp_file.write_all(b"print('test')").expect("fs write failed");
535        temp_file.flush().expect("unexpected failure");
536
537        let result =
538            plugin.transpile_file(temp_file.path(), Language::Python).expect("unexpected failure");
539        assert!(result.contains("print('test')"));
540    }
541
542    #[test]
543    fn test_plugin_default_validate() {
544        let plugin = MinimalPlugin;
545        assert!(plugin.validate("original", "transpiled").is_ok());
546    }
547
548    #[test]
549    fn test_plugin_default_cleanup() {
550        let mut plugin = MinimalPlugin;
551        assert!(plugin.cleanup().is_ok());
552    }
553
554    // ============================================================================
555    // PLUGIN STAGE TESTS
556    // ============================================================================
557
558    #[test]
559    fn test_plugin_stage_construction() {
560        let plugin = Box::new(TestPlugin {
561            name: "stage-test".to_string(),
562            languages: vec![Language::Python],
563        });
564
565        let stage = PluginStage::new(plugin);
566        assert_eq!(stage.name(), "stage-test");
567    }
568
569    #[tokio::test]
570    async fn test_plugin_stage_execute_no_language() {
571        let plugin =
572            Box::new(TestPlugin { name: "test".to_string(), languages: vec![Language::Python] });
573
574        let stage = PluginStage::new(plugin);
575        let ctx = PipelineContext::new(
576            std::path::PathBuf::from("/tmp/source"),
577            std::path::PathBuf::from("/tmp/output"),
578        );
579
580        let result = stage.execute(ctx).await;
581        assert!(result.is_err());
582        assert!(result.unwrap_err().to_string().contains("No primary language"));
583    }
584
585    #[tokio::test]
586    async fn test_plugin_stage_execute_unsupported_language() {
587        let plugin = Box::new(TestPlugin {
588            name: "python-only".to_string(),
589            languages: vec![Language::Python],
590        });
591
592        let stage = PluginStage::new(plugin);
593        let mut ctx = PipelineContext::new(
594            std::path::PathBuf::from("/tmp/source"),
595            std::path::PathBuf::from("/tmp/output"),
596        );
597        ctx.primary_language = Some(Language::Rust);
598
599        let result = stage.execute(ctx).await;
600        assert!(result.is_err());
601        assert!(result.unwrap_err().to_string().contains("does not support"));
602    }
603
604    #[tokio::test]
605    async fn test_plugin_stage_execute_success() {
606        use std::fs;
607        use tempfile::TempDir;
608
609        let temp_dir = TempDir::new().expect("tempdir creation failed");
610        let source_path = temp_dir.path().join("input.py");
611        let output_path = temp_dir.path().join("output.rs");
612
613        fs::write(&source_path, "print('hello')").expect("fs write failed");
614
615        let plugin = Box::new(TestPlugin {
616            name: "transpiler".to_string(),
617            languages: vec![Language::Python],
618        });
619
620        let stage = PluginStage::new(plugin);
621        let mut ctx =
622            PipelineContext::new(temp_dir.path().to_path_buf(), temp_dir.path().to_path_buf());
623        ctx.primary_language = Some(Language::Python);
624        ctx.file_mappings.push((source_path.clone(), output_path.clone()));
625
626        let result = stage.execute(ctx).await;
627        assert!(result.is_ok());
628
629        let ctx = result.expect("operation failed");
630        assert!(ctx.metadata.contains_key("plugin_transpiler"));
631        assert!(output_path.exists());
632
633        let content = fs::read_to_string(&output_path).expect("fs read failed");
634        assert!(content.contains("Transpiled by transpiler"));
635    }
636
637    #[test]
638    fn test_plugin_stage_validate_success() {
639        use std::fs;
640        use tempfile::TempDir;
641
642        let temp_dir = TempDir::new().expect("tempdir creation failed");
643        let source_path = temp_dir.path().join("source.py");
644        let output_path = temp_dir.path().join("output.rs");
645
646        fs::write(&source_path, "original").expect("fs write failed");
647        fs::write(&output_path, "transpiled").expect("fs write failed");
648
649        let plugin = Box::new(TestPlugin {
650            name: "validator".to_string(),
651            languages: vec![Language::Python],
652        });
653
654        let stage = PluginStage::new(plugin);
655        let mut ctx =
656            PipelineContext::new(temp_dir.path().to_path_buf(), temp_dir.path().to_path_buf());
657        ctx.file_mappings.push((source_path, output_path));
658
659        let result = stage.validate(&ctx).expect("validation failed");
660        assert!(result.passed);
661        assert_eq!(result.stage, "validator");
662        assert!(result.message.contains("validation passed"));
663    }
664
665    // ============================================================================
666    // PLUGIN REGISTRY TESTS
667    // ============================================================================
668
669    #[test]
670    fn test_registry_default() {
671        let registry = PluginRegistry::default();
672        assert_eq!(registry.len(), 0);
673        assert!(registry.is_empty());
674    }
675
676    #[test]
677    fn test_registry_is_empty() {
678        let mut registry = PluginRegistry::new();
679        assert!(registry.is_empty());
680
681        let plugin =
682            Box::new(TestPlugin { name: "test".to_string(), languages: vec![Language::Python] });
683        registry.register(plugin).expect("registration failed");
684
685        assert!(!registry.is_empty());
686    }
687
688    #[test]
689    fn test_registry_len() {
690        let mut registry = PluginRegistry::new();
691        assert_eq!(registry.len(), 0);
692
693        registry
694            .register(Box::new(TestPlugin {
695                name: "plugin1".to_string(),
696                languages: vec![Language::Python],
697            }))
698            .expect("unexpected failure");
699        assert_eq!(registry.len(), 1);
700
701        registry
702            .register(Box::new(TestPlugin {
703                name: "plugin2".to_string(),
704                languages: vec![Language::Rust],
705            }))
706            .expect("unexpected failure");
707        assert_eq!(registry.len(), 2);
708    }
709
710    #[test]
711    fn test_registry_get_mut() {
712        let mut registry = PluginRegistry::new();
713
714        registry
715            .register(Box::new(TestPlugin {
716                name: "mutable-test".to_string(),
717                languages: vec![Language::Python],
718            }))
719            .expect("unexpected failure");
720
721        let plugin = registry.get_mut("mutable-test");
722        assert!(plugin.is_some());
723        assert_eq!(plugin.expect("unexpected failure").metadata().name, "mutable-test");
724
725        let none_plugin = registry.get_mut("nonexistent");
726        assert!(none_plugin.is_none());
727    }
728
729    #[test]
730    fn test_registry_list_plugins() {
731        let mut registry = PluginRegistry::new();
732
733        registry
734            .register(Box::new(TestPlugin {
735                name: "plugin-a".to_string(),
736                languages: vec![Language::Python],
737            }))
738            .expect("unexpected failure");
739
740        registry
741            .register(Box::new(TestPlugin {
742                name: "plugin-b".to_string(),
743                languages: vec![Language::Rust],
744            }))
745            .expect("unexpected failure");
746
747        let list = registry.list_plugins();
748        assert_eq!(list.len(), 2);
749        assert!(list.contains(&"plugin-a".to_string()));
750        assert!(list.contains(&"plugin-b".to_string()));
751    }
752
753    #[test]
754    fn test_registry_supported_languages() {
755        let mut registry = PluginRegistry::new();
756
757        registry
758            .register(Box::new(TestPlugin {
759                name: "python-plugin".to_string(),
760                languages: vec![Language::Python],
761            }))
762            .expect("unexpected failure");
763
764        registry
765            .register(Box::new(TestPlugin {
766                name: "multi-plugin".to_string(),
767                languages: vec![Language::Rust, Language::C],
768            }))
769            .expect("unexpected failure");
770
771        let langs = registry.supported_languages();
772        assert!(langs.len() >= 3);
773        assert!(langs.contains(&Language::Python));
774        assert!(langs.contains(&Language::Rust));
775        assert!(langs.contains(&Language::C));
776    }
777
778    #[test]
779    fn test_registry_multiple_plugins_same_language() {
780        let mut registry = PluginRegistry::new();
781
782        registry
783            .register(Box::new(TestPlugin {
784                name: "python-plugin-1".to_string(),
785                languages: vec![Language::Python],
786            }))
787            .expect("unexpected failure");
788
789        registry
790            .register(Box::new(TestPlugin {
791                name: "python-plugin-2".to_string(),
792                languages: vec![Language::Python],
793            }))
794            .expect("unexpected failure");
795
796        let plugins = registry.get_for_language(&Language::Python);
797        assert_eq!(plugins.len(), 2);
798    }
799
800    #[test]
801    fn test_registry_cleanup_all() {
802        let mut registry = PluginRegistry::new();
803
804        registry
805            .register(Box::new(TestPlugin {
806                name: "cleanup1".to_string(),
807                languages: vec![Language::Python],
808            }))
809            .expect("unexpected failure");
810
811        registry
812            .register(Box::new(TestPlugin {
813                name: "cleanup2".to_string(),
814                languages: vec![Language::Rust],
815            }))
816            .expect("unexpected failure");
817
818        assert_eq!(registry.len(), 2);
819
820        registry.cleanup_all().expect("unexpected failure");
821
822        assert_eq!(registry.len(), 0);
823        assert!(registry.is_empty());
824        assert_eq!(registry.supported_languages().len(), 0);
825    }
826
827    #[test]
828    fn test_registry_unregister_nonexistent() {
829        let mut registry = PluginRegistry::new();
830
831        // Unregistering nonexistent plugin should not error
832        let result = registry.unregister("nonexistent");
833        assert!(result.is_ok());
834    }
835
836    #[test]
837    fn test_registry_unregister_updates_language_map() {
838        let mut registry = PluginRegistry::new();
839
840        registry
841            .register(Box::new(TestPlugin {
842                name: "only-python".to_string(),
843                languages: vec![Language::Python],
844            }))
845            .expect("unexpected failure");
846
847        assert!(registry.supported_languages().contains(&Language::Python));
848
849        registry.unregister("only-python").expect("unregistration failed");
850
851        // Language map should be updated
852        assert!(!registry.supported_languages().contains(&Language::Python));
853    }
854
855    #[test]
856    fn test_registry_get_nonexistent() {
857        let registry = PluginRegistry::new();
858        assert!(registry.get("nonexistent").is_none());
859    }
860
861    #[test]
862    fn test_registry_get_for_language_empty() {
863        let registry = PluginRegistry::new();
864        let plugins = registry.get_for_language(&Language::Python);
865        assert_eq!(plugins.len(), 0);
866    }
867
868    #[test]
869    fn test_plugin_multiple_languages() {
870        let mut registry = PluginRegistry::new();
871
872        registry
873            .register(Box::new(TestPlugin {
874                name: "multi-lang".to_string(),
875                languages: vec![Language::Python, Language::Rust, Language::C],
876            }))
877            .expect("unexpected failure");
878
879        // Should be accessible from all three languages
880        assert_eq!(registry.get_for_language(&Language::Python).len(), 1);
881        assert_eq!(registry.get_for_language(&Language::Rust).len(), 1);
882        assert_eq!(registry.get_for_language(&Language::C).len(), 1);
883        assert_eq!(registry.get_for_language(&Language::Shell).len(), 0);
884    }
885
886    // Test initialization failure handling
887    struct FailingInitPlugin;
888
889    impl TranspilerPlugin for FailingInitPlugin {
890        fn metadata(&self) -> PluginMetadata {
891            PluginMetadata {
892                name: "failing".to_string(),
893                version: "1.0.0".to_string(),
894                description: "Fails on init".to_string(),
895                author: "Test".to_string(),
896                supported_languages: vec![Language::Python],
897            }
898        }
899
900        fn initialize(&mut self) -> Result<()> {
901            Err(anyhow!("Initialization failed"))
902        }
903
904        fn transpile(&self, _source: &str, _language: Language) -> Result<String> {
905            Ok("".to_string())
906        }
907    }
908
909    #[test]
910    fn test_plugin_initialization_failure() {
911        let mut registry = PluginRegistry::new();
912
913        let plugin = Box::new(FailingInitPlugin);
914        let result = registry.register(plugin);
915
916        assert!(result.is_err());
917        assert!(result.unwrap_err().to_string().contains("Initialization failed"));
918        assert_eq!(registry.len(), 0);
919    }
920}