1use anyhow::{Context, Result};
17use serde::de::DeserializeOwned;
18
19pub fn parse_jsonc<T: DeserializeOwned>(raw: &str, context: &str) -> Result<T> {
22 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 serde_json::from_str::<T>(raw)
30 .with_context(|| format!("parse {} as JSON/JSONC: {}", context, jsonc_err))
31 }
32 }
33}
34
35pub 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 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}