rust-bucket-cli 0.6.3

Long-horizon agentic coding scaffold for Rust projects
Documentation
// Configuration file parsing and management

use serde::{Deserialize, Serialize};
use std::fs;
use std::path::Path;
use thiserror::Error;

/// Errors that can occur when working with configuration files
#[derive(Debug, Error)]
pub enum ConfigError {
    /// IO error when reading or writing the config file
    #[error("IO error: {0}")]
    IoError(#[from] std::io::Error),

    /// Error parsing the TOML config file
    #[error("Parse error: {0}")]
    ParseError(#[from] toml::de::Error),

    /// Error serializing the config to TOML
    #[error("Serialize error: {0}")]
    SerializeError(#[from] toml::ser::Error),
}

/// Configuration for rust-bucket
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    /// Version of rust-bucket that generated this config
    pub rust_bucket_version: String,
    /// Test timeout in seconds (default: 120)
    pub test_timeout: u32,
    /// Name of the project
    pub project_name: String,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            rust_bucket_version: env!("CARGO_PKG_VERSION").to_string(),
            test_timeout: 120,
            project_name: env!("CARGO_PKG_NAME").to_string(),
        }
    }
}

impl Config {
    /// Load configuration from a TOML file
    pub fn load(path: &Path) -> Result<Config, ConfigError> {
        let contents = fs::read_to_string(path)?;
        let config = toml::from_str(&contents)?;
        Ok(config)
    }

    /// Save configuration to a TOML file with a header comment
    pub fn save(&self, path: &Path) -> Result<(), ConfigError> {
        let toml_string = toml::to_string_pretty(self)?;
        let version = env!("CARGO_PKG_VERSION");
        let header = format!(
            "# Generated by rust-bucket v{}. DO NOT EDIT BY HAND.\n\n",
            version
        );
        let contents = header + &toml_string;
        fs::write(path, contents)?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::PathBuf;

    #[test]
    fn test_default_config() {
        let config = Config::default();
        assert_eq!(config.rust_bucket_version, env!("CARGO_PKG_VERSION"));
        assert_eq!(config.test_timeout, 120);
        assert_eq!(config.project_name, env!("CARGO_PKG_NAME"));
    }

    #[test]
    fn test_save_and_load_roundtrip() {
        let temp_dir = tempfile::tempdir().unwrap();
        let config_path = temp_dir.path().join("rust-bucket.toml");

        // Create a config with custom values
        let original_config = Config {
            rust_bucket_version: "0.1.0".to_string(),
            test_timeout: 300,
            project_name: "test-project".to_string(),
        };

        // Save the config
        original_config.save(&config_path).unwrap();

        // Verify the file contains the header comment
        let file_contents = fs::read_to_string(&config_path).unwrap();
        assert!(file_contents.starts_with("# Generated by rust-bucket v"));
        assert!(file_contents.contains("DO NOT EDIT BY HAND"));

        // Load the config back
        let loaded_config = Config::load(&config_path).unwrap();

        // Verify the values match
        assert_eq!(loaded_config.rust_bucket_version, "0.1.0");
        assert_eq!(loaded_config.test_timeout, 300);
        assert_eq!(loaded_config.project_name, "test-project");
    }

    #[test]
    fn test_load_nonexistent_file() {
        let path = PathBuf::from("/nonexistent/path/config.toml");
        let result = Config::load(&path);
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), ConfigError::IoError(_)));
    }

    #[test]
    fn test_load_invalid_toml() {
        let temp_dir = tempfile::tempdir().unwrap();
        let config_path = temp_dir.path().join("invalid.toml");

        // Write invalid TOML
        fs::write(&config_path, "this is not valid toml { [ }").unwrap();

        let result = Config::load(&config_path);
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), ConfigError::ParseError(_)));
    }

    #[test]
    fn test_save_to_readonly_directory() {
        // This test would require setting up a read-only directory
        // which is platform-dependent and may require elevated privileges.
        // Skipping for simplicity, but the error handling is in place.
    }
}