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, compute_indent, wrap_whitespace};

/// Parses a JSON string into a value, capturing its formatting.
pub fn parse_json<T>(text: &str, options: Option<FormatOptions>) -> serde_json::Result<Formatted<T>>
where
    T: DeserializeOwned,
{
    let opts = options.unwrap_or_default();
    let value = serde_json::from_str(text)?;
    Ok(Formatted::new(text, value, &opts))
}

/// Stringifies a JSON value with preserved or configured formatting.
pub fn stringify_json<T>(
    formatted: &Formatted<T>,
    options: Option<FormatOptions>,
) -> serde_json::Result<String>
where
    T: Serialize,
{
    stringify_json_with_format(&formatted.value, &formatted.format, options)
}

/// Stringifies a JSON value with explicit format info (avoids cloning when caller has value + format).
pub(crate) fn stringify_json_with_format<T>(
    value: &T,
    format: &crate::format::FormatInfo,
    options: Option<FormatOptions>,
) -> serde_json::Result<String>
where
    T: Serialize,
{
    let opts = options.unwrap_or_default();
    let indent = compute_indent(format, &opts);
    let json = serde_json::to_string_pretty(value)?;
    let indent_str = " ".repeat(indent);

    let indented = json
        .lines()
        .map(|line| {
            if line.is_empty() {
                line.to_string()
            } else {
                let trimmed = line.trim_start();
                let mut s = String::with_capacity(indent_str.len() + trimmed.len());
                s.push_str(&indent_str);
                s.push_str(trimmed);
                s
            }
        })
        .collect::<Vec<_>>()
        .join("\n");
    Ok(wrap_whitespace(&indented, format))
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{FormatInfo, Formatted};
    use serde_json::Value as JsonValue;

    /// 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
    ],
    "object": {
      "key": "value"
    },
    "null": null,
    "date": "1979-05-27T07:32:00-08:00"
  }
}
"#;

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

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

        let formatted = parse_json::<Root>(FIXTURE, None).unwrap();
        assert!(formatted.value.types.boolean);
        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.object["key"].as_str(), Some("value"));
        assert!(formatted.value.types.null.is_none());
        assert_eq!(
            formatted.value.types.date,
            "1979-05-27T07:32:00-08:00".to_string()
        );
    }

    #[test]
    fn stringify_roundtrip() {
        let formatted = parse_json::<JsonValue>(FIXTURE, None).unwrap();
        let out = stringify_json(&formatted, None).unwrap();
        let out_val: JsonValue = serde_json::from_str(&out).unwrap();
        let expected_val: JsonValue = serde_json::from_str(FIXTURE).unwrap();
        assert_eq!(out_val, expected_val);
    }

    #[test]
    fn stringify_from_raw_preserves_structure() {
        let value: JsonValue = serde_json::from_str(FIXTURE).unwrap();
        let formatted = Formatted {
            value,
            format: FormatInfo {
                sample: None,
                whitespace_start: String::new(),
                whitespace_end: String::new(),
            },
        };
        let out = stringify_json(&formatted, None).unwrap();
        let out_val: JsonValue = serde_json::from_str(&out).unwrap();
        let expected_val: JsonValue = serde_json::from_str(FIXTURE).unwrap();
        assert_eq!(out_val, expected_val);
    }

    #[test]
    fn stringify_respects_explicit_indent() {
        let formatted = parse_json::<JsonValue>(FIXTURE, None).unwrap();
        let mut opts = FormatOptions::default();
        opts.indent = Some(4);

        let out = stringify_json(&formatted, Some(opts)).unwrap();

        // 第一行是空行(前导换行),第二行应为带 4 个空格缩进的 "{".
        let mut lines = out.lines();
        assert_eq!(lines.next(), Some(""));
        if let Some(second) = lines.next() {
            let prefix = &second[..4.min(second.len())];
            assert_eq!(prefix, "    ");
        } else {
            panic!("expected at least two lines in JSON output");
        }
    }

    #[test]
    fn preserves_outer_whitespace() {
        let text = " \n{ \"a\": 1 }\n\t";
        let formatted = parse_json::<JsonValue>(text, None).unwrap();
        let out = stringify_json(&formatted, None).unwrap();

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