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.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    /// Sets the output to the bare version string.
101    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    /// Appends the header to the output.
118    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}