Skip to main content

rust_bucket/
config.rs

1// Configuration file parsing and management
2
3use serde::{Deserialize, Serialize};
4use std::fs;
5use std::path::Path;
6use thiserror::Error;
7
8/// Errors that can occur when working with configuration files
9#[derive(Debug, Error)]
10pub enum ConfigError {
11    /// IO error when reading or writing the config file
12    #[error("IO error: {0}")]
13    IoError(#[from] std::io::Error),
14
15    /// Error parsing the TOML config file
16    #[error("Parse error: {0}")]
17    ParseError(#[from] toml::de::Error),
18
19    /// Error serializing the config to TOML
20    #[error("Serialize error: {0}")]
21    SerializeError(#[from] toml::ser::Error),
22}
23
24/// Configuration for rust-bucket
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct Config {
27    /// Version of rust-bucket that generated this config
28    pub rust_bucket_version: String,
29    /// Test timeout in seconds (default: 120)
30    pub test_timeout: u32,
31    /// Name of the project
32    pub project_name: String,
33}
34
35// `Config` intentionally has no `Default`: `project_name` is meaningful only
36// relative to a target repository, so it must always be supplied explicitly
37// (see `apply::derive_project_name`). A crate-name-derived default would
38// silently mislabel every target as the rust-bucket crate itself.
39
40impl Config {
41    /// Load configuration from a TOML file
42    pub fn load(path: &Path) -> Result<Config, ConfigError> {
43        let contents = fs::read_to_string(path)?;
44        let config = toml::from_str(&contents)?;
45        Ok(config)
46    }
47
48    /// Save configuration to a TOML file with a header comment
49    pub fn save(&self, path: &Path) -> Result<(), ConfigError> {
50        let toml_string = toml::to_string_pretty(self)?;
51        let version = env!("CARGO_PKG_VERSION");
52        let header = format!(
53            "# Generated by rust-bucket v{}. DO NOT EDIT BY HAND.\n\n",
54            version
55        );
56        let contents = header + &toml_string;
57        fs::write(path, contents)?;
58        Ok(())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65    use std::path::PathBuf;
66
67    #[test]
68    fn test_save_and_load_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
69        let temp_dir = tempfile::tempdir()?;
70        let config_path = temp_dir.path().join("rust-bucket.toml");
71
72        // Create a config with custom values
73        let original_config = Config {
74            rust_bucket_version: "0.1.0".to_string(),
75            test_timeout: 300,
76            project_name: "test-project".to_string(),
77        };
78
79        // Save the config
80        original_config.save(&config_path)?;
81
82        // Verify the file contains the header comment
83        let file_contents = fs::read_to_string(&config_path)?;
84        assert!(file_contents.starts_with("# Generated by rust-bucket v"));
85        assert!(file_contents.contains("DO NOT EDIT BY HAND"));
86
87        // Load the config back
88        let loaded_config = Config::load(&config_path)?;
89
90        // Verify the values match
91        assert_eq!(loaded_config.rust_bucket_version, "0.1.0");
92        assert_eq!(loaded_config.test_timeout, 300);
93        assert_eq!(loaded_config.project_name, "test-project");
94        Ok(())
95    }
96
97    #[test]
98    fn test_load_nonexistent_file() {
99        let path = PathBuf::from("/nonexistent/path/config.toml");
100        let result = Config::load(&path);
101        assert!(result.is_err());
102        assert!(matches!(result.unwrap_err(), ConfigError::IoError(_)));
103    }
104
105    #[test]
106    fn test_load_invalid_toml() -> Result<(), Box<dyn std::error::Error>> {
107        let temp_dir = tempfile::tempdir()?;
108        let config_path = temp_dir.path().join("invalid.toml");
109
110        // Write invalid TOML
111        fs::write(&config_path, "this is not valid toml { [ }")?;
112
113        let result = Config::load(&config_path);
114        assert!(result.is_err());
115        assert!(matches!(result.unwrap_err(), ConfigError::ParseError(_)));
116        Ok(())
117    }
118
119    #[test]
120    fn test_save_to_readonly_directory() {
121        // This test would require setting up a read-only directory
122        // which is platform-dependent and may require elevated privileges.
123        // Skipping for simplicity, but the error handling is in place.
124    }
125}