#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ToolMatcherError {
Empty,
BlankPattern,
UnsupportedGlob { pattern: String },
UnknownTool { pattern: String },
}
#[derive(Clone, Debug, PartialEq, Eq)]
enum Pattern {
Exact(String),
Prefix(String),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToolMatcher {
patterns: Option<Vec<Pattern>>,
}
impl ToolMatcher {
pub fn any() -> Self {
Self { patterns: None }
}
pub fn new(patterns: Vec<String>, canonical: &[&str]) -> Result<Self, ToolMatcherError> {
if patterns.is_empty() {
return Err(ToolMatcherError::Empty);
}
let mut parsed = Vec::with_capacity(patterns.len());
for pattern in patterns {
parsed.push(parse_pattern(&pattern, canonical)?);
}
Ok(Self {
patterns: Some(parsed),
})
}
pub fn matches(&self, tool: &str) -> bool {
let Some(patterns) = &self.patterns else {
return true;
};
patterns.iter().any(|pattern| match pattern {
Pattern::Exact(name) => name == tool,
Pattern::Prefix(prefix) => tool.starts_with(prefix.as_str()),
})
}
pub fn describe(&self) -> String {
match &self.patterns {
None => "*".into(),
Some(patterns) => patterns
.iter()
.map(|pattern| match pattern {
Pattern::Exact(name) => name.clone(),
Pattern::Prefix(prefix) => format!("{prefix}*"),
})
.collect::<Vec<_>>()
.join(", "),
}
}
}
fn parse_pattern(pattern: &str, canonical: &[&str]) -> Result<Pattern, ToolMatcherError> {
let trimmed = pattern.trim();
if trimmed.is_empty() {
return Err(ToolMatcherError::BlankPattern);
}
let Some(prefix) = trimmed.strip_suffix('*') else {
if trimmed.contains('*') {
return Err(ToolMatcherError::UnsupportedGlob {
pattern: trimmed.to_owned(),
});
}
if !canonical.contains(&trimmed) {
return Err(ToolMatcherError::UnknownTool {
pattern: trimmed.to_owned(),
});
}
return Ok(Pattern::Exact(trimmed.to_owned()));
};
if prefix.contains('*') {
return Err(ToolMatcherError::UnsupportedGlob {
pattern: trimmed.to_owned(),
});
}
if !canonical.iter().any(|name| name.starts_with(prefix)) {
return Err(ToolMatcherError::UnknownTool {
pattern: trimmed.to_owned(),
});
}
Ok(Pattern::Prefix(prefix.to_owned()))
}
#[cfg(test)]
#[path = "matcher_tests.rs"]
mod tests;