1use crate::error::WriteError;
24
25pub(crate) fn child_path(path: &str, label: &str, index: usize) -> String {
36 let mut s = String::with_capacity(path.len() + label.len() + 8);
37 s.push_str(path);
38 push_child_path(&mut s, label, index);
39 s
40}
41
42pub(crate) fn push_child_path(buf: &mut String, label: &str, index: usize) {
47 use std::fmt::Write;
48 buf.push('.');
49 buf.push_str(label);
50 if index != 0 {
51 write!(buf, "[{index}]").expect("writing to a String never fails");
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub enum Severity {
60 Warning,
62 Error,
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct Adjustment {
70 pub path: String,
72 pub code: String,
74 pub message: String,
76 pub severity: Severity,
78}
79
80#[derive(Debug, Clone, Default, PartialEq, Eq)]
84pub struct WriteReport {
85 adjustments: Vec<Adjustment>,
86}
87
88impl WriteReport {
89 pub fn new() -> Self {
91 Self::default()
92 }
93
94 pub fn add(
96 &mut self,
97 path: impl Into<String>,
98 code: impl Into<String>,
99 message: impl Into<String>,
100 severity: Severity,
101 ) {
102 self.adjustments.push(Adjustment {
103 path: path.into(),
104 code: code.into(),
105 message: message.into(),
106 severity,
107 });
108 }
109
110 pub fn adjustments(&self) -> &[Adjustment] {
112 &self.adjustments
113 }
114
115 pub fn warnings(&self) -> Vec<&Adjustment> {
117 self.adjustments
118 .iter()
119 .filter(|a| a.severity == Severity::Warning)
120 .collect()
121 }
122
123 pub fn errors(&self) -> Vec<&Adjustment> {
125 self.adjustments
126 .iter()
127 .filter(|a| a.severity == Severity::Error)
128 .collect()
129 }
130
131 pub fn is_ok(&self) -> bool {
134 self.errors().is_empty()
135 }
136
137 pub fn is_empty(&self) -> bool {
139 self.adjustments.is_empty()
140 }
141
142 pub fn len(&self) -> usize {
144 self.adjustments.len()
145 }
146
147 pub fn iter(&self) -> std::slice::Iter<'_, Adjustment> {
149 self.adjustments.iter()
150 }
151}
152
153impl<'a> IntoIterator for &'a WriteReport {
154 type Item = &'a Adjustment;
155 type IntoIter = std::slice::Iter<'a, Adjustment>;
156
157 fn into_iter(self) -> Self::IntoIter {
158 self.adjustments.iter()
159 }
160}
161
162impl std::fmt::Display for WriteReport {
163 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164 if self.adjustments.is_empty() {
165 return write!(f, "no adjustments");
166 }
167 let mut first = true;
168 for a in &self.adjustments {
169 if !first {
170 writeln!(f)?;
171 }
172 first = false;
173 let sev = match a.severity {
174 Severity::Warning => "warning",
175 Severity::Error => "error",
176 };
177 write!(f, "{sev}: {}: {}", a.path, a.message)?;
178 }
179 Ok(())
180 }
181}
182
183pub fn finish_write(
190 text: String,
191 rep: WriteReport,
192 strict: bool,
193 report: Option<&mut WriteReport>,
194) -> Result<String, WriteError> {
195 if let Some(out) = report {
196 out.adjustments.extend(rep.adjustments.iter().cloned());
197 }
198 if strict && !rep.is_empty() {
199 return Err(WriteError::with_report(rep.to_string(), rep));
200 }
201 Ok(text)
202}
203
204pub(crate) fn unsupported_value_error(path: &str, detail: impl std::fmt::Display) -> WriteError {
223 WriteError::new(format!("{path}: write.unsupported-value: {detail}"))
224}
225
226#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
231 fn new_report_is_empty_and_ok() {
232 let rep = WriteReport::new();
233 assert!(rep.is_empty());
234 assert_eq!(rep.len(), 0);
235 assert!(rep.is_ok());
236 assert_eq!(rep.to_string(), "no adjustments");
237 }
238
239 #[test]
240 fn add_records_warning_and_error_separately() {
241 let mut rep = WriteReport::new();
242 rep.add(
243 "$.a",
244 "temporal.stringified",
245 "written as a string",
246 Severity::Warning,
247 );
248 rep.add(
249 "$.b",
250 "float.special",
251 "NaN is not valid JSON",
252 Severity::Error,
253 );
254 assert_eq!(rep.len(), 2);
255 assert!(!rep.is_empty());
256 assert_eq!(rep.warnings().len(), 1);
257 assert_eq!(rep.errors().len(), 1);
258 assert!(
259 !rep.is_ok(),
260 "an error-severity entry makes the report falsy"
261 );
262 }
263
264 #[test]
265 fn warnings_only_report_is_still_ok() {
266 let mut rep = WriteReport::new();
267 rep.add(
268 "$.a",
269 "temporal.stringified",
270 "written as a string",
271 Severity::Warning,
272 );
273 assert!(rep.is_ok());
274 }
275
276 #[test]
277 fn display_lists_each_adjustment() {
278 let mut rep = WriteReport::new();
279 rep.add("$.a", "code.a", "message a", Severity::Warning);
280 rep.add("$.b", "code.b", "message b", Severity::Error);
281 assert_eq!(
282 rep.to_string(),
283 "warning: $.a: message a\nerror: $.b: message b"
284 );
285 }
286
287 #[test]
288 fn iter_and_into_iter_yield_adjustments_in_order() {
289 let mut rep = WriteReport::new();
290 rep.add("$.a", "code.a", "m", Severity::Warning);
291 rep.add("$.b", "code.b", "m", Severity::Warning);
292 let paths: Vec<&str> = rep.iter().map(|a| a.path.as_str()).collect();
293 assert_eq!(paths, vec!["$.a", "$.b"]);
294 let paths2: Vec<&str> = (&rep).into_iter().map(|a| a.path.as_str()).collect();
295 assert_eq!(paths2, vec!["$.a", "$.b"]);
296 }
297
298 #[test]
299 fn adjustments_accessor_matches_add_order() {
300 let mut rep = WriteReport::new();
301 rep.add("$.a", "code.a", "m", Severity::Warning);
302 assert_eq!(rep.adjustments().len(), 1);
303 assert_eq!(rep.adjustments()[0].code, "code.a");
304 }
305
306 #[test]
307 fn finish_write_lenient_returns_text_regardless_of_adjustments() {
308 let mut rep = WriteReport::new();
309 rep.add("$.a", "code.a", "m", Severity::Error);
310 let out = finish_write("text".to_string(), rep, false, None).unwrap();
311 assert_eq!(out, "text");
312 }
313
314 #[test]
315 fn finish_write_strict_raises_on_any_adjustment() {
316 let mut rep = WriteReport::new();
317 rep.add(
318 "$.a",
319 "float.special",
320 "NaN is not valid JSON",
321 Severity::Warning,
322 );
323 let err = finish_write("text".to_string(), rep, true, None).unwrap_err();
324 assert!(!err.to_string().contains("float.special"));
326 assert!(err.to_string().contains("$.a"));
327 assert_eq!(err.report().unwrap().len(), 1);
328 }
329
330 #[test]
331 fn finish_write_strict_with_no_adjustments_still_returns_text() {
332 let rep = WriteReport::new();
333 let out = finish_write("text".to_string(), rep, true, None).unwrap();
334 assert_eq!(out, "text");
335 }
336
337 #[test]
338 fn finish_write_copies_into_caller_supplied_report() {
339 let mut rep = WriteReport::new();
340 rep.add("$.a", "code.a", "m", Severity::Warning);
341 let mut out_report = WriteReport::new();
342 let out = finish_write("text".to_string(), rep, false, Some(&mut out_report)).unwrap();
343 assert_eq!(out, "text");
344 assert_eq!(out_report.len(), 1);
345 assert_eq!(out_report.adjustments()[0].path, "$.a");
346 }
347
348 #[test]
349 fn finish_write_strict_still_populates_caller_report_before_erroring() {
350 let mut rep = WriteReport::new();
351 rep.add("$.a", "code.a", "m", Severity::Error);
352 let mut out_report = WriteReport::new();
353 let err = finish_write("text".to_string(), rep, true, Some(&mut out_report)).unwrap_err();
354 assert_eq!(out_report.len(), 1);
355 assert_eq!(err.report().unwrap().len(), 1);
356 }
357}