c12-parser 1.1.0

Toolbox for parsing and stringifying various formats, including JSON, JSON5, JSONC, INI, TOML, and YAML.
Documentation
use std::collections::HashMap;
use std::fmt::Write as _;

/// Parses an INI string into a simple nested map structure:
/// `HashMap<section, HashMap<key, Option<value>>>`.
///
/// Style/indentation are not preserved.
pub fn parse_ini(text: &str) -> HashMap<String, HashMap<String, Option<String>>> {
    ini::inistr!(text)
}

/// Stringifies an INI-like nested map back into INI text.
///
/// Note: This does **not** preserve exact original formatting.
pub fn stringify_ini(map: &HashMap<String, HashMap<String, Option<String>>>) -> String {
    let mut out = String::new();
    for (section, kv) in map {
        if !section.eq_ignore_ascii_case("default") {
            let _ = writeln!(&mut out, "[{}]", section);
        }
        for (key, value) in kv {
            match value {
                Some(v) => {
                    let _ = writeln!(&mut out, "{} = {}", key, v);
                }
                None => {
                    let _ = writeln!(&mut out, "{}", key);
                }
            }
        }
    }
    out
}

#[cfg(test)]
mod tests {
    use super::*;

    /// Sample document used by parse/stringify tests.
    const FIXTURE: &str = r#"
[types]
boolean = true
integer = 1
float = 3.14
string = hello
array[] = 1
array[] = 2
array[] = 3
object.key = value
null = null
date = 1979-05-27T15:32:00.000Z
"#;

    #[test]
    fn parse_ok() {
        let map = parse_ini(FIXTURE);
        assert!(map.contains_key("types"));
        let types = &map["types"];
        assert_eq!(
            types.get("boolean").and_then(|v| v.as_deref()),
            Some("true")
        );
        assert_eq!(types.get("integer").and_then(|v| v.as_deref()), Some("1"));
        assert_eq!(types.get("float").and_then(|v| v.as_deref()), Some("3.14"));
        assert_eq!(
            types.get("string").and_then(|v| v.as_deref()),
            Some("hello")
        );
    }

    #[test]
    fn stringify_roundtrip() {
        let map = parse_ini(FIXTURE);
        let out = stringify_ini(&map);

        let reparsed = parse_ini(&out);
        assert_eq!(reparsed, map);
    }

    #[test]
    fn handles_default_section_without_header() {
        let ini = r#"
key1 = value1

[section]
key2 = value2
"#;

        let map = parse_ini(ini);

        assert!(map.contains_key("default"));
        let default = &map["default"];
        assert_eq!(
            default.get("key1").and_then(|v| v.as_deref()),
            Some("value1")
        );
    }
}