espforge_lib/
template_utils.rs

1use anyhow::Result;
2use include_dir::{Dir, include_dir};
3use std::collections::HashMap;
4use std::fs;
5use std::path::Path;
6
7// Embed the templates directory relative to this file's location in the crate
8pub static TEMPLATES_DIR: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/templates");
9
10pub fn get_templates() -> &'static Dir<'static> {
11    &TEMPLATES_DIR
12}
13
14/// Finds the full path within the embedded directory for a given short name (e.g. "blink")
15pub fn find_template_path(name: &str) -> Option<String> {
16    if name == "_dynamic" {
17        return Some("_dynamic".to_string());
18    }
19
20    // Search for the template folder in categories
21    for entry in TEMPLATES_DIR.find("**/*").ok()? {
22        if let Some(dir) = entry.as_dir()
23            && dir.path().file_name().and_then(|n| n.to_str()) == Some(name) {
24                return Some(dir.path().to_string_lossy().to_string());
25            }
26    }
27    None
28}
29
30/// Helper to list examples for the CLI
31pub fn list_examples_by_category() -> HashMap<String, Vec<String>> {
32    let mut map = HashMap::new();
33    for entry in TEMPLATES_DIR.dirs() {
34        let category = entry
35            .path()
36            .file_name()
37            .unwrap_or_default()
38            .to_string_lossy()
39            .to_string();
40        if category.starts_with('_') {
41            continue;
42        } // skip _dynamic
43
44        let mut examples = Vec::new();
45        for sub in entry.dirs() {
46            examples.push(
47                sub.path()
48                    .file_name()
49                    .unwrap_or_default()
50                    .to_string_lossy()
51                    .to_string(),
52            );
53        }
54        examples.sort();
55        if !examples.is_empty() {
56            map.insert(category, examples);
57        }
58    }
59    map
60}
61
62/// Processes a template directory: renders .tera files and copies others
63pub fn process_template_directory(
64    template_path: &str,
65    project_name: &str,
66    context: &tera::Context,
67) -> Result<()> {
68    let root = get_templates();
69    let dir = root
70        .get_dir(template_path)
71        .ok_or_else(|| anyhow::anyhow!("Template dir not found: {}", template_path))?;
72
73    for entry in dir.find("**/*")? {
74        if let Some(file) = entry.as_file() {
75            let path = file.path();
76            // Get path relative to the specific template folder
77            let relative_path = path.strip_prefix(template_path).unwrap_or(path);
78            let dest_path = Path::new(project_name).join(relative_path);
79
80            if let Some(parent) = dest_path.parent() {
81                fs::create_dir_all(parent)?;
82            }
83
84            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
85
86            if name.ends_with(".tera") {
87                let content = file
88                    .contents_utf8()
89                    .ok_or_else(|| anyhow::anyhow!("Invalid UTF-8 in template"))?;
90                // Render
91                let rendered = tera::Tera::one_off(content, context, true)
92                    .map_err(|e| anyhow::anyhow!("Template error in {}: {}", name, e))?;
93
94                // Save without .tera extension
95                let dest_clean = dest_path.with_extension("");
96                fs::write(dest_clean, rendered)?;
97            } else {
98                // Copy raw file
99                fs::write(&dest_path, file.contents())?;
100            }
101        }
102    }
103    Ok(())
104}
105
106/// Copies a file verbatim from the embedded directory to disk
107pub fn copy_verbatim(file: &include_dir::File, root_path: &Path, target_base: &str) -> Result<()> {
108    // Calculate path relative to the template root to preserve structure
109    let relative_path = file.path().strip_prefix(root_path).unwrap_or(file.path());
110    let dest = Path::new(target_base).join(relative_path);
111
112    if let Some(p) = dest.parent() {
113        fs::create_dir_all(p)?;
114    }
115    fs::write(dest, file.contents())?;
116    Ok(())
117}
118
119/// Helper to write generated config string to disk
120pub fn write_template(path: &Path, content: &str) -> Result<()> {
121    if let Some(p) = path.parent() {
122        fs::create_dir_all(p)?;
123    }
124    fs::write(path, content)?;
125    Ok(())
126}