Skip to content

Commit

Permalink
feat(sub): Allow regexes for substitutions
Browse files Browse the repository at this point in the history
  • Loading branch information
epage committed Apr 20, 2024
1 parent 57fd660 commit a5b95b6
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 15 deletions.
38 changes: 30 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions crates/snapbox/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ path = ["dep:tempfile", "dep:walkdir", "dep:dunce", "detect-encoding", "dep:file
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"]
Expand Down Expand Up @@ -94,6 +96,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 }
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 }
Expand Down
97 changes: 90 additions & 7 deletions crates/snapbox/src/substitutions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ impl Substitutions {
/// let mut subst = snapbox::Substitutions::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
/// `replace`.
///
/// ```rust
/// # #[cfg(feature = "regex")] {
/// let mut subst = snapbox::Substitutions::new();
/// subst.insert("[OBJECT]", regex::Regex::new("(?<replace>(world|moon))").unwrap());
/// # }
/// ```
pub fn insert(
&mut self,
key: &'static str,
Expand Down Expand Up @@ -98,12 +109,7 @@ impl Substitutions {
fn clear<'v>(&self, pattern: &'v str) -> Cow<'v, str> {
if !self.unused.is_empty() && pattern.contains('[') {
let mut pattern = pattern.to_owned();
replace_many(
&mut pattern,
self.unused
.iter()
.map(|var| (var, "")),
);
replace_many(&mut pattern, self.unused.iter().map(|var| (var, "")));
Cow::Owned(pattern)
} else {
Cow::Borrowed(pattern)
Expand All @@ -116,17 +122,34 @@ pub struct SubstitutionValue {
inner: Option<SubstitutionValueInner>,
}

#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq)]
#[derive(Clone, Debug)]
enum SubstitutionValueInner {
Str(&'static str),
String(String),
#[cfg(feature = "regex")]
Regex(regex::Regex),
}

impl SubstitutionValueInner {
fn find_in(&self, buffer: &str) -> Option<std::ops::Range<usize>> {
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("replace").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(),
}
}
}
Expand Down Expand Up @@ -161,6 +184,42 @@ impl From<&'_ String> for SubstitutionValue {
}
}

#[cfg(feature = "regex")]
impl From<regex::Regex> for SubstitutionValue {
fn from(inner: regex::Regex) -> Self {
Self {
inner: Some(SubstitutionValueInner::Regex(inner)),
}
}
}

#[cfg(feature = "regex")]
impl From<&'_ regex::Regex> for SubstitutionValue {
fn from(inner: &'_ regex::Regex) -> Self {
inner.clone().into()
}
}

impl PartialOrd for SubstitutionValueInner {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.as_cmp().partial_cmp(other.as_cmp())
}
}

impl Ord for SubstitutionValueInner {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.as_cmp().cmp(other.as_cmp())
}
}

impl PartialEq for SubstitutionValueInner {
fn eq(&self, other: &Self) -> bool {
self.as_cmp().eq(other.as_cmp())
}
}

impl Eq for SubstitutionValueInner {}

/// Replacements is `(from, to)`
fn replace_many<'a>(
buffer: &mut String,
Expand Down Expand Up @@ -472,4 +531,28 @@ 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 = Substitutions::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 = Substitutions::new();
sub.insert("[OBJECT]", regex::Regex::new("(?<replace>world)!").unwrap())
.unwrap();
let actual = normalize(input, pattern, &sub);
assert_eq!(actual, pattern);
}
}

0 comments on commit a5b95b6

Please sign in to comment.