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 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
45pub fn config_path(base: &Path) -> PathBuf {
47 base.join(CONFIG_FILE)
48}
49
50pub 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
62pub 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}