pub fn normalize(s: &str) -> String {
s.chars()
.filter(|c| c.is_alphanumeric())
.collect::<String>()
.to_lowercase()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelMatch {
One(String),
Ambiguous(Vec<String>),
None,
}
pub fn match_model(reference: &str, catalogue: &[String]) -> ModelMatch {
let needle = normalize(reference);
if needle.is_empty() {
return ModelMatch::None;
}
let normalized: Vec<(String, &String)> = catalogue
.iter()
.map(|m| (normalize(m), m))
.filter(|(n, _)| !n.is_empty())
.collect();
for tier in [
|n: &str, needle: &str| n == needle,
|n: &str, needle: &str| n.starts_with(needle),
|n: &str, needle: &str| n.contains(needle),
] {
let hits: Vec<String> = normalized
.iter()
.filter(|(n, _)| tier(n, &needle))
.map(|(_, original)| (*original).clone())
.collect();
match hits.len() {
0 => continue,
1 => return ModelMatch::One(hits.into_iter().next().unwrap_or_default()),
_ => return ModelMatch::Ambiguous(hits),
}
}
ModelMatch::None
}
pub fn split_provider_and_model(arg: &str) -> Option<(String, String)> {
let mut tokens = arg.split_whitespace();
let provider = tokens.next()?.to_string();
let model: Vec<&str> = tokens.collect();
if model.is_empty() {
return None;
}
Some((provider, model.join(" ")))
}
pub fn match_provider(reference: &str, known: &[String]) -> ModelMatch {
match_model(reference, known)
}