Skip to main content

bears/
config.rs

1use crate::error::{Error, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::{Path, PathBuf};
5
6const CONFIG_FILE: &str = ".bears.yml";
7
8const DEFAULT_ID_LENGTH: u8 = 3;
9const MIN_ID_LENGTH: u8 = 2;
10const MAX_ID_LENGTH: u8 = 8;
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
13pub struct Config {
14    #[serde(rename = "id-length", default = "default_id_length")]
15    pub id_length: u8,
16}
17
18fn default_id_length() -> u8 {
19    DEFAULT_ID_LENGTH
20}
21
22impl Default for Config {
23    fn default() -> Self {
24        Self {
25            id_length: DEFAULT_ID_LENGTH,
26        }
27    }
28}
29
30impl Config {
31    /// Validate the config values, returning an error on invalid settings.
32    pub fn validate(&self) -> Result<()> {
33        if self.id_length < MIN_ID_LENGTH || self.id_length > MAX_ID_LENGTH {
34            return Err(Error::InvalidConfig {
35                reason: format!(
36                    "id-length must be between {MIN_ID_LENGTH} and {MAX_ID_LENGTH}, got {}",
37                    self.id_length
38                ),
39            });
40        }
41        Ok(())
42    }
43}
44
45/// Path to the config file.
46pub fn config_path(base: &Path) -> PathBuf {
47    base.join(CONFIG_FILE)
48}
49
50/// Load config from `.bears.yml`. Returns default config if file doesn't exist.
51pub fn load(base: &Path) -> Result<Config> {
52    let path = config_path(base);
53    if !path.exists() {
54        return Ok(Config::default());
55    }
56    let content = fs::read_to_string(&path)?;
57    let config: Config = serde_yml::from_str(&content)?;
58    config.validate()?;
59    Ok(config)
60}
61
62/// Write the default config to `.bears.yml`.
63/// Leaves an existing config file untouched so re-running `bea init` is safe.
64pub fn create_default(base: &Path) -> Result<PathBuf> {
65    let path = config_path(base);
66    if path.exists() {
67        return Ok(path);
68    }
69    let config = Config::default();
70    let content = serde_yml::to_string(&config)?;
71    fs::write(&path, content)?;
72    Ok(path)
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78    use tempfile::TempDir;
79
80    #[test]
81    fn test_default_config() {
82        let config = Config::default();
83        assert_eq!(config.id_length, 3);
84    }
85
86    #[test]
87    fn test_load_missing_file_returns_default() {
88        let tmp = TempDir::new().unwrap();
89        let config = load(tmp.path()).unwrap();
90        assert_eq!(config, Config::default());
91    }
92
93    #[test]
94    fn test_create_and_load() {
95        let tmp = TempDir::new().unwrap();
96        create_default(tmp.path()).unwrap();
97        let config = load(tmp.path()).unwrap();
98        assert_eq!(config.id_length, 3);
99    }
100
101    #[test]
102    fn test_create_default_preserves_existing_config() {
103        let tmp = TempDir::new().unwrap();
104        fs::write(config_path(tmp.path()), "id-length: 6\n").unwrap();
105        create_default(tmp.path()).unwrap();
106        let config = load(tmp.path()).unwrap();
107        assert_eq!(config.id_length, 6, "re-init must not clobber config");
108    }
109
110    #[test]
111    fn test_load_custom_id_length() {
112        let tmp = TempDir::new().unwrap();
113        fs::write(config_path(tmp.path()), "id-length: 6\n").unwrap();
114        let config = load(tmp.path()).unwrap();
115        assert_eq!(config.id_length, 6);
116    }
117
118    #[test]
119    fn test_validate_too_small() {
120        let config = Config { id_length: 1 };
121        assert!(config.validate().is_err());
122    }
123
124    #[test]
125    fn test_validate_too_large() {
126        let config = Config { id_length: 10 };
127        assert!(config.validate().is_err());
128    }
129
130    #[test]
131    fn test_validate_bounds() {
132        Config { id_length: 2 }.validate().unwrap();
133        Config { id_length: 8 }.validate().unwrap();
134    }
135
136    #[test]
137    fn test_load_invalid_id_length() {
138        let tmp = TempDir::new().unwrap();
139        fs::write(config_path(tmp.path()), "id-length: 1\n").unwrap();
140        assert!(load(tmp.path()).is_err());
141    }
142
143    #[test]
144    fn test_missing_id_length_uses_default() {
145        let tmp = TempDir::new().unwrap();
146        fs::write(config_path(tmp.path()), "{}\n").unwrap();
147        let config = load(tmp.path()).unwrap();
148        assert_eq!(config.id_length, DEFAULT_ID_LENGTH);
149    }
150}