Skip to main content

qubit_config/source/
toml_config_source.rs

1/*******************************************************************************
2 *
3 *    Copyright (c) 2025 - 2026.
4 *    Haixing Hu, Qubit Co. Ltd.
5 *
6 *    All rights reserved.
7 *
8 ******************************************************************************/
9//! # TOML File Configuration Source
10//!
11//! Loads configuration from TOML format files.
12//!
13//! # Flattening Strategy
14//!
15//! Nested TOML tables are flattened using dot-separated keys.
16//! For example:
17//!
18//! ```toml
19//! [server]
20//! host = "localhost"
21//! port = 8080
22//! ```
23//!
24//! becomes `server.host = "localhost"` and `server.port = 8080`.
25//!
26//! Arrays are stored as multi-value properties.
27//!
28//! # Author
29//!
30//! Haixing Hu
31
32use 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/// Configuration source that loads from TOML format files
41///
42/// # Examples
43///
44/// ```rust
45/// use qubit_config::source::{TomlConfigSource, ConfigSource};
46/// use qubit_config::Config;
47///
48/// let temp_dir = tempfile::tempdir().unwrap();
49/// let path = temp_dir.path().join("config.toml");
50/// std::fs::write(&path, "server.port = 8080\n").unwrap();
51/// let source = TomlConfigSource::from_file(path);
52/// let mut config = Config::new();
53/// source.load(&mut config).unwrap();
54/// assert_eq!(config.get::<i64>("server.port").unwrap(), 8080);
55/// ```
56///
57/// # Author
58///
59/// Haixing Hu
60#[derive(Debug, Clone)]
61pub struct TomlConfigSource {
62    path: PathBuf,
63}
64
65impl TomlConfigSource {
66    /// Creates a new `TomlConfigSource` from a file path
67    ///
68    /// # Parameters
69    ///
70    /// * `path` - Path to the TOML file
71    #[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
100/// Recursively flattens a TOML value into the config using dot-separated keys.
101///
102/// Scalar types are stored with their native types (integer → i64, float → f64,
103/// bool → bool, null/empty → empty property). String and datetime values are
104/// stored as `String`.
105pub(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            // Detect the element type of the first non-table/non-array item.
123            // All elements must be the same scalar type; mixed-type arrays fall
124            // back to string representation to avoid silent data loss.
125            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
146/// Flattens a TOML array into multi-value config entries.
147///
148/// Homogeneous scalar arrays are stored with their native types. Empty arrays
149/// are stored as explicit empty string lists because TOML carries no element
150/// type for them. Mixed or nested arrays fall back to string representation.
151fn 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    // Determine the dominant scalar type from the first element.
158    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    // Check that all elements match the first element's type; fall back to string if not.
183    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        // Mixed types → fall back to string
198        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
252/// Converts a TOML scalar value to a string (used as fallback for mixed arrays)
253fn 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}