c12-parser 1.1.0

Toolbox for parsing and stringifying various formats, including JSON, JSON5, JSONC, INI, TOML, and YAML.
Documentation
use jsonc_parser::{ParseOptions as JsoncParseOptions, parse_to_serde_value};
use serde_json::Value as JsonValue;

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

/// Extra options for JSONC parsing.
#[derive(Clone, Debug, Default)]
pub struct JsoncExtraOptions {
    pub disallow_comments: bool,
    pub allow_trailing_comma: bool,
}

/// Parses a JSONC string into a serde_json::Value, capturing formatting.
pub fn parse_jsonc(
    text: &str,
    fmt_options: Option<FormatOptions>,
    jsonc_options: Option<JsoncExtraOptions>,
) -> Result<Formatted<JsonValue>, Box<dyn std::error::Error>> {
    let fmt_opts = fmt_options.unwrap_or_default();
    let extra = jsonc_options.unwrap_or_default();

    let parse_opts = JsoncParseOptions {
        allow_comments: !extra.disallow_comments,
        allow_trailing_commas: extra.allow_trailing_comma,
        ..Default::default()
    };

    let value_opt = parse_to_serde_value(text, &parse_opts)?;
    let value = value_opt.unwrap_or(JsonValue::Null);
    Ok(Formatted::new(text, value, &fmt_opts))
}

/// Stringifies a JSONC value (as plain JSON) with preserved formatting.
pub fn stringify_jsonc(
    formatted: &Formatted<JsonValue>,
    options: Option<FormatOptions>,
) -> serde_json::Result<String> {
    // JSONC comments/trailing commas are not preserved; we emit plain JSON.
    crate::json::stringify_json_with_format(&formatted.value, &formatted.format, options)
}

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

    /// Sample document used by parse/stringify tests.
    const FIXTURE: &str = r#"
{
  // comment
  "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() {
        let formatted = parse_jsonc(FIXTURE, None, None).unwrap();
        let types = &formatted.value["types"];
        assert_eq!(types["boolean"], JsonValue::Bool(true));
        assert_eq!(types["integer"], JsonValue::from(1));
        assert!((types["float"].as_f64().unwrap() - 3.14).abs() < f64::EPSILON);
        assert_eq!(types["string"], JsonValue::from("hello"));
        assert_eq!(types["array"], JsonValue::from(vec![1, 2, 3]));
        assert_eq!(types["object"]["key"], JsonValue::from("value"));
        assert!(types["null"].is_null());
        assert_eq!(types["date"], JsonValue::from("1979-05-27T07:32:00-08:00"));
    }

    #[test]
    fn stringify_roundtrip() {
        let formatted = parse_jsonc(FIXTURE, None, None).unwrap();
        let out = stringify_jsonc(&formatted, None).unwrap();
        let without_comments = strip_line_comments(FIXTURE, "//");
        let expected_val: JsonValue = serde_json::from_str(&without_comments).unwrap();
        let out_val: JsonValue = serde_json::from_str(&out).unwrap();
        assert_eq!(out_val, expected_val);
    }

    #[test]
    fn respects_disallow_comments_flag() {
        let opts = JsoncExtraOptions {
            disallow_comments: true,
            allow_trailing_comma: false,
        };

        let result = parse_jsonc(FIXTURE, None, Some(opts));
        assert!(
            result.is_err(),
            "expected error when comments are disallowed"
        );
    }

    #[test]
    fn trailing_commas_controlled_by_flag() {
        const TRAILING_COMMA: &str = r#"
{
  "a": 1,
}
"#;

        // 默认不允许尾逗号,应当报错。
        let res_default = parse_jsonc(TRAILING_COMMA, None, None);
        assert!(res_default.is_err());

        // 显式允许尾逗号,应当解析成功。
        let opts = JsoncExtraOptions {
            disallow_comments: false,
            allow_trailing_comma: true,
        };
        let res_ok = parse_jsonc(TRAILING_COMMA, None, Some(opts));
        assert!(res_ok.is_ok());
    }
}