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(raw, &Default::default()) {
24 Ok(Some(value)) => {
25 serde_json::from_value(value).with_context(|| format!("parse {} from JSONC", context))
26 }
27 Ok(None) => {
28 serde_json::from_str::<T>(raw)
30 .with_context(|| format!("parse {} as JSON (empty file)", context))
31 }
32 Err(jsonc_err) => {
33 serde_json::from_str::<T>(raw)
35 .with_context(|| format!("parse {} as JSON/JSONC: {}", context, jsonc_err))
36 }
37 }
38}
39
40pub fn to_string_pretty<T: serde::Serialize>(value: &T) -> Result<String> {
43 serde_json::to_string_pretty(value).context("serialize to JSON")
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49 use serde::Deserialize;
50 use serde::Serialize;
51
52 #[derive(Debug, Deserialize, Serialize, PartialEq)]
53 struct TestConfig {
54 version: u32,
55 name: String,
56 }
57
58 #[test]
59 fn parse_jsonc_accepts_standard_json() {
60 let json = r#"{"version": 1, "name": "test"}"#;
61 let config: TestConfig = parse_jsonc(json, "test config").unwrap();
62 assert_eq!(config.version, 1);
63 assert_eq!(config.name, "test");
64 }
65
66 #[test]
67 fn parse_jsonc_accepts_single_line_comments() {
68 let jsonc = r#"{
69 // This is a comment
70 "version": 1,
71 "name": "test"
72 }"#;
73 let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
74 assert_eq!(config.version, 1);
75 assert_eq!(config.name, "test");
76 }
77
78 #[test]
79 fn parse_jsonc_accepts_multi_line_comments() {
80 let jsonc = r#"{
81 /* This is a
82 multi-line comment */
83 "version": 1,
84 "name": "test"
85 }"#;
86 let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
87 assert_eq!(config.version, 1);
88 assert_eq!(config.name, "test");
89 }
90
91 #[test]
92 fn parse_jsonc_accepts_trailing_commas() {
93 let jsonc = r#"{
94 "version": 1,
95 "name": "test",
96 }"#;
97 let config: TestConfig = parse_jsonc(jsonc, "test config").unwrap();
98 assert_eq!(config.version, 1);
99 assert_eq!(config.name, "test");
100 }
101
102 #[test]
103 fn parse_jsonc_rejects_invalid_json() {
104 let invalid = r#"{"version": 1, "name": }"#;
105 let result: Result<TestConfig> = parse_jsonc(invalid, "test config");
106 assert!(result.is_err());
107 }
108
109 #[test]
110 fn to_string_pretty_outputs_valid_json() {
111 let config = TestConfig {
112 version: 1,
113 name: "test".to_string(),
114 };
115 let json = to_string_pretty(&config).unwrap();
116 let _: TestConfig = serde_json::from_str(&json).unwrap();
118 assert!(json.contains("\"version\": 1"));
119 assert!(json.contains("\"name\": \"test\""));
120 }
121
122 #[test]
123 fn parse_jsonc_handles_mixed_comments_and_trailing_commas() {
124 use serde::Deserialize;
125
126 #[derive(Debug, Deserialize, PartialEq)]
127 struct Task {
128 id: String,
129 title: String,
130 }
131
132 #[derive(Debug, Deserialize, PartialEq)]
133 struct QueueFile {
134 version: u32,
135 tasks: Vec<Task>,
136 }
137
138 let jsonc = r#"{
139 // Single line comment
140 "version": 1,
141 /* Multi-line
142 comment */
143 "tasks": [{
144 "id": "RQ-0001",
145 "title": "Test", // inline comment
146 },]
147 }"#;
148
149 let result: Result<QueueFile> = parse_jsonc(jsonc, "test queue");
150 assert!(
151 result.is_ok(),
152 "Should parse JSONC with mixed comments and trailing commas: {:?}",
153 result.err()
154 );
155 let queue = result.unwrap();
156 assert_eq!(queue.tasks.len(), 1);
157 assert_eq!(queue.tasks[0].id, "RQ-0001");
158 }
159}