kdeets_lib/
crate_versions.rs

1use crate::Error;
2
3use clap::Parser;
4use clap_verbosity::Verbosity;
5use colorful::Colorful;
6use tame_index::{KrateName, index::FileLock};
7
8#[derive(Parser, Debug, Default)]
9#[clap(author, version, about, long_about = None)]
10pub struct CrateVersions {
11    #[clap(flatten)]
12    logging: Verbosity,
13    /// The name of the crate
14    crate_: String,
15    /// First version ever published. May be yanked.
16    #[clap(short = 'e', long = "earliest")]
17    earliest: bool,
18    /// Returns crate version with the highest version number according to semver, but excludes pre-release and yanked versions.
19    #[clap(short = 'n', long = "normal")]
20    normal: bool,
21    /// The highest version as per semantic versioning specification
22    #[clap(short = 't', long = "top")]
23    highest: bool,
24    /// The last release by date, even if it’s yanked or less than highest version.
25    #[clap(short = 'r', long = "recent")]
26    recent: bool,
27    /// List all versions of the crate
28    #[clap(short = 'l', long = "list")]
29    list: bool,
30    /// List key values (equivalent to `-entr`)
31    #[clap(short = 'k', long = "key")]
32    key: bool,
33    /// List all versions and key values (equivalent to `-entrl`)
34    #[clap(short = 'a', long = "all")]
35    all: bool,
36}
37
38impl CrateVersions {
39    pub fn run(&self, no_colour: bool) -> Result<String, Error> {
40        log::info!("Getting details for crate: {}", self.crate_);
41        let lock = FileLock::unlocked();
42        let index = crate::get_remote_combo_index()?;
43        let index_crate = index.krate(KrateName::crates_io(&self.crate_)?, true, &lock)?;
44
45        let Some(index_crate) = index_crate else {
46            return Err(Error::CrateNotFoundOnIndex);
47        };
48
49        let mut output = format!(
50            "\n {}",
51            if no_colour {
52                format!("Crate versions for {}.", index_crate.name())
53            } else {
54                format!("Crate versions for {}.", index_crate.name().cyan())
55                    .bold()
56                    .to_string()
57            }
58        );
59
60        let mut i = 0;
61        let mut line = String::from(" ");
62
63        while i < 20 + index_crate.name().len() {
64            line.push('🭶');
65            i += 1;
66        }
67
68        output = format!("{output}\n{line}\n");
69
70        if self.earliest | self.all | self.key {
71            output = format!(
72                "{}   Earliest version: {}\n",
73                output,
74                index_crate.earliest_version().version
75            );
76        };
77
78        if self.normal | self.all | self.key {
79            output = format!(
80                "{}   {}\n",
81                output,
82                if no_colour {
83                    format!(
84                        "Highest normal version: {}",
85                        index_crate
86                            .highest_normal_version()
87                            .unwrap_or_else(|| index_crate.highest_version())
88                            .version
89                    )
90                } else {
91                    format!(
92                        "Highest normal version: {}",
93                        index_crate
94                            .highest_normal_version()
95                            .unwrap_or_else(|| index_crate.highest_version())
96                            .version
97                    )
98                    .blue()
99                    .to_string()
100                }
101            );
102        };
103
104        if self.highest | self.all | self.key {
105            output = format!(
106                "{}   {}\n",
107                output,
108                if no_colour {
109                    format!("Highest version: {}", index_crate.highest_version().version)
110                } else {
111                    format!("Highest version: {}", index_crate.highest_version().version)
112                        .green()
113                        .to_string()
114                }
115            );
116        };
117
118        if self.recent | self.all | self.key {
119            output = format!(
120                "{}   {}\n",
121                output,
122                if no_colour {
123                    format!(
124                        "Most recent version: {}",
125                        index_crate.most_recent_version().version
126                    )
127                } else {
128                    format!(
129                        "Most recent version: {}",
130                        index_crate.most_recent_version().version
131                    )
132                    .yellow()
133                    .to_string()
134                }
135            );
136        };
137
138        if self.list | self.all {
139            const BASE_HEADER: &str = " Yanked  Version ";
140
141            let mut header = BASE_HEADER.to_string();
142
143            let rows = index_crate
144                .versions
145                .iter()
146                .map(|x| {
147                    format!(
148                        "   {}     {}",
149                        match (x.yanked, no_colour) {
150                            (true, true) => "Yes".to_string(),
151                            (false, true) => " No".to_string(),
152                            (true, false) => "Yes".red().to_string(),
153                            (false, false) => " No".green().to_string(),
154                        },
155                        x.version
156                    )
157                })
158                .collect::<Vec<String>>();
159
160            log::debug!("Rows: {rows:#?}!");
161
162            let max_row = &rows
163                .iter()
164                .map(|x| {
165                    log::debug!("Line: `{}`, len: `{}`!", x, x.chars().count(),);
166                    x.len() - 12
167                })
168                .max()
169                .unwrap_or(BASE_HEADER.len());
170            log::debug!("Max row length: {max_row}!");
171
172            while header.len() < *max_row {
173                header = format!("{header} ");
174            }
175            log::debug!("Output: {output}!");
176            log::debug!("Header: {header}!");
177
178            let rows = format!("   {}\n", rows.join("\n   "));
179
180            output = format!(
181                "{}   {}\n{}",
182                output,
183                if no_colour {
184                    header.to_string()
185                } else {
186                    header.underlined().to_string()
187                },
188                rows
189            );
190        }
191
192        Ok(output)
193    }
194}
195
196#[cfg(test)]
197mod tests {
198
199    use colorful::Colorful;
200    use rstest::fixture;
201
202    use crate::crate_versions::CrateVersions;
203
204    #[fixture]
205    fn header(#[default("some_crate")] name: &str) -> String {
206        let output = format!(
207            "\n {}",
208            format!("Crate versions for {}.", name.cyan()).bold()
209        );
210
211        let mut i = 0;
212        let mut line = String::from(" ");
213
214        while i < 20 + name.len() {
215            line.push('🭶');
216            i += 1;
217        }
218
219        format!("{output}\n{line}\n")
220    }
221
222    #[fixture]
223    fn earliest() -> String {
224        "   Earliest version: 0.1.0\n".to_string()
225    }
226
227    #[fixture]
228    fn highest_normal() -> String {
229        format!("   {}\n", "Highest normal version: 0.2.1".blue())
230    }
231
232    #[fixture]
233    fn highest() -> String {
234        format!("   {}\n", "Highest version: 0.2.1".green())
235    }
236
237    #[fixture]
238    fn recent() -> String {
239        format!("   {}\n", "Most recent version: 0.2.1".yellow())
240    }
241
242    #[fixture]
243    fn list() -> String {
244        "   \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".to_string()
245    }
246
247    #[test]
248    fn test_run_earliest() {
249        let name = "some_crate";
250        let expected = format!("{}{}", header(name), &earliest());
251
252        let crate_versions = CrateVersions {
253            crate_: "some_crate".to_string(),
254            earliest: true,
255            ..Default::default()
256        };
257
258        assert_eq!(crate_versions.crate_, "some_crate".to_string());
259        assert!(crate_versions.earliest);
260        assert!(!crate_versions.normal);
261        assert!(!crate_versions.highest);
262        assert!(!crate_versions.recent);
263        assert!(!crate_versions.list);
264        assert!(!crate_versions.all);
265        assert!(!crate_versions.key);
266
267        let result = crate_versions.run(false);
268        assert!(result.is_ok());
269        let output = result.unwrap();
270        assert_eq!(output, expected);
271    }
272
273    #[test]
274    fn test_run_normal() {
275        let name = "some_crate";
276        let expected = format!("{}{}", header(name), &highest_normal());
277
278        let crate_versions = CrateVersions {
279            crate_: "some_crate".to_string(),
280            normal: true,
281            ..Default::default()
282        };
283
284        let result = crate_versions.run(false);
285        assert!(result.is_ok());
286        let output = result.unwrap();
287        assert_eq!(output, expected);
288    }
289
290    #[test]
291    fn test_run_top() {
292        let name = "some_crate";
293        let expected = format!("{}{}", header(name), &highest());
294
295        let crate_versions = CrateVersions {
296            crate_: "some_crate".to_string(),
297            highest: true,
298            ..Default::default()
299        };
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_recent() {
309        let name = "some_crate";
310        let expected = format!("{}{}", header(name), &recent());
311
312        let crate_versions = CrateVersions {
313            crate_: "some_crate".to_string(),
314            recent: 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_list() {
326        let name = "some_crate";
327        let expected = format!("{}{}", header(name), &list());
328
329        let crate_versions = CrateVersions {
330            crate_: "some_crate".to_string(),
331            list: 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_all() {
343        let name = "some_crate";
344        let expected = format!(
345            "{}{}{}{}{}{}",
346            header(name),
347            &earliest(),
348            &highest_normal(),
349            &highest(),
350            &recent(),
351            &list()
352        );
353
354        let crate_versions = CrateVersions {
355            crate_: "some_crate".to_string(),
356            all: true,
357            ..Default::default()
358        };
359
360        let result = crate_versions.run(false);
361        assert!(result.is_ok());
362        let output = result.unwrap();
363        assert_eq!(output, expected);
364    }
365
366    #[test]
367    fn test_run_key() {
368        let name = "some_crate";
369        let expected = format!(
370            "{}{}{}{}{}",
371            header(name),
372            &earliest(),
373            &highest_normal(),
374            &highest(),
375            &recent(),
376        );
377
378        let crate_versions = CrateVersions {
379            crate_: "some_crate".to_string(),
380            key: true,
381            ..Default::default()
382        };
383
384        let result = crate_versions.run(false);
385        assert!(result.is_ok());
386        let output = result.unwrap();
387        assert_eq!(output, expected);
388    }
389
390    #[test]
391    fn test_run_invalid_crate() {
392        let crate_versions = CrateVersions {
393            crate_: "some_non-existing_crate".to_string(),
394            ..Default::default()
395        };
396
397        let result = crate_versions.run(false);
398        assert!(result.is_err());
399    }
400
401    #[test]
402    fn test_run_invalid_crate_earliest() {
403        let crate_versions = CrateVersions {
404            crate_: "sdc_apis".to_string(),
405            earliest: true,
406            ..Default::default()
407        };
408
409        let result = crate_versions.run(false);
410        assert!(result.is_ok());
411    }
412}