Skip to main content

kdeets_lib/
crate_versions.rs

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    /// The name of the crate
15    crate_: String,
16    /// Display bare version number without text for recent, highest normal, higest or earliest version.
17    #[clap(short = 'b', long = "bare")]
18    bare: bool,
19    /// First version ever published. May be yanked.
20    #[clap(short = 'e', long = "earliest")]
21    earliest: bool,
22    /// Returns crate version with the highest version number according to semver, but excludes pre-release and yanked versions.
23    #[clap(short = 'n', long = "normal")]
24    normal: bool,
25    /// The highest version as per semantic versioning specification
26    #[clap(short = 't', long = "top")]
27    highest: bool,
28    /// The last release by date, even if it’s yanked or less than highest version.
29    #[clap(short = 'r', long = "recent")]
30    recent: bool,
31    /// List all versions of the crate
32    #[clap(short = 'l', long = "list")]
33    list: bool,
34    /// List key values (equivalent to `-entr`)
35    #[clap(short = 'k', long = "key")]
36    key: bool,
37    /// List all versions and key values (equivalent to `-entrl`)
38    #[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}