mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Template engine for code generation
//!
//! Simple template engine that supports variable substitution using {{variable}} syntax.
//! Templates are loaded from the `templates/` directory and processed with user-provided variables.

use anyhow::{Context, Result};
use std::collections::HashMap;
use std::path::{Path, PathBuf};

/// Template engine for generating files from templates
pub struct TemplateEngine {
    /// Base directory where templates are stored
    template_dir: PathBuf,
}

impl TemplateEngine {
    /// Create a new template engine
    ///
    /// # Arguments
    /// * `template_dir` - Base directory containing template files
    pub fn new(template_dir: impl Into<PathBuf>) -> Self {
        Self {
            template_dir: template_dir.into(),
        }
    }

    /// Create a default template engine using the CLI's bundled templates
    pub fn default() -> Self {
        // Templates are located at packages/cli/templates/
        let template_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
        Self::new(template_dir)
    }

    /// Render a template with variables
    ///
    /// # Arguments
    /// * `template_path` - Path to template file relative to template_dir (e.g., "drivers/sensor.rs.template")
    /// * `variables` - HashMap of variable names to values
    ///
    /// # Returns
    /// Rendered template content as String
    ///
    /// # Example
    /// ```rust,ignore
    /// let mut vars = HashMap::new();
    /// vars.insert("name", "camera_driver");
    /// vars.insert("PascalName", "CameraDriver");
    ///
    /// let engine = TemplateEngine::default();
    /// let rendered = engine.render("drivers/sensor.rs.template", &vars)?;
    /// ```
    pub fn render(&self, template_path: &str, variables: &HashMap<&str, &str>) -> Result<String> {
        let full_path = self.template_dir.join(template_path);

        // Read template file
        let template_content =
            std::fs::read_to_string(&full_path).context(format!("Failed to read template: {}", full_path.display()))?;

        // Replace variables
        let mut result = template_content;
        for (key, value) in variables {
            let placeholder = format!("{{{{{}}}}}", key);
            result = result.replace(&placeholder, value);
        }

        Ok(result)
    }

    /// Render a template and write it to a file
    ///
    /// # Arguments
    /// * `template_path` - Path to template file relative to template_dir
    /// * `output_path` - Destination file path
    /// * `variables` - HashMap of variable names to values
    pub async fn render_to_file(
        &self,
        template_path: &str,
        output_path: impl AsRef<Path>,
        variables: &HashMap<&str, &str>,
    ) -> Result<()> {
        let rendered = self.render(template_path, variables)?;

        tokio::fs::write(output_path.as_ref(), rendered)
            .await
            .context(format!("Failed to write file: {}", output_path.as_ref().display()))?;

        Ok(())
    }

    /// List available templates in a category
    ///
    /// # Arguments
    /// * `category` - Template category (e.g., "drivers", "nodes")
    ///
    /// # Returns
    /// Vector of template names (without .template extension)
    pub fn list_templates(&self, category: &str) -> Result<Vec<String>> {
        let category_path = self.template_dir.join(category);

        if !category_path.exists() {
            return Ok(Vec::new());
        }

        let mut templates = Vec::new();
        let entries = std::fs::read_dir(&category_path).context(format!(
            "Failed to read template directory: {}",
            category_path.display()
        ))?;

        for entry in entries {
            let entry = entry?;
            let path = entry.path();

            if path.is_file() {
                if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
                    if file_name.ends_with(".template") {
                        // Remove .template extension
                        let name = file_name.trim_end_matches(".template").to_string();
                        templates.push(name);
                    }
                }
            }
        }

        templates.sort();
        Ok(templates)
    }

    /// Check if a template exists
    pub fn template_exists(&self, template_path: &str) -> bool {
        self.template_dir.join(template_path).exists()
    }
}

/// Helper to create common variable mappings
pub struct TemplateVars {
    vars: HashMap<String, String>,
}

impl TemplateVars {
    /// Create a new template variables builder
    pub fn new() -> Self {
        Self { vars: HashMap::new() }
    }

    /// Add a variable
    #[allow(dead_code)]
    pub fn add(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
        self.vars.insert(key.into(), value.into());
        self
    }

    /// Add common name transformations
    ///
    /// Given a name like "camera_driver", this adds:
    /// - name: camera_driver
    /// - PascalName: CameraDriver
    /// - UPPER_NAME: CAMERA_DRIVER
    pub fn add_name(&mut self, name: &str) -> &mut Self {
        use crate::utils::{to_pascal_case, to_snake_case};

        self.vars.insert("name".to_string(), name.to_string());
        self.vars.insert("PascalName".to_string(), to_pascal_case(name));
        self.vars.insert("snake_name".to_string(), to_snake_case(name));
        self.vars
            .insert("UPPER_NAME".to_string(), to_snake_case(name).to_uppercase());
        self
    }

    /// Convert to HashMap with &str keys and values for template rendering
    pub fn to_hashmap(&self) -> HashMap<&str, &str> {
        self.vars.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
    }
}

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