#[derive(Debug, Clone, PartialEq)]
pub enum MatchResult {
Matched(String),
NoMatch,
}
impl MatchResult {
pub fn is_match(&self) -> bool { matches!(self, MatchResult::Matched(_)) }
pub fn matched_prefix(&self) -> Option<&str> {
if let MatchResult::Matched(s) = self { Some(s) } else { None }
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum MatchMode {
CaseSensitive,
CaseInsensitive,
}
#[derive(Debug, Clone, Default)]
pub struct PrefixMatcher {
prefixes: Vec<String>,
mode: Option<MatchMode>,
trim_before_check: bool,
}
impl PrefixMatcher {
pub fn new() -> Self { Self::default() }
pub fn require(mut self, prefix: impl Into<String>) -> Self {
self.prefixes.push(prefix.into()); self
}
pub fn require_any(mut self, prefixes: &[&str]) -> Self {
self.prefixes.extend(prefixes.iter().map(|s| s.to_string())); self
}
pub fn case_insensitive(mut self) -> Self {
self.mode = Some(MatchMode::CaseInsensitive); self
}
pub fn trim(mut self) -> Self { self.trim_before_check = true; self }
pub fn check(&self, text: &str) -> MatchResult {
let candidate = if self.trim_before_check { text.trim_start() } else { text };
let is_ci = self.mode == Some(MatchMode::CaseInsensitive);
for prefix in &self.prefixes {
let matches = if is_ci {
candidate.to_lowercase().starts_with(&prefix.to_lowercase())
} else {
candidate.starts_with(prefix.as_str())
};
if matches {
return MatchResult::Matched(prefix.clone());
}
}
MatchResult::NoMatch
}
pub fn strip(&self, text: &str) -> Option<String> {
let candidate = if self.trim_before_check { text.trim_start() } else { text };
let is_ci = self.mode == Some(MatchMode::CaseInsensitive);
for prefix in &self.prefixes {
let matches = if is_ci {
candidate.to_lowercase().starts_with(&prefix.to_lowercase())
} else {
candidate.starts_with(prefix.as_str())
};
if matches {
return Some(candidate[prefix.len()..].trim_start().to_string());
}
}
None
}
pub fn is_valid(&self, text: &str) -> bool {
self.check(text).is_match()
}
pub fn prefix_count(&self) -> usize { self.prefixes.len() }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_prefix_match() {
let m = PrefixMatcher::new().require("YES");
assert_eq!(m.check("YES I agree"), MatchResult::Matched("YES".into()));
}
#[test]
fn single_prefix_no_match() {
let m = PrefixMatcher::new().require("YES");
assert_eq!(m.check("NO"), MatchResult::NoMatch);
}
#[test]
fn multiple_prefixes_first_match_wins() {
let m = PrefixMatcher::new().require("YES").require("NO");
assert_eq!(m.check("YES: sure"), MatchResult::Matched("YES".into()));
}
#[test]
fn require_any() {
let m = PrefixMatcher::new().require_any(&["YES", "NO", "MAYBE"]);
assert!(m.check("MAYBE later").is_match());
}
#[test]
fn case_insensitive_match() {
let m = PrefixMatcher::new().require("yes").case_insensitive();
assert!(m.check("YES I agree").is_match());
}
#[test]
fn case_sensitive_no_match() {
let m = PrefixMatcher::new().require("yes");
assert!(!m.check("YES").is_match());
}
#[test]
fn trim_leading_whitespace() {
let m = PrefixMatcher::new().require("OK").trim();
assert!(m.check(" OK great").is_match());
}
#[test]
fn strip_prefix_returns_remainder() {
let m = PrefixMatcher::new().require("YES:");
let rest = m.strip("YES: I agree");
assert_eq!(rest.as_deref(), Some("I agree"));
}
#[test]
fn strip_no_match_returns_none() {
let m = PrefixMatcher::new().require("YES");
assert!(m.strip("NO").is_none());
}
#[test]
fn is_valid() {
let m = PrefixMatcher::new().require("OK");
assert!(m.is_valid("OK then"));
assert!(!m.is_valid("bad"));
}
#[test]
fn prefix_count() {
let m = PrefixMatcher::new().require("A").require("B").require("C");
assert_eq!(m.prefix_count(), 3);
}
#[test]
fn empty_prefix_always_matches() {
let m = PrefixMatcher::new().require("");
assert!(m.check("anything").is_match());
}
#[test]
fn matched_prefix_accessor() {
let m = PrefixMatcher::new().require("YES");
let res = m.check("YES!");
assert_eq!(res.matched_prefix(), Some("YES"));
}
}