nmbrs_workload/
suggest.rs1use std::path::Path;
27
28pub const MAX_DEPTH: usize = 3;
32
33const MAX_ENTRIES_SCANNED: usize = 4000;
36
37const MAX_LISTED: usize = 10;
40
41const SKIP_DIRS: &[&str] = &["target", "node_modules", "logs", ".git"];
43
44pub fn suggest_workloads(partial: &str) -> Vec<String> {
54 if partial.is_empty() {
55 return Vec::new();
56 }
57 let needle = leaf_of(partial);
58 let mut out = Vec::new();
59 let mut budget = MAX_ENTRIES_SCANNED;
60 walk(
61 Path::new("."),
62 String::new(),
63 partial,
64 needle,
65 0,
66 &mut budget,
67 &mut out,
68 );
69 out.extend(bundled_matches(partial, needle));
70 out.sort();
71 out.dedup();
72 out
73}
74
75pub fn suggest_bundled(partial: &str) -> Vec<String> {
79 if partial.is_empty() {
80 return Vec::new();
81 }
82 let mut out = bundled_matches(partial, leaf_of(partial));
83 out.sort();
84 out.dedup();
85 out
86}
87
88pub fn did_you_mean(hits: &[String]) -> String {
95 if hits.is_empty() {
96 return String::new();
97 }
98 let shown: Vec<&str> = hits.iter().take(MAX_LISTED).map(String::as_str).collect();
99 let overflow = hits.len() - shown.len();
100 let list = shown.join(", ");
101 if overflow > 0 {
102 format!(
103 " Did you mean one of: {list} \
104 (+{overflow} more — `nmbrs describe workloads --all`)"
105 )
106 } else {
107 format!(" Did you mean: {list}?")
108 }
109}
110
111fn bundled_matches(partial: &str, needle: &str) -> Vec<String> {
114 crate::catalog::iter()
115 .map(|w| w.name)
116 .filter(|n| n.starts_with(partial) || leaf_of(n).starts_with(needle))
117 .map(str::to_string)
118 .collect()
119}
120
121fn leaf_of(s: &str) -> &str {
123 s.rsplit('/').next().unwrap_or(s)
124}
125
126fn is_catalog_duplicate(rel: &str) -> bool {
135 catalog_name_for_local(rel).is_some_and(|n| crate::catalog::lookup(&n).is_some())
136}
137
138fn catalog_name_for_local(rel: &str) -> Option<String> {
144 let stem = rel
145 .strip_suffix(".yaml")
146 .or_else(|| rel.strip_suffix(".yml"))?;
147 if let Some(rest) = stem.strip_prefix("examples/workloads/") {
148 return Some(format!("examples/{rest}"));
149 }
150 stem.strip_prefix("workloads/").map(str::to_string)
151}
152
153fn walk(
158 dir: &Path,
159 rel: String,
160 partial: &str,
161 needle: &str,
162 depth: usize,
163 budget: &mut usize,
164 out: &mut Vec<String>,
165) {
166 if *budget == 0 {
167 return;
168 }
169 let Ok(entries) = std::fs::read_dir(dir) else {
170 return;
171 };
172 for entry in entries.flatten() {
173 if *budget == 0 {
174 return;
175 }
176 *budget -= 1;
177 let name = entry.file_name().to_string_lossy().into_owned();
178 if name.starts_with('.') {
179 continue;
180 }
181 let path = entry.path();
182 let child_rel = if rel.is_empty() {
183 name.clone()
184 } else {
185 format!("{rel}/{name}")
186 };
187 if path.is_dir() {
188 if SKIP_DIRS.contains(&name.as_str()) {
189 continue;
190 }
191 if depth < MAX_DEPTH {
192 walk(&path, child_rel, partial, needle, depth + 1, budget, out);
193 }
194 continue;
195 }
196 let Some(stem) = name
197 .strip_suffix(".yaml")
198 .or_else(|| name.strip_suffix(".yml"))
199 else {
200 continue;
201 };
202 if (child_rel.starts_with(partial) || stem.starts_with(needle))
203 && !is_catalog_duplicate(&child_rel)
204 {
205 out.push(child_rel);
206 }
207 }
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use std::fs;
214
215 fn touch(p: &Path) {
216 if let Some(parent) = p.parent() {
217 fs::create_dir_all(parent).unwrap();
218 }
219 fs::write(p, "ops: { a: { raw: x } }\n").unwrap();
220 }
221
222 fn local_hits(root: &Path, partial: &str) -> Vec<String> {
225 let needle = leaf_of(partial);
226 let mut out = Vec::new();
227 let mut budget = MAX_ENTRIES_SCANNED;
228 walk(
229 root,
230 String::new(),
231 partial,
232 needle,
233 0,
234 &mut budget,
235 &mut out,
236 );
237 out.sort();
238 out
239 }
240
241 #[test]
242 fn leaf_match_surfaces_buried_stem() {
243 let dir = std::env::temp_dir().join(format!("nmbrs-suggest-{}", std::process::id()));
244 let _ = fs::remove_dir_all(&dir);
245 touch(&dir.join("examples/workloads/controls/phase_poll_smoke.yaml"));
246 let hits = local_hits(&dir, "phase_poll");
247 assert_eq!(
248 hits,
249 vec!["examples/workloads/controls/phase_poll_smoke.yaml"]
250 );
251 }
252
253 #[test]
254 fn depth_bound_excludes_level_four() {
255 let dir = std::env::temp_dir().join(format!("nmbrs-suggest-depth-{}", std::process::id()));
256 let _ = fs::remove_dir_all(&dir);
257 touch(&dir.join("a/b/c/match_me.yaml"));
259 touch(&dir.join("a/b/c/d/match_me.yaml"));
260 let hits = local_hits(&dir, "match_me");
261 assert_eq!(hits, vec!["a/b/c/match_me.yaml"]);
262 }
263
264 #[test]
265 fn slash_qualified_partial_matches_by_full_prefix() {
266 let dir = std::env::temp_dir().join(format!("nmbrs-suggest-slash-{}", std::process::id()));
267 let _ = fs::remove_dir_all(&dir);
268 touch(&dir.join("cursors/all_cursor/enumerate.yaml"));
269 let hits = local_hits(&dir, "cursors/all_cursor/enum");
270 assert_eq!(hits, vec!["cursors/all_cursor/enumerate.yaml"]);
271 }
272
273 #[test]
274 fn skip_dirs_are_not_walked() {
275 let dir = std::env::temp_dir().join(format!("nmbrs-suggest-skip-{}", std::process::id()));
276 let _ = fs::remove_dir_all(&dir);
277 touch(&dir.join("target/buried/match_me.yaml"));
278 touch(&dir.join("kept/match_me.yaml"));
279 let hits = local_hits(&dir, "match_me");
280 assert_eq!(hits, vec!["kept/match_me.yaml"]);
281 }
282
283 #[test]
284 fn local_path_maps_to_catalog_name() {
285 assert_eq!(
288 catalog_name_for_local("examples/workloads/controls/phase_poll_smoke.yaml").as_deref(),
289 Some("examples/controls/phase_poll_smoke")
290 );
291 assert_eq!(
292 catalog_name_for_local("workloads/keyvalue.yaml").as_deref(),
293 Some("keyvalue")
294 );
295 assert_eq!(
296 catalog_name_for_local("workloads/cql/baselinesv3/keyvalue.yml").as_deref(),
297 Some("cql/baselinesv3/keyvalue")
298 );
299 assert_eq!(catalog_name_for_local("my/own/workload.yaml"), None);
301 assert_eq!(catalog_name_for_local("examples/notes/readme.txt"), None);
302 }
303
304 #[test]
305 fn empty_partial_yields_nothing() {
306 assert!(suggest_workloads("").is_empty());
307 assert!(suggest_bundled("").is_empty());
308 }
309
310 #[test]
311 fn did_you_mean_formats_short_and_capped_lists() {
312 assert_eq!(did_you_mean(&[]), "");
313 assert_eq!(
314 did_you_mean(&["a".to_string(), "b".to_string()]),
315 " Did you mean: a, b?"
316 );
317 let many: Vec<String> = (0..MAX_LISTED + 3).map(|i| format!("w{i}")).collect();
318 let tail = did_you_mean(&many);
319 assert!(tail.contains("+3 more"), "overflow note: {tail}");
320 assert!(
321 tail.contains("Did you mean one of:"),
322 "capped phrasing: {tail}"
323 );
324 }
325}