use crate::error::WriteError;
pub(crate) fn child_path(path: &str, label: &str, index: usize) -> String {
let mut s = String::with_capacity(path.len() + label.len() + 8);
s.push_str(path);
push_child_path(&mut s, label, index);
s
}
pub(crate) fn push_child_path(buf: &mut String, label: &str, index: usize) {
use std::fmt::Write;
buf.push('.');
buf.push_str(label);
if index != 0 {
write!(buf, "[{index}]").expect("writing to a String never fails");
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Severity {
Warning,
Error,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Adjustment {
pub path: String,
pub code: String,
pub message: String,
pub severity: Severity,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WriteReport {
adjustments: Vec<Adjustment>,
}
impl WriteReport {
pub fn new() -> Self {
Self::default()
}
pub fn add(
&mut self,
path: impl Into<String>,
code: impl Into<String>,
message: impl Into<String>,
severity: Severity,
) {
self.adjustments.push(Adjustment {
path: path.into(),
code: code.into(),
message: message.into(),
severity,
});
}
pub fn adjustments(&self) -> &[Adjustment] {
&self.adjustments
}
pub fn warnings(&self) -> Vec<&Adjustment> {
self.adjustments
.iter()
.filter(|a| a.severity == Severity::Warning)
.collect()
}
pub fn errors(&self) -> Vec<&Adjustment> {
self.adjustments
.iter()
.filter(|a| a.severity == Severity::Error)
.collect()
}
pub fn is_ok(&self) -> bool {
self.errors().is_empty()
}
pub fn is_empty(&self) -> bool {
self.adjustments.is_empty()
}
pub fn len(&self) -> usize {
self.adjustments.len()
}
pub fn iter(&self) -> std::slice::Iter<'_, Adjustment> {
self.adjustments.iter()
}
}
impl<'a> IntoIterator for &'a WriteReport {
type Item = &'a Adjustment;
type IntoIter = std::slice::Iter<'a, Adjustment>;
fn into_iter(self) -> Self::IntoIter {
self.adjustments.iter()
}
}
impl std::fmt::Display for WriteReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.adjustments.is_empty() {
return write!(f, "no adjustments");
}
let mut first = true;
for a in &self.adjustments {
if !first {
writeln!(f)?;
}
first = false;
let sev = match a.severity {
Severity::Warning => "warning",
Severity::Error => "error",
};
write!(f, "{sev}: {}: {}", a.path, a.message)?;
}
Ok(())
}
}
pub fn finish_write(
text: String,
rep: WriteReport,
strict: bool,
report: Option<&mut WriteReport>,
) -> Result<String, WriteError> {
if let Some(out) = report {
out.adjustments.extend(rep.adjustments.iter().cloned());
}
if strict && !rep.is_empty() {
return Err(WriteError::with_report(rep.to_string(), rep));
}
Ok(text)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_report_is_empty_and_ok() {
let rep = WriteReport::new();
assert!(rep.is_empty());
assert_eq!(rep.len(), 0);
assert!(rep.is_ok());
assert_eq!(rep.to_string(), "no adjustments");
}
#[test]
fn add_records_warning_and_error_separately() {
let mut rep = WriteReport::new();
rep.add(
"$.a",
"temporal.stringified",
"written as a string",
Severity::Warning,
);
rep.add(
"$.b",
"float.special",
"NaN is not valid JSON",
Severity::Error,
);
assert_eq!(rep.len(), 2);
assert!(!rep.is_empty());
assert_eq!(rep.warnings().len(), 1);
assert_eq!(rep.errors().len(), 1);
assert!(
!rep.is_ok(),
"an error-severity entry makes the report falsy"
);
}
#[test]
fn warnings_only_report_is_still_ok() {
let mut rep = WriteReport::new();
rep.add(
"$.a",
"temporal.stringified",
"written as a string",
Severity::Warning,
);
assert!(rep.is_ok());
}
#[test]
fn display_lists_each_adjustment() {
let mut rep = WriteReport::new();
rep.add("$.a", "code.a", "message a", Severity::Warning);
rep.add("$.b", "code.b", "message b", Severity::Error);
assert_eq!(
rep.to_string(),
"warning: $.a: message a\nerror: $.b: message b"
);
}
#[test]
fn iter_and_into_iter_yield_adjustments_in_order() {
let mut rep = WriteReport::new();
rep.add("$.a", "code.a", "m", Severity::Warning);
rep.add("$.b", "code.b", "m", Severity::Warning);
let paths: Vec<&str> = rep.iter().map(|a| a.path.as_str()).collect();
assert_eq!(paths, vec!["$.a", "$.b"]);
let paths2: Vec<&str> = (&rep).into_iter().map(|a| a.path.as_str()).collect();
assert_eq!(paths2, vec!["$.a", "$.b"]);
}
#[test]
fn adjustments_accessor_matches_add_order() {
let mut rep = WriteReport::new();
rep.add("$.a", "code.a", "m", Severity::Warning);
assert_eq!(rep.adjustments().len(), 1);
assert_eq!(rep.adjustments()[0].code, "code.a");
}
#[test]
fn finish_write_lenient_returns_text_regardless_of_adjustments() {
let mut rep = WriteReport::new();
rep.add("$.a", "code.a", "m", Severity::Error);
let out = finish_write("text".to_string(), rep, false, None).unwrap();
assert_eq!(out, "text");
}
#[test]
fn finish_write_strict_raises_on_any_adjustment() {
let mut rep = WriteReport::new();
rep.add(
"$.a",
"float.special",
"NaN is not valid JSON",
Severity::Warning,
);
let err = finish_write("text".to_string(), rep, true, None).unwrap_err();
assert!(!err.to_string().contains("float.special"));
assert!(err.to_string().contains("$.a"));
assert_eq!(err.report().unwrap().len(), 1);
}
#[test]
fn finish_write_strict_with_no_adjustments_still_returns_text() {
let rep = WriteReport::new();
let out = finish_write("text".to_string(), rep, true, None).unwrap();
assert_eq!(out, "text");
}
#[test]
fn finish_write_copies_into_caller_supplied_report() {
let mut rep = WriteReport::new();
rep.add("$.a", "code.a", "m", Severity::Warning);
let mut out_report = WriteReport::new();
let out = finish_write("text".to_string(), rep, false, Some(&mut out_report)).unwrap();
assert_eq!(out, "text");
assert_eq!(out_report.len(), 1);
assert_eq!(out_report.adjustments()[0].path, "$.a");
}
#[test]
fn finish_write_strict_still_populates_caller_report_before_erroring() {
let mut rep = WriteReport::new();
rep.add("$.a", "code.a", "m", Severity::Error);
let mut out_report = WriteReport::new();
let err = finish_write("text".to_string(), rep, true, Some(&mut out_report)).unwrap_err();
assert_eq!(out_report.len(), 1);
assert_eq!(err.report().unwrap().len(), 1);
}
}