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