1use crate::error::WriteError;
24
25pub(crate) fn child_path(path: &str, label: &str, index: Option<usize>) -> String {
38 let mut s = String::with_capacity(path.len() + label.len() + 8);
39 s.push_str(path);
40 push_child_path(&mut s, label, index);
41 s
42}
43
44pub(crate) fn push_child_path(buf: &mut String, label: &str, index: Option<usize>) {
49 use std::fmt::Write;
50 push_label_segment(buf, label);
51 if let Some(index) = index {
52 write!(buf, "[{index}]").expect("writing to a String never fails");
53 }
54}
55
56fn is_unsafe_path_char(c: char) -> bool {
60 matches!(c, '.' | '[' | ']' | '"' | '\\')
61 || c <= '\u{1f}'
62 || matches!(c, '\u{7f}' | '\u{85}' | '\u{2028}' | '\u{2029}')
63}
64
65pub(crate) fn push_label_segment(buf: &mut String, label: &str) {
70 use std::fmt::Write;
71 if !label.is_empty() && !label.chars().any(is_unsafe_path_char) {
72 buf.push('.');
73 buf.push_str(label);
74 return;
75 }
76 buf.push_str("[\"");
77 for c in label.chars() {
78 match c {
79 '"' => buf.push_str("\\\""),
80 '\\' => buf.push_str("\\\\"),
81 '\n' => buf.push_str("\\n"),
82 '\r' => buf.push_str("\\r"),
83 '\t' => buf.push_str("\\t"),
84 c if is_unsafe_path_char(c) && !matches!(c, '.' | '[' | ']') => {
85 write!(buf, "\\u{:04x}", c as u32).expect("writing to a String never fails");
86 }
87 c => buf.push(c),
88 }
89 }
90 buf.push_str("\"]");
91}
92
93pub(crate) fn occurrence_index(i: usize, count: usize) -> Option<usize> {
98 (count > 1).then_some(i)
99}
100
101#[derive(Debug, Clone, Copy, PartialEq, Eq)]
105pub enum Severity {
106 Warning,
108 Error,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
115pub struct Adjustment {
116 pub path: String,
118 pub code: String,
120 pub message: String,
122 pub severity: Severity,
124}
125
126#[derive(Debug, Clone, Default, PartialEq, Eq)]
130pub struct WriteReport {
131 adjustments: Vec<Adjustment>,
132}
133
134impl WriteReport {
135 pub fn new() -> Self {
137 Self::default()
138 }
139
140 pub fn add(
142 &mut self,
143 path: impl Into<String>,
144 code: impl Into<String>,
145 message: impl Into<String>,
146 severity: Severity,
147 ) {
148 self.adjustments.push(Adjustment {
149 path: path.into(),
150 code: code.into(),
151 message: message.into(),
152 severity,
153 });
154 }
155
156 pub(crate) fn reindex_prefix(&mut self, range: std::ops::Range<usize>, from: &str, to: &str) {
163 for a in &mut self.adjustments[range] {
164 if let Some(rest) = a.path.strip_prefix(from)
165 && (rest.is_empty() || rest.starts_with(['.', '[']))
166 {
167 a.path = format!("{to}{rest}");
168 }
169 }
170 }
171
172 pub fn adjustments(&self) -> &[Adjustment] {
174 &self.adjustments
175 }
176
177 pub fn warnings(&self) -> Vec<&Adjustment> {
179 self.adjustments
180 .iter()
181 .filter(|a| a.severity == Severity::Warning)
182 .collect()
183 }
184
185 pub fn errors(&self) -> Vec<&Adjustment> {
187 self.adjustments
188 .iter()
189 .filter(|a| a.severity == Severity::Error)
190 .collect()
191 }
192
193 pub fn is_ok(&self) -> bool {
196 self.errors().is_empty()
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.adjustments.is_empty()
202 }
203
204 pub fn len(&self) -> usize {
206 self.adjustments.len()
207 }
208
209 pub fn iter(&self) -> std::slice::Iter<'_, Adjustment> {
211 self.adjustments.iter()
212 }
213}
214
215impl<'a> IntoIterator for &'a WriteReport {
216 type Item = &'a Adjustment;
217 type IntoIter = std::slice::Iter<'a, Adjustment>;
218
219 fn into_iter(self) -> Self::IntoIter {
220 self.adjustments.iter()
221 }
222}
223
224impl std::fmt::Display for WriteReport {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 if self.adjustments.is_empty() {
227 return write!(f, "no adjustments");
228 }
229 let mut first = true;
230 for a in &self.adjustments {
231 if !first {
232 writeln!(f)?;
233 }
234 first = false;
235 let sev = match a.severity {
236 Severity::Warning => "warning",
237 Severity::Error => "error",
238 };
239 write!(f, "{sev}: {}: {}", a.path, a.message)?;
240 }
241 Ok(())
242 }
243}
244
245pub fn finish_write(
252 text: String,
253 rep: WriteReport,
254 strict: bool,
255 report: Option<&mut WriteReport>,
256) -> Result<String, WriteError> {
257 if let Some(out) = report {
258 out.adjustments.extend(rep.adjustments.iter().cloned());
259 }
260 if strict && !rep.is_empty() {
261 return Err(WriteError::with_report(rep.to_string(), rep));
262 }
263 Ok(text)
264}
265
266pub(crate) fn unsupported_value_error(path: &str, detail: impl std::fmt::Display) -> WriteError {
284 WriteError::with_diagnostic(
285 path,
286 "write.unsupported-value",
287 format!("{path}: write.unsupported-value: {detail}"),
288 )
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294
295 #[test]
296 fn new_report_is_empty_and_ok() {
297 let rep = WriteReport::new();
298 assert!(rep.is_empty());
299 assert_eq!(rep.len(), 0);
300 assert!(rep.is_ok());
301 assert_eq!(rep.to_string(), "no adjustments");
302 }
303
304 #[test]
305 fn add_records_warning_and_error_separately() {
306 let mut rep = WriteReport::new();
307 rep.add(
308 "$.a",
309 "temporal.stringified",
310 "written as a string",
311 Severity::Warning,
312 );
313 rep.add(
314 "$.b",
315 "float.special",
316 "NaN is not valid JSON",
317 Severity::Error,
318 );
319 assert_eq!(rep.len(), 2);
320 assert!(!rep.is_empty());
321 assert_eq!(rep.warnings().len(), 1);
322 assert_eq!(rep.errors().len(), 1);
323 assert!(
324 !rep.is_ok(),
325 "an error-severity entry makes the report falsy"
326 );
327 }
328
329 #[test]
330 fn warnings_only_report_is_still_ok() {
331 let mut rep = WriteReport::new();
332 rep.add(
333 "$.a",
334 "temporal.stringified",
335 "written as a string",
336 Severity::Warning,
337 );
338 assert!(rep.is_ok());
339 }
340
341 #[test]
342 fn display_lists_each_adjustment() {
343 let mut rep = WriteReport::new();
344 rep.add("$.a", "code.a", "message a", Severity::Warning);
345 rep.add("$.b", "code.b", "message b", Severity::Error);
346 assert_eq!(
347 rep.to_string(),
348 "warning: $.a: message a\nerror: $.b: message b"
349 );
350 }
351
352 #[test]
353 fn iter_and_into_iter_yield_adjustments_in_order() {
354 let mut rep = WriteReport::new();
355 rep.add("$.a", "code.a", "m", Severity::Warning);
356 rep.add("$.b", "code.b", "m", Severity::Warning);
357 let paths: Vec<&str> = rep.iter().map(|a| a.path.as_str()).collect();
358 assert_eq!(paths, vec!["$.a", "$.b"]);
359 let paths2: Vec<&str> = (&rep).into_iter().map(|a| a.path.as_str()).collect();
360 assert_eq!(paths2, vec!["$.a", "$.b"]);
361 }
362
363 #[test]
364 fn adjustments_accessor_matches_add_order() {
365 let mut rep = WriteReport::new();
366 rep.add("$.a", "code.a", "m", Severity::Warning);
367 assert_eq!(rep.adjustments().len(), 1);
368 assert_eq!(rep.adjustments()[0].code, "code.a");
369 }
370
371 #[test]
372 fn finish_write_lenient_returns_text_regardless_of_adjustments() {
373 let mut rep = WriteReport::new();
374 rep.add("$.a", "code.a", "m", Severity::Error);
375 let out = finish_write("text".to_string(), rep, false, None).unwrap();
376 assert_eq!(out, "text");
377 }
378
379 #[test]
380 fn finish_write_strict_raises_on_any_adjustment() {
381 let mut rep = WriteReport::new();
382 rep.add(
383 "$.a",
384 "float.special",
385 "NaN is not valid JSON",
386 Severity::Warning,
387 );
388 let err = finish_write("text".to_string(), rep, true, None).unwrap_err();
389 assert!(!err.to_string().contains("float.special"));
391 assert!(err.to_string().contains("$.a"));
392 assert_eq!(err.report().unwrap().len(), 1);
393 }
394
395 #[test]
396 fn finish_write_strict_with_no_adjustments_still_returns_text() {
397 let rep = WriteReport::new();
398 let out = finish_write("text".to_string(), rep, true, None).unwrap();
399 assert_eq!(out, "text");
400 }
401
402 #[test]
403 fn finish_write_copies_into_caller_supplied_report() {
404 let mut rep = WriteReport::new();
405 rep.add("$.a", "code.a", "m", Severity::Warning);
406 let mut out_report = WriteReport::new();
407 let out = finish_write("text".to_string(), rep, false, Some(&mut out_report)).unwrap();
408 assert_eq!(out, "text");
409 assert_eq!(out_report.len(), 1);
410 assert_eq!(out_report.adjustments()[0].path, "$.a");
411 }
412
413 #[test]
414 fn finish_write_strict_still_populates_caller_report_before_erroring() {
415 let mut rep = WriteReport::new();
416 rep.add("$.a", "code.a", "m", Severity::Error);
417 let mut out_report = WriteReport::new();
418 let err = finish_write("text".to_string(), rep, true, Some(&mut out_report)).unwrap_err();
419 assert_eq!(out_report.len(), 1);
420 assert_eq!(err.report().unwrap().len(), 1);
421 }
422}
423
424#[cfg(test)]
425mod label_segment_tests {
426 use super::*;
427
428 fn seg(label: &str) -> String {
429 let mut s = String::new();
430 push_label_segment(&mut s, label);
431 s
432 }
433
434 #[test]
435 fn a_label_that_needs_no_quoting_is_bare() {
436 for l in ["name", "a b", "$", "é", "0", "1bad", "日本"] {
437 assert_eq!(seg(l), format!(".{l}"));
438 }
439 }
440
441 #[test]
442 fn a_label_that_needs_quoting_is_bracket_quoted_with_the_pinned_escapes() {
443 for (label, want) in [
444 ("", r#"[""]"#),
445 ("a.b", r#"["a.b"]"#),
446 ("item[0]", r#"["item[0]"]"#),
447 ("a]b", r#"["a]b"]"#),
448 ("q\"x", r#"["q\"x"]"#),
449 ("a\\b", r#"["a\\b"]"#),
450 ("l1\nl2", r#"["l1\nl2"]"#),
451 ("a\tb\rc", r#"["a\tb\rc"]"#),
452 ("a\u{1f}b", r#"["a\u001fb"]"#),
453 ("a\0b", r#"["a\u0000b"]"#),
454 ("a\u{7f}b", r#"["a\u007fb"]"#),
455 ("a\u{85}b", r#"["a\u0085b"]"#),
456 ("a\u{2028}b", "[\"a\\u2028b\"]"),
457 ("a\u{2029}b", "[\"a\\u2029b\"]"),
458 ("é.ü", r#"["é.ü"]"#),
459 ] {
460 assert_eq!(seg(label), want, "label {label:?}");
461 }
462 }
463
464 #[test]
465 fn the_occurrence_index_follows_either_form() {
466 assert_eq!(child_path("$", "a.b", Some(1)), r#"$["a.b"][1]"#);
467 assert_eq!(child_path("$.a", "b", None), "$.a.b");
468 assert_eq!(child_path("$", "item", Some(0)), "$.item[0]");
469 assert_eq!(child_path("$", "a", None), "$.a");
470 assert_eq!(
471 crate::document::join(&child_path("$", "x.y", None), "z"),
472 r#"$["x.y"].z"#
473 );
474 }
475}