1use crate::Error;
2
3use clap::Parser;
4use clap_verbosity::Verbosity;
5use colorful::Colorful;
6use smol_str::SmolStr;
7use tame_index::{IndexKrate, KrateName, index::FileLock};
8
9#[derive(Parser, Debug, Default)]
10#[clap(author, version, about, long_about = None)]
11pub struct CrateVersions {
12 #[clap(flatten)]
13 logging: Verbosity,
14 crate_: String,
16 #[clap(short = 'b', long = "bare")]
18 bare: bool,
19 #[clap(short = 'e', long = "earliest")]
21 earliest: bool,
22 #[clap(short = 'n', long = "normal")]
24 normal: bool,
25 #[clap(short = 't', long = "top")]
27 highest: bool,
28 #[clap(short = 'r', long = "recent")]
30 recent: bool,
31 #[clap(short = 'l', long = "list")]
33 list: bool,
34 #[clap(short = 'k', long = "key")]
36 key: bool,
37 #[clap(short = 'a', long = "all")]
39 all: bool,
40
41 #[clap(skip)]
42 output: String,
43}
44
45impl CrateVersions {
46 pub fn run(&mut self, no_colour: bool) -> Result<String, Error> {
47 log::info!("Getting details for crate: {}", self.crate_);
48 let lock = FileLock::unlocked();
49 let index = crate::get_remote_combo_index()?;
50 let index_crate = index.krate(KrateName::crates_io(&self.crate_)?, true, &lock)?;
51
52 let Some(index_crate) = index_crate else {
53 return Err(Error::CrateNotFoundOnIndex);
54 };
55
56 if self.bare {
57 self.output = if self.recent {
58 index_crate.most_recent_version().version.to_string()
59 } else if self.highest {
60 index_crate.highest_version().version.to_string()
61 } else if self.normal {
62 index_crate
63 .highest_normal_version()
64 .unwrap_or_else(|| index_crate.highest_version())
65 .version
66 .to_string()
67 } else {
68 index_crate.earliest_version().version.to_string()
69 }
70 } else {
71 self.append_header(no_colour, index_crate.name());
72
73 if self.earliest | self.all | self.key {
74 let description = "Earliest version";
75 let version = &index_crate.earliest_version().version;
76 let colour = TextColour::None;
77 self.append_specific_version(description, version, colour);
78 };
79
80 if self.normal | self.all | self.key {
81 let description = "Highest normal version";
82 let version = &index_crate
83 .highest_normal_version()
84 .unwrap_or_else(|| index_crate.highest_version())
85 .version;
86 let colour = if no_colour {
87 TextColour::None
88 } else {
89 TextColour::Blue
90 };
91 self.append_specific_version(description, version, colour);
92 };
93
94 if self.highest | self.all | self.key {
95 let description = "Highest version";
96 let version = &index_crate.highest_version().version;
97 let colour = if no_colour {
98 TextColour::None
99 } else {
100 TextColour::Green
101 };
102 self.append_specific_version(description, version, colour);
103 };
104
105 if self.recent | self.all | self.key {
106 let description = "Most recent version";
107 let version = &index_crate.most_recent_version().version;
108 let colour = if no_colour {
109 TextColour::None
110 } else {
111 TextColour::Yellow
112 };
113 self.append_specific_version(description, version, colour);
114 };
115
116 if self.list | self.all {
117 self.append_list(index_crate, no_colour);
118 }
119 };
120
121 Ok(self.output.to_string())
122 }
123
124 fn append_header(&mut self, no_colour: bool, crate_name: &str) {
125 let output = format!(
126 "\n {}",
127 if no_colour {
128 format!("Crate versions for {crate_name}.")
129 } else {
130 format!("Crate versions for {}.", crate_name.cyan())
131 .bold()
132 .to_string()
133 }
134 );
135
136 let mut i = 0;
137 let mut line = String::from(" ");
138
139 while i < 20 + crate_name.len() {
140 line.push('🭶');
141 i += 1;
142 }
143
144 self.output = format!("{output}\n{line}\n");
145 }
146
147 fn append_specific_version(
148 &mut self,
149 description: &str,
150 version: &SmolStr,
151 colour: TextColour,
152 ) {
153 let addition = format!("{description}: {version}");
154 let addition = colour.paint(addition);
155 self.output = format!("{} {}\n", self.output, addition)
156 }
157
158 fn append_list(&mut self, index_crate: IndexKrate, no_colour: bool) {
159 const BASE_HEADER: &str = " Yanked Version ";
160
161 let mut header = BASE_HEADER.to_string();
162
163 let rows = index_crate
164 .versions
165 .iter()
166 .map(|x| {
167 format!(
168 " {} {}",
169 match (x.yanked, no_colour) {
170 (true, true) => "Yes".to_string(),
171 (false, true) => " No".to_string(),
172 (true, false) => "Yes".red().to_string(),
173 (false, false) => " No".green().to_string(),
174 },
175 x.version
176 )
177 })
178 .collect::<Vec<String>>();
179
180 log::debug!("Rows: {rows:#?}!");
181
182 let max_row = &rows
183 .iter()
184 .map(|x| {
185 log::debug!("Line: `{}`, len: `{}`!", x, x.chars().count(),);
186 x.len() - 12
187 })
188 .max()
189 .unwrap_or(BASE_HEADER.len());
190 log::debug!("Max row length: {max_row}!");
191
192 while header.len() < *max_row {
193 header = format!("{header} ");
194 }
195 log::debug!("Output: {}!", self.output);
196 log::debug!("Header: {header}!");
197
198 let rows = format!(" {}\n", rows.join("\n "));
199
200 self.output = format!(
201 "{} {}\n{}",
202 self.output,
203 if no_colour {
204 header.to_string()
205 } else {
206 header.underlined().to_string()
207 },
208 rows
209 );
210 }
211}
212
213enum TextColour {
214 None,
215 Blue,
216 Green,
217 Yellow,
218}
219
220impl TextColour {
221 fn paint(&self, text: String) -> String {
222 match self {
223 TextColour::None => text,
224 TextColour::Blue => text.blue().to_string(),
225 TextColour::Green => text.green().to_string(),
226 TextColour::Yellow => text.yellow().to_string(),
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233
234 use colorful::Colorful;
235 use rstest::fixture;
236
237 use crate::crate_versions::CrateVersions;
238
239 #[fixture]
240 fn header(#[default("some_crate")] name: &str) -> String {
241 let output = format!(
242 "\n {}",
243 format!("Crate versions for {}.", name.cyan()).bold()
244 );
245
246 let mut i = 0;
247 let mut line = String::from(" ");
248
249 while i < 20 + name.len() {
250 line.push('🭶');
251 i += 1;
252 }
253
254 format!("{output}\n{line}\n")
255 }
256
257 #[fixture]
258 fn earliest() -> String {
259 " Earliest version: 0.1.0\n".to_string()
260 }
261
262 #[fixture]
263 fn highest_normal() -> String {
264 format!(" {}\n", "Highest normal version: 0.2.1".blue())
265 }
266
267 #[fixture]
268 fn highest() -> String {
269 format!(" {}\n", "Highest version: 0.2.1".green())
270 }
271
272 #[fixture]
273 fn recent() -> String {
274 format!(" {}\n", "Most recent version: 0.2.1".yellow())
275 }
276
277 #[fixture]
278 fn list() -> String {
279 " \u{1b}[4m Yanked Version \u{1b}[0m\n \u{1b}[38;5;2m No\u{1b}[0m 0.1.0\n \u{1b}[38;5;2m No\u{1b}[0m 0.1.1\n \u{1b}[38;5;2m No\u{1b}[0m 0.1.3\n \u{1b}[38;5;2m No\u{1b}[0m 0.2.1\n"
280 .to_string()
281 }
282
283 #[test]
284 fn test_run_earliest() {
285 let name = "some_crate";
286 let expected = format!("{}{}", header(name), &earliest());
287
288 let mut crate_versions = CrateVersions {
289 crate_: "some_crate".to_string(),
290 earliest: true,
291 ..Default::default()
292 };
293
294 assert_eq!(crate_versions.crate_, "some_crate".to_string());
295 assert!(crate_versions.earliest);
296 assert!(!crate_versions.normal);
297 assert!(!crate_versions.highest);
298 assert!(!crate_versions.recent);
299 assert!(!crate_versions.list);
300 assert!(!crate_versions.all);
301 assert!(!crate_versions.key);
302
303 let result = crate_versions.run(false);
304 assert!(result.is_ok());
305 let output = result.unwrap();
306 assert_eq!(output, expected);
307 }
308
309 #[test]
310 fn test_run_normal() {
311 let name = "some_crate";
312 let expected = format!("{}{}", header(name), &highest_normal());
313
314 let mut crate_versions = CrateVersions {
315 crate_: "some_crate".to_string(),
316 normal: true,
317 ..Default::default()
318 };
319
320 let result = crate_versions.run(false);
321 assert!(result.is_ok());
322 let output = result.unwrap();
323 assert_eq!(output, expected);
324 }
325
326 #[test]
327 fn test_run_top() {
328 let name = "some_crate";
329 let expected = format!("{}{}", header(name), &highest());
330
331 let mut crate_versions = CrateVersions {
332 crate_: "some_crate".to_string(),
333 highest: true,
334 ..Default::default()
335 };
336
337 let result = crate_versions.run(false);
338 assert!(result.is_ok());
339 let output = result.unwrap();
340 assert_eq!(output, expected);
341 }
342
343 #[test]
344 fn test_run_recent() {
345 let name = "some_crate";
346 let expected = format!("{}{}", header(name), &recent());
347
348 let mut crate_versions = CrateVersions {
349 crate_: "some_crate".to_string(),
350 recent: true,
351 ..Default::default()
352 };
353
354 let result = crate_versions.run(false);
355 assert!(result.is_ok());
356 let output = result.unwrap();
357 assert_eq!(output, expected);
358 }
359
360 #[test]
361 fn test_run_list() {
362 let name = "some_crate";
363 let expected = format!("{}{}", header(name), &list());
364
365 let mut crate_versions = CrateVersions {
366 crate_: "some_crate".to_string(),
367 list: true,
368 ..Default::default()
369 };
370
371 let result = crate_versions.run(false);
372 assert!(result.is_ok());
373 let output = result.unwrap();
374 assert_eq!(output, expected);
375 }
376
377 #[test]
378 fn test_run_all() {
379 let name = "some_crate";
380 let expected = format!(
381 "{}{}{}{}{}{}",
382 header(name),
383 &earliest(),
384 &highest_normal(),
385 &highest(),
386 &recent(),
387 &list()
388 );
389
390 let mut crate_versions = CrateVersions {
391 crate_: "some_crate".to_string(),
392 all: true,
393 ..Default::default()
394 };
395
396 let result = crate_versions.run(false);
397 assert!(result.is_ok());
398 let output = result.unwrap();
399 println!("Expected:\n`{expected}`\n\nGot:\n`{output}`");
400 assert_eq!(output, expected);
401 }
402
403 #[test]
404 fn test_run_key() {
405 let name = "some_crate";
406 let expected = format!(
407 "{}{}{}{}{}",
408 header(name),
409 &earliest(),
410 &highest_normal(),
411 &highest(),
412 &recent(),
413 );
414
415 let mut crate_versions = CrateVersions {
416 crate_: "some_crate".to_string(),
417 key: true,
418 ..Default::default()
419 };
420
421 let result = crate_versions.run(false);
422 assert!(result.is_ok());
423 let output = result.unwrap();
424 assert_eq!(output, expected);
425 }
426
427 #[test]
428 fn test_run_invalid_crate() {
429 let mut crate_versions = CrateVersions {
430 crate_: "some_non-existing_crate".to_string(),
431 ..Default::default()
432 };
433
434 let result = crate_versions.run(false);
435 assert!(result.is_err());
436 }
437
438 #[test]
439 fn test_run_invalid_crate_earliest() {
440 let mut crate_versions = CrateVersions {
441 crate_: "sdc_apis".to_string(),
442 earliest: true,
443 ..Default::default()
444 };
445
446 let result = crate_versions.run(false);
447 assert!(result.is_ok());
448 }
449}