Skip to main content

vault_core/
toml_import.rs

1//! Import TOML config values as environment-style name/value pairs.
2
3use std::collections::BTreeMap;
4
5/// Parse TOML and flatten tables into uppercase underscore-separated names.
6pub fn parse(source: &str) -> Result<Vec<(String, String)>, String> {
7    let value: toml::Value = toml::from_str(source).map_err(|error| error.to_string())?;
8    let mut values = BTreeMap::new();
9    flatten("", &value, &mut values)?;
10    Ok(values.into_iter().collect())
11}
12
13fn flatten(
14    prefix: &str,
15    value: &toml::Value,
16    output: &mut BTreeMap<String, String>,
17) -> Result<(), String> {
18    if let Some(table) = value.as_table() {
19        for (key, value) in table {
20            let key = key
21                .chars()
22                .map(|ch| {
23                    if ch.is_ascii_alphanumeric() {
24                        ch.to_ascii_uppercase()
25                    } else {
26                        '_'
27                    }
28                })
29                .collect::<String>();
30            let name = if prefix.is_empty() {
31                key
32            } else {
33                format!("{prefix}_{key}")
34            };
35            flatten(&name, value, output)?;
36        }
37    } else {
38        if prefix.is_empty() {
39            return Err("TOML root must be a table".into());
40        }
41        if output.insert(prefix.to_owned(), scalar(value)).is_some() {
42            return Err(format!(
43                "TOML keys collide after environment-name conversion: {prefix}"
44            ));
45        }
46    }
47    Ok(())
48}
49
50fn scalar(value: &toml::Value) -> String {
51    match value {
52        toml::Value::String(value) => value.clone(),
53        _ => value.to_string(),
54    }
55}
56
57#[cfg(test)]
58mod tests {
59    use super::parse;
60
61    #[test]
62    fn flattens_tables_and_preserves_scalar_values() {
63        assert_eq!(
64            parse("token = 'abc'\n[bot]\nchannel_id = 123456789012345678\ncolour = 0x800000")
65                .unwrap(),
66            vec![
67                ("BOT_CHANNEL_ID".into(), "123456789012345678".into()),
68                ("BOT_COLOUR".into(), "8388608".into()),
69                ("TOKEN".into(), "abc".into()),
70            ]
71        );
72    }
73
74    #[test]
75    fn refuses_invalid_toml_and_colliding_names() {
76        assert!(parse("token = [").is_err());
77        assert!(parse("a-b = 1\na_b = 2").is_err());
78    }
79}