1use std::path::{Path, PathBuf};
33
34use toml::{Table as TomlTable, Value as TomlValue};
35
36use crate::{Config, ConfigError, ConfigResult};
37
38use super::ConfigSource;
39
40#[derive(Debug, Clone)]
61pub struct TomlConfigSource {
62 path: PathBuf,
63}
64
65impl TomlConfigSource {
66 #[inline]
72 pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
73 Self {
74 path: path.as_ref().to_path_buf(),
75 }
76 }
77}
78
79impl ConfigSource for TomlConfigSource {
80 fn load(&self, config: &mut Config) -> ConfigResult<()> {
81 let content = std::fs::read_to_string(&self.path).map_err(|e| {
82 ConfigError::IoError(std::io::Error::new(
83 e.kind(),
84 format!("Failed to read TOML file '{}': {}", self.path.display(), e),
85 ))
86 })?;
87
88 let table: TomlTable = content.parse().map_err(|e| {
89 ConfigError::ParseError(format!(
90 "Failed to parse TOML file '{}': {}",
91 self.path.display(),
92 e
93 ))
94 })?;
95
96 flatten_toml_value("", &TomlValue::Table(table), config)
97 }
98}
99
100pub(crate) fn flatten_toml_value(
106 prefix: &str,
107 value: &TomlValue,
108 config: &mut Config,
109) -> ConfigResult<()> {
110 match value {
111 TomlValue::Table(table) => {
112 for (k, v) in table {
113 let key = if prefix.is_empty() {
114 k.clone()
115 } else {
116 format!("{}.{}", prefix, k)
117 };
118 flatten_toml_value(&key, v, config)?;
119 }
120 }
121 TomlValue::Array(arr) => {
122 flatten_toml_array(prefix, arr, config)?;
126 }
127 TomlValue::String(s) => {
128 config.set(prefix, s.clone())?;
129 }
130 TomlValue::Integer(i) => {
131 config.set(prefix, *i)?;
132 }
133 TomlValue::Float(f) => {
134 config.set(prefix, *f)?;
135 }
136 TomlValue::Boolean(b) => {
137 config.set(prefix, *b)?;
138 }
139 TomlValue::Datetime(dt) => {
140 config.set(prefix, dt.to_string())?;
141 }
142 }
143 Ok(())
144}
145
146fn flatten_toml_array(prefix: &str, arr: &[TomlValue], config: &mut Config) -> ConfigResult<()> {
152 if arr.is_empty() {
153 config.set(prefix, Vec::<String>::new())?;
154 return Ok(());
155 }
156
157 enum ArrayKind {
159 Integer,
160 Float,
161 Bool,
162 String,
163 }
164
165 let kind = match &arr[0] {
166 TomlValue::Integer(_) => ArrayKind::Integer,
167 TomlValue::Float(_) => ArrayKind::Float,
168 TomlValue::Boolean(_) => ArrayKind::Bool,
169 TomlValue::Table(_) => {
170 return Err(ConfigError::ParseError(format!(
171 "Unsupported nested TOML table inside array at key '{prefix}'"
172 )));
173 }
174 TomlValue::Array(_) => {
175 return Err(ConfigError::ParseError(format!(
176 "Unsupported nested TOML array at key '{prefix}'"
177 )));
178 }
179 _ => ArrayKind::String,
180 };
181
182 let all_same = arr.iter().all(|item| {
184 matches!(
185 (&kind, item),
186 (ArrayKind::Integer, TomlValue::Integer(_))
187 | (ArrayKind::Float, TomlValue::Float(_))
188 | (ArrayKind::Bool, TomlValue::Boolean(_))
189 | (
190 ArrayKind::String,
191 TomlValue::String(_) | TomlValue::Datetime(_)
192 )
193 )
194 });
195
196 if !all_same {
197 let values = arr
199 .iter()
200 .map(|item| toml_scalar_to_string(item, prefix))
201 .collect::<ConfigResult<Vec<_>>>()?;
202 config.set(prefix, values)?;
203 return Ok(());
204 }
205
206 match kind {
207 ArrayKind::Integer => {
208 let values = arr
209 .iter()
210 .map(|item| {
211 item.as_integer()
212 .expect("TOML integer array was validated before insertion")
213 })
214 .collect::<Vec<_>>();
215 config.set(prefix, values)?;
216 }
217 ArrayKind::Float => {
218 let values = arr
219 .iter()
220 .map(|item| {
221 item.as_float()
222 .expect("TOML float array was validated before insertion")
223 })
224 .collect::<Vec<_>>();
225 config.set(prefix, values)?;
226 }
227 ArrayKind::Bool => {
228 let values = arr
229 .iter()
230 .map(|item| {
231 item.as_bool()
232 .expect("TOML bool array was validated before insertion")
233 })
234 .collect::<Vec<_>>();
235 config.set(prefix, values)?;
236 }
237 ArrayKind::String => {
238 let values = arr
239 .iter()
240 .map(|item| {
241 toml_scalar_to_string(item, prefix)
242 .expect("TOML string array was validated before insertion")
243 })
244 .collect::<Vec<_>>();
245 config.set(prefix, values)?;
246 }
247 }
248
249 Ok(())
250}
251
252fn toml_scalar_to_string(value: &TomlValue, key: &str) -> ConfigResult<String> {
254 match value {
255 TomlValue::String(s) => Ok(s.clone()),
256 TomlValue::Integer(i) => Ok(i.to_string()),
257 TomlValue::Float(f) => Ok(f.to_string()),
258 TomlValue::Boolean(b) => Ok(b.to_string()),
259 TomlValue::Datetime(dt) => Ok(dt.to_string()),
260 TomlValue::Array(_) | TomlValue::Table(_) => {
261 let key = if key.is_empty() { "<root>" } else { key };
262 Err(ConfigError::ParseError(format!(
263 "Unsupported nested TOML structure at key '{}'",
264 key
265 )))
266 }
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273 use crate::Property;
274 use qubit_value::MultiValues;
275
276 fn config_with_final_property(name: &str) -> Config {
277 let mut config = Config::new();
278 let mut property = Property::with_value(name, MultiValues::String(vec!["old".to_string()]));
279 property.set_final(true);
280 config.insert_property(name, property).unwrap();
281 config
282 }
283
284 fn expect_final_error(result: ConfigResult<()>, name: &str) {
285 let err = result.expect_err("writing a final TOML property should fail");
286 assert_eq!(
287 err.to_string(),
288 format!("Property '{name}' is final and cannot be overridden"),
289 );
290 }
291
292 #[test]
293 fn test_toml_scalar_to_string_float() {
294 let val = TomlValue::Float(1.5);
295 assert_eq!(toml_scalar_to_string(&val, "key").unwrap(), "1.5");
296 }
297
298 #[test]
299 fn test_toml_scalar_to_string_bool() {
300 let val = TomlValue::Boolean(true);
301 assert_eq!(toml_scalar_to_string(&val, "key").unwrap(), "true");
302 }
303
304 #[test]
305 fn test_toml_scalar_to_string_datetime() {
306 let val = TomlValue::Datetime(
307 "1979-05-27T07:32:00Z"
308 .parse()
309 .expect("test TOML datetime should parse"),
310 );
311 assert_eq!(
312 toml_scalar_to_string(&val, "key").unwrap(),
313 "1979-05-27T07:32:00Z",
314 );
315 }
316
317 #[test]
318 fn test_toml_scalar_to_string_nested_array_empty_key() {
319 let val = TomlValue::Array(vec![]);
320 let result = toml_scalar_to_string(&val, "");
321 assert!(result.is_err());
322 let msg = format!("{}", result.unwrap_err());
323 assert!(msg.contains("<root>"));
324 }
325
326 #[test]
327 fn test_toml_scalar_to_string_nested_table_with_key() {
328 let val = TomlValue::Table(toml::Table::new());
329 let result = toml_scalar_to_string(&val, "my.key");
330 assert!(result.is_err());
331 let msg = format!("{}", result.unwrap_err());
332 assert!(msg.contains("my.key"));
333 }
334
335 #[test]
336 fn test_flatten_toml_array_mixed_int_string_fallback() {
337 let arr = vec![TomlValue::Integer(1), TomlValue::String("two".to_string())];
340 let mut config = Config::new();
341 flatten_toml_array("mixed", &arr, &mut config).unwrap();
342 let vals: Vec<String> = config.get_list("mixed").unwrap();
344 assert_eq!(vals.len(), 2);
345 }
346
347 #[test]
348 fn test_flatten_toml_array_mixed_float_string_fallback() {
349 let arr = vec![TomlValue::Float(1.5), TomlValue::String("two".to_string())];
350 let mut config = Config::new();
351 flatten_toml_array("mixed", &arr, &mut config).unwrap();
352 let vals: Vec<String> = config.get_list("mixed").unwrap();
353 assert_eq!(vals.len(), 2);
354 }
355
356 #[test]
357 fn test_flatten_toml_array_mixed_bool_string_fallback() {
358 let arr = vec![
359 TomlValue::Boolean(true),
360 TomlValue::String("two".to_string()),
361 ];
362 let mut config = Config::new();
363 flatten_toml_array("mixed", &arr, &mut config).unwrap();
364 let vals: Vec<String> = config.get_list("mixed").unwrap();
365 assert_eq!(vals.len(), 2);
366 }
367
368 #[test]
369 fn test_flatten_toml_array_mixed_nested_value_returns_error() {
370 let arr = vec![TomlValue::Integer(1), TomlValue::Array(vec![])];
371 let mut config = Config::new();
372 flatten_toml_array("mixed", &arr, &mut config)
373 .expect_err("mixed TOML array with a nested value should fail");
374 }
375
376 #[test]
377 fn test_flatten_toml_scalar_respects_final_property() {
378 let datetime = "1979-05-27T07:32:00Z"
379 .parse()
380 .expect("test TOML datetime should parse");
381 let cases = [
382 TomlValue::Integer(1),
383 TomlValue::Float(1.5),
384 TomlValue::Boolean(true),
385 TomlValue::Datetime(datetime),
386 ];
387
388 for value in cases {
389 let mut config = config_with_final_property("locked");
390 expect_final_error(flatten_toml_value("locked", &value, &mut config), "locked");
391 }
392 }
393
394 #[test]
395 fn test_flatten_toml_array_respects_final_property() {
396 let cases = [
397 Vec::new(),
398 vec![TomlValue::Integer(1), TomlValue::Integer(2)],
399 vec![TomlValue::Float(1.5), TomlValue::Float(2.5)],
400 vec![TomlValue::Boolean(true), TomlValue::Boolean(false)],
401 vec![
402 TomlValue::String("one".to_string()),
403 TomlValue::String("two".to_string()),
404 ],
405 vec![TomlValue::Integer(1), TomlValue::String("two".to_string())],
406 ];
407
408 for values in cases {
409 let mut config = config_with_final_property("locked");
410 expect_final_error(flatten_toml_array("locked", &values, &mut config), "locked");
411 }
412 }
413}