Skip to main content

cmt/config/
file.rs

1use std::fs;
2use std::io;
3use std::path::PathBuf;
4
5use super::defaults;
6use super::ConfigError;
7
8/// Create a new configuration file at the specified path
9pub fn create_config_file(path: Option<&str>) -> Result<PathBuf, ConfigError> {
10    let config_path = if let Some(path) = path {
11        PathBuf::from(path)
12    } else {
13        PathBuf::from(defaults::DEFAULT_CONFIG_FILENAME)
14    };
15
16    // Check if file already exists
17    if config_path.exists() {
18        return Err(ConfigError::IoError(io::Error::new(
19            io::ErrorKind::AlreadyExists,
20            format!("Configuration file already exists at {:?}", config_path),
21        )));
22    }
23
24    // Create parent directories if needed
25    if let Some(parent) = config_path.parent() {
26        if !parent.exists() {
27            fs::create_dir_all(parent)?;
28        }
29    }
30
31    // Write example config
32    fs::write(&config_path, defaults::example_config())?;
33
34    Ok(config_path)
35}
36
37/// Get the global configuration directory
38pub fn global_config_dir() -> Option<PathBuf> {
39    if let Ok(home) = std::env::var("HOME") {
40        Some(PathBuf::from(home).join(defaults::GLOBAL_CONFIG_DIRNAME))
41    } else {
42        None
43    }
44}
45
46/// Get the global configuration file path
47pub fn global_config_file() -> Option<PathBuf> {
48    global_config_dir().map(|dir| dir.join(defaults::GLOBAL_CONFIG_FILENAME))
49}
50
51/// Create the global configuration directory and file
52pub fn create_global_config() -> Result<PathBuf, ConfigError> {
53    let global_dir = global_config_dir().ok_or_else(|| {
54        ConfigError::IoError(io::Error::new(
55            io::ErrorKind::NotFound,
56            "Could not determine home directory",
57        ))
58    })?;
59
60    // Create directory if it doesn't exist
61    if !global_dir.exists() {
62        fs::create_dir_all(&global_dir)?;
63    }
64
65    let global_file = global_dir.join(defaults::GLOBAL_CONFIG_FILENAME);
66
67    // Create file if it doesn't exist
68    if !global_file.exists() {
69        fs::write(&global_file, defaults::example_config())?;
70    }
71
72    Ok(global_file)
73}
74
75/// Find the project configuration file by walking up the directory tree
76pub fn find_project_config() -> Option<PathBuf> {
77    let current_dir = std::env::current_dir().ok()?;
78    let mut dir = current_dir.as_path();
79
80    loop {
81        let config_path = dir.join(defaults::DEFAULT_CONFIG_FILENAME);
82        if config_path.exists() {
83            return Some(config_path);
84        }
85
86        if let Some(parent) = dir.parent() {
87            dir = parent;
88        } else {
89            break;
90        }
91    }
92
93    None
94}
95
96/// Get the template directory
97pub fn template_dir() -> Option<PathBuf> {
98    global_config_dir().map(|dir| dir.join("templates"))
99}
100
101/// Create the template directory and default templates
102pub fn create_template_dir() -> Result<PathBuf, ConfigError> {
103    let template_dir = template_dir().ok_or_else(|| {
104        ConfigError::IoError(io::Error::new(
105            io::ErrorKind::NotFound,
106            "Could not determine template directory",
107        ))
108    })?;
109
110    // Create directory if it doesn't exist
111    if !template_dir.exists() {
112        fs::create_dir_all(&template_dir)?;
113    }
114
115    // Create default templates
116    let simple_path = template_dir.join("simple.hbs");
117    if !simple_path.exists() {
118        fs::write(&simple_path, defaults::simple_template())?;
119    }
120
121    let conventional_path = template_dir.join("conventional.hbs");
122    if !conventional_path.exists() {
123        fs::write(&conventional_path, defaults::conventional_template())?;
124    }
125
126    let detailed_path = template_dir.join("detailed.hbs");
127    if !detailed_path.exists() {
128        fs::write(&detailed_path, defaults::detailed_template())?;
129    }
130
131    Ok(template_dir)
132}
133
134/// Get a list of available templates
135pub fn list_templates() -> Result<Vec<String>, ConfigError> {
136    let template_dir = template_dir().ok_or_else(|| {
137        ConfigError::IoError(io::Error::new(
138            io::ErrorKind::NotFound,
139            "Could not determine template directory",
140        ))
141    })?;
142
143    if !template_dir.exists() {
144        return Ok(Vec::new());
145    }
146
147    let entries = fs::read_dir(template_dir)?;
148    let mut templates = Vec::new();
149
150    for entry in entries {
151        let entry = entry?;
152        let path = entry.path();
153
154        if path.is_file() {
155            if let Some(extension) = path.extension() {
156                if extension == "hbs" {
157                    if let Some(name) = path.file_stem() {
158                        if let Some(name_str) = name.to_str() {
159                            templates.push(name_str.to_string());
160                        }
161                    }
162                }
163            }
164        }
165    }
166
167    Ok(templates)
168}
169
170/// Get the path to a template, prioritizing file system templates over defaults
171pub fn get_template_path(name: &str) -> Result<PathBuf, ConfigError> {
172    // First check if the template exists in the file system
173    if let Some(template_dir) = template_dir() {
174        let template_path = template_dir.join(format!("{}.hbs", name));
175        if template_path.exists() {
176            return Ok(template_path);
177        }
178    }
179
180    // If not found in file system, check if it's a built-in template
181    match name {
182        "simple" | "conventional" | "detailed" => {
183            // For built-in templates, we don't have a real path, so we create a placeholder
184            // This indicates it's a built-in template that should be handled specially
185            Ok(PathBuf::from(format!("__builtin__/{}.hbs", name)))
186        }
187        _ => Err(ConfigError::IoError(io::Error::new(
188            io::ErrorKind::NotFound,
189            format!("Template '{}' not found", name),
190        ))),
191    }
192}
193
194/// Get the content of a template
195pub fn get_template(name: &str) -> Result<String, ConfigError> {
196    // Get the template path, which prioritizes file system templates
197    let template_path = get_template_path(name)?;
198
199    // Check if it's a built-in template
200    if let Some(path_str) = template_path.to_str() {
201        if path_str.starts_with("__builtin__/") {
202            // It's a built-in template, return the appropriate content
203            match name {
204                "simple" => return Ok(defaults::simple_template()),
205                "conventional" => return Ok(defaults::conventional_template()),
206                "detailed" => return Ok(defaults::detailed_template()),
207                _ => {} // This shouldn't happen given the logic in get_template_path
208            }
209        }
210    }
211
212    // It's a file system template, read its content
213    match fs::read_to_string(&template_path) {
214        Ok(content) => Ok(content),
215        Err(e) => Err(ConfigError::IoError(e)),
216    }
217}
218
219/// Save a template
220pub fn save_template(name: &str, content: &str) -> Result<(), ConfigError> {
221    let template_dir = template_dir().ok_or_else(|| {
222        ConfigError::IoError(io::Error::new(
223            io::ErrorKind::NotFound,
224            "Could not determine template directory",
225        ))
226    })?;
227
228    // Create directory if it doesn't exist
229    if !template_dir.exists() {
230        fs::create_dir_all(&template_dir)?;
231    }
232
233    let template_path = template_dir.join(format!("{}.hbs", name));
234    fs::write(template_path, content)?;
235
236    Ok(())
237}
238
239#[cfg(test)]
240mod tests {
241    use super::*;
242    use std::env;
243    use std::fs;
244    use tempfile::TempDir;
245
246    #[test]
247    #[serial_test::serial]
248    fn test_get_template_from_file() {
249        // Create a temporary directory for testing
250        let temp_dir = TempDir::new().unwrap();
251
252        // Override the HOME environment variable to use our temp directory
253        let original_home = env::var("HOME").unwrap_or_default();
254        env::set_var("HOME", temp_dir.path());
255
256        // Create the config directory structure
257        let config_dir = temp_dir.path().join(defaults::GLOBAL_CONFIG_DIRNAME);
258        let template_dir = config_dir.join("templates");
259        fs::create_dir_all(&template_dir).unwrap();
260
261        // Create a test template file with a unique name that doesn't conflict with built-ins
262        let template_name = "custom-test-template";
263        let template_content = "Test template content";
264        let template_path = template_dir.join(format!("{}.hbs", template_name));
265        fs::write(&template_path, template_content).unwrap();
266
267        // Test getting the template
268        let result = get_template(template_name);
269        assert!(result.is_ok());
270        assert_eq!(result.unwrap(), template_content);
271
272        // Restore the original HOME environment variable
273        if original_home.is_empty() {
274            env::remove_var("HOME");
275        } else {
276            env::set_var("HOME", original_home);
277        }
278    }
279
280    #[test]
281    #[serial_test::serial]
282    fn test_get_builtin_template() {
283        // Create a temporary directory for testing
284        let temp_dir = TempDir::new().unwrap();
285
286        // Override the HOME environment variable to use our temp directory
287        // This ensures we don't find any existing template files
288        let original_home = env::var("HOME").unwrap_or_default();
289        env::set_var("HOME", temp_dir.path());
290
291        // Test getting built-in templates
292        let simple_result = get_template("simple");
293        assert!(simple_result.is_ok());
294        assert_eq!(simple_result.unwrap(), defaults::simple_template());
295
296        let conventional_result = get_template("conventional");
297        assert!(conventional_result.is_ok());
298        assert_eq!(
299            conventional_result.unwrap(),
300            defaults::conventional_template()
301        );
302
303        let detailed_result = get_template("detailed");
304        assert!(detailed_result.is_ok());
305        assert_eq!(detailed_result.unwrap(), defaults::detailed_template());
306
307        // Restore the original HOME environment variable
308        if original_home.is_empty() {
309            env::remove_var("HOME");
310        } else {
311            env::set_var("HOME", original_home);
312        }
313    }
314
315    #[test]
316    #[serial_test::serial]
317    fn test_get_nonexistent_template() {
318        // Create a temporary directory for testing
319        let temp_dir = TempDir::new().unwrap();
320
321        // Override the HOME environment variable to use our temp directory
322        let original_home = env::var("HOME").unwrap_or_default();
323        env::set_var("HOME", temp_dir.path());
324
325        // Test getting a non-existent template
326        let result = get_template("nonexistent-template");
327        assert!(result.is_err());
328        let error = result.unwrap_err();
329        match error {
330            ConfigError::IoError(e) => {
331                assert_eq!(e.kind(), io::ErrorKind::NotFound);
332                assert!(e
333                    .to_string()
334                    .contains("Template 'nonexistent-template' not found"));
335            }
336            _ => panic!("Expected IoError, got {:?}", error),
337        }
338
339        // Restore the original HOME environment variable
340        if original_home.is_empty() {
341            env::remove_var("HOME");
342        } else {
343            env::set_var("HOME", original_home);
344        }
345    }
346}