use std::fmt;
use std::sync::Arc;
use super::ModelSpec;
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Matched {
Exact,
SnapshotOf(String),
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq)]
pub struct Resolved<'a> {
pub spec: &'a ModelSpec,
pub matched: Matched,
pub(super) shared: &'a Arc<ModelSpec>,
}
impl Resolved<'_> {
pub fn shared(&self) -> Arc<ModelSpec> {
Arc::clone(self.shared)
}
}
#[non_exhaustive]
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NotFound {
pub reference: String,
pub suggestions: Vec<String>,
}
impl fmt::Display for NotFound {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "the catalog lists no model `{}`", self.reference)?;
match self.suggestions.as_slice() {
[] => Ok(()),
[only] => write!(f, "; did you mean `{only}`?"),
many => write!(f, "; did you mean one of `{}`?", many.join("`, `")),
}
}
}
impl std::error::Error for NotFound {}
const SUGGESTIONS: usize = 5;
pub(super) fn snapshot_bases(model: &str) -> impl Iterator<Item = &str> {
let mut bases: Vec<&str> = model
.match_indices("-20")
.filter(|(at, _)| {
model
.get(at + 3..at + 5)
.is_some_and(|year| year.bytes().all(|byte| byte.is_ascii_digit()))
})
.filter_map(|(at, _)| model.get(..at))
.filter(|base| !base.is_empty())
.collect();
bases.reverse();
bases.into_iter()
}
pub(super) fn suggestions<'a>(
reference: &str,
candidates: impl Iterator<Item = &'a ModelSpec>,
) -> Vec<String> {
let wanted = reference.to_lowercase();
let model = wanted
.split_once('/')
.map_or(wanted.as_str(), |(_, model)| model);
let limit = wanted.chars().count() / 3 + 1;
closest(&wanted, candidates, |spec, distance| {
let contains = !model.is_empty() && spec.id.to_lowercase().contains(model);
contains || distance <= limit
})
}
pub(super) fn vendor_models<'a>(
reference: &str,
candidates: impl Iterator<Item = &'a ModelSpec>,
) -> Vec<String> {
let wanted = reference.to_lowercase();
let vendor = wanted.split_once('/').map_or("", |(vendor, _)| vendor);
closest(&wanted, candidates, |spec, _| {
spec.provider.vendor().eq_ignore_ascii_case(vendor)
})
}
fn closest<'a>(
wanted: &str,
candidates: impl Iterator<Item = &'a ModelSpec>,
keep: impl Fn(&ModelSpec, usize) -> bool,
) -> Vec<String> {
let mut close: Vec<(usize, String)> = candidates
.filter(|spec| !spec.deprecated)
.filter_map(|spec| {
let candidate = format!("{}/{}", spec.provider.vendor(), spec.id);
let distance = edit_distance(wanted, &candidate.to_lowercase());
keep(spec, distance).then_some((distance, candidate))
})
.collect();
close.sort();
close.truncate(SUGGESTIONS);
close.into_iter().map(|(_, candidate)| candidate).collect()
}
pub(crate) fn edit_distance(a: &str, b: &str) -> usize {
let a: Vec<char> = a.chars().collect();
let b: Vec<char> = b.chars().collect();
let at = |row: &[usize], k: usize| row.get(k).copied().unwrap_or(usize::MAX);
let mut before: Vec<usize> = Vec::new();
let mut previous: Vec<usize> = (0..=b.len()).collect();
for (i, left) in a.iter().enumerate() {
let mut current = Vec::with_capacity(b.len() + 1);
current.push(i + 1);
for (j, right) in b.iter().enumerate() {
let substitute = at(&previous, j).saturating_add(usize::from(left != right));
let delete = at(&previous, j + 1).saturating_add(1);
let insert = at(¤t, j).saturating_add(1);
let mut best = substitute.min(delete).min(insert);
let swapped = i > 0
&& j > 0
&& a.get(i - 1) == Some(right)
&& b.get(j - 1) == Some(left)
&& left != right;
if swapped {
best = best.min(at(&before, j - 1).saturating_add(1));
}
current.push(best);
}
before = std::mem::replace(&mut previous, current);
}
previous.last().copied().unwrap_or(0)
}