1use std::path::Path;
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(from = "Versions", into = "Versions")]
11pub struct Config {
12 allowed_kinds: Vec<String>,
19
20 digits: usize,
27
28 pub allow_unrecognised: bool,
31
32 pub allow_invalid: bool,
35
36 pub subfolders_are_namespaces: bool,
48}
49
50impl Default for Config {
51 fn default() -> Self {
52 Self {
53 allowed_kinds: Vec::new(),
54 digits: default_digits(),
55 allow_unrecognised: false,
56 allow_invalid: false,
57 subfolders_are_namespaces: false,
58 }
59 }
60}
61
62impl Config {
63 pub fn load(path: &Path) -> Result<Self, String> {
70 let content = std::fs::read_to_string(path)
71 .map_err(|e| format!("Failed to read config file: {e}"))?;
72 toml::from_str(&content).map_err(|e| format!("Failed to parse config file: {e}"))
73 }
74
75 pub fn save(&self, path: &Path) -> Result<(), String> {
82 let content =
83 toml::to_string_pretty(self).map_err(|e| format!("Failed to serialize config: {e}"))?;
84 std::fs::write(path, content).map_err(|e| format!("Failed to write config file: {e}"))
85 }
86
87 #[must_use]
89 pub const fn digits(&self) -> usize {
90 self.digits
91 }
92
93 #[must_use]
95 pub fn allowed_kinds(&self) -> &[String] {
96 &self.allowed_kinds
97 }
98
99 pub const fn set_subfolders_are_namespaces(&mut self, value: bool) {
101 self.subfolders_are_namespaces = value;
102 }
103}
104
105const fn default_digits() -> usize {
106 3
107}
108
109#[derive(Debug, Serialize, Deserialize)]
113#[serde(tag = "_version")]
114enum Versions {
115 #[serde(rename = "1")]
116 V1 {
117 #[serde(default, skip_serializing_if = "Vec::is_empty")]
118 allowed_kinds: Vec<String>,
119
120 #[serde(default = "default_digits")]
127 digits: usize,
128
129 #[serde(default)]
130 allow_unrecognised: bool,
131
132 #[serde(default)]
133 allow_invalid: bool,
134
135 #[serde(default)]
136 subfolders_are_namespaces: bool,
137 },
138}
139
140impl From<Versions> for super::Config {
141 fn from(versions: Versions) -> Self {
142 match versions {
143 Versions::V1 {
144 allowed_kinds,
145 digits,
146 allow_unrecognised,
147 allow_invalid,
148 subfolders_are_namespaces,
149 } => Self {
150 allowed_kinds,
151 digits,
152 allow_unrecognised,
153 allow_invalid,
154 subfolders_are_namespaces,
155 },
156 }
157 }
158}
159
160impl From<super::Config> for Versions {
161 fn from(config: super::Config) -> Self {
162 Self::V1 {
163 allowed_kinds: config.allowed_kinds,
164 digits: config.digits,
165 allow_unrecognised: config.allow_unrecognised,
166 allow_invalid: config.allow_invalid,
167 subfolders_are_namespaces: config.subfolders_are_namespaces,
168 }
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use std::io::Write;
175
176 use super::*;
177
178 #[test]
179 fn load_reads_valid_file() {
180 let mut file = tempfile::NamedTempFile::new().unwrap();
181 file.write_all(
182 b"_version = \"1\"\nallowed_kinds = [\"USR\", \"SYS\"]\ndigits = 4\nallow_unrecognised = true\nallow_invalid = true\nsubfolders_are_namespaces = true\n",
183 )
184 .unwrap();
185
186 let config = Config::load(file.path()).unwrap();
187
188 assert_eq!(
189 config.allowed_kinds(),
190 &["USR".to_string(), "SYS".to_string()]
191 );
192 assert_eq!(config.digits(), 4);
193 assert!(config.allow_unrecognised);
194 assert!(config.allow_invalid);
195 assert!(config.subfolders_are_namespaces);
196 }
197
198 #[test]
199 fn load_missing_file_returns_error() {
200 let tmp = tempfile::tempdir().unwrap();
201 let missing = tmp.path().join("missing.toml");
202
203 let error = Config::load(&missing).unwrap_err();
204 assert!(error.starts_with("Failed to read config file:"));
205 }
206
207 #[test]
208 fn load_invalid_toml_returns_error() {
209 let mut file = tempfile::NamedTempFile::new().unwrap();
210 file.write_all(b"_version = \"1\"\ndigits = \"three\"\n")
211 .unwrap();
212
213 let error = Config::load(file.path()).unwrap_err();
214 assert!(error.starts_with("Failed to parse config file:"));
215 }
216
217 #[test]
218 fn empty_file_returns_default() {
219 let expected = Config::default();
221 let actual: Config = toml::from_str(r#"_version = "1""#).unwrap();
222 assert_eq!(actual, expected);
223 }
224}