Skip to main content

mecha10_cli_core/framework/
template_engine.rs

1//! Template engine for code generation
2//!
3//! Simple template engine that supports variable substitution using {{variable}} syntax.
4//! Templates are loaded from the `templates/` directory and processed with user-provided variables.
5
6use anyhow::{Context, Result};
7use std::collections::HashMap;
8use std::path::{Path, PathBuf};
9
10/// Template engine for generating files from templates
11pub struct TemplateEngine {
12    /// Base directory where templates are stored
13    template_dir: PathBuf,
14}
15
16impl TemplateEngine {
17    /// Create a new template engine
18    ///
19    /// # Arguments
20    /// * `template_dir` - Base directory containing template files
21    pub fn new(template_dir: impl Into<PathBuf>) -> Self {
22        Self {
23            template_dir: template_dir.into(),
24        }
25    }
26
27    /// Render a template with variables
28    ///
29    /// # Arguments
30    /// * `template_path` - Path to template file relative to template_dir (e.g., "drivers/sensor.rs.template")
31    /// * `variables` - HashMap of variable names to values
32    ///
33    /// # Returns
34    /// Rendered template content as String
35    ///
36    /// # Example
37    /// ```rust,ignore
38    /// let mut vars = HashMap::new();
39    /// vars.insert("name", "camera_driver");
40    /// vars.insert("PascalName", "CameraDriver");
41    ///
42    /// let engine = TemplateEngine::default();
43    /// let rendered = engine.render("drivers/sensor.rs.template", &vars)?;
44    /// ```
45    pub fn render(&self, template_path: &str, variables: &HashMap<&str, &str>) -> Result<String> {
46        let full_path = self.template_dir.join(template_path);
47
48        // Read template file
49        let template_content =
50            std::fs::read_to_string(&full_path).context(format!("Failed to read template: {}", full_path.display()))?;
51
52        // Replace variables
53        let mut result = template_content;
54        for (key, value) in variables {
55            let placeholder = format!("{{{{{}}}}}", key);
56            result = result.replace(&placeholder, value);
57        }
58
59        Ok(result)
60    }
61
62    /// Render a template and write it to a file
63    ///
64    /// # Arguments
65    /// * `template_path` - Path to template file relative to template_dir
66    /// * `output_path` - Destination file path
67    /// * `variables` - HashMap of variable names to values
68    pub async fn render_to_file(
69        &self,
70        template_path: &str,
71        output_path: impl AsRef<Path>,
72        variables: &HashMap<&str, &str>,
73    ) -> Result<()> {
74        let rendered = self.render(template_path, variables)?;
75
76        tokio::fs::write(output_path.as_ref(), rendered)
77            .await
78            .context(format!("Failed to write file: {}", output_path.as_ref().display()))?;
79
80        Ok(())
81    }
82
83    /// List available templates in a category
84    ///
85    /// # Arguments
86    /// * `category` - Template category (e.g., "drivers", "nodes")
87    ///
88    /// # Returns
89    /// Vector of template names (without .template extension)
90    pub fn list_templates(&self, category: &str) -> Result<Vec<String>> {
91        let category_path = self.template_dir.join(category);
92
93        if !category_path.exists() {
94            return Ok(Vec::new());
95        }
96
97        let mut templates = Vec::new();
98        let entries = std::fs::read_dir(&category_path).context(format!(
99            "Failed to read template directory: {}",
100            category_path.display()
101        ))?;
102
103        for entry in entries {
104            let entry = entry?;
105            let path = entry.path();
106
107            if path.is_file() {
108                if let Some(file_name) = path.file_name().and_then(|n| n.to_str()) {
109                    if file_name.ends_with(".template") {
110                        // Remove .template extension
111                        let name = file_name.trim_end_matches(".template").to_string();
112                        templates.push(name);
113                    }
114                }
115            }
116        }
117
118        templates.sort();
119        Ok(templates)
120    }
121
122    /// Check if a template exists
123    pub fn template_exists(&self, template_path: &str) -> bool {
124        self.template_dir.join(template_path).exists()
125    }
126}
127
128impl Default for TemplateEngine {
129    /// Create a default template engine using the CLI's bundled templates
130    fn default() -> Self {
131        // Templates are located at packages/cli/templates/
132        let template_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("templates");
133        Self::new(template_dir)
134    }
135}
136
137/// Helper to create common variable mappings
138pub struct TemplateVars {
139    vars: HashMap<String, String>,
140}
141
142impl TemplateVars {
143    /// Create a new template variables builder
144    pub fn new() -> Self {
145        Self { vars: HashMap::new() }
146    }
147
148    /// Add a variable
149    #[allow(dead_code)]
150    pub fn add(&mut self, key: impl Into<String>, value: impl Into<String>) -> &mut Self {
151        self.vars.insert(key.into(), value.into());
152        self
153    }
154
155    /// Add common name transformations
156    ///
157    /// Given a name like "camera_driver", this adds:
158    /// - name: camera_driver
159    /// - PascalName: CameraDriver
160    /// - UPPER_NAME: CAMERA_DRIVER
161    pub fn add_name(&mut self, name: &str) -> &mut Self {
162        use crate::utils::{to_pascal_case, to_snake_case};
163
164        self.vars.insert("name".to_string(), name.to_string());
165        self.vars.insert("PascalName".to_string(), to_pascal_case(name));
166        self.vars.insert("snake_name".to_string(), to_snake_case(name));
167        self.vars
168            .insert("UPPER_NAME".to_string(), to_snake_case(name).to_uppercase());
169        self
170    }
171
172    /// Convert to HashMap with &str keys and values for template rendering
173    pub fn to_hashmap(&self) -> HashMap<&str, &str> {
174        self.vars.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect()
175    }
176}
177
178impl Default for TemplateVars {
179    fn default() -> Self {
180        Self::new()
181    }
182}