vault_core/
toml_import.rs1use std::collections::BTreeMap;
4
5pub 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}