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