c12-parser 1.1.0

Toolbox for parsing and stringifying various formats, including JSON, JSON5, JSONC, INI, TOML, and YAML.
Documentation
use serde::{Serialize, de::DeserializeOwned};

use crate::format::{FormatOptions, Formatted, wrap_whitespace};

/// Parses a TOML string into a value, capturing outer whitespace only.
pub fn parse_toml<T>(
    text: &str,
    options: Option<FormatOptions>,
) -> Result<Formatted<T>, toml::de::Error>
where
    T: DeserializeOwned,
{
    let opts = FormatOptions {
        preserve_indentation: false,
        ..options.unwrap_or_default()
    };
    let value = toml::from_str(text)?;
    Ok(Formatted::new(text, value, &opts))
}

/// Stringifies a TOML value with preserved outer whitespace.
pub fn stringify_toml<T>(
    formatted: &Formatted<T>,
    _options: Option<FormatOptions>,
) -> Result<String, toml::ser::Error>
where
    T: Serialize,
{
    let toml_str = toml::to_string(&formatted.value)?;
    Ok(wrap_whitespace(&toml_str, &formatted.format))
}

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

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

[types.object]
key = "value"
"#;

    #[test]
    fn parse_ok() {
        #[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
        struct Types {
            boolean: bool,
            integer: i64,
            float: f64,
            string: String,
            array: Vec<i64>,
            null: String,
            date: String,
            object: Object,
        }

        #[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
        struct Object {
            key: String,
        }

        #[derive(Debug, serde::Deserialize, serde::Serialize, PartialEq)]
        struct Root {
            types: Types,
        }

        let formatted = parse_toml::<Root>(FIXTURE, None).unwrap();
        assert_eq!(formatted.value.types.boolean, true);
        assert_eq!(formatted.value.types.integer, 1);
        assert!((formatted.value.types.float - 3.14).abs() < f64::EPSILON);
        assert_eq!(formatted.value.types.string, "hello");
        assert_eq!(formatted.value.types.array, vec![1, 2, 3]);
        assert_eq!(formatted.value.types.null, "null");
        assert_eq!(formatted.value.types.date, "1979-05-27T15:32:00.000Z");
        assert_eq!(formatted.value.types.object.key, "value");
    }

    #[test]
    fn stringify_roundtrip() {
        #[derive(serde::Deserialize, serde::Serialize)]
        struct Root {
            types: std::collections::HashMap<String, toml::Value>,
        }
        let formatted = parse_toml::<Root>(FIXTURE, None).unwrap();
        let out = stringify_toml(&formatted, None).unwrap();

        let without_comments = strip_line_comments(FIXTURE, "#");
        let expected = without_comments.trim();

        let expected_val: toml::Value = toml::from_str(expected).unwrap();
        let out_val: toml::Value = toml::from_str(out.trim()).unwrap();
        assert_eq!(out_val, expected_val);
    }

    #[test]
    fn preserves_outer_whitespace() {
        let text = " \n[section]\nkey = 1\n\n";
        #[derive(serde::Deserialize, serde::Serialize)]
        struct Sectioned {
            section: std::collections::HashMap<String, toml::Value>,
        }

        let formatted = parse_toml::<Sectioned>(text, None).unwrap();
        let out = stringify_toml(&formatted, None).unwrap();

        assert!(out.starts_with(" \n"));
        assert!(out.ends_with("\n\n"));
    }
}