qubit_config/source/
yaml_config_source.rs1use 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}