From 3570cc365f03f99520f8150c9f56da2f714756a0 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Fri, 19 Apr 2024 15:50:52 -0500 Subject: [PATCH] feat(filter): Allow regexes for substitutions Cherry pick b5220cd90edbb654f958b74c86a8b94c59c3c90b (#281) Cherry pick 38c4fb8887414b53d2bc462e6e754d26289a9a14 (#285) --- Cargo.lock | 1 + crates/snapbox/Cargo.toml | 3 + crates/snapbox/src/filter/redactions.rs | 93 ++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index b4554403..6f750544 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -808,6 +808,7 @@ dependencies = [ "libc", "normalize-line-endings", "os_pipe", + "regex", "serde", "serde_json", "similar", diff --git a/crates/snapbox/Cargo.toml b/crates/snapbox/Cargo.toml index 0e4e8752..ea231fc1 100644 --- a/crates/snapbox/Cargo.toml +++ b/crates/snapbox/Cargo.toml @@ -42,6 +42,8 @@ path = ["dir"] cmd = ["dep:os_pipe", "dep:wait-timeout", "dep:libc", "dep:windows-sys"] ## Building of examples for snapshotting examples = ["dep:escargot"] +## Regex text substitutions +regex = ["dep:regex"] ## Snapshotting of json json = ["structured-data", "dep:serde_json", "dep:serde"] @@ -92,6 +94,7 @@ document-features = { version = "0.2.6", optional = true } serde_json = { version = "1.0.85", optional = true} anstyle-svg = { version = "0.1.3", optional = true } serde = { version = "1.0.198", optional = true } +regex = { version = "1.10.4", optional = true, default-features = false, features = ["std"] } [target.'cfg(windows)'.dependencies] windows-sys = { version = "0.52.0", features = ["Win32_Foundation"], optional = true } diff --git a/crates/snapbox/src/filter/redactions.rs b/crates/snapbox/src/filter/redactions.rs index c4a3e3c2..5dda69d5 100644 --- a/crates/snapbox/src/filter/redactions.rs +++ b/crates/snapbox/src/filter/redactions.rs @@ -32,6 +32,17 @@ impl Redactions { /// let mut subst = snapbox::Redactions::new(); /// subst.insert("[EXE]", std::env::consts::EXE_SUFFIX); /// ``` + /// + /// With the `regex` feature, you can define patterns using regexes. + /// You can choose to replace a subset of the regex by giving it the named capture group + /// `redacted`. + /// + /// ```rust + /// # #[cfg(feature = "regex")] { + /// let mut subst = snapbox::Redactions::new(); + /// subst.insert("[OBJECT]", regex::Regex::new("(?(world|moon))").unwrap()); + /// # } + /// ``` pub fn insert( &mut self, placeholder: &'static str, @@ -108,10 +119,12 @@ pub struct RedactedValue { inner: Option, } -#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)] +#[derive(Clone, Debug)] enum RedactedValueInner { Str(&'static str), String(String), + #[cfg(feature = "regex")] + Regex(regex::Regex), } impl RedactedValueInner { @@ -119,6 +132,21 @@ impl RedactedValueInner { match self { Self::Str(s) => buffer.find(s).map(|offset| offset..(offset + s.len())), Self::String(s) => buffer.find(s).map(|offset| offset..(offset + s.len())), + #[cfg(feature = "regex")] + Self::Regex(r) => { + let captures = r.captures(buffer)?; + let m = captures.name("redacted").or_else(|| captures.get(0))?; + Some(m.range()) + } + } + } + + fn as_cmp(&self) -> &str { + match self { + Self::Str(s) => s, + Self::String(s) => s, + #[cfg(feature = "regex")] + Self::Regex(s) => s.as_str(), } } } @@ -166,6 +194,42 @@ impl From> for RedactedValue { } } +#[cfg(feature = "regex")] +impl From for RedactedValue { + fn from(inner: regex::Regex) -> Self { + Self { + inner: Some(RedactedValueInner::Regex(inner)), + } + } +} + +#[cfg(feature = "regex")] +impl From<&'_ regex::Regex> for RedactedValue { + fn from(inner: &'_ regex::Regex) -> Self { + inner.clone().into() + } +} + +impl PartialOrd for RedactedValueInner { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.as_cmp().cmp(other.as_cmp())) + } +} + +impl Ord for RedactedValueInner { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.as_cmp().cmp(other.as_cmp()) + } +} + +impl PartialEq for RedactedValueInner { + fn eq(&self, other: &Self) -> bool { + self.as_cmp().eq(other.as_cmp()) + } +} + +impl Eq for RedactedValueInner {} + /// Replacements is `(from, to)` fn replace_many<'a>( buffer: &mut String, @@ -478,4 +542,31 @@ mod test { let actual = normalize(input, pattern, &sub); assert_eq!(actual, pattern); } + + #[test] + #[cfg(feature = "regex")] + fn substitute_regex_unnamed() { + let input = "Hello world!"; + let pattern = "Hello [OBJECT]!"; + let mut sub = Redactions::new(); + sub.insert("[OBJECT]", regex::Regex::new("world").unwrap()) + .unwrap(); + let actual = normalize(input, pattern, &sub); + assert_eq!(actual, pattern); + } + + #[test] + #[cfg(feature = "regex")] + fn substitute_regex_named() { + let input = "Hello world!"; + let pattern = "Hello [OBJECT]!"; + let mut sub = Redactions::new(); + sub.insert( + "[OBJECT]", + regex::Regex::new("(?world)!").unwrap(), + ) + .unwrap(); + let actual = normalize(input, pattern, &sub); + assert_eq!(actual, pattern); + } }