From 412f9693cc6d97e02a960e8d816ee5b42649a694 Mon Sep 17 00:00:00 2001 From: Scott Schafer Date: Mon, 22 Aug 2022 16:32:52 -0500 Subject: [PATCH] feat(snapbox): Add initial JSON support --- Cargo.lock | 6 ++- crates/snapbox/Cargo.toml | 7 +++ crates/snapbox/src/data.rs | 108 +++++++++++++++++++++++++++++++------ 3 files changed, 102 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8ff13bcf..04997669 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -679,10 +679,11 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.82" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ + "indexmap", "itoa", "ryu", "serde", @@ -714,6 +715,7 @@ dependencies = [ "libtest-mimic", "normalize-line-endings", "os_pipe", + "serde_json", "similar", "snapbox-macros", "tempfile", diff --git a/crates/snapbox/Cargo.toml b/crates/snapbox/Cargo.toml index 4ece5c69..7140935d 100644 --- a/crates/snapbox/Cargo.toml +++ b/crates/snapbox/Cargo.toml @@ -49,6 +49,11 @@ path = ["tempfile", "walkdir", "dunce", "detect-encoding", "filetime"] ## Snapshotting of commands cmd = ["os_pipe", "wait-timeout"] +## Snapshotting of json +json = ["structured-data"] +## Snapshotting of structured data +structured-data = ["serde_json"] + ## Extra debugging information debug = ["snapbox-macros/debug", "backtrace"] @@ -89,3 +94,5 @@ yansi = { version = "0.5.0", optional = true } concolor = { version = "0.0.8", optional = true } document-features = { version = "0.2.3", optional = true } + +serde_json = { version = "1.0.85", optional = true, features = ["preserve_order"]} diff --git a/crates/snapbox/src/data.rs b/crates/snapbox/src/data.rs index 5add77ef..e4d5cf22 100644 --- a/crates/snapbox/src/data.rs +++ b/crates/snapbox/src/data.rs @@ -10,12 +10,16 @@ pub struct Data { enum DataInner { Binary(Vec), Text(String), + #[cfg(feature = "structured-data")] + Json(serde_json::Value), } #[derive(Clone, Debug, PartialEq, Eq, Copy, Hash)] pub enum DataFormat { Binary, Text, + #[cfg(feature = "json")] + Json, } impl Default for DataFormat { @@ -39,6 +43,13 @@ impl Data { } } + #[cfg(feature = "json")] + pub fn json(raw: impl Into) -> Self { + Self { + inner: DataInner::Json(raw.into()), + } + } + /// Empty test data pub fn new() -> Self { Self::text("") @@ -61,11 +72,26 @@ impl Data { .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; Self::text(data) } + #[cfg(feature = "json")] + DataFormat::Json => { + let data = std::fs::read_to_string(&path) + .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; + Self::json(serde_json::from_str::(&data).unwrap()) + } }, None => { let data = std::fs::read(&path) .map_err(|e| format!("Failed to read {}: {}", path.display(), e))?; - Self::binary(data).try_coerce(DataFormat::Text) + let data = Self::binary(data); + match path + .extension() + .and_then(|e| e.to_str()) + .unwrap_or_default() + { + #[cfg(feature = "json")] + "json" => data.try_coerce(DataFormat::Json), + _ => data.try_coerce(DataFormat::Text), + } } }; Ok(data) @@ -96,6 +122,8 @@ impl Data { match &self.inner { DataInner::Binary(_) => None, DataInner::Text(data) => Some(data.to_owned()), + #[cfg(feature = "json")] + DataInner::Json(value) => Some(serde_json::to_string_pretty(value).unwrap()), } } @@ -103,27 +131,38 @@ impl Data { match &self.inner { DataInner::Binary(data) => data.clone(), DataInner::Text(data) => data.clone().into_bytes(), + #[cfg(feature = "json")] + DataInner::Json(value) => serde_json::to_vec_pretty(value).unwrap(), } } pub fn try_coerce(self, format: DataFormat) -> Self { - match format { - DataFormat::Binary => Self::binary(self.to_bytes()), - DataFormat::Text => match self.inner { - DataInner::Binary(data) => { - if is_binary(&data) { - Self::binary(data) - } else { - match String::from_utf8(data) { - Ok(data) => Self::text(data), - Err(err) => { - let data = err.into_bytes(); - Self::binary(data) - } - } - } + match (self.inner, format) { + (DataInner::Binary(inner), DataFormat::Binary) => Self::binary(inner), + (DataInner::Text(inner), DataFormat::Text) => Self::text(inner), + #[cfg(feature = "json")] + (DataInner::Json(inner), DataFormat::Json) => Self::json(inner), + (DataInner::Binary(inner), _) => { + if is_binary(&inner) { + Self::binary(inner) + } else if let Ok(str) = String::from_utf8(inner.clone()) { + Self::text(str).try_coerce(format) + } else { + Self::binary(inner) + } + } + (DataInner::Text(inner), DataFormat::Binary) => Self::binary(inner.into_bytes()), + #[cfg(feature = "json")] + (DataInner::Text(inner), DataFormat::Json) => { + match serde_json::from_str::(&inner) { + Ok(json) => Self::json(json), + Err(_) => Self::text(inner), } - DataInner::Text(data) => Self::text(data), + } + #[cfg(feature = "json")] + (DataInner::Json(inner), _) => match serde_json::to_vec_pretty(&inner) { + Ok(bin) => Self::binary(bin).try_coerce(format), + Err(_) => Self::json(inner), }, } } @@ -133,6 +172,8 @@ impl Data { match &self.inner { DataInner::Binary(_) => DataFormat::Binary, DataInner::Text(_) => DataFormat::Text, + #[cfg(feature = "json")] + DataInner::Json(_) => DataFormat::Json, } } } @@ -142,6 +183,8 @@ impl std::fmt::Display for Data { match &self.inner { DataInner::Binary(data) => String::from_utf8_lossy(data).fmt(f), DataInner::Text(data) => data.fmt(f), + #[cfg(feature = "json")] + DataInner::Json(data) => std::fmt::Debug::fmt(data, f), } } } @@ -201,6 +244,12 @@ impl Normalize for NormalizeNewlines { let lines = crate::utils::normalize_lines(&text); Data::text(lines) } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + normalize_value(&mut value, crate::utils::normalize_lines); + Data::json(value) + } } } } @@ -214,6 +263,12 @@ impl Normalize for NormalizePaths { let lines = crate::utils::normalize_paths(&text); Data::text(lines) } + #[cfg(feature = "json")] + DataInner::Json(value) => { + let mut value = value; + normalize_value(&mut value, crate::utils::normalize_paths); + Data::json(value) + } } } } @@ -242,7 +297,26 @@ impl Normalize for NormalizeMatches<'_> { .normalize(&text, &self.pattern.render().unwrap()); Data::text(lines) } + #[cfg(feature = "json")] + DataInner::Json(_) => todo!("unsure of how to do matches here"), + } + } +} + +#[cfg(feature = "structured-data")] +fn normalize_value(value: &mut serde_json::Value, op: fn(&str) -> String) { + match value { + serde_json::Value::String(str) => { + *str = op(str); + } + serde_json::Value::Array(arr) => { + arr.iter_mut().for_each(|value| normalize_value(value, op)); + } + serde_json::Value::Object(obj) => { + obj.iter_mut() + .for_each(|(_, value)| normalize_value(value, op)); } + _ => {} } }