razeri-plugin-api 0.11.0

Extension traits and registries for Razeri plugins
Documentation
// Copyright (C) 2026 Gabriel Shea
// Licensed under either of the Apache License, Version 2.0 or the MIT
// license, at your option.

//! Language renderer factory trait and registry.
//!
//! [`LanguageRendererFactory`] is the extension point for adding new
//! code-generation targets (Kotlin, Go, C#, etc.) without modifying core crates.
//! [`LanguageRenderer`] renders a design into code for a specific language.
//! [`LanguageRegistry`] provides string-keyed lookup by language name.

use crate::error::LanguageError;
use std::collections::HashMap;
use std::sync::Arc;

// ── LanguageRenderer trait ─────────────────────────────────────────────

/// Renders Razeri designs into code for a specific programming language.
///
/// The design is passed as a serialized `serde_json::Value` so the plugin
/// API does not depend on Razeri's design model. Concrete implementations
/// can deserialize this into the `Design` struct if needed.
pub trait LanguageRenderer: Send + Sync {
    /// Language identifier (e.g., "kotlin", "go", "csharp").
    fn language(&self) -> &str;

    /// Render a serialized design document into source code.
    ///
    /// # Errors
    ///
    /// Returns an error if the operation fails.
    fn render_design(&self, design: &serde_json::Value) -> Result<String, LanguageError>;
}

// ── LanguageRendererFactory trait ──────────────────────────────────────

/// Factory that creates [`LanguageRenderer`] instances from configuration.
pub trait LanguageRendererFactory: Send + Sync {
    /// Language identifier.
    fn language(&self) -> &str;

    /// Human-readable display name.
    fn display_name(&self) -> &str;

    /// Create a renderer from configuration.
    ///
    /// # Errors
    ///
    /// Returns an error if the operation fails.
    fn create(
        &self,
        config: &HashMap<String, String>,
    ) -> Result<Arc<dyn LanguageRenderer>, LanguageError>;
}

// ── LanguageRegistry ──────────────────────────────────────────────────

/// Registry for language renderer factories (string-keyed by language name).
pub struct LanguageRegistry {
    renderers: HashMap<String, Arc<dyn LanguageRendererFactory>>,
}

impl LanguageRegistry {
    /// Create an empty registry.
    #[must_use]
    pub fn new() -> Self {
        Self { renderers: HashMap::new() }
    }

    /// Register a language renderer factory.
    pub fn register(&mut self, language: String, factory: Arc<dyn LanguageRendererFactory>) {
        self.renderers.insert(language, factory);
    }

    /// Look up a factory by language name.
    #[must_use]
    pub fn get(&self, language: &str) -> Option<&Arc<dyn LanguageRendererFactory>> {
        self.renderers.get(language)
    }

    /// List all registered languages as (language, `display_name`) pairs.
    #[must_use]
    pub fn list(&self) -> Vec<(&str, &str)> {
        self.renderers.values().map(|f| (f.language(), f.display_name())).collect()
    }
}

impl Default for LanguageRegistry {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    struct MockRenderer {
        lang: String,
    }

    impl LanguageRenderer for MockRenderer {
        fn language(&self) -> &str {
            &self.lang
        }

        fn render_design(&self, _design: &serde_json::Value) -> Result<String, LanguageError> {
            Ok(format!("// Generated {} code", self.lang))
        }
    }

    struct MockRendererFactory {
        lang: &'static str,
        display: &'static str,
    }

    impl LanguageRendererFactory for MockRendererFactory {
        fn language(&self) -> &str {
            self.lang
        }

        fn display_name(&self) -> &str {
            self.display
        }

        fn create(
            &self,
            _config: &HashMap<String, String>,
        ) -> Result<Arc<dyn LanguageRenderer>, LanguageError> {
            Ok(Arc::new(MockRenderer { lang: self.lang.to_string() }))
        }
    }

    #[test]
    fn language_registry_resolves_registered_language() {
        let mut registry = LanguageRegistry::new();
        registry.register(
            "kotlin".to_string(),
            Arc::new(MockRendererFactory { lang: "kotlin", display: "Kotlin" }),
        );
        assert!(registry.get("kotlin").is_some());
        assert!(registry.get("java").is_none());
    }

    #[test]
    fn language_registry_lists_languages() {
        let mut registry = LanguageRegistry::new();
        for (lang, display) in
            [("go", "Go"), ("java", "Java"), ("python", "Python"), ("kotlin", "Kotlin")]
        {
            registry.register(lang.to_string(), Arc::new(MockRendererFactory { lang, display }));
        }
        let list = registry.list();
        assert_eq!(list.len(), 4);
    }

    #[test]
    fn renderer_produces_output() {
        let factory = MockRendererFactory { lang: "kotlin", display: "Kotlin" };
        let renderer = factory.create(&HashMap::new()).unwrap();
        let design = serde_json::json!({"name": "test"});
        let output = renderer.render_design(&design).unwrap();
        assert!(output.contains("kotlin"));
    }
}