Skip to main content

kdeets_lib/
rust_versions.rs

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