kdeets_lib/
rust_versions.rs1use 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 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#[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(®istry).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(®istry).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(®istry).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(®istry).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}