Skip to main content

ralph/
jsonc.rs

1//! JSONC parsing utilities for Ralph.
2//!
3//! Responsibilities:
4//! - Provide JSONC (JSON with Comments) parsing with comment support.
5//! - Maintain backward compatibility with standard JSON.
6//! - Integrate with existing JSON repair logic for malformed files.
7//!
8//! Not handled here:
9//! - File I/O (callers read/write file contents).
10//! - Round-tripping comments (comments are stripped on rewrite).
11//!
12//! Invariants/assumptions:
13//! - Input is valid UTF-8.
14//! - jsonc-parser is used for parsing; serde_json for serialization.
15
16use anyhow::{Context, Result};
17use serde::de::DeserializeOwned;
18
19/// Parse JSONC (JSON with Comments) into a typed struct.
20/// Falls back to standard JSON parsing for backward compatibility.
21pub fn parse_jsonc<T: DeserializeOwned>(raw: &str, context: &str) -> Result<T> {
22    // Try JSONC parsing first (handles comments and trailing commas)
23    match jsonc_parser::parse_to_serde_value::<serde_json::Value>(raw, &Default::default()) {
24        Ok(value) => {
25            serde_json::from_value(value).with_context(|| format!("parse {} from JSONC", context))
26        }
27        Err(jsonc_err) => {
28            // Fall back to standard JSON for backward compatibility
29            serde_json::from_str::<T>(raw)
30                .with_context(|| format!("parse {} as JSON/JSONC: {}", context, jsonc_err))
31        }
32    }
33}
34
35/// Serialize to pretty-printed JSON (no comments preserved).
36/// Output is always standard JSON format.
37pub fn to_string_pretty<T: serde::Serialize>(value: &T) -> Result<String> {
38    serde_json::to_string_pretty(value).context("serialize to JSON")
39}
40
41#[cfg(test)]
42mod tests {
43    use super::*;
44    use serde::Deserialize;
45    use serde::Serialize;
46
47    #[derive(Debug, Deserialize, Serialize, PartialEq)]
48    struct TestConfig {
49        version: u32,
50        name: String,
51    }
52
53    #[test]
54    fn parse_jsonc_accepts_standard_json() {
55        let json = r#"{"version": 1, "name": "test"}"#;
56        let config: TestConfig = parse_jsonc(json, "test config").unwrap();
57        assert_eq!(config.version, 1);
58        assert_eq!(config.name, "test");
59    }
60
61    #[test]
62    fn parse_jsonc_accepts_single_line_comments() {
63        let jsonc = r#"{
64            // This is a comment
65            "version": 1,
66            "name": "test"
67        }"#;
68        let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
69        assert_eq!(config.version, 1);
70        assert_eq!(config.name, "test");
71    }
72
73    #[test]
74    fn parse_jsonc_accepts_multi_line_comments() {
75        let jsonc = r#"{
76            /* This is a
77               multi-line comment */
78            "version": 1,
79            "name": "test"
80        }"#;
81        let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
82        assert_eq!(config.version, 1);
83        assert_eq!(config.name, "test");
84    }
85
86    #[test]
87    fn parse_jsonc_accepts_trailing_commas() {
88        let jsonc = r#"{
89            "version": 1,
90            "name": "test",
91        }"#;
92        let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
93        assert_eq!(config.version, 1);
94        assert_eq!(config.name, "test");
95    }
96
97    #[test]
98    fn parse_jsonc_rejects_invalid_json() {
99        let invalid = r#"{"version": 1, "name": }"#;
100        let result: Result<TestConfig> = parse_jsonc(invalid, "test config");
101        assert!(result.is_err());
102    }
103
104    #[test]
105    fn to_string_pretty_outputs_valid_json() {
106        let config = TestConfig {
107            version: 1,
108            name: "test".to_string(),
109        };
110        let json = to_string_pretty(&config).unwrap();
111        // Verify it's valid JSON by parsing it back
112        let _: TestConfig = serde_json::from_str(&json).unwrap();
113        assert!(json.contains("\"version\": 1"));
114        assert!(json.contains("\"name\": \"test\""));
115    }
116
117    #[test]
118    fn parse_jsonc_handles_mixed_comments_and_trailing_commas() {
119        use serde::Deserialize;
120
121        #[derive(Debug, Deserialize, PartialEq)]
122        struct Task {
123            id: String,
124            title: String,
125        }
126
127        #[derive(Debug, Deserialize, PartialEq)]
128        struct QueueFile {
129            version: u32,
130            tasks: Vec<Task>,
131        }
132
133        let jsonc = r#"{
134            // Single line comment
135            "version": 1,
136            /* Multi-line
137               comment */
138            "tasks": [{
139                "id": "RQ-0001",
140                "title": "Test", // inline comment
141            },]
142        }"#;
143
144        let result: Result<QueueFile> = parse_jsonc(jsonc, "test queue");
145        assert!(
146            result.is_ok(),
147            "Should parse JSONC with mixed comments and trailing commas: {:?}",
148            result.err()
149        );
150        let queue = result.unwrap();
151        assert_eq!(queue.tasks.len(), 1);
152        assert_eq!(queue.tasks[0].id, "RQ-0001");
153    }
154}