rig_core/catalog/
lookup.rs1use std::fmt;
5use std::sync::Arc;
6
7use super::ModelSpec;
8
9#[non_exhaustive]
11#[derive(Clone, Debug, PartialEq, Eq)]
12pub enum Matched {
13 Exact,
15 SnapshotOf(String),
19}
20
21#[non_exhaustive]
23#[derive(Clone, Debug, PartialEq)]
24pub struct Resolved<'a> {
25 pub spec: &'a ModelSpec,
27 pub matched: Matched,
29 pub(super) shared: &'a Arc<ModelSpec>,
31}
32
33impl Resolved<'_> {
34 pub fn shared(&self) -> Arc<ModelSpec> {
37 Arc::clone(self.shared)
38 }
39}
40
41#[non_exhaustive]
45#[derive(Clone, Debug, PartialEq, Eq)]
46pub struct NotFound {
47 pub reference: String,
49 pub suggestions: Vec<String>,
52}
53
54impl fmt::Display for NotFound {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "the catalog lists no model `{}`", self.reference)?;
57 match self.suggestions.as_slice() {
58 [] => Ok(()),
59 [only] => write!(f, "; did you mean `{only}`?"),
60 many => write!(f, "; did you mean one of `{}`?", many.join("`, `")),
61 }
62 }
63}
64
65impl std::error::Error for NotFound {}
66
67const SUGGESTIONS: usize = 5;
69
70pub(super) fn snapshot_bases(model: &str) -> impl Iterator<Item = &str> {
73 let mut bases: Vec<&str> = model
74 .match_indices("-20")
75 .filter(|(at, _)| {
76 model
77 .get(at + 3..at + 5)
78 .is_some_and(|year| year.bytes().all(|byte| byte.is_ascii_digit()))
79 })
80 .filter_map(|(at, _)| model.get(..at))
81 .filter(|base| !base.is_empty())
82 .collect();
83 bases.reverse();
84 bases.into_iter()
85}
86
87pub(super) fn suggestions<'a>(
92 reference: &str,
93 candidates: impl Iterator<Item = &'a ModelSpec>,
94) -> Vec<String> {
95 let wanted = reference.to_lowercase();
96 let model = wanted
97 .split_once('/')
98 .map_or(wanted.as_str(), |(_, model)| model);
99 let limit = wanted.chars().count() / 3 + 1;
100 closest(&wanted, candidates, |spec, distance| {
101 let contains = !model.is_empty() && spec.id.to_lowercase().contains(model);
102 contains || distance <= limit
103 })
104}
105
106pub(super) fn vendor_models<'a>(
109 reference: &str,
110 candidates: impl Iterator<Item = &'a ModelSpec>,
111) -> Vec<String> {
112 let wanted = reference.to_lowercase();
113 let vendor = wanted.split_once('/').map_or("", |(vendor, _)| vendor);
114 closest(&wanted, candidates, |spec, _| {
115 spec.provider.vendor().eq_ignore_ascii_case(vendor)
116 })
117}
118
119fn closest<'a>(
123 wanted: &str,
124 candidates: impl Iterator<Item = &'a ModelSpec>,
125 keep: impl Fn(&ModelSpec, usize) -> bool,
126) -> Vec<String> {
127 let mut close: Vec<(usize, String)> = candidates
128 .filter(|spec| !spec.deprecated)
129 .filter_map(|spec| {
130 let candidate = format!("{}/{}", spec.provider.vendor(), spec.id);
131 let distance = edit_distance(wanted, &candidate.to_lowercase());
132 keep(spec, distance).then_some((distance, candidate))
133 })
134 .collect();
135 close.sort();
136 close.truncate(SUGGESTIONS);
137 close.into_iter().map(|(_, candidate)| candidate).collect()
138}
139
140pub(crate) fn edit_distance(a: &str, b: &str) -> usize {
144 let a: Vec<char> = a.chars().collect();
145 let b: Vec<char> = b.chars().collect();
146 let at = |row: &[usize], k: usize| row.get(k).copied().unwrap_or(usize::MAX);
147 let mut before: Vec<usize> = Vec::new();
148 let mut previous: Vec<usize> = (0..=b.len()).collect();
149 for (i, left) in a.iter().enumerate() {
150 let mut current = Vec::with_capacity(b.len() + 1);
151 current.push(i + 1);
152 for (j, right) in b.iter().enumerate() {
153 let substitute = at(&previous, j).saturating_add(usize::from(left != right));
154 let delete = at(&previous, j + 1).saturating_add(1);
155 let insert = at(¤t, j).saturating_add(1);
156 let mut best = substitute.min(delete).min(insert);
157 let swapped = i > 0
158 && j > 0
159 && a.get(i - 1) == Some(right)
160 && b.get(j - 1) == Some(left)
161 && left != right;
162 if swapped {
163 best = best.min(at(&before, j - 1).saturating_add(1));
164 }
165 current.push(best);
166 }
167 before = std::mem::replace(&mut previous, current);
168 }
169 previous.last().copied().unwrap_or(0)
170}