Skip to main content

razeri_plugin_api/
language.rs

1// Copyright (C) 2026 Gabriel Shea <gshea@ideapivot.com> - All Rights Reserved
2
3//! Language renderer factory trait and registry.
4//!
5//! [`LanguageRendererFactory`] is the extension point for adding new
6//! code-generation targets (Kotlin, Go, C#, etc.) without modifying core crates.
7//! [`LanguageRenderer`] renders a design into code for a specific language.
8//! [`LanguageRegistry`] provides string-keyed lookup by language name.
9
10use crate::error::LanguageError;
11use std::collections::HashMap;
12use std::sync::Arc;
13
14// ── LanguageRenderer trait ─────────────────────────────────────────────
15
16/// Renders Razeri designs into code for a specific programming language.
17///
18/// The design is passed as a serialized `serde_json::Value` to avoid
19/// coupling the plugin API to `razeri-design`. Concrete implementations
20/// can deserialize this into the `Design` struct if needed.
21pub trait LanguageRenderer: Send + Sync {
22    /// Language identifier (e.g., "kotlin", "go", "csharp").
23    fn language(&self) -> &str;
24
25    /// Render a serialized design document into source code.
26    ///
27    /// # Errors
28    ///
29    /// Returns an error if the operation fails.
30    fn render_design(&self, design: &serde_json::Value) -> Result<String, LanguageError>;
31}
32
33// ── LanguageRendererFactory trait ──────────────────────────────────────
34
35/// Factory that creates [`LanguageRenderer`] instances from configuration.
36pub trait LanguageRendererFactory: Send + Sync {
37    /// Language identifier.
38    fn language(&self) -> &str;
39
40    /// Human-readable display name.
41    fn display_name(&self) -> &str;
42
43    /// Create a renderer from configuration.
44    ///
45    /// # Errors
46    ///
47    /// Returns an error if the operation fails.
48    fn create(
49        &self,
50        config: &HashMap<String, String>,
51    ) -> Result<Arc<dyn LanguageRenderer>, LanguageError>;
52}
53
54// ── LanguageRegistry ──────────────────────────────────────────────────
55
56/// Registry for language renderer factories (string-keyed by language name).
57pub struct LanguageRegistry {
58    renderers: HashMap<String, Arc<dyn LanguageRendererFactory>>,
59}
60
61impl LanguageRegistry {
62    /// Create an empty registry.
63    #[must_use]
64    pub fn new() -> Self {
65        Self { renderers: HashMap::new() }
66    }
67
68    /// Register a language renderer factory.
69    pub fn register(&mut self, language: String, factory: Arc<dyn LanguageRendererFactory>) {
70        self.renderers.insert(language, factory);
71    }
72
73    /// Look up a factory by language name.
74    #[must_use]
75    pub fn get(&self, language: &str) -> Option<&Arc<dyn LanguageRendererFactory>> {
76        self.renderers.get(language)
77    }
78
79    /// List all registered languages as (language, `display_name`) pairs.
80    #[must_use]
81    pub fn list(&self) -> Vec<(&str, &str)> {
82        self.renderers.values().map(|f| (f.language(), f.display_name())).collect()
83    }
84}
85
86impl Default for LanguageRegistry {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    struct MockRenderer {
97        lang: String,
98    }
99
100    impl LanguageRenderer for MockRenderer {
101        fn language(&self) -> &str {
102            &self.lang
103        }
104
105        fn render_design(&self, _design: &serde_json::Value) -> Result<String, LanguageError> {
106            Ok(format!("// Generated {} code", self.lang))
107        }
108    }
109
110    struct MockRendererFactory {
111        lang: &'static str,
112        display: &'static str,
113    }
114
115    impl LanguageRendererFactory for MockRendererFactory {
116        fn language(&self) -> &str {
117            self.lang
118        }
119
120        fn display_name(&self) -> &str {
121            self.display
122        }
123
124        fn create(
125            &self,
126            _config: &HashMap<String, String>,
127        ) -> Result<Arc<dyn LanguageRenderer>, LanguageError> {
128            Ok(Arc::new(MockRenderer { lang: self.lang.to_string() }))
129        }
130    }
131
132    #[test]
133    fn language_registry_resolves_registered_language() {
134        let mut registry = LanguageRegistry::new();
135        registry.register(
136            "kotlin".to_string(),
137            Arc::new(MockRendererFactory { lang: "kotlin", display: "Kotlin" }),
138        );
139        assert!(registry.get("kotlin").is_some());
140        assert!(registry.get("java").is_none());
141    }
142
143    #[test]
144    fn language_registry_lists_languages() {
145        let mut registry = LanguageRegistry::new();
146        for (lang, display) in
147            [("go", "Go"), ("java", "Java"), ("python", "Python"), ("kotlin", "Kotlin")]
148        {
149            registry.register(lang.to_string(), Arc::new(MockRendererFactory { lang, display }));
150        }
151        let list = registry.list();
152        assert_eq!(list.len(), 4);
153    }
154
155    #[test]
156    fn renderer_produces_output() {
157        let factory = MockRendererFactory { lang: "kotlin", display: "Kotlin" };
158        let renderer = factory.create(&HashMap::new()).unwrap();
159        let design = serde_json::json!({"name": "test"});
160        let output = renderer.render_design(&design).unwrap();
161        assert!(output.contains("kotlin"));
162    }
163}