Skip to main content

razeri_plugin_api/
language.rs

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