qubit_config/source/
toml_config_source.rs1use std::path::{Path, PathBuf};
31
32use toml::{Table as TomlTable, Value as TomlValue};
33
34use crate::{Config, ConfigError, ConfigResult};
35
36use super::ConfigSource;
37
38#[derive(Debug, Clone)]
56pub struct TomlConfigSource {
57 path: PathBuf,
58}
59
60impl TomlConfigSource {
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 TomlConfigSource {
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 TOML file '{}': {}", self.path.display(), e),
80 ))
81 })?;
82
83 let table: TomlTable = content.parse().map_err(|e| {
84 ConfigError::ParseError(format!(
85 "Failed to parse TOML file '{}': {}",
86 self.path.display(),
87 e
88 ))
89 })?;
90
91 let mut staged = config.clone();
92 flatten_toml_value("", &TomlValue::Table(table), &mut staged)?;
93 *config = staged;
94 Ok(())
95 }
96}
97
98pub(crate) fn flatten_toml_value(
104 prefix: &str,
105 value: &TomlValue,
106 config: &mut Config,
107) -> ConfigResult<()> {
108 match value {
109 TomlValue::Table(table) => {
110 for (k, v) in table {
111 let key = if prefix.is_empty() {
112 k.clone()
113 } else {
114 format!("{}.{}", prefix, k)
115 };
116 flatten_toml_value(&key, v, config)?;
117 }
118 }
119 TomlValue::Array(arr) => {
120 flatten_toml_array(prefix, arr, config)?;
124 }
125 TomlValue::String(s) => {
126 config.set(prefix, s.clone())?;
127 }
128 TomlValue::Integer(i) => {
129 config.set(prefix, *i)?;
130 }
131 TomlValue::Float(f) => {
132 config.set(prefix, *f)?;
133 }
134 TomlValue::Boolean(b) => {
135 config.set(prefix, *b)?;
136 }
137 TomlValue::Datetime(dt) => {
138 config.set(prefix, dt.to_string())?;
139 }
140 }
141 Ok(())
142}
143
144fn flatten_toml_array(prefix: &str, arr: &[TomlValue], config: &mut Config) -> ConfigResult<()> {
150 if arr.is_empty() {
151 config.set(prefix, Vec::<String>::new())?;
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 let values = arr
197 .iter()
198 .map(|item| toml_scalar_to_string(item, prefix))
199 .collect::<ConfigResult<Vec<_>>>()?;
200 config.set(prefix, values)?;
201 return Ok(());
202 }
203
204 match kind {
205 ArrayKind::Integer => {
206 let values = arr
207 .iter()
208 .map(|item| {
209 item.as_integer()
210 .expect("TOML integer array was validated before insertion")
211 })
212 .collect::<Vec<_>>();
213 config.set(prefix, values)?;
214 }
215 ArrayKind::Float => {
216 let values = arr
217 .iter()
218 .map(|item| {
219 item.as_float()
220 .expect("TOML float array was validated before insertion")
221 })
222 .collect::<Vec<_>>();
223 config.set(prefix, values)?;
224 }
225 ArrayKind::Bool => {
226 let values = arr
227 .iter()
228 .map(|item| {
229 item.as_bool()
230 .expect("TOML bool array was validated before insertion")
231 })
232 .collect::<Vec<_>>();
233 config.set(prefix, values)?;
234 }
235 ArrayKind::String => {
236 let values = arr
237 .iter()
238 .map(|item| {
239 toml_scalar_to_string(item, prefix)
240 .expect("TOML string array was validated before insertion")
241 })
242 .collect::<Vec<_>>();
243 config.set(prefix, values)?;
244 }
245 }
246
247 Ok(())
248}
249
250fn toml_scalar_to_string(value: &TomlValue, key: &str) -> ConfigResult<String> {
252 match value {
253 TomlValue::String(s) => Ok(s.clone()),
254 TomlValue::Integer(i) => Ok(i.to_string()),
255 TomlValue::Float(f) => Ok(f.to_string()),
256 TomlValue::Boolean(b) => Ok(b.to_string()),
257 TomlValue::Datetime(dt) => Ok(dt.to_string()),
258 TomlValue::Array(_) | TomlValue::Table(_) => {
259 let key = if key.is_empty() { "<root>" } else { key };
260 Err(ConfigError::ParseError(format!(
261 "Unsupported nested TOML structure at key '{}'",
262 key
263 )))
264 }
265 }
266}