Skip to main content

rust_bucket/
templates.rs

1// Embedded template management
2
3use rust_embed::RustEmbed;
4use std::fs;
5use std::path::PathBuf;
6use tempfile::TempDir;
7use thiserror::Error;
8
9/// Error type for template operations
10#[derive(Debug, Error)]
11pub enum TemplateError {
12    #[error("Failed to create temporary directory: {0}")]
13    TempDirCreation(#[from] std::io::Error),
14
15    #[error("Failed to extract template file '{path}': {source}")]
16    FileExtraction {
17        path: String,
18        source: std::io::Error,
19    },
20
21    #[error("Template file '{0}' not found in embedded templates")]
22    TemplateNotFound(String),
23}
24
25/// Embedded templates from the templates/ directory
26#[derive(RustEmbed)]
27#[folder = "templates/"]
28pub struct Templates;
29
30/// Extracts all embedded templates to a temporary directory.
31///
32/// Returns the path to the temporary directory containing all extracted templates.
33/// The temporary directory will be cleaned up when the returned `TempDir` is dropped.
34///
35/// # Errors
36///
37/// Returns `TemplateError` if:
38/// - The temporary directory cannot be created
39/// - Any template file cannot be extracted or written
40pub fn extract_to_temp() -> Result<(TempDir, PathBuf), TemplateError> {
41    let temp_dir = TempDir::new()?;
42    let temp_path = temp_dir.path().to_path_buf();
43
44    for file_path in Templates::iter() {
45        let file_data = Templates::get(&file_path)
46            .ok_or_else(|| TemplateError::TemplateNotFound(file_path.to_string()))?;
47
48        let target_path = temp_path.join(file_path.as_ref());
49
50        // Create parent directories if needed
51        if let Some(parent) = target_path.parent() {
52            fs::create_dir_all(parent).map_err(|e| TemplateError::FileExtraction {
53                path: file_path.to_string(),
54                source: e,
55            })?;
56        }
57
58        // Write the file
59        fs::write(&target_path, file_data.data.as_ref()).map_err(|e| {
60            TemplateError::FileExtraction {
61                path: file_path.to_string(),
62                source: e,
63            }
64        })?;
65    }
66
67    Ok((temp_dir, temp_path))
68}
69
70/// Returns the .gitignore entries that rust-bucket requires in the target repository.
71pub fn required_gitignore_lines() -> Vec<&'static str> {
72    vec![
73        ".beads/.br_history/",
74        ".beads/beads.db",
75        ".beads/beads.db-wal",
76        ".beads/last-touched",
77    ]
78}
79
80pub fn managed_files() -> Vec<&'static str> {
81    vec![
82        "AGENTS.md",
83        "CLAUDE.md", // symlink to AGENTS.md, created separately
84        "RUST_STYLE_GUIDE.md",
85        "TESTING.md",
86        ".claude/agents/coordinator.md",
87        ".claude/agents/coding.md",
88        ".claude/agents/judge.md",
89        ".claude/agents/tidy.md",
90        ".claude/agents/reflection.md",
91        ".config/nextest.toml",
92        "deny.toml",
93        "rustfmt.toml",
94        ".devcontainer/Dockerfile",
95        ".devcontainer/devcontainer.json",
96        ".beads/config.yaml",
97        "justfile-rustbucket",
98    ]
99}
100
101/// Seed files are written into the target only if absent and are never
102/// overwritten on re-apply; the project owns them once present.
103///
104/// Each entry maps an embedded template path (relative to `templates/`) to its
105/// destination path (relative to the target directory). Seed templates must NOT
106/// appear in `managed_files()`, and `render()` skips them so they are written
107/// only via the seed-if-missing path.
108pub fn seed_files() -> Vec<(&'static str, &'static str)> {
109    vec![
110        ("ratchets.toml.liquid", "ratchets.toml"),
111        ("STYLE_GUIDE.md.liquid", "STYLE_GUIDE.md"),
112    ]
113}
114
115#[cfg(test)]
116mod tests {
117    use super::*;
118
119    #[test]
120    fn test_extract_to_temp() -> Result<(), Box<dyn std::error::Error>> {
121        let (_temp_dir, temp_path) = extract_to_temp()?;
122        assert!(temp_path.exists());
123        assert!(temp_path.is_dir());
124        Ok(())
125    }
126
127    #[test]
128    fn test_managed_files_not_empty() {
129        let files = managed_files();
130        assert!(!files.is_empty());
131        assert_eq!(files.len(), 16);
132    }
133
134    #[test]
135    fn test_managed_files_includes_expected() {
136        let files = managed_files();
137        assert!(files.contains(&"AGENTS.md"));
138        assert!(files.contains(&"RUST_STYLE_GUIDE.md"));
139        assert!(files.contains(&".config/nextest.toml"));
140        assert!(files.contains(&".devcontainer/Dockerfile"));
141    }
142
143    #[test]
144    fn test_seed_files_registers_ratchets_toml() {
145        let seeds = seed_files();
146        assert!(seeds.contains(&("ratchets.toml.liquid", "ratchets.toml")));
147    }
148
149    #[test]
150    fn test_seed_files_registers_style_guide() {
151        let seeds = seed_files();
152        assert!(seeds.contains(&("STYLE_GUIDE.md.liquid", "STYLE_GUIDE.md")));
153    }
154
155    #[test]
156    fn test_ratchets_toml_not_managed() {
157        let managed = managed_files();
158        assert!(!managed.contains(&"ratchets.toml"));
159        assert_eq!(managed.len(), 16);
160    }
161
162    #[test]
163    fn test_style_guide_not_managed() {
164        let managed = managed_files();
165        assert!(!managed.contains(&"STYLE_GUIDE.md"));
166        assert_eq!(managed.len(), 16);
167    }
168}