use std::str::Chars;
pub(crate) fn matches(pattern: &str, text: &str) -> bool {
let mut pattern_rest = pattern.chars();
let mut text_rest = text.chars();
let mut widest_star: Option<(Chars<'_>, Chars<'_>)> = None;
loop {
let mut pattern_next = pattern_rest.clone();
match pattern_next.next() {
Some('*') => {
pattern_rest = pattern_next;
widest_star = Some((pattern_rest.clone(), text_rest.clone()));
continue;
}
Some(expected) => {
let mut text_next = text_rest.clone();
if let Some(actual) = text_next.next()
&& (expected == '?' || expected == actual)
{
pattern_rest = pattern_next;
text_rest = text_next;
continue;
}
}
None => {
if text_rest.clone().next().is_none() {
return true;
}
}
}
let Some((star_pattern, star_text)) = widest_star.as_mut() else {
return false;
};
if star_text.next().is_none() {
return false;
}
pattern_rest = star_pattern.clone();
text_rest = star_text.clone();
}
}
#[cfg(test)]
mod tests {
use super::matches;
const PREVIEW: &str =
r#"{"body":"cargo test","cwd":"/Users/dev/basis","mode":"command","target":"mac"}"#;
#[test]
fn a_star_crosses_a_path_separator() {
assert!(matches(r#"*"target":"mac"*"#, PREVIEW));
assert!(matches(r#"*"mode":"command"*"#, PREVIEW));
}
#[test]
fn two_stars_mean_what_one_star_means() {
assert!(matches(r#"**"target":"mac"**"#, PREVIEW));
assert!(matches("**", PREVIEW));
assert!(matches("***", PREVIEW));
}
#[test]
fn json_punctuation_is_literal() {
assert!(matches(r#"{"body":"cargo test"*"#, PREVIEW));
assert!(matches(r#"*"cwd":"/Users/dev/basis"*"#, PREVIEW));
assert!(!matches("{a,b}", "a"));
assert!(matches("{a,b}", "{a,b}"));
assert!(!matches("[abc]", "a"));
assert!(matches("[abc]", "[abc]"));
}
#[test]
fn a_question_mark_matches_exactly_one_character() {
assert!(matches("a?c", "abc"));
assert!(matches("a?c", "a/c"));
assert!(!matches("a?c", "ac"));
assert!(!matches("a?c", "abbc"));
}
#[test]
fn a_question_mark_counts_characters_not_bytes() {
assert!(matches("a?c", "aéc"));
assert!(matches("?", "é"));
assert!(!matches("??", "é"));
}
#[test]
fn matching_is_anchored() {
assert!(matches("cargo test", "cargo test"));
assert!(!matches("cargo", "cargo test"));
assert!(!matches("test", "cargo test"));
assert!(matches("cargo*", "cargo test"));
assert!(matches("*test", "cargo test"));
}
#[test]
fn empty_pattern_matches_only_empty_text() {
assert!(matches("", ""));
assert!(!matches("", "a"));
assert!(matches("*", ""));
}
#[test]
fn a_star_backtracks_until_the_rest_of_the_pattern_fits() {
assert!(matches("*a*b", "xaybzb"));
assert!(matches("*ab*cd*", "zzabzzcdzz"));
assert!(!matches("*a*b", "xaybz"));
}
#[test]
fn a_pattern_that_names_something_absent_does_not_match() {
assert!(!matches(r#"**"target":"linux"**"#, PREVIEW));
assert!(!matches(r#"**"mode":"shell"**"#, PREVIEW));
}
}