qubit_config/source/
toml_config_source.rs1use std::path::{Path, PathBuf};
33
34use toml::{Table as TomlTable, Value as TomlValue};
35
36use crate::{Config, ConfigError, ConfigResult};
37
38use super::ConfigSource;
39
40#[derive(Debug, Clone)]
61pub struct TomlConfigSource {
62 path: PathBuf,
63}
64
65impl TomlConfigSource {
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 TomlConfigSource {
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 TOML file '{}': {}", self.path.display(), e),
85 ))
86 })?;
87
88 let table: TomlTable = content.parse().map_err(|e| {
89 ConfigError::ParseError(format!(
90 "Failed to parse TOML file '{}': {}",
91 self.path.display(),
92 e
93 ))
94 })?;
95
96 flatten_toml_value("", &TomlValue::Table(table), config)
97 }
98}
99
100pub(crate) fn flatten_toml_value(
106 prefix: &str,
107 value: &TomlValue,
108 config: &mut Config,
109) -> ConfigResult<()> {
110 match value {
111 TomlValue::Table(table) => {
112 for (k, v) in table {
113 let key = if prefix.is_empty() {
114 k.clone()
115 } else {
116 format!("{}.{}", prefix, k)
117 };
118 flatten_toml_value(&key, v, config)?;
119 }
120 }
121 TomlValue::Array(arr) => {
122 flatten_toml_array(prefix, arr, config)?;
126 }
127 TomlValue::String(s) => {
128 config.set(prefix, s.clone())?;
129 }
130 TomlValue::Integer(i) => {
131 config.set(prefix, *i)?;
132 }
133 TomlValue::Float(f) => {
134 config.set(prefix, *f)?;
135 }
136 TomlValue::Boolean(b) => {
137 config.set(prefix, *b)?;
138 }
139 TomlValue::Datetime(dt) => {
140 config.set(prefix, dt.to_string())?;
141 }
142 }
143 Ok(())
144}
145
146fn flatten_toml_array(prefix: &str, arr: &[TomlValue], config: &mut Config) -> ConfigResult<()> {
151 if arr.is_empty() {
152 return Ok(());
153 }
154
155 enum ArrayKind {
157 Integer,
158 Float,
159 Bool,
160 String,
161 }
162
163 let kind = match &arr[0] {
164 TomlValue::Integer(_) => ArrayKind::Integer,
165 TomlValue::Float(_) => ArrayKind::Float,
166 TomlValue::Boolean(_) => ArrayKind::Bool,
167 TomlValue::Table(_) => {
168 return Err(ConfigError::ParseError(format!(
169 "Unsupported nested TOML table inside array at key '{prefix}'"
170 )));
171 }
172 TomlValue::Array(_) => {
173 return Err(ConfigError::ParseError(format!(
174 "Unsupported nested TOML array at key '{prefix}'"
175 )));
176 }
177 _ => ArrayKind::String,
178 };
179
180 let all_same = arr.iter().all(|item| {
182 matches!(
183 (&kind, item),
184 (ArrayKind::Integer, TomlValue::Integer(_))
185 | (ArrayKind::Float, TomlValue::Float(_))
186 | (ArrayKind::Bool, TomlValue::Boolean(_))
187 | (
188 ArrayKind::String,
189 TomlValue::String(_) | TomlValue::Datetime(_)
190 )
191 )
192 });
193
194 if !all_same {
195 for item in arr {
197 config.add(prefix, toml_scalar_to_string(item, prefix)?)?;
198 }
199 return Ok(());
200 }
201
202 match kind {
203 ArrayKind::Integer => {
204 for item in arr {
205 if let TomlValue::Integer(i) = item {
206 config.add(prefix, *i)?;
207 }
208 }
209 }
210 ArrayKind::Float => {
211 for item in arr {
212 if let TomlValue::Float(f) = item {
213 config.add(prefix, *f)?;
214 }
215 }
216 }
217 ArrayKind::Bool => {
218 for item in arr {
219 if let TomlValue::Boolean(b) = item {
220 config.add(prefix, *b)?;
221 }
222 }
223 }
224 ArrayKind::String => {
225 for item in arr {
226 config.add(prefix, toml_scalar_to_string(item, prefix)?)?;
227 }
228 }
229 }
230
231 Ok(())
232}
233
234fn toml_scalar_to_string(value: &TomlValue, key: &str) -> ConfigResult<String> {
236 match value {
237 TomlValue::String(s) => Ok(s.clone()),
238 TomlValue::Integer(i) => Ok(i.to_string()),
239 TomlValue::Float(f) => Ok(f.to_string()),
240 TomlValue::Boolean(b) => Ok(b.to_string()),
241 TomlValue::Datetime(dt) => Ok(dt.to_string()),
242 TomlValue::Array(_) | TomlValue::Table(_) => {
243 let key = if key.is_empty() { "<root>" } else { key };
244 Err(ConfigError::ParseError(format!(
245 "Unsupported nested TOML structure at key '{}'",
246 key
247 )))
248 }
249 }
250}
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255
256 #[test]
257 fn test_toml_scalar_to_string_float() {
258 let val = TomlValue::Float(1.5);
259 assert_eq!(toml_scalar_to_string(&val, "key").unwrap(), "1.5");
260 }
261
262 #[test]
263 fn test_toml_scalar_to_string_bool() {
264 let val = TomlValue::Boolean(true);
265 assert_eq!(toml_scalar_to_string(&val, "key").unwrap(), "true");
266 }
267
268 #[test]
269 fn test_toml_scalar_to_string_nested_array_empty_key() {
270 let val = TomlValue::Array(vec![]);
271 let result = toml_scalar_to_string(&val, "");
272 assert!(result.is_err());
273 let msg = format!("{}", result.unwrap_err());
274 assert!(msg.contains("<root>"));
275 }
276
277 #[test]
278 fn test_toml_scalar_to_string_nested_table_with_key() {
279 let val = TomlValue::Table(toml::Table::new());
280 let result = toml_scalar_to_string(&val, "my.key");
281 assert!(result.is_err());
282 let msg = format!("{}", result.unwrap_err());
283 assert!(msg.contains("my.key"));
284 }
285
286 #[test]
287 fn test_flatten_toml_array_mixed_int_string_fallback() {
288 let arr = vec![TomlValue::Integer(1), TomlValue::String("two".to_string())];
291 let mut config = Config::new();
292 flatten_toml_array("mixed", &arr, &mut config).unwrap();
293 let vals: Vec<String> = config.get_list("mixed").unwrap();
295 assert_eq!(vals.len(), 2);
296 }
297
298 #[test]
299 fn test_flatten_toml_array_mixed_float_string_fallback() {
300 let arr = vec![TomlValue::Float(1.5), TomlValue::String("two".to_string())];
301 let mut config = Config::new();
302 flatten_toml_array("mixed", &arr, &mut config).unwrap();
303 let vals: Vec<String> = config.get_list("mixed").unwrap();
304 assert_eq!(vals.len(), 2);
305 }
306
307 #[test]
308 fn test_flatten_toml_array_mixed_bool_string_fallback() {
309 let arr = vec![
310 TomlValue::Boolean(true),
311 TomlValue::String("two".to_string()),
312 ];
313 let mut config = Config::new();
314 flatten_toml_array("mixed", &arr, &mut config).unwrap();
315 let vals: Vec<String> = config.get_list("mixed").unwrap();
316 assert_eq!(vals.len(), 2);
317 }
318}