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            let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
80
81            // SKIP Cargo.toml.tera to prevent overwriting the generated Cargo.toml
82            // This is handled by generate::cargo::update_manifest
83            if name == "Cargo.toml.tera" {
84                continue;
85            }
86
87            if let Some(parent) = dest_path.parent() {
88                fs::create_dir_all(parent)?;
89            }
90
91            if name.ends_with(".tera") {
92                let content = file
93                    .contents_utf8()
94                    .ok_or_else(|| anyhow::anyhow!("Invalid UTF-8 in template"))?;
95                // Render
96                let rendered = tera::Tera::one_off(content, context, true)
97                    .map_err(|e| anyhow::anyhow!("Template error in {}: {}", name, e))?;
98
99                // Save without .tera extension
100                let dest_clean = dest_path.with_extension("");
101                fs::write(dest_clean, rendered)?;
102            } else {
103                // Copy raw file
104                fs::write(&dest_path, file.contents())?;
105            }
106        }
107    }
108    Ok(())
109}
110
111/// Copies a file verbatim from the embedded directory to disk
112pub fn copy_verbatim(file: &include_dir::File, root_path: &Path, target_base: &str) -> Result<()> {
113    // Calculate path relative to the template root to preserve structure
114    let relative_path = file.path().strip_prefix(root_path).unwrap_or(file.path());
115    let dest = Path::new(target_base).join(relative_path);
116
117    if let Some(p) = dest.parent() {
118        fs::create_dir_all(p)?;
119    }
120    fs::write(dest, file.contents())?;
121    Ok(())
122}
123
124/// Helper to write generated config string to disk
125pub fn write_template(path: &Path, content: &str) -> Result<()> {
126    if let Some(p) = path.parent() {
127        fs::create_dir_all(p)?;
128    }
129    fs::write(path, content)?;
130    Ok(())
131}
132