1use std::collections::HashMap;
2use std::fmt::Write as _;
3
4pub fn parse_ini(text: &str) -> HashMap<String, HashMap<String, Option<String>>> {
9 ini::inistr!(text)
10}
11
12pub 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.eq_ignore_ascii_case("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 FIXTURE: &str = r#"
41[types]
42boolean = true
43integer = 1
44float = 3.14
45string = hello
46array[] = 1
47array[] = 2
48array[] = 3
49object.key = value
50null = null
51date = 1979-05-27T15:32:00.000Z
52"#;
53
54 #[test]
55 fn parse_ok() {
56 let map = parse_ini(FIXTURE);
57 assert!(map.contains_key("types"));
58 let types = &map["types"];
59 assert_eq!(
60 types.get("boolean").and_then(|v| v.as_deref()),
61 Some("true")
62 );
63 assert_eq!(types.get("integer").and_then(|v| v.as_deref()), Some("1"));
64 assert_eq!(types.get("float").and_then(|v| v.as_deref()), Some("3.14"));
65 assert_eq!(
66 types.get("string").and_then(|v| v.as_deref()),
67 Some("hello")
68 );
69 }
70
71 #[test]
72 fn stringify_roundtrip() {
73 let map = parse_ini(FIXTURE);
74 let out = stringify_ini(&map);
75
76 let reparsed = parse_ini(&out);
77 assert_eq!(reparsed, map);
78 }
79
80 #[test]
81 fn handles_default_section_without_header() {
82 let ini = r#"
83key1 = value1
84
85[section]
86key2 = value2
87"#;
88
89 let map = parse_ini(ini);
90
91 assert!(map.contains_key("default"));
92 let default = &map["default"];
93 assert_eq!(
94 default.get("key1").and_then(|v| v.as_deref()),
95 Some("value1")
96 );
97 }
98}