1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct PluginMetadata {
70 pub name: String,
72
73 pub version: String,
75
76 pub description: String,
78
79 pub author: String,
81
82 pub supported_languages: Vec<Language>,
84}
85
86impl PluginMetadata {
87 pub fn supports_language(&self, lang: &Language) -> bool {
89 self.supported_languages.contains(lang)
90 }
91}
92
93pub trait TranspilerPlugin: Send + Sync {
97 fn metadata(&self) -> PluginMetadata;
99
100 fn initialize(&mut self) -> Result<()> {
107 Ok(())
108 }
109
110 fn transpile(&self, source: &str, language: Language) -> Result<String>;
121
122 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 fn validate(&self, _original: &str, _transpiled: &str) -> Result<()> {
135 Ok(())
136 }
137
138 fn cleanup(&mut self) -> Result<()> {
142 Ok(())
143 }
144}
145
146pub 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 let language =
170 ctx.primary_language.clone().ok_or_else(|| anyhow!("No primary language detected"))?;
171
172 if !metadata.supports_language(&language) {
174 return Err(anyhow!("Plugin '{}' does not support {:?}", metadata.name, language));
175 }
176
177 for (source_path, output_path) in &ctx.file_mappings.clone() {
179 let transpiled = self.plugin.transpile_file(source_path, language.clone())?;
180
181 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 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 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
224pub struct PluginRegistry {
226 plugins: Vec<Box<dyn TranspilerPlugin>>,
227 language_map: HashMap<Language, Vec<String>>, }
229
230impl PluginRegistry {
231 pub fn new() -> Self {
233 Self { plugins: Vec::new(), language_map: HashMap::new() }
234 }
235
236 pub fn register(&mut self, mut plugin: Box<dyn TranspilerPlugin>) -> Result<()> {
238 plugin.initialize()?;
240
241 let metadata = plugin.metadata();
242
243 for lang in &metadata.supported_languages {
245 self.language_map.entry(lang.clone()).or_default().push(metadata.name.clone());
246 }
247
248 self.plugins.push(plugin);
250
251 Ok(())
252 }
253
254 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 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 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 pub fn list_plugins(&self) -> Vec<String> {
280 self.plugins.iter().map(|p| p.metadata().name.clone()).collect()
281 }
282
283 pub fn supported_languages(&self) -> Vec<Language> {
285 self.language_map.keys().cloned().collect()
286 }
287
288 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 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 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 pub fn len(&self) -> usize {
321 self.plugins.len()
322 }
323
324 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 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 #[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 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 #[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 #[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 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 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 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 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}