kdeets_lib/
rust_versions.rs

1use std::fmt::Display;
2
3use crate::{Error, HEADER, LINE_CHAR};
4
5use clap::Parser;
6use clap_verbosity::Verbosity;
7use colorful::Colorful;
8use semver::{Version, VersionReq};
9use smol_str::SmolStr;
10use tame_index::{
11    IndexKrate, KrateName,
12    index::{ComboIndex, FileLock},
13};
14
15#[derive(Parser, Debug, Default)]
16#[clap(author, version, about, long_about = None)]
17pub struct RustVersions {
18    #[clap(flatten)]
19    logging: Verbosity,
20    /// The name of the crate
21    crate_: String,
22}
23
24impl RustVersions {
25    pub fn run(&self) -> Result<String, Error> {
26        log::info!("Getting details for crate: {}", self.crate_);
27        let lock = FileLock::unlocked();
28        let index = crate::get_remote_combo_index()?;
29        let index_crate = index.krate(KrateName::crates_io(&self.crate_)?, true, &lock)?;
30
31        let Some(index_crate) = index_crate else {
32            return Err(Error::CrateNotFoundOnIndex);
33        };
34
35        let mut output = RustVersionOutput::new(index_crate);
36
37        output.set_rust_version()?;
38
39        output.set_minimum_rust_version_required(&index)?;
40
41        Ok(output.to_string())
42    }
43}
44
45fn get_rust_version(
46    index: &ComboIndex,
47    name: &str,
48    version_reference: VersionReq,
49) -> Result<Option<SmolStr>, Error> {
50    let crate_name = KrateName::crates_io(name)?;
51    let lock = FileLock::unlocked();
52    let index_crate = index.krate(crate_name, true, &lock)?;
53
54    let Some(index_crate) = index_crate else {
55        return Err(Error::CrateNotFoundOnIndex);
56    };
57
58    for version in index_crate.versions {
59        if version_reference.matches(&Version::parse(&version.version)?) {
60            return Ok(version.rust_version);
61        }
62    }
63
64    Ok(None)
65}
66
67#[derive(Debug)]
68struct RustVersionOutput {
69    index_crate: IndexKrate,
70    header: String,
71    rust_version: Option<String>,
72    minimum_required_rust: Option<String>,
73}
74
75impl RustVersionOutput {
76    fn new(index_crate: IndexKrate) -> Self {
77        let mut header = String::from("\n  ");
78        header.push_str(HEADER);
79        header.push(' ');
80        header.push_str(index_crate.name().cyan().to_string().as_str());
81        header.push('.');
82        header.push_str("\n  ");
83        let mut i = 0;
84        while i < HEADER.len() + 2 + index_crate.name().len() {
85            header.push(LINE_CHAR);
86            i += 1;
87        }
88        header.push('\n');
89
90        Self {
91            index_crate,
92            header,
93            rust_version: None,
94            minimum_required_rust: None,
95        }
96    }
97
98    fn set_rust_version(&mut self) -> Result<(), Error> {
99        let mut rust_version = String::from("    Most recent version: ");
100        rust_version.push_str(
101            self.index_crate
102                .most_recent_version()
103                .version
104                .to_string()
105                .as_str(),
106        );
107        rust_version.push_str(" (Rust version: ");
108        let rv = if let Some(rv) = &self.index_crate.most_recent_version().rust_version {
109            rv.to_string()
110        } else {
111            "not specified".to_string()
112        }
113        .blue()
114        .bold()
115        .to_string();
116        rust_version.push_str(&rv);
117        rust_version.push_str(")\n");
118
119        self.rust_version = Some(rust_version);
120
121        Ok(())
122    }
123
124    fn set_minimum_rust_version_required(&mut self, index: &ComboIndex) -> Result<(), Error> {
125        let mut rust_versions = vec![];
126
127        let deps = self.index_crate.most_recent_version().dependencies();
128        for dep in deps {
129            let rust_version =
130                get_rust_version(index, dep.crate_name(), dep.version_requirement())?;
131            rust_versions.push(rust_version.clone());
132            log::debug!(
133                "    {}   {}  {:?}\n",
134                dep.crate_name(),
135                dep.version_requirement(),
136                rust_version,
137            );
138        }
139
140        let minimum_rust = rust_versions
141            .iter()
142            .filter_map(|rv_opt| rv_opt.as_ref())
143            .max();
144
145        let mut minimum_required_rust = String::from("    Minimum Rust version: ");
146        if let Some(minimum_rust) = minimum_rust {
147            minimum_required_rust.push_str(minimum_rust.to_string().as_str());
148        } else {
149            minimum_required_rust.push_str("not specified");
150        }
151
152        if rust_versions.iter().any(|rv| rv.is_none()) {
153            minimum_required_rust.push_str(" (");
154            minimum_required_rust.push_str(
155                " (WARNING: Some dependencies do not specify a Rust version)"
156                    .yellow()
157                    .to_string()
158                    .as_str(),
159            );
160            minimum_required_rust.push(')');
161        }
162        minimum_required_rust.push('\n');
163
164        self.minimum_required_rust = Some(minimum_required_rust);
165
166        Ok(())
167    }
168}
169
170impl Display for RustVersionOutput {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        write!(f, "{}", self.header)?;
173        if let Some(rust_version) = &self.rust_version {
174            write!(f, "{rust_version}")?;
175        }
176        if let Some(minimum_required_rust) = &self.minimum_required_rust {
177            write!(f, "{minimum_required_rust}")?;
178        }
179        Ok(())
180    }
181}
182
183// Forestry - single dependency with rust_version specified
184// walkdir - No dependency has rust_version specified
185
186#[cfg(test)]
187mod tests {
188
189    use super::*;
190
191    use crate::rust_versions::RustVersions;
192    use clap::Parser;
193
194    #[test]
195    fn test_rust_versions_new() {
196        let rust_versions = RustVersions::parse_from(["program", "test-crate"]);
197        assert_eq!(rust_versions.crate_, "test-crate");
198    }
199
200    #[test]
201    fn test_rust_versions_empty_crate() {
202        let result = RustVersions::try_parse_from(["program"]);
203        assert!(result.is_err());
204    }
205
206    #[test]
207    fn test_rust_versions_with_verbose() {
208        let rust_versions = RustVersions::parse_from(["program", "-v", "test-crate"]);
209        assert_eq!(rust_versions.crate_, "test-crate");
210    }
211
212    #[test]
213    fn test_rust_versions_with_quiet() {
214        let rust_versions = RustVersions::parse_from(["program", "-q", "test-crate"]);
215        assert_eq!(rust_versions.crate_, "test-crate");
216    }
217
218    #[test]
219    fn test_rust_versions_with_multiple_flags() {
220        let rust_versions = RustVersions::parse_from(["program", "-vv", "test-crate"]);
221        assert_eq!(rust_versions.crate_, "test-crate");
222    }
223
224    #[test]
225    fn test_add_crate_and_set_header() {
226        let expected = "\n  Crate versions for \u{1b}[38;5;6mforestry\u{1b}[0m.\n  🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶\n";
227
228        let (_temp_dir, registry) = crate::tests::get_temp_local_registry();
229        let lock = FileLock::unlocked();
230        let index = crate::tests::get_test_index(&registry).unwrap();
231        let index_crate = index
232            .krate(KrateName::crates_io("forestry").unwrap(), true, &lock)
233            .unwrap()
234            .unwrap();
235
236        let output = RustVersionOutput::new(index_crate);
237
238        assert_eq!(output.to_string(), expected);
239    }
240
241    #[test]
242    fn test_set_rust_version_output_with_specified_version() {
243        let expected = "\n  Crate versions for \u{1b}[38;5;6mforestry\u{1b}[0m.\n  🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶\n    Most recent version: 1.4.1 (Rust version: \u{1b}[38;5;4;1mnot specified\u{1b}[0m)\n";
244
245        let (_temp_dir, registry) = crate::tests::get_temp_local_registry();
246        let lock = FileLock::unlocked();
247        let index = crate::tests::get_test_index(&registry).unwrap();
248        let index_crate = index
249            .krate(KrateName::crates_io("forestry").unwrap(), true, &lock)
250            .unwrap()
251            .unwrap();
252
253        let mut output = RustVersionOutput::new(index_crate);
254        output.set_rust_version().unwrap();
255
256        assert_eq!(output.to_string(), expected);
257    }
258
259    #[test]
260    fn test_set_rust_version_output_with_minimum_rust() {
261        let expected = "\n  Crate versions for \u{1b}[38;5;6mforestry\u{1b}[0m.\n  🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶\n    Minimum Rust version: not specified (\u{1b}[38;5;3m (WARNING: Some dependencies do not specify a Rust version)\u{1b}[0m)\n";
262
263        let (_temp_dir, registry) = crate::tests::get_temp_local_registry();
264        let lock = FileLock::unlocked();
265        let index = crate::tests::get_test_index(&registry).unwrap();
266        let index_crate = index
267            .krate(KrateName::crates_io("forestry").unwrap(), true, &lock)
268            .unwrap()
269            .unwrap();
270
271        let mut output = RustVersionOutput::new(index_crate);
272
273        output.set_minimum_rust_version_required(&index).unwrap();
274
275        assert_eq!(output.to_string(), expected);
276    }
277
278    #[test]
279    fn test_set_rust_version_output_with_specified_version_and_minimum_rust() {
280        let expected = "\n  Crate versions for \u{1b}[38;5;6mforestry\u{1b}[0m.\n  🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶🭶\n    Most recent version: 1.4.1 (Rust version: \u{1b}[38;5;4;1mnot specified\u{1b}[0m)\n    Minimum Rust version: not specified (\u{1b}[38;5;3m (WARNING: Some dependencies do not specify a Rust version)\u{1b}[0m)\n";
281
282        let (_temp_dir, registry) = crate::tests::get_temp_local_registry();
283        let lock = FileLock::unlocked();
284        let index = crate::tests::get_test_index(&registry).unwrap();
285        let index_crate = index
286            .krate(KrateName::crates_io("forestry").unwrap(), true, &lock)
287            .unwrap()
288            .unwrap();
289
290        let mut output = RustVersionOutput::new(index_crate);
291        output.set_rust_version().unwrap();
292        output.set_minimum_rust_version_required(&index).unwrap();
293
294        assert_eq!(output.to_string(), expected);
295    }
296}