qubit_config/source/
yaml_config_source.rs1use std::path::{Path, PathBuf};
31
32use serde_yaml::Value as YamlValue;
33
34use crate::{Config, ConfigError, ConfigResult};
35
36use super::ConfigSource;
37
38#[derive(Debug, Clone)]
56pub struct YamlConfigSource {
57 path: PathBuf,
58}
59
60impl YamlConfigSource {
61 #[inline]
67 pub fn from_file<P: AsRef<Path>>(path: P) -> Self {
68 Self {
69 path: path.as_ref().to_path_buf(),
70 }
71 }
72}
73
74impl ConfigSource for YamlConfigSource {
75 fn load(&self, config: &mut Config) -> ConfigResult<()> {
76 let content = std::fs::read_to_string(&self.path).map_err(|e| {
77 ConfigError::IoError(std::io::Error::new(
78 e.kind(),
79 format!("Failed to read YAML file '{}': {}", self.path.display(), e),
80 ))
81 })?;
82
83 let value: YamlValue = serde_yaml::from_str(&content).map_err(|e| {
84 ConfigError::ParseError(format!(
85 "Failed to parse YAML file '{}': {}",
86 self.path.display(),
87 e
88 ))
89 })?;
90
91 flatten_yaml_value("", &value, config)
92 }
93}
94
95pub(crate) fn flatten_yaml_value(
104 prefix: &str,
105 value: &YamlValue,
106 config: &mut Config,
107) -> ConfigResult<()> {
108 match value {
109 YamlValue::Mapping(map) => {
110 for (k, v) in map {
111 let key_str = yaml_key_to_string(k)?;
112 let key = if prefix.is_empty() {
113 key_str
114 } else {
115 format!("{}.{}", prefix, key_str)
116 };
117 flatten_yaml_value(&key, v, config)?;
118 }
119 }
120 YamlValue::Sequence(seq) => {
121 flatten_yaml_sequence(prefix, seq, config)?;
122 }
123 YamlValue::Null => {
124 use qubit_datatype::DataType;
126 config.set_null(prefix, DataType::String)?;
127 }
128 YamlValue::Bool(b) => {
129 config.set(prefix, *b)?;
130 }
131 YamlValue::Number(n) => {
132 if let Some(i) = n.as_i64() {
133 config.set(prefix, i)?;
134 } else {
135 let f = n
136 .as_f64()
137 .expect("YAML number should be representable as i64 or f64");
138 config.set(prefix, f)?;
139 }
140 }
141 YamlValue::String(s) => {
142 config.set(prefix, s.clone())?;
143 }
144 YamlValue::Tagged(tagged) => {
145 flatten_yaml_value(prefix, &tagged.value, config)?;
146 }
147 }
148 Ok(())
149}
150
151fn flatten_yaml_sequence(prefix: &str, seq: &[YamlValue], config: &mut Config) -> ConfigResult<()> {
161 if seq.is_empty() {
162 config.set(prefix, Vec::<String>::new())?;
163 return Ok(());
164 }
165
166 enum SeqKind {
167 Integer,
168 Float,
169 Bool,
170 String,
171 }
172
173 let kind = match &seq[0] {
174 YamlValue::Number(n) if n.is_i64() => SeqKind::Integer,
175 YamlValue::Number(_) => SeqKind::Float,
176 YamlValue::Bool(_) => SeqKind::Bool,
177 YamlValue::Mapping(_) | YamlValue::Sequence(_) | YamlValue::Tagged(_) => {
178 return Err(unsupported_yaml_sequence_element_error(prefix, &seq[0]));
179 }
180 _ => SeqKind::String,
181 };
182
183 let all_same = seq.iter().all(|item| match (&kind, item) {
184 (SeqKind::Integer, YamlValue::Number(n)) => n.is_i64(),
185 (SeqKind::Float, YamlValue::Number(_)) => true,
186 (SeqKind::Bool, YamlValue::Bool(_)) => true,
187 (SeqKind::String, YamlValue::String(_)) => true,
188 _ => false,
189 });
190
191 if !all_same {
192 let values = seq
193 .iter()
194 .map(|item| yaml_scalar_to_string(item, prefix))
195 .collect::<ConfigResult<Vec<_>>>()?;
196 config.set(prefix, values)?;
197 return Ok(());
198 }
199
200 match kind {
201 SeqKind::Integer => {
202 let values = seq
203 .iter()
204 .map(|item| {
205 item.as_i64()
206 .expect("YAML integer sequence was validated before insertion")
207 })
208 .collect::<Vec<_>>();
209 config.set(prefix, values)?;
210 }
211 SeqKind::Float => {
212 let values = seq
213 .iter()
214 .map(|item| {
215 item.as_f64()
216 .expect("YAML float sequence was validated before insertion")
217 })
218 .collect::<Vec<_>>();
219 config.set(prefix, values)?;
220 }
221 SeqKind::Bool => {
222 let values = seq
223 .iter()
224 .map(|item| {
225 item.as_bool()
226 .expect("YAML bool sequence was validated before insertion")
227 })
228 .collect::<Vec<_>>();
229 config.set(prefix, values)?;
230 }
231 SeqKind::String => {
232 let values = seq
233 .iter()
234 .map(|item| {
235 yaml_scalar_to_string(item, prefix)
236 .expect("YAML string sequence was validated before insertion")
237 })
238 .collect::<Vec<_>>();
239 config.set(prefix, values)?;
240 }
241 }
242
243 Ok(())
244}
245
246fn yaml_key_to_string(value: &YamlValue) -> ConfigResult<String> {
248 match value {
249 YamlValue::String(s) => Ok(s.clone()),
250 YamlValue::Number(n) => Ok(n.to_string()),
251 YamlValue::Bool(b) => Ok(b.to_string()),
252 YamlValue::Null => Ok("null".to_string()),
253 _ => Err(ConfigError::ParseError(format!(
254 "Unsupported YAML mapping key type: {value:?}"
255 ))),
256 }
257}
258
259fn yaml_scalar_to_string(value: &YamlValue, key: &str) -> ConfigResult<String> {
265 match value {
266 YamlValue::String(s) => Ok(s.clone()),
267 YamlValue::Number(n) => Ok(n.to_string()),
268 YamlValue::Bool(b) => Ok(b.to_string()),
269 YamlValue::Null => Ok(String::new()),
270 YamlValue::Sequence(_) | YamlValue::Mapping(_) | YamlValue::Tagged(_) => {
271 Err(unsupported_yaml_sequence_element_error(key, value))
272 }
273 }
274}
275
276fn unsupported_yaml_sequence_element_error(key: &str, value: &YamlValue) -> ConfigError {
278 let key = if key.is_empty() { "<root>" } else { key };
279 ConfigError::ParseError(format!(
280 "Unsupported nested YAML structure at key '{key}': {value:?}"
281 ))
282}