Skip to main content

c12_parser/
ini_format.rs

1use std::collections::HashMap;
2use std::fmt::Write as _;
3
4/// Parses an INI string into a simple nested map structure:
5/// `HashMap<section, HashMap<key, Option<value>>>`.
6///
7/// Style/indentation are not preserved.
8pub fn parse_ini(text: &str) -> HashMap<String, HashMap<String, Option<String>>> {
9    ini::inistr!(text)
10}
11
12/// Stringifies an INI-like nested map back into INI text.
13///
14/// Note: This does **not** preserve exact original formatting.
15pub fn stringify_ini(map: &HashMap<String, HashMap<String, Option<String>>>) -> String {
16    let mut out = String::new();
17    for (section, kv) in map {
18        if section.to_lowercase() != "default" {
19            let _ = writeln!(&mut out, "[{}]", section);
20        }
21        for (key, value) in kv {
22            match value {
23                Some(v) => {
24                    let _ = writeln!(&mut out, "{} = {}", key, v);
25                }
26                None => {
27                    let _ = writeln!(&mut out, "{}", key);
28                }
29            }
30        }
31    }
32    out
33}
34
35#[cfg(test)]
36mod tests {
37    use super::*;
38
39    const INI_FIXTURE: &str = r#"
40[types]
41boolean = true
42integer = 1
43float = 3.14
44string = hello
45array[] = 1
46array[] = 2
47array[] = 3
48object.key = value
49null = null
50date = 1979-05-27T15:32:00.000Z
51"#;
52
53    #[test]
54    fn ini_parse_ok() {
55        let map = parse_ini(INI_FIXTURE);
56        assert!(map.contains_key("types"));
57        let types = &map["types"];
58        assert_eq!(
59            types.get("boolean").and_then(|v| v.as_deref()),
60            Some("true")
61        );
62        assert_eq!(types.get("integer").and_then(|v| v.as_deref()), Some("1"));
63        assert_eq!(types.get("float").and_then(|v| v.as_deref()), Some("3.14"));
64        assert_eq!(
65            types.get("string").and_then(|v| v.as_deref()),
66            Some("hello")
67        );
68    }
69
70    #[test]
71    fn ini_stringify_exact_fixture_trim_start() {
72        let map = parse_ini(INI_FIXTURE);
73        let out = stringify_ini(&map);
74
75        let reparsed = parse_ini(&out);
76        assert_eq!(reparsed, map);
77    }
78
79    #[test]
80    fn ini_handles_default_section_without_header() {
81        let ini = r#"
82key1 = value1
83
84[section]
85key2 = value2
86"#;
87
88        let map = parse_ini(ini);
89
90        assert!(map.contains_key("default"));
91        let default = &map["default"];
92        assert_eq!(
93            default.get("key1").and_then(|v| v.as_deref()),
94            Some("value1")
95        );
96    }
97}