1use std::path::{Path, PathBuf};
33
34use serde_yaml::Value as YamlValue;
35
36use crate::{Config, ConfigError, ConfigResult};
37
38use super::ConfigSource;
39
40#[derive(Debug, Clone)]
61pub struct YamlConfigSource {
62 path: PathBuf,
63}
64
65impl YamlConfigSource {
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 YamlConfigSource {
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 YAML file '{}': {}", self.path.display(), e),
85 ))
86 })?;
87
88 let value: YamlValue = serde_yaml::from_str(&content).map_err(|e| {
89 ConfigError::ParseError(format!(
90 "Failed to parse YAML file '{}': {}",
91 self.path.display(),
92 e
93 ))
94 })?;
95
96 flatten_yaml_value("", &value, config)
97 }
98}
99
100pub(crate) fn flatten_yaml_value(
109 prefix: &str,
110 value: &YamlValue,
111 config: &mut Config,
112) -> ConfigResult<()> {
113 match value {
114 YamlValue::Mapping(map) => {
115 for (k, v) in map {
116 let key_str = yaml_key_to_string(k)?;
117 let key = if prefix.is_empty() {
118 key_str
119 } else {
120 format!("{}.{}", prefix, key_str)
121 };
122 flatten_yaml_value(&key, v, config)?;
123 }
124 }
125 YamlValue::Sequence(seq) => {
126 flatten_yaml_sequence(prefix, seq, config)?;
127 }
128 YamlValue::Null => {
129 use qubit_common::DataType;
131 config.set_null(prefix, DataType::String)?;
132 }
133 YamlValue::Bool(b) => {
134 config.set(prefix, *b)?;
135 }
136 YamlValue::Number(n) => {
137 if let Some(i) = n.as_i64() {
138 config.set(prefix, i)?;
139 } else {
140 let f = n
141 .as_f64()
142 .expect("YAML number should be representable as i64 or f64");
143 config.set(prefix, f)?;
144 }
145 }
146 YamlValue::String(s) => {
147 config.set(prefix, s.clone())?;
148 }
149 YamlValue::Tagged(tagged) => {
150 flatten_yaml_value(prefix, &tagged.value, config)?;
151 }
152 }
153 Ok(())
154}
155
156fn flatten_yaml_sequence(prefix: &str, seq: &[YamlValue], config: &mut Config) -> ConfigResult<()> {
166 if seq.is_empty() {
167 config.set(prefix, Vec::<String>::new())?;
168 return Ok(());
169 }
170
171 enum SeqKind {
172 Integer,
173 Float,
174 Bool,
175 String,
176 }
177
178 let kind = match &seq[0] {
179 YamlValue::Number(n) if n.is_i64() => SeqKind::Integer,
180 YamlValue::Number(_) => SeqKind::Float,
181 YamlValue::Bool(_) => SeqKind::Bool,
182 YamlValue::Mapping(_) | YamlValue::Sequence(_) | YamlValue::Tagged(_) => {
183 return Err(unsupported_yaml_sequence_element_error(prefix, &seq[0]));
184 }
185 _ => SeqKind::String,
186 };
187
188 let all_same = seq.iter().all(|item| match (&kind, item) {
189 (SeqKind::Integer, YamlValue::Number(n)) => n.is_i64(),
190 (SeqKind::Float, YamlValue::Number(_)) => true,
191 (SeqKind::Bool, YamlValue::Bool(_)) => true,
192 (SeqKind::String, YamlValue::String(_)) => true,
193 _ => false,
194 });
195
196 if !all_same {
197 let values = seq
198 .iter()
199 .map(|item| yaml_scalar_to_string(item, prefix))
200 .collect::<ConfigResult<Vec<_>>>()?;
201 config.set(prefix, values)?;
202 return Ok(());
203 }
204
205 match kind {
206 SeqKind::Integer => {
207 let values = seq
208 .iter()
209 .map(|item| {
210 item.as_i64()
211 .expect("YAML integer sequence was validated before insertion")
212 })
213 .collect::<Vec<_>>();
214 config.set(prefix, values)?;
215 }
216 SeqKind::Float => {
217 let values = seq
218 .iter()
219 .map(|item| {
220 item.as_f64()
221 .expect("YAML float sequence was validated before insertion")
222 })
223 .collect::<Vec<_>>();
224 config.set(prefix, values)?;
225 }
226 SeqKind::Bool => {
227 let values = seq
228 .iter()
229 .map(|item| {
230 item.as_bool()
231 .expect("YAML bool sequence was validated before insertion")
232 })
233 .collect::<Vec<_>>();
234 config.set(prefix, values)?;
235 }
236 SeqKind::String => {
237 let values = seq
238 .iter()
239 .map(|item| {
240 yaml_scalar_to_string(item, prefix)
241 .expect("YAML string sequence was validated before insertion")
242 })
243 .collect::<Vec<_>>();
244 config.set(prefix, values)?;
245 }
246 }
247
248 Ok(())
249}
250
251fn yaml_key_to_string(value: &YamlValue) -> ConfigResult<String> {
253 match value {
254 YamlValue::String(s) => Ok(s.clone()),
255 YamlValue::Number(n) => Ok(n.to_string()),
256 YamlValue::Bool(b) => Ok(b.to_string()),
257 YamlValue::Null => Ok("null".to_string()),
258 _ => Err(ConfigError::ParseError(format!(
259 "Unsupported YAML mapping key type: {value:?}"
260 ))),
261 }
262}
263
264fn yaml_scalar_to_string(value: &YamlValue, key: &str) -> ConfigResult<String> {
270 match value {
271 YamlValue::String(s) => Ok(s.clone()),
272 YamlValue::Number(n) => Ok(n.to_string()),
273 YamlValue::Bool(b) => Ok(b.to_string()),
274 YamlValue::Null => Ok(String::new()),
275 YamlValue::Sequence(_) | YamlValue::Mapping(_) | YamlValue::Tagged(_) => {
276 Err(unsupported_yaml_sequence_element_error(key, value))
277 }
278 }
279}
280
281fn unsupported_yaml_sequence_element_error(key: &str, value: &YamlValue) -> ConfigError {
283 let key = if key.is_empty() { "<root>" } else { key };
284 ConfigError::ParseError(format!(
285 "Unsupported nested YAML structure at key '{key}': {value:?}"
286 ))
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292 use crate::Property;
293 use std::path::PathBuf;
294
295 use qubit_value::MultiValues;
296
297 fn config_with_final_property(name: &str) -> Config {
298 let mut config = Config::new();
299 let mut property = Property::with_value(name, MultiValues::String(vec!["old".to_string()]));
300 property.set_final(true);
301 config.insert_property(name, property).unwrap();
302 config
303 }
304
305 fn expect_final_error(result: ConfigResult<()>, name: &str) {
306 let err = result.expect_err("writing a final YAML property should fail");
307 assert_eq!(
308 err.to_string(),
309 format!("Property '{name}' is final and cannot be overridden"),
310 );
311 }
312
313 #[test]
314 fn test_from_file_stores_path() {
315 let path = PathBuf::from("config.yaml");
316 let source = YamlConfigSource::from_file(&path);
317 let cloned = source.clone();
318 assert_eq!(source.path, path);
319 assert_eq!(cloned.path, PathBuf::from("config.yaml"));
320 }
321
322 #[test]
323 fn test_load_yaml_file_success() {
324 let dir = tempfile::tempdir().unwrap();
325 let path = dir.path().join("config.yaml");
326 std::fs::write(&path, "server:\n port: 8080\n").unwrap();
327
328 let source = YamlConfigSource::from_file(&path);
329 let mut config = Config::new();
330
331 source.load(&mut config).unwrap();
332
333 assert_eq!(config.get::<i64>("server.port").unwrap(), 8080);
334 }
335
336 #[test]
337 fn test_load_missing_yaml_file_returns_io_error() {
338 let source = YamlConfigSource::from_file("missing.yaml");
339 let mut config = Config::new();
340
341 source
342 .load(&mut config)
343 .expect_err("missing YAML file should fail");
344 }
345
346 #[test]
347 fn test_load_invalid_yaml_file_returns_parse_error() {
348 let dir = tempfile::tempdir().unwrap();
349 let path = dir.path().join("invalid.yaml");
350 std::fs::write(&path, "key: [unterminated\n").unwrap();
351
352 let source = YamlConfigSource::from_file(&path);
353 let mut config = Config::new();
354
355 source
356 .load(&mut config)
357 .expect_err("invalid YAML file should fail");
358 }
359
360 #[test]
361 fn test_yaml_key_to_string_number() {
362 let key = YamlValue::Number(serde_yaml::Number::from(42));
363 assert_eq!(yaml_key_to_string(&key).unwrap(), "42");
364 }
365
366 #[test]
367 fn test_yaml_key_to_string_bool() {
368 let key = YamlValue::Bool(true);
369 assert_eq!(yaml_key_to_string(&key).unwrap(), "true");
370 }
371
372 #[test]
373 fn test_yaml_key_to_string_null() {
374 let key = YamlValue::Null;
375 assert_eq!(yaml_key_to_string(&key).unwrap(), "null");
376 }
377
378 #[test]
379 fn test_yaml_scalar_to_string_bool() {
380 assert_eq!(
381 yaml_scalar_to_string(&YamlValue::Bool(false), "k").unwrap(),
382 "false"
383 );
384 }
385
386 #[test]
387 fn test_yaml_scalar_to_string_null() {
388 assert_eq!(yaml_scalar_to_string(&YamlValue::Null, "k").unwrap(), "");
389 }
390
391 #[test]
392 fn test_yaml_scalar_to_string_sequence_returns_error() {
393 yaml_scalar_to_string(&YamlValue::Sequence(vec![]), "arr")
394 .expect_err("nested YAML sequence should fail scalar conversion");
395 }
396
397 #[test]
398 fn test_yaml_scalar_to_string_mapping_returns_error() {
399 yaml_scalar_to_string(&YamlValue::Mapping(serde_yaml::Mapping::new()), "obj")
400 .expect_err("nested YAML mapping should fail scalar conversion");
401 }
402
403 #[test]
404 fn test_flatten_yaml_sequence_mixed_int_null_fallback() {
405 let seq = vec![
407 YamlValue::Number(serde_yaml::Number::from(1i64)),
408 YamlValue::Null,
409 ];
410 let mut config = Config::new();
411 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
412 assert!(config.contains("mixed"));
414 }
415
416 #[test]
417 fn test_flatten_yaml_sequence_mixed_float_string_fallback() {
418 let seq = vec![
420 YamlValue::Number(serde_yaml::Number::from(1.5f64)),
421 YamlValue::String("two".to_string()),
422 ];
423 let mut config = Config::new();
424 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
425 assert!(config.contains("mixed"));
426 }
427
428 #[test]
429 fn test_flatten_yaml_sequence_mixed_bool_string_fallback() {
430 let seq = vec![YamlValue::Bool(true), YamlValue::String("two".to_string())];
432 let mut config = Config::new();
433 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
434 assert!(config.contains("mixed"));
435 }
436
437 #[test]
438 fn test_flatten_yaml_sequence_mixed_string_int_fallback() {
439 let seq = vec![
441 YamlValue::String("one".to_string()),
442 YamlValue::Number(serde_yaml::Number::from(2i64)),
443 ];
444 let mut config = Config::new();
445 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
446 assert!(config.contains("mixed"));
447 }
448
449 #[test]
450 fn test_flatten_yaml_sequence_mixed_nested_value_returns_error() {
451 let seq = vec![
452 YamlValue::Number(serde_yaml::Number::from(1i64)),
453 YamlValue::Mapping(serde_yaml::Mapping::new()),
454 ];
455 let mut config = Config::new();
456 flatten_yaml_sequence("mixed", &seq, &mut config)
457 .expect_err("mixed YAML sequence with a nested value should fail");
458 }
459
460 #[test]
461 fn test_flatten_yaml_sequence_nested_mapping_returns_error() {
462 let seq = vec![YamlValue::Mapping(serde_yaml::Mapping::new())];
463 let mut config = Config::new();
464 flatten_yaml_sequence("nested", &seq, &mut config)
465 .expect_err("nested YAML mapping should fail sequence flattening");
466 }
467
468 #[test]
469 fn test_flatten_yaml_sequence_nested_sequence_returns_error() {
470 let seq = vec![YamlValue::Sequence(vec![YamlValue::Bool(true)])];
471 let mut config = Config::new();
472 flatten_yaml_sequence("nested", &seq, &mut config)
473 .expect_err("nested YAML sequence should fail sequence flattening");
474 }
475
476 #[test]
477 fn test_flatten_yaml_value_tagged() {
478 use serde_yaml::value::Tag;
479 use serde_yaml::value::TaggedValue;
480 let tagged = YamlValue::Tagged(Box::new(TaggedValue {
481 tag: Tag::new("!!str"),
482 value: YamlValue::String("hello".to_string()),
483 }));
484 let mut config = Config::new();
485 flatten_yaml_value("key", &tagged, &mut config).unwrap();
486 assert_eq!(config.get_string("key").unwrap(), "hello");
487 }
488
489 #[test]
490 fn test_flatten_yaml_value_number_no_i64() {
491 let num = serde_yaml::Number::from(f64::MAX);
493 let val = YamlValue::Number(num);
494 let mut config = Config::new();
495 flatten_yaml_value("key", &val, &mut config).unwrap();
496 assert!(config.contains("key"));
497 }
498
499 #[test]
500 fn test_flatten_yaml_scalar_respects_final_property() {
501 let cases = [
502 YamlValue::Null,
503 YamlValue::Bool(true),
504 YamlValue::Number(serde_yaml::Number::from(1i64)),
505 YamlValue::Number(serde_yaml::Number::from(1.5f64)),
506 YamlValue::String("value".to_string()),
507 YamlValue::Tagged(Box::new(serde_yaml::value::TaggedValue {
508 tag: serde_yaml::value::Tag::new("!!str"),
509 value: YamlValue::String("tagged".to_string()),
510 })),
511 ];
512
513 for value in cases {
514 let mut config = config_with_final_property("locked");
515 expect_final_error(flatten_yaml_value("locked", &value, &mut config), "locked");
516 }
517 }
518
519 #[test]
520 fn test_flatten_yaml_sequence_respects_final_property() {
521 let cases = [
522 Vec::new(),
523 vec![
524 YamlValue::Number(serde_yaml::Number::from(1i64)),
525 YamlValue::Number(serde_yaml::Number::from(2i64)),
526 ],
527 vec![
528 YamlValue::Number(serde_yaml::Number::from(1.5f64)),
529 YamlValue::Number(serde_yaml::Number::from(2.5f64)),
530 ],
531 vec![YamlValue::Bool(true), YamlValue::Bool(false)],
532 vec![
533 YamlValue::String("one".to_string()),
534 YamlValue::String("two".to_string()),
535 ],
536 vec![
537 YamlValue::Number(serde_yaml::Number::from(1i64)),
538 YamlValue::Null,
539 ],
540 ];
541
542 for values in cases {
543 let mut config = config_with_final_property("locked");
544 expect_final_error(
545 flatten_yaml_sequence("locked", &values, &mut config),
546 "locked",
547 );
548 }
549 }
550
551 #[test]
552 fn test_unsupported_yaml_sequence_element_error_uses_root_label() {
553 let seq = vec![YamlValue::Mapping(serde_yaml::Mapping::new())];
554 let mut config = Config::new();
555 let err = flatten_yaml_sequence("", &seq, &mut config)
556 .expect_err("nested YAML sequence at root should be rejected");
557 assert!(err.to_string().contains("<root>"));
558 }
559}