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<()> {
152 if arr.is_empty() {
153 config.set(prefix, Vec::<String>::new())?;
154 return Ok(());
155 }
156
157 enum ArrayKind {
159 Integer,
160 Float,
161 Bool,
162 String,
163 }
164
165 let kind = match &arr[0] {
166 TomlValue::Integer(_) => ArrayKind::Integer,
167 TomlValue::Float(_) => ArrayKind::Float,
168 TomlValue::Boolean(_) => ArrayKind::Bool,
169 TomlValue::Table(_) => {
170 return Err(ConfigError::ParseError(format!(
171 "Unsupported nested TOML table inside array at key '{prefix}'"
172 )));
173 }
174 TomlValue::Array(_) => {
175 return Err(ConfigError::ParseError(format!(
176 "Unsupported nested TOML array at key '{prefix}'"
177 )));
178 }
179 _ => ArrayKind::String,
180 };
181
182 let all_same = arr.iter().all(|item| {
184 matches!(
185 (&kind, item),
186 (ArrayKind::Integer, TomlValue::Integer(_))
187 | (ArrayKind::Float, TomlValue::Float(_))
188 | (ArrayKind::Bool, TomlValue::Boolean(_))
189 | (
190 ArrayKind::String,
191 TomlValue::String(_) | TomlValue::Datetime(_)
192 )
193 )
194 });
195
196 if !all_same {
197 let values = arr
199 .iter()
200 .map(|item| toml_scalar_to_string(item, prefix))
201 .collect::<ConfigResult<Vec<_>>>()?;
202 config.set(prefix, values)?;
203 return Ok(());
204 }
205
206 match kind {
207 ArrayKind::Integer => {
208 let values = arr
209 .iter()
210 .map(|item| {
211 item.as_integer()
212 .expect("TOML integer array was validated before insertion")
213 })
214 .collect::<Vec<_>>();
215 config.set(prefix, values)?;
216 }
217 ArrayKind::Float => {
218 let values = arr
219 .iter()
220 .map(|item| {
221 item.as_float()
222 .expect("TOML float array was validated before insertion")
223 })
224 .collect::<Vec<_>>();
225 config.set(prefix, values)?;
226 }
227 ArrayKind::Bool => {
228 let values = arr
229 .iter()
230 .map(|item| {
231 item.as_bool()
232 .expect("TOML bool array was validated before insertion")
233 })
234 .collect::<Vec<_>>();
235 config.set(prefix, values)?;
236 }
237 ArrayKind::String => {
238 let values = arr
239 .iter()
240 .map(|item| {
241 toml_scalar_to_string(item, prefix)
242 .expect("TOML string array was validated before insertion")
243 })
244 .collect::<Vec<_>>();
245 config.set(prefix, values)?;
246 }
247 }
248
249 Ok(())
250}
251
252fn toml_scalar_to_string(value: &TomlValue, key: &str) -> ConfigResult<String> {
254 match value {
255 TomlValue::String(s) => Ok(s.clone()),
256 TomlValue::Integer(i) => Ok(i.to_string()),
257 TomlValue::Float(f) => Ok(f.to_string()),
258 TomlValue::Boolean(b) => Ok(b.to_string()),
259 TomlValue::Datetime(dt) => Ok(dt.to_string()),
260 TomlValue::Array(_) | TomlValue::Table(_) => {
261 let key = if key.is_empty() { "<root>" } else { key };
262 Err(ConfigError::ParseError(format!(
263 "Unsupported nested TOML structure at key '{}'",
264 key
265 )))
266 }
267 }
268}