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)]
57pub struct YamlConfigSource {
58 path: PathBuf,
59}
60
61impl YamlConfigSource {
62 #[inline]
68 pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
69 Self {
70 path: path.as_ref().to_path_buf(),
71 }
72 }
73}
74
75impl ConfigSource for YamlConfigSource {
76 fn load(&self, config: &mut Config) -> ConfigResult<()> {
77 let content = std::fs::read_to_string(&self.path).map_err(|e| {
78 ConfigError::IoError(std::io::Error::new(
79 e.kind(),
80 format!("Failed to read YAML file '{}': {}", self.path.display(), e),
81 ))
82 })?;
83
84 let value: YamlValue = serde_yaml::from_str(&content).map_err(|e| {
85 ConfigError::ParseError(format!(
86 "Failed to parse YAML file '{}': {}",
87 self.path.display(),
88 e
89 ))
90 })?;
91
92 flatten_yaml_value("", &value, config)
93 }
94}
95
96pub(crate) fn flatten_yaml_value(
105 prefix: &str,
106 value: &YamlValue,
107 config: &mut Config,
108) -> ConfigResult<()> {
109 match value {
110 YamlValue::Mapping(map) => {
111 for (k, v) in map {
112 let key_str = yaml_key_to_string(k)?;
113 let key = if prefix.is_empty() {
114 key_str
115 } else {
116 format!("{}.{}", prefix, key_str)
117 };
118 flatten_yaml_value(&key, v, config)?;
119 }
120 }
121 YamlValue::Sequence(seq) => {
122 flatten_yaml_sequence(prefix, seq, config)?;
123 }
124 YamlValue::Null => {
125 use crate::Property;
128 use qubit_common::DataType;
129 use qubit_value::MultiValues;
130 config
131 .properties_mut()
132 .entry(prefix.to_string())
133 .or_insert_with(|| {
134 Property::with_value(prefix, MultiValues::Empty(DataType::String))
135 });
136 }
137 YamlValue::Bool(b) => {
138 config.set(prefix, *b)?;
139 }
140 YamlValue::Number(n) => {
141 if let Some(i) = n.as_i64() {
142 config.set(prefix, i)?;
143 } else if let Some(f) = n.as_f64() {
144 config.set(prefix, f)?;
145 } else {
146 config.set(prefix, n.to_string())?;
147 }
148 }
149 YamlValue::String(s) => {
150 config.set(prefix, s.clone())?;
151 }
152 YamlValue::Tagged(tagged) => {
153 flatten_yaml_value(prefix, &tagged.value, config)?;
154 }
155 }
156 Ok(())
157}
158
159fn flatten_yaml_sequence(prefix: &str, seq: &[YamlValue], config: &mut Config) -> ConfigResult<()> {
164 if seq.is_empty() {
165 return Ok(());
166 }
167
168 enum SeqKind {
169 Integer,
170 Float,
171 Bool,
172 String,
173 }
174
175 let kind = match &seq[0] {
176 YamlValue::Number(n) if n.is_i64() => SeqKind::Integer,
177 YamlValue::Number(_) => SeqKind::Float,
178 YamlValue::Bool(_) => SeqKind::Bool,
179 YamlValue::Mapping(_) | YamlValue::Sequence(_) => {
180 for item in seq {
182 config.add(prefix, yaml_scalar_to_string(item))?;
183 }
184 return Ok(());
185 }
186 _ => SeqKind::String,
187 };
188
189 let all_same = seq.iter().all(|item| match (&kind, item) {
190 (SeqKind::Integer, YamlValue::Number(n)) => n.is_i64(),
191 (SeqKind::Float, YamlValue::Number(_)) => true,
192 (SeqKind::Bool, YamlValue::Bool(_)) => true,
193 (SeqKind::String, YamlValue::String(_)) => true,
194 _ => false,
195 });
196
197 if !all_same {
198 for item in seq {
199 config.add(prefix, yaml_scalar_to_string(item))?;
200 }
201 return Ok(());
202 }
203
204 match kind {
205 SeqKind::Integer => {
206 for item in seq {
207 if let YamlValue::Number(n) = item {
208 if let Some(i) = n.as_i64() {
209 config.add(prefix, i)?;
210 }
211 }
212 }
213 }
214 SeqKind::Float => {
215 for item in seq {
216 if let YamlValue::Number(n) = item {
217 if let Some(f) = n.as_f64() {
218 config.add(prefix, f)?;
219 }
220 }
221 }
222 }
223 SeqKind::Bool => {
224 for item in seq {
225 if let YamlValue::Bool(b) = item {
226 config.add(prefix, *b)?;
227 }
228 }
229 }
230 SeqKind::String => {
231 for item in seq {
232 config.add(prefix, yaml_scalar_to_string(item))?;
233 }
234 }
235 }
236
237 Ok(())
238}
239
240fn yaml_key_to_string(value: &YamlValue) -> ConfigResult<String> {
242 match value {
243 YamlValue::String(s) => Ok(s.clone()),
244 YamlValue::Number(n) => Ok(n.to_string()),
245 YamlValue::Bool(b) => Ok(b.to_string()),
246 YamlValue::Null => Ok("null".to_string()),
247 _ => Err(ConfigError::ParseError(format!(
248 "Unsupported YAML mapping key type: {value:?}"
249 ))),
250 }
251}
252
253fn yaml_scalar_to_string(value: &YamlValue) -> String {
255 match value {
256 YamlValue::String(s) => s.clone(),
257 YamlValue::Number(n) => n.to_string(),
258 YamlValue::Bool(b) => b.to_string(),
259 YamlValue::Null => String::new(),
260 YamlValue::Sequence(_) | YamlValue::Mapping(_) | YamlValue::Tagged(_) => String::new(),
261 }
262}
263
264#[cfg(test)]
265mod tests {
266 use super::*;
267
268 #[test]
269 fn test_yaml_key_to_string_number() {
270 let key = YamlValue::Number(serde_yaml::Number::from(42));
271 assert_eq!(yaml_key_to_string(&key).unwrap(), "42");
272 }
273
274 #[test]
275 fn test_yaml_key_to_string_bool() {
276 let key = YamlValue::Bool(true);
277 assert_eq!(yaml_key_to_string(&key).unwrap(), "true");
278 }
279
280 #[test]
281 fn test_yaml_key_to_string_null() {
282 let key = YamlValue::Null;
283 assert_eq!(yaml_key_to_string(&key).unwrap(), "null");
284 }
285
286 #[test]
287 fn test_yaml_scalar_to_string_bool() {
288 assert_eq!(yaml_scalar_to_string(&YamlValue::Bool(false)), "false");
289 }
290
291 #[test]
292 fn test_yaml_scalar_to_string_null() {
293 assert_eq!(yaml_scalar_to_string(&YamlValue::Null), "");
294 }
295
296 #[test]
297 fn test_yaml_scalar_to_string_sequence() {
298 assert_eq!(yaml_scalar_to_string(&YamlValue::Sequence(vec![])), "");
299 }
300
301 #[test]
302 fn test_yaml_scalar_to_string_mapping() {
303 assert_eq!(
304 yaml_scalar_to_string(&YamlValue::Mapping(serde_yaml::Mapping::new())),
305 ""
306 );
307 }
308
309 #[test]
310 fn test_flatten_yaml_sequence_mixed_int_null_fallback() {
311 let seq = vec![
313 YamlValue::Number(serde_yaml::Number::from(1i64)),
314 YamlValue::Null,
315 ];
316 let mut config = Config::new();
317 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
318 assert!(config.contains("mixed"));
320 }
321
322 #[test]
323 fn test_flatten_yaml_sequence_mixed_float_string_fallback() {
324 let seq = vec![
326 YamlValue::Number(serde_yaml::Number::from(1.5f64)),
327 YamlValue::String("two".to_string()),
328 ];
329 let mut config = Config::new();
330 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
331 assert!(config.contains("mixed"));
332 }
333
334 #[test]
335 fn test_flatten_yaml_sequence_mixed_bool_string_fallback() {
336 let seq = vec![YamlValue::Bool(true), YamlValue::String("two".to_string())];
338 let mut config = Config::new();
339 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
340 assert!(config.contains("mixed"));
341 }
342
343 #[test]
344 fn test_flatten_yaml_sequence_mixed_string_int_fallback() {
345 let seq = vec![
347 YamlValue::String("one".to_string()),
348 YamlValue::Number(serde_yaml::Number::from(2i64)),
349 ];
350 let mut config = Config::new();
351 flatten_yaml_sequence("mixed", &seq, &mut config).unwrap();
352 assert!(config.contains("mixed"));
353 }
354
355 #[test]
356 fn test_flatten_yaml_value_tagged() {
357 use serde_yaml::value::Tag;
358 use serde_yaml::value::TaggedValue;
359 let tagged = YamlValue::Tagged(Box::new(TaggedValue {
360 tag: Tag::new("!!str"),
361 value: YamlValue::String("hello".to_string()),
362 }));
363 let mut config = Config::new();
364 flatten_yaml_value("key", &tagged, &mut config).unwrap();
365 assert_eq!(config.get_string("key").unwrap(), "hello");
366 }
367
368 #[test]
369 fn test_flatten_yaml_value_number_no_i64() {
370 let num = serde_yaml::Number::from(f64::MAX);
372 let val = YamlValue::Number(num);
373 let mut config = Config::new();
374 flatten_yaml_value("key", &val, &mut config).unwrap();
375 assert!(config.contains("key"));
376 }
377}