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 if !config.contains(prefix) {
132 config.set_null(prefix, DataType::String)?;
133 }
134 }
135 YamlValue::Bool(b) => {
136 config.set(prefix, *b)?;
137 }
138 YamlValue::Number(n) => {
139 if let Some(i) = n.as_i64() {
140 config.set(prefix, i)?;
141 } else if let Some(f) = n.as_f64() {
142 config.set(prefix, f)?;
143 } else {
144 config.set(prefix, n.to_string())?;
145 }
146 }
147 YamlValue::String(s) => {
148 config.set(prefix, s.clone())?;
149 }
150 YamlValue::Tagged(tagged) => {
151 flatten_yaml_value(prefix, &tagged.value, config)?;
152 }
153 }
154 Ok(())
155}
156
157fn flatten_yaml_sequence(prefix: &str, seq: &[YamlValue], config: &mut Config) -> ConfigResult<()> {
165 if seq.is_empty() {
166 return Ok(());
167 }
168
169 enum SeqKind {
170 Integer,
171 Float,
172 Bool,
173 String,
174 }
175
176 let kind = match &seq[0] {
177 YamlValue::Number(n) if n.is_i64() => SeqKind::Integer,
178 YamlValue::Number(_) => SeqKind::Float,
179 YamlValue::Bool(_) => SeqKind::Bool,
180 YamlValue::Mapping(_) | YamlValue::Sequence(_) | YamlValue::Tagged(_) => {
181 return Err(unsupported_yaml_sequence_element_error(prefix, &seq[0]));
182 }
183 _ => SeqKind::String,
184 };
185
186 let all_same = seq.iter().all(|item| match (&kind, item) {
187 (SeqKind::Integer, YamlValue::Number(n)) => n.is_i64(),
188 (SeqKind::Float, YamlValue::Number(_)) => true,
189 (SeqKind::Bool, YamlValue::Bool(_)) => true,
190 (SeqKind::String, YamlValue::String(_)) => true,
191 _ => false,
192 });
193
194 if !all_same {
195 for item in seq {
196 config.add(prefix, yaml_scalar_to_string(item, prefix)?)?;
197 }
198 return Ok(());
199 }
200
201 match kind {
202 SeqKind::Integer => {
203 for item in seq {
204 if let YamlValue::Number(n) = item
205 && let Some(i) = n.as_i64()
206 {
207 config.add(prefix, i)?;
208 }
209 }
210 }
211 SeqKind::Float => {
212 for item in seq {
213 if let YamlValue::Number(n) = item
214 && let Some(f) = n.as_f64()
215 {
216 config.add(prefix, f)?;
217 }
218 }
219 }
220 SeqKind::Bool => {
221 for item in seq {
222 if let YamlValue::Bool(b) = item {
223 config.add(prefix, *b)?;
224 }
225 }
226 }
227 SeqKind::String => {
228 for item in seq {
229 config.add(prefix, yaml_scalar_to_string(item, prefix)?)?;
230 }
231 }
232 }
233
234 Ok(())
235}
236
237fn yaml_key_to_string(value: &YamlValue) -> ConfigResult<String> {
239 match value {
240 YamlValue::String(s) => Ok(s.clone()),
241 YamlValue::Number(n) => Ok(n.to_string()),
242 YamlValue::Bool(b) => Ok(b.to_string()),
243 YamlValue::Null => Ok("null".to_string()),
244 _ => Err(ConfigError::ParseError(format!(
245 "Unsupported YAML mapping key type: {value:?}"
246 ))),
247 }
248}
249
250fn yaml_scalar_to_string(value: &YamlValue, key: &str) -> ConfigResult<String> {
256 match value {
257 YamlValue::String(s) => Ok(s.clone()),
258 YamlValue::Number(n) => Ok(n.to_string()),
259 YamlValue::Bool(b) => Ok(b.to_string()),
260 YamlValue::Null => Ok(String::new()),
261 YamlValue::Sequence(_) | YamlValue::Mapping(_) | YamlValue::Tagged(_) => {
262 Err(unsupported_yaml_sequence_element_error(key, value))
263 }
264 }
265}
266
267fn unsupported_yaml_sequence_element_error(key: &str, value: &YamlValue) -> ConfigError {
269 let key = if key.is_empty() { "<root>" } else { key };
270 ConfigError::ParseError(format!(
271 "Unsupported nested YAML structure at key '{key}': {value:?}"
272 ))
273}
274
275#[cfg(test)]
276mod tests {
277 use super::*;
278
279 #[test]
280 fn test_yaml_key_to_string_number() {
281 let key = YamlValue::Number(serde_yaml::Number::from(42));
282 assert_eq!(yaml_key_to_string(&key).unwrap(), "42");
283 }
284
285 #[test]
286 fn test_yaml_key_to_string_bool() {
287 let key = YamlValue::Bool(true);
288 assert_eq!(yaml_key_to_string(&key).unwrap(), "true");
289 }
290
291 #[test]
292 fn test_yaml_key_to_string_null() {
293 let key = YamlValue::Null;
294 assert_eq!(yaml_key_to_string(&key).unwrap(), "null");
295 }
296
297 #[test]
298 fn test_yaml_scalar_to_string_bool() {
299 assert_eq!(
300 yaml_scalar_to_string(&YamlValue::Bool(false), "k").unwrap(),
301 "false"
302 );
303 }
304
305 #[test]
306 fn test_yaml_scalar_to_string_null() {
307 assert_eq!(yaml_scalar_to_string(&YamlValue::Null, "k").unwrap(), "");
308 }
309
310 #[test]
311 fn test_yaml_scalar_to_string_sequence_returns_error() {
312 let result = yaml_scalar_to_string(&YamlValue::Sequence(vec![]), "arr");
313 assert!(matches!(result, Err(ConfigError::ParseError(_))));
314 }
315
316 #[test]
317 fn test_yaml_scalar_to_string_mapping_returns_error() {
318 let result = yaml_scalar_to_string(&YamlValue::Mapping(serde_yaml::Mapping::new()), "obj");
319 assert!(matches!(result, Err(ConfigError::ParseError(_))));
320 }
321
322 #[test]
323 fn test_flatten_yaml_sequence_mixed_int_null_fallback() {
324 let seq = vec![
326 YamlValue::Number(serde_yaml::Number::from(1i64)),
327 YamlValue::Null,
328 ];
329 let mut config = Config::new();
330 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
331 assert!(config.contains("mixed"));
333 }
334
335 #[test]
336 fn test_flatten_yaml_sequence_mixed_float_string_fallback() {
337 let seq = vec![
339 YamlValue::Number(serde_yaml::Number::from(1.5f64)),
340 YamlValue::String("two".to_string()),
341 ];
342 let mut config = Config::new();
343 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
344 assert!(config.contains("mixed"));
345 }
346
347 #[test]
348 fn test_flatten_yaml_sequence_mixed_bool_string_fallback() {
349 let seq = vec![YamlValue::Bool(true), YamlValue::String("two".to_string())];
351 let mut config = Config::new();
352 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
353 assert!(config.contains("mixed"));
354 }
355
356 #[test]
357 fn test_flatten_yaml_sequence_mixed_string_int_fallback() {
358 let seq = vec![
360 YamlValue::String("one".to_string()),
361 YamlValue::Number(serde_yaml::Number::from(2i64)),
362 ];
363 let mut config = Config::new();
364 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
365 assert!(config.contains("mixed"));
366 }
367
368 #[test]
369 fn test_flatten_yaml_sequence_nested_mapping_returns_error() {
370 let seq = vec![YamlValue::Mapping(serde_yaml::Mapping::new())];
371 let mut config = Config::new();
372 let result = flatten_yaml_sequence("nested", &seq, &mut config);
373 assert!(matches!(result, Err(ConfigError::ParseError(_))));
374 }
375
376 #[test]
377 fn test_flatten_yaml_sequence_nested_sequence_returns_error() {
378 let seq = vec![YamlValue::Sequence(vec![YamlValue::Bool(true)])];
379 let mut config = Config::new();
380 let result = flatten_yaml_sequence("nested", &seq, &mut config);
381 assert!(matches!(result, Err(ConfigError::ParseError(_))));
382 }
383
384 #[test]
385 fn test_flatten_yaml_value_tagged() {
386 use serde_yaml::value::Tag;
387 use serde_yaml::value::TaggedValue;
388 let tagged = YamlValue::Tagged(Box::new(TaggedValue {
389 tag: Tag::new("!!str"),
390 value: YamlValue::String("hello".to_string()),
391 }));
392 let mut config = Config::new();
393 flatten_yaml_value("key", &tagged, &mut config).unwrap();
394 assert_eq!(config.get_string("key").unwrap(), "hello");
395 }
396
397 #[test]
398 fn test_flatten_yaml_value_number_no_i64() {
399 let num = serde_yaml::Number::from(f64::MAX);
401 let val = YamlValue::Number(num);
402 let mut config = Config::new();
403 flatten_yaml_value("key", &val, &mut config).unwrap();
404 assert!(config.contains("key"));
405 }
406}