Skip to content

Commit

Permalink
feat(snap): Jsonlines support
Browse files Browse the repository at this point in the history
Looks like ndjson was merged into jsonlines, so went with that.

One difference at one point was allowing blank lines from what I've
heard.  I'm not seeing it mentioned in jsonlines but went with
supporting it.

Part of assert-rs#92

Cherry pick ae6bce4 (assert-rs#277)
Cherry pick 62ddc26 (assert-rs#290)
  • Loading branch information
epage committed May 23, 2024
1 parent 70bcbd0 commit 7b9f98b
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 5 deletions.
7 changes: 7 additions & 0 deletions crates/snapbox/src/data/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub enum DataFormat {
Text,
#[cfg(feature = "json")]
Json,
/// Streamed JSON output according to <https://jsonlines.org/>
#[cfg(feature = "json")]
JsonLines,
/// [ANSI escape codes](https://en.wikipedia.org/wiki/ANSI_escape_code#DOS_and_Windows)
/// rendered as [svg](https://docs.rs/anstyle-svg)
#[cfg(feature = "term-svg")]
Expand All @@ -25,6 +28,8 @@ impl DataFormat {
Self::Text => "txt",
#[cfg(feature = "json")]
Self::Json => "json",
#[cfg(feature = "json")]
Self::JsonLines => "jsonl",
#[cfg(feature = "term-svg")]
Self::TermSvg => "term.svg",
}
Expand All @@ -44,6 +49,8 @@ impl From<&std::path::Path> for DataFormat {
match ext {
#[cfg(feature = "json")]
"json" => DataFormat::Json,
#[cfg(feature = "json")]
"jsonl" => DataFormat::JsonLines,
#[cfg(feature = "term-svg")]
"term.svg" => Self::TermSvg,
_ => DataFormat::Text,
Expand Down
71 changes: 66 additions & 5 deletions crates/snapbox/src/data/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,9 @@ pub(crate) enum DataInner {
Text(String),
#[cfg(feature = "json")]
Json(serde_json::Value),
// Always a `Value::Array` but using `Value` for easier bookkeeping
#[cfg(feature = "json")]
JsonLines(serde_json::Value),
#[cfg(feature = "term-svg")]
TermSvg(String),
}
Expand Down Expand Up @@ -227,6 +230,11 @@ impl Data {
Self::with_inner(DataInner::Json(raw.into()))
}

#[cfg(feature = "json")]
pub fn jsonlines(raw: impl Into<Vec<serde_json::Value>>) -> Self {
Self::with_inner(DataInner::JsonLines(serde_json::Value::Array(raw.into())))
}

fn error(raw: impl Into<crate::assert::Error>, intended: DataFormat) -> Self {
Self::with_inner(DataInner::Error(DataError {
error: raw.into(),
Expand Down Expand Up @@ -290,7 +298,7 @@ impl Data {
let inferred_format = DataFormat::from(path);
match inferred_format {
#[cfg(feature = "json")]
DataFormat::Json => data.coerce_to(inferred_format),
DataFormat::Json | DataFormat::JsonLines => data.coerce_to(inferred_format),
#[cfg(feature = "term-svg")]
DataFormat::TermSvg => {
let data = data.coerce_to(DataFormat::Text);
Expand Down Expand Up @@ -334,7 +342,9 @@ impl Data {
DataInner::Binary(_) => None,
DataInner::Text(data) => Some(data.to_owned()),
#[cfg(feature = "json")]
DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()),
DataInner::Json(_) => Some(self.to_string()),
#[cfg(feature = "json")]
DataInner::JsonLines(_) => Some(self.to_string()),
#[cfg(feature = "term-svg")]
DataInner::TermSvg(data) => Some(data.to_owned()),
}
Expand All @@ -346,9 +356,9 @@ impl Data {
DataInner::Binary(data) => Ok(data.clone()),
DataInner::Text(data) => Ok(data.clone().into_bytes()),
#[cfg(feature = "json")]
DataInner::Json(value) => {
serde_json::to_vec_pretty(value).map_err(|err| format!("{err}").into())
}
DataInner::Json(_) => Ok(self.to_string().into_bytes()),
#[cfg(feature = "json")]
DataInner::JsonLines(_) => Ok(self.to_string().into_bytes()),
#[cfg(feature = "term-svg")]
DataInner::TermSvg(data) => Ok(data.clone().into_bytes()),
}
Expand All @@ -374,6 +384,8 @@ impl Data {
(DataInner::Text(inner), DataFormat::Text) => DataInner::Text(inner),
#[cfg(feature = "json")]
(DataInner::Json(inner), DataFormat::Json) => DataInner::Json(inner),
#[cfg(feature = "json")]
(DataInner::JsonLines(inner), DataFormat::JsonLines) => DataInner::JsonLines(inner),
#[cfg(feature = "term-svg")]
(DataInner::TermSvg(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner),
(DataInner::Binary(inner), _) => {
Expand All @@ -386,6 +398,11 @@ impl Data {
.map_err(|err| err.to_string())?;
DataInner::Json(inner)
}
#[cfg(feature = "json")]
(DataInner::Text(inner), DataFormat::JsonLines) => {
let inner = parse_jsonlines(&inner).map_err(|err| err.to_string())?;
DataInner::JsonLines(serde_json::Value::Array(inner))
}
#[cfg(feature = "term-svg")]
(DataInner::Text(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner),
(inner, DataFormat::Binary) => {
Expand Down Expand Up @@ -423,6 +440,8 @@ impl Data {
(DataInner::Text(inner), DataFormat::Text) => DataInner::Text(inner),
#[cfg(feature = "json")]
(DataInner::Json(inner), DataFormat::Json) => DataInner::Json(inner),
#[cfg(feature = "json")]
(DataInner::JsonLines(inner), DataFormat::JsonLines) => DataInner::JsonLines(inner),
#[cfg(feature = "term-svg")]
(DataInner::TermSvg(inner), DataFormat::TermSvg) => DataInner::TermSvg(inner),
(DataInner::Binary(inner), _) => {
Expand Down Expand Up @@ -456,6 +475,14 @@ impl Data {
DataInner::Text(inner)
}
}
#[cfg(feature = "json")]
(DataInner::Text(inner), DataFormat::JsonLines) => {
if let Ok(jsonlines) = parse_jsonlines(&inner) {
DataInner::JsonLines(serde_json::Value::Array(jsonlines))
} else {
DataInner::Text(inner)
}
}
#[cfg(feature = "term-svg")]
(DataInner::Text(inner), DataFormat::TermSvg) => {
DataInner::TermSvg(anstyle_svg::Term::new().render_svg(&inner))
Expand All @@ -480,6 +507,10 @@ impl Data {
(inner, DataFormat::Json) => inner,
// reachable if more than one structured data format is enabled
#[allow(unreachable_patterns)]
#[cfg(feature = "json")]
(inner, DataFormat::JsonLines) => inner,
// reachable if more than one structured data format is enabled
#[allow(unreachable_patterns)]
#[cfg(feature = "term-svg")]
(inner, DataFormat::TermSvg) => inner,
};
Expand All @@ -503,6 +534,8 @@ impl Data {
DataInner::Text(_) => DataFormat::Text,
#[cfg(feature = "json")]
DataInner::Json(_) => DataFormat::Json,
#[cfg(feature = "json")]
DataInner::JsonLines(_) => DataFormat::JsonLines,
#[cfg(feature = "term-svg")]
DataInner::TermSvg(_) => DataFormat::TermSvg,
}
Expand All @@ -515,6 +548,8 @@ impl Data {
DataInner::Text(_) => DataFormat::Text,
#[cfg(feature = "json")]
DataInner::Json(_) => DataFormat::Json,
#[cfg(feature = "json")]
DataInner::JsonLines(_) => DataFormat::JsonLines,
#[cfg(feature = "term-svg")]
DataInner::TermSvg(_) => DataFormat::TermSvg,
}
Expand All @@ -527,6 +562,8 @@ impl Data {
DataInner::Text(_) => None,
#[cfg(feature = "json")]
DataInner::Json(_) => None,
#[cfg(feature = "json")]
DataInner::JsonLines(_) => None,
#[cfg(feature = "term-svg")]
DataInner::TermSvg(data) => text_elem(data),
}
Expand All @@ -541,6 +578,14 @@ impl std::fmt::Display for Data {
DataInner::Text(data) => data.fmt(f),
#[cfg(feature = "json")]
DataInner::Json(data) => serde_json::to_string_pretty(data).unwrap().fmt(f),
#[cfg(feature = "json")]
DataInner::JsonLines(data) => {
let array = data.as_array().expect("jsonlines is always an array");
for value in array {
writeln!(f, "{}", serde_json::to_string(value).unwrap())?;
}
Ok(())
}
#[cfg(feature = "term-svg")]
DataInner::TermSvg(data) => data.fmt(f),
}
Expand All @@ -555,6 +600,8 @@ impl PartialEq for Data {
(DataInner::Text(left), DataInner::Text(right)) => left == right,
#[cfg(feature = "json")]
(DataInner::Json(left), DataInner::Json(right)) => left == right,
#[cfg(feature = "json")]
(DataInner::JsonLines(left), DataInner::JsonLines(right)) => left == right,
#[cfg(feature = "term-svg")]
(DataInner::TermSvg(left), DataInner::TermSvg(right)) => {
// HACK: avoid including `width` and `height` in the comparison
Expand All @@ -579,6 +626,20 @@ impl std::fmt::Display for DataError {
}
}

#[cfg(feature = "json")]
fn parse_jsonlines(text: &str) -> Result<Vec<serde_json::Value>, serde_json::Error> {
let mut lines = Vec::new();
for line in text.lines() {
let line = line.trim();
if line.is_empty() {
continue;
}
let json = serde_json::from_str::<serde_json::Value>(line)?;
lines.push(json);
}
Ok(lines)
}

#[cfg(feature = "term-svg")]
fn text_elem(svg: &str) -> Option<&str> {
let open_elem_start_idx = svg.find("<text")?;
Expand Down
20 changes: 20 additions & 0 deletions crates/snapbox/src/filter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ impl Filter for FilterNewlines {
normalize_value(&mut value, normalize_lines);
DataInner::Json(value)
}
#[cfg(feature = "json")]
DataInner::JsonLines(value) => {
let mut value = value;
normalize_value(&mut value, normalize_lines);
DataInner::JsonLines(value)
}
#[cfg(feature = "term-svg")]
DataInner::TermSvg(text) => {
let lines = normalize_lines(&text);
Expand Down Expand Up @@ -77,6 +83,12 @@ impl Filter for FilterPaths {
normalize_value(&mut value, normalize_paths);
DataInner::Json(value)
}
#[cfg(feature = "json")]
DataInner::JsonLines(value) => {
let mut value = value;
normalize_value(&mut value, normalize_paths);
DataInner::JsonLines(value)
}
#[cfg(feature = "term-svg")]
DataInner::TermSvg(text) => {
let lines = normalize_paths(&text);
Expand Down Expand Up @@ -142,6 +154,14 @@ impl Filter for FilterRedactions<'_> {
}
DataInner::Json(value)
}
#[cfg(feature = "json")]
DataInner::JsonLines(value) => {
let mut value = value;
if let DataInner::Json(exp) = &self.pattern.inner {
normalize_value_matches(&mut value, exp, self.substitutions);
}
DataInner::JsonLines(value)
}
#[cfg(feature = "term-svg")]
DataInner::TermSvg(text) => {
if let Some(pattern) = self.pattern.render() {
Expand Down

0 comments on commit 7b9f98b

Please sign in to comment.