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