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 flatten_toml_value("", &TomlValue::Table(table), config)
92 }
93}
94
95pub(crate) fn flatten_toml_value(
101 prefix: &str,
102 value: &TomlValue,
103 config: &mut Config,
104) -> ConfigResult<()> {
105 match value {
106 TomlValue::Table(table) => {
107 for (k, v) in table {
108 let key = if prefix.is_empty() {
109 k.clone()
110 } else {
111 format!("{}.{}", prefix, k)
112 };
113 flatten_toml_value(&key, v, config)?;
114 }
115 }
116 TomlValue::Array(arr) => {
117 flatten_toml_array(prefix, arr, config)?;
121 }
122 TomlValue::String(s) => {
123 config.set(prefix, s.clone())?;
124 }
125 TomlValue::Integer(i) => {
126 config.set(prefix, *i)?;
127 }
128 TomlValue::Float(f) => {
129 config.set(prefix, *f)?;
130 }
131 TomlValue::Boolean(b) => {
132 config.set(prefix, *b)?;
133 }
134 TomlValue::Datetime(dt) => {
135 config.set(prefix, dt.to_string())?;
136 }
137 }
138 Ok(())
139}
140
141fn flatten_toml_array(prefix: &str, arr: &[TomlValue], config: &mut Config) -> ConfigResult<()> {
147 if arr.is_empty() {
148 config.set(prefix, Vec::<String>::new())?;
149 return Ok(());
150 }
151
152 enum ArrayKind {
154 Integer,
155 Float,
156 Bool,
157 String,
158 }
159
160 let kind = match &arr[0] {
161 TomlValue::Integer(_) => ArrayKind::Integer,
162 TomlValue::Float(_) => ArrayKind::Float,
163 TomlValue::Boolean(_) => ArrayKind::Bool,
164 TomlValue::Table(_) => {
165 return Err(ConfigError::ParseError(format!(
166 "Unsupported nested TOML table inside array at key '{prefix}'"
167 )));
168 }
169 TomlValue::Array(_) => {
170 return Err(ConfigError::ParseError(format!(
171 "Unsupported nested TOML array at key '{prefix}'"
172 )));
173 }
174 _ => ArrayKind::String,
175 };
176
177 let all_same = arr.iter().all(|item| {
179 matches!(
180 (&kind, item),
181 (ArrayKind::Integer, TomlValue::Integer(_))
182 | (ArrayKind::Float, TomlValue::Float(_))
183 | (ArrayKind::Bool, TomlValue::Boolean(_))
184 | (
185 ArrayKind::String,
186 TomlValue::String(_) | TomlValue::Datetime(_)
187 )
188 )
189 });
190
191 if !all_same {
192 let values = arr
194 .iter()
195 .map(|item| toml_scalar_to_string(item, prefix))
196 .collect::<ConfigResult<Vec<_>>>()?;
197 config.set(prefix, values)?;
198 return Ok(());
199 }
200
201 match kind {
202 ArrayKind::Integer => {
203 let values = arr
204 .iter()
205 .map(|item| {
206 item.as_integer()
207 .expect("TOML integer array was validated before insertion")
208 })
209 .collect::<Vec<_>>();
210 config.set(prefix, values)?;
211 }
212 ArrayKind::Float => {
213 let values = arr
214 .iter()
215 .map(|item| {
216 item.as_float()
217 .expect("TOML float array was validated before insertion")
218 })
219 .collect::<Vec<_>>();
220 config.set(prefix, values)?;
221 }
222 ArrayKind::Bool => {
223 let values = arr
224 .iter()
225 .map(|item| {
226 item.as_bool()
227 .expect("TOML bool array was validated before insertion")
228 })
229 .collect::<Vec<_>>();
230 config.set(prefix, values)?;
231 }
232 ArrayKind::String => {
233 let values = arr
234 .iter()
235 .map(|item| {
236 toml_scalar_to_string(item, prefix)
237 .expect("TOML string array was validated before insertion")
238 })
239 .collect::<Vec<_>>();
240 config.set(prefix, values)?;
241 }
242 }
243
244 Ok(())
245}
246
247fn toml_scalar_to_string(value: &TomlValue, key: &str) -> ConfigResult<String> {
249 match value {
250 TomlValue::String(s) => Ok(s.clone()),
251 TomlValue::Integer(i) => Ok(i.to_string()),
252 TomlValue::Float(f) => Ok(f.to_string()),
253 TomlValue::Boolean(b) => Ok(b.to_string()),
254 TomlValue::Datetime(dt) => Ok(dt.to_string()),
255 TomlValue::Array(_) | TomlValue::Table(_) => {
256 let key = if key.is_empty() { "<root>" } else { key };
257 Err(ConfigError::ParseError(format!(
258 "Unsupported nested TOML structure at key '{}'",
259 key
260 )))
261 }
262 }
263}