nomoreide_core/
js_json.rs1use serde_json::Value;
38
39pub fn parse(raw: &str) -> Result<Value, String> {
41 serde_json::from_str(raw).map_err(|error| message(raw, &error))
42}
43
44fn message(raw: &str, error: &serde_json::Error) -> String {
45 let chars: Vec<char> = raw.chars().collect();
46 let position = position_of(&chars, error.line(), error.column());
47 let text = error.to_string();
51 if text.starts_with("EOF while parsing a string") {
52 return format!("Unterminated string in JSON {}", at(&chars, chars.len()));
55 }
56 if error.classify() == serde_json::error::Category::Eof {
57 return "Unexpected end of JSON input".to_string();
58 }
59 if text.starts_with("trailing characters") {
60 return format!(
61 "Unexpected non-whitespace character after JSON {}",
62 at(&chars, position)
63 );
64 }
65 if text.starts_with("expected `,` or `]`") {
66 return format!(
67 "Expected ',' or ']' after array element in JSON {}",
68 at(&chars, position)
69 );
70 }
71 if text.starts_with("key must be a string") {
72 return format!(
73 "Expected property name or '}}' in JSON {}",
74 at(&chars, position)
75 );
76 }
77 match chars.get(position) {
78 Some(token) => format!(
79 "Unexpected token '{token}', {} is not valid JSON",
80 snippet(&chars, position)
81 ),
82 None => "Unexpected end of JSON input".to_string(),
83 }
84}
85
86fn position_of(chars: &[char], line: usize, column: usize) -> usize {
88 let mut index = 0;
89 for _ in 1..line {
90 match chars[index..]
91 .iter()
92 .position(|character| *character == '\n')
93 {
94 Some(offset) => index += offset + 1,
95 None => return chars.len(),
96 }
97 }
98 (index + column.saturating_sub(1)).min(chars.len())
99}
100
101fn at(chars: &[char], position: usize) -> String {
104 let mut line = 1;
105 let mut column = 1;
106 for character in &chars[..position.min(chars.len())] {
107 if *character == '\n' {
108 line += 1;
109 column = 1;
110 } else {
111 column += 1;
112 }
113 }
114 format!("at position {position} (line {line} column {column})")
115}
116
117fn snippet(chars: &[char], position: usize) -> String {
124 if chars.len() <= 20 {
125 return format!("\"{}\"", chars.iter().collect::<String>());
126 }
127 let start = position.saturating_sub(10);
128 let end = (position + 10).min(chars.len());
129 let window: String = chars[start..end].iter().collect();
130 format!(
131 "{}\"{window}\"{}",
132 if start > 0 { "..." } else { "" },
133 if end < chars.len() { "..." } else { "" }
134 )
135}
136
137#[cfg(test)]
138mod tests {
139 use super::*;
140
141 fn failure(raw: &str) -> String {
144 parse(raw).expect_err(raw)
145 }
146
147 #[test]
148 fn says_what_v8_says() {
149 assert_eq!(
150 failure("oops"),
151 "Unexpected token 'o', \"oops\" is not valid JSON"
152 );
153 assert_eq!(
154 failure("[1,x]"),
155 "Unexpected token 'x', \"[1,x]\" is not valid JSON"
156 );
157 assert_eq!(
158 failure("@"),
159 "Unexpected token '@', \"@\" is not valid JSON"
160 );
161 assert_eq!(
162 failure("[1,2,]"),
163 "Unexpected token ']', \"[1,2,]\" is not valid JSON"
164 );
165 }
166
167 #[test]
168 fn a_document_that_ran_out_has_no_position() {
169 for raw in ["", " ", "[", "[1,", "nul", "tru"] {
170 assert_eq!(failure(raw), "Unexpected end of JSON input", "{raw:?}");
171 }
172 }
173
174 #[test]
175 fn content_after_a_complete_value_is_its_own_complaint() {
176 assert_eq!(
177 failure("[1] x"),
178 "Unexpected non-whitespace character after JSON at position 4 (line 1 column 5)"
179 );
180 assert_eq!(
181 failure("[1]]"),
182 "Unexpected non-whitespace character after JSON at position 3 (line 1 column 4)"
183 );
184 assert_eq!(
185 failure("[1,2]junk"),
186 "Unexpected non-whitespace character after JSON at position 5 (line 1 column 6)"
187 );
188 }
189
190 #[test]
191 fn the_named_syntax_failures() {
192 assert_eq!(
193 failure("\"unterminated"),
194 "Unterminated string in JSON at position 13 (line 1 column 14)"
195 );
196 assert_eq!(
197 failure("[\"a"),
198 "Unterminated string in JSON at position 3 (line 1 column 4)"
199 );
200 assert_eq!(
201 failure("[1,\"a\"o]"),
202 "Expected ',' or ']' after array element in JSON at position 6 (line 1 column 7)"
203 );
204 assert_eq!(
205 failure("{a:1}"),
206 "Expected property name or '}' in JSON at position 1 (line 1 column 2)"
207 );
208 }
209
210 #[test]
212 fn the_window_opens_at_twenty_one_characters() {
213 assert_eq!(
214 failure(&"x".repeat(20)),
215 format!(
216 "Unexpected token 'x', \"{}\" is not valid JSON",
217 "x".repeat(20)
218 )
219 );
220 assert_eq!(
221 failure(&"x".repeat(21)),
222 "Unexpected token 'x', \"xxxxxxxxxx\"... is not valid JSON"
223 );
224 assert_eq!(
225 failure(&format!("[{}@]", "1,".repeat(9))),
226 "Unexpected token '@', ...\"1,1,1,1,1,@]\" is not valid JSON"
227 );
228 }
229
230 #[test]
232 fn a_multi_line_document_reports_a_multi_line_snippet() {
233 assert_eq!(
234 failure("[\n1,\noops\n]"),
235 "Unexpected token 'o', \"[\n1,\noops\n]\" is not valid JSON"
236 );
237 }
238
239 #[test]
240 fn a_document_that_parses_comes_back_parsed() {
241 assert_eq!(parse("[1,2]").unwrap(), serde_json::json!([1, 2]));
242 }
243}