use anyhow::{Context, Result};
use serde::de::DeserializeOwned;
pub fn parse_jsonc<T: DeserializeOwned>(raw: &str, context: &str) -> Result<T> {
match jsonc_parser::parse_to_serde_value::<serde_json::Value>(raw, &Default::default()) {
Ok(value) => {
serde_json::from_value(value).with_context(|| format!("parse {} from JSONC", context))
}
Err(jsonc_err) => {
serde_json::from_str::<T>(raw)
.with_context(|| format!("parse {} as JSON/JSONC: {}", context, jsonc_err))
}
}
}
pub fn to_string_pretty<T: serde::Serialize>(value: &T) -> Result<String> {
serde_json::to_string_pretty(value).context("serialize to JSON")
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
use serde::Serialize;
#[derive(Debug, Deserialize, Serialize, PartialEq)]
struct TestConfig {
version: u32,
name: String,
}
#[test]
fn parse_jsonc_accepts_standard_json() {
let json = r#"{"version": 1, "name": "test"}"#;
let config: TestConfig = parse_jsonc(json, "test config").unwrap();
assert_eq!(config.version, 1);
assert_eq!(config.name, "test");
}
#[test]
fn parse_jsonc_accepts_single_line_comments() {
let jsonc = r#"{
// This is a comment
"version": 1,
"name": "test"
}"#;
let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
assert_eq!(config.version, 1);
assert_eq!(config.name, "test");
}
#[test]
fn parse_jsonc_accepts_multi_line_comments() {
let jsonc = r#"{
/* This is a
multi-line comment */
"version": 1,
"name": "test"
}"#;
let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
assert_eq!(config.version, 1);
assert_eq!(config.name, "test");
}
#[test]
fn parse_jsonc_accepts_trailing_commas() {
let jsonc = r#"{
"version": 1,
"name": "test",
}"#;
let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
assert_eq!(config.version, 1);
assert_eq!(config.name, "test");
}
#[test]
fn parse_jsonc_rejects_invalid_json() {
let invalid = r#"{"version": 1, "name": }"#;
let result: Result<TestConfig> = parse_jsonc(invalid, "test config");
assert!(result.is_err());
}
#[test]
fn to_string_pretty_outputs_valid_json() {
let config = TestConfig {
version: 1,
name: "test".to_string(),
};
let json = to_string_pretty(&config).unwrap();
let _: TestConfig = serde_json::from_str(&json).unwrap();
assert!(json.contains("\"version\": 1"));
assert!(json.contains("\"name\": \"test\""));
}
#[test]
fn parse_jsonc_handles_mixed_comments_and_trailing_commas() {
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct Task {
id: String,
title: String,
}
#[derive(Debug, Deserialize, PartialEq)]
struct QueueFile {
version: u32,
tasks: Vec<Task>,
}
let jsonc = r#"{
// Single line comment
"version": 1,
/* Multi-line
comment */
"tasks": [{
"id": "RQ-0001",
"title": "Test", // inline comment
},]
}"#;
let result: Result<QueueFile> = parse_jsonc(jsonc, "test queue");
assert!(
result.is_ok(),
"Should parse JSONC with mixed comments and trailing commas: {:?}",
result.err()
);
let queue = result.unwrap();
assert_eq!(queue.tasks.len(), 1);
assert_eq!(queue.tasks[0].id, "RQ-0001");
}
}