1use crate::cli::rust_releases_opts::RustReleasesOpts;
10use crate::cli::shared_opts::SharedOpts;
11use crate::cli::toolchain_opts::ToolchainOpts;
12
13use crate::error::{CargoMSRVError, InvalidUtf8Error, IoError, IoErrorSource, PathError};
14use crate::manifest::bare_version::BareVersion;
15use camino::{Utf8Path, Utf8PathBuf};
16use clap::ValueEnum;
17use std::convert::{TryFrom, TryInto};
18use std::path::Path;
19use std::str::FromStr;
20use std::{env, fmt};
21
22pub mod find;
23pub mod list;
24pub mod set;
25pub mod show;
26pub mod verify;
27
28use crate::cli::custom_check_opts::CustomCheckOpts;
29use crate::cli::rust_releases_opts::Edition;
30use crate::cli::{CargoMsrvOpts, SubCommand};
31use crate::log_level::LogLevel;
32use crate::reporter::event::SelectedPackage;
33use crate::rust::default_target::default_target;
34pub use find::FindContext;
35pub use list::ListContext;
36pub use set::SetContext;
37pub use show::ShowContext;
38pub use verify::VerifyContext;
39
40#[derive(Debug)]
59pub enum Context {
60 Find(FindContext),
61 List(ListContext),
62 Set(SetContext),
63 Show(ShowContext),
64 Verify(VerifyContext),
65}
66
67impl Context {
68 pub fn reporting_name(&self) -> &'static str {
69 match self {
70 Context::Find(_) => "find",
71 Context::List(_) => "list",
72 Context::Set(_) => "set",
73 Context::Show(_) => "show",
74 Context::Verify(_) => "verify",
75 }
76 }
77
78 pub fn environment_context(&self) -> &EnvironmentContext {
79 match self {
80 Context::Find(ctx) => &ctx.environment,
81 Context::List(ctx) => &ctx.environment,
82 Context::Set(ctx) => &ctx.environment,
83 Context::Show(ctx) => &ctx.environment,
84 Context::Verify(ctx) => &ctx.environment,
85 }
86 }
87
88 pub fn to_find_context(self) -> Option<FindContext> {
90 if let Self::Find(ctx) = self {
91 Some(ctx)
92 } else {
93 None
94 }
95 }
96
97 pub fn to_verify_context(self) -> Option<VerifyContext> {
99 if let Self::Verify(ctx) = self {
100 Some(ctx)
101 } else {
102 None
103 }
104 }
105}
106
107impl TryFrom<CargoMsrvOpts> for Context {
108 type Error = CargoMSRVError;
109
110 fn try_from(opts: CargoMsrvOpts) -> Result<Self, Self::Error> {
111 let ctx = match opts.subcommand {
112 SubCommand::Find(_) => Self::Find(FindContext::try_from(opts)?),
113 SubCommand::List(_) => Self::List(ListContext::try_from(opts)?),
114 SubCommand::Set(_) => Self::Set(SetContext::try_from(opts)?),
115 SubCommand::Show => Self::Show(ShowContext::try_from(opts)?),
116 SubCommand::Verify(_) => Self::Verify(VerifyContext::try_from(opts)?),
117 };
118
119 Ok(ctx)
120 }
121}
122
123#[derive(Clone, Debug, Default)]
124pub struct RustReleasesContext {
125 pub minimum_rust_version: Option<BareVersion>,
127
128 pub maximum_rust_version: Option<BareVersion>,
130
131 pub consider_patch_releases: bool,
133
134 pub release_source: ReleaseSource,
136}
137
138impl From<RustReleasesOpts> for RustReleasesContext {
139 fn from(opts: RustReleasesOpts) -> Self {
140 Self {
141 minimum_rust_version: opts.min.map(|min| min.as_bare_version()),
142 maximum_rust_version: opts.max,
143 consider_patch_releases: opts.include_all_patch_releases,
144 release_source: opts.release_source,
145 }
146 }
147}
148
149impl RustReleasesContext {
150 pub fn resolve_minimum_version(
154 &self,
155 env: &EnvironmentContext,
156 ) -> Result<Option<BareVersion>, CargoMSRVError> {
157 if let Some(min) = &self.minimum_rust_version {
159 return Ok(Some(min.clone()));
160 }
161
162 let manifest = env.manifest();
164 let contents = std::fs::read_to_string(&manifest).map_err(|error| IoError {
165 error,
166 source: IoErrorSource::ReadFile(manifest.clone()),
167 })?;
168
169 let document = contents
170 .parse::<toml_edit::DocumentMut>()
171 .map_err(CargoMSRVError::ParseToml)?;
172
173 if let Some(edition) = document
174 .as_table()
175 .get("package")
176 .and_then(toml_edit::Item::as_table)
177 .and_then(|package_table| package_table.get("edition"))
178 .and_then(toml_edit::Item::as_str)
179 {
180 let edition = edition.parse::<Edition>()?;
181
182 return Ok(Some(edition.as_bare_version()));
183 }
184
185 Ok(None)
186 }
187}
188
189#[derive(Debug)]
190pub struct ToolchainContext {
191 pub target: &'static str,
193
194 pub components: &'static [&'static str],
196}
197
198impl TryFrom<ToolchainOpts> for ToolchainContext {
199 type Error = CargoMSRVError;
200
201 fn try_from(opts: ToolchainOpts) -> Result<Self, Self::Error> {
202 let target = if let Some(target) = opts.target {
203 target
204 } else {
205 default_target()?
206 };
207
208 let target: &'static str = String::leak(target);
209
210 let components: &'static [&'static str] = Vec::leak(
211 opts.component
212 .into_iter()
213 .map(|s| {
214 let s: &'static str = String::leak(s);
215 s
216 })
217 .collect(),
218 );
219
220 Ok(Self { target, components })
221 }
222}
223
224#[derive(Debug)]
225pub struct CheckCommandContext {
226 pub cargo_features: Option<Vec<String>>,
227
228 pub cargo_all_features: bool,
229
230 pub cargo_no_default_features: bool,
231
232 pub rustup_command: Option<Vec<String>>,
234}
235
236impl From<CustomCheckOpts> for CheckCommandContext {
237 fn from(opts: CustomCheckOpts) -> Self {
238 Self {
239 cargo_features: opts.features,
240 cargo_all_features: opts.all_features,
241 cargo_no_default_features: opts.no_default_features,
242 rustup_command: opts.custom_check_opts,
243 }
244 }
245}
246
247#[derive(Clone, Debug)]
248pub struct EnvironmentContext {
249 pub root_crate_path: Utf8PathBuf,
256
257 pub workspace_packages: WorkspacePackages,
259}
260
261impl<'shared_opts> TryFrom<&'shared_opts SharedOpts> for EnvironmentContext {
262 type Error = CargoMSRVError;
263
264 fn try_from(opts: &'shared_opts SharedOpts) -> Result<Self, Self::Error> {
265 let path = if let Some(path) = opts.path.as_ref() {
266 Ok(path.clone())
269 } else if let Some(path) = opts.manifest_path.as_ref() {
270 dunce::canonicalize(path)
274 .map_err(|_| CargoMSRVError::Path(PathError::DoesNotExist(path.to_path_buf())))
275 .and_then(|p| {
276 p.parent().map(Path::to_path_buf).ok_or_else(|| {
277 CargoMSRVError::Path(PathError::NoParent(path.to_path_buf()))
278 })
279 })
280 } else {
281 env::current_dir().map_err(|error| {
283 CargoMSRVError::Io(IoError {
284 error,
285 source: IoErrorSource::CurrentDir,
286 })
287 })
288 }?;
289
290 let root_crate_path: Utf8PathBuf = path.try_into().map_err(|err| {
291 CargoMSRVError::Path(PathError::InvalidUtf8(InvalidUtf8Error::from(err)))
292 })?;
293
294 let workspace_packages = if let Ok(metadata) = cargo_metadata::MetadataCommand::new()
299 .manifest_path(root_crate_path.join("Cargo.toml"))
300 .exec()
301 {
302 let partition = opts.workspace.partition_packages(&metadata);
303 let selected = partition.0.into_iter().cloned().collect();
304 let excluded = partition.1;
305
306 info!(
307 action = "detect_cargo_workspace_packages",
308 method = "cargo_metadata",
309 success = true,
310 ?selected,
311 ?excluded
312 );
313
314 WorkspacePackages::from_vec(selected)
315 } else {
316 info!(
317 action = "detect_cargo_workspace_packages",
318 method = "cargo_metadata",
319 success = false,
320 );
321
322 WorkspacePackages::default()
323 };
324
325 Ok(Self {
326 root_crate_path,
327 workspace_packages,
328 })
329 }
330}
331
332impl EnvironmentContext {
333 pub fn root(&self) -> &Utf8Path {
335 &self.root_crate_path
336 }
337
338 pub fn manifest(&self) -> Utf8PathBuf {
340 self.root_crate_path.join("Cargo.toml")
341 }
342
343 pub fn lock(&self) -> Utf8PathBuf {
345 self.root_crate_path.join("Cargo.lock")
346 }
347}
348
349#[derive(Clone, Debug, Default)]
352pub struct WorkspacePackages {
353 selected: Option<Vec<cargo_metadata::Package>>,
354}
355
356impl WorkspacePackages {
357 pub fn from_vec(selected: Vec<cargo_metadata::Package>) -> Self {
358 Self {
359 selected: Some(selected),
360 }
361 }
362
363 pub fn selected(&self) -> Option<Vec<SelectedPackage>> {
364 self.selected.as_deref().map(|pks| {
365 pks.iter()
366 .map(|pkg| SelectedPackage {
367 name: pkg.name.to_string(),
368 path: pkg.manifest_path.to_path_buf(),
369 })
370 .collect()
371 })
372 }
373
374 pub fn use_default_package(&self) -> bool {
381 self.selected_packages().is_empty()
382 }
383
384 pub fn selected_packages(&self) -> &[cargo_metadata::Package] {
388 self.selected.as_deref().unwrap_or_default()
389 }
390}
391
392#[derive(Clone, Copy, Debug, Default, PartialEq, ValueEnum)]
393pub enum OutputFormat {
394 #[default]
396 Human,
397 Json,
399 Minimal,
401 #[value(skip)]
403 None,
404}
405
406impl fmt::Display for OutputFormat {
407 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
408 match self {
409 Self::Human => write!(f, "human"),
410 Self::Json => write!(f, "json"),
411 Self::Minimal => write!(f, "minimal"),
412 Self::None => write!(f, "none"),
413 }
414 }
415}
416
417impl FromStr for OutputFormat {
418 type Err = CargoMSRVError;
419
420 fn from_str(s: &str) -> Result<Self, Self::Err> {
421 match s {
422 "human" => Ok(Self::Human),
423 "json" => Ok(Self::Json),
424 "minimal" => Ok(Self::Minimal),
425 unknown => Err(CargoMSRVError::InvalidConfig(format!(
426 "Given output format '{}' is not valid",
427 unknown
428 ))),
429 }
430 }
431}
432
433#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, serde::Serialize, ValueEnum)]
434#[serde(rename_all = "snake_case")]
435pub enum ReleaseSource {
436 #[default]
437 RustChangelog,
438 #[cfg(feature = "rust-releases-dist-source")]
439 RustDist,
440}
441
442impl FromStr for ReleaseSource {
443 type Err = CargoMSRVError;
444
445 fn from_str(s: &str) -> Result<Self, Self::Err> {
446 s.try_into()
447 }
448}
449
450impl From<ReleaseSource> for &'static str {
451 fn from(value: ReleaseSource) -> Self {
452 match value {
453 ReleaseSource::RustChangelog => "rust-changelog",
454 #[cfg(feature = "rust-releases-dist-source")]
455 ReleaseSource::RustDist => "rust-dist",
456 }
457 }
458}
459
460impl TryFrom<&str> for ReleaseSource {
461 type Error = CargoMSRVError;
462
463 fn try_from(source: &str) -> Result<Self, Self::Error> {
464 match source {
465 "rust-changelog" => Ok(Self::RustChangelog),
466 #[cfg(feature = "rust-releases-dist-source")]
467 "rust-dist" => Ok(Self::RustDist),
468 s => Err(CargoMSRVError::RustReleasesSourceParseError(s.to_string())),
469 }
470 }
471}
472
473impl fmt::Display for ReleaseSource {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 match self {
476 Self::RustChangelog => write!(f, "rust-changelog"),
477 #[cfg(feature = "rust-releases-dist-source")]
478 Self::RustDist => write!(f, "rust-dist"),
479 }
480 }
481}
482
483#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, serde::Serialize)]
484#[serde(rename_all = "snake_case")]
485pub enum SearchMethod {
486 Linear,
487 #[default]
488 Bisect,
489}
490
491impl From<SearchMethod> for &'static str {
492 fn from(method: SearchMethod) -> Self {
493 match method {
494 SearchMethod::Linear => "linear",
495 SearchMethod::Bisect => "bisect",
496 }
497 }
498}
499
500#[derive(Debug, Clone)]
501pub struct TracingOptions {
502 target: TracingTargetOption,
503 level: LogLevel,
504}
505
506impl TracingOptions {
507 pub fn new(target: TracingTargetOption, level: LogLevel) -> Self {
508 Self { target, level }
509 }
510}
511
512impl Default for TracingOptions {
513 fn default() -> Self {
514 Self {
515 target: TracingTargetOption::File,
516 level: LogLevel::default(),
517 }
518 }
519}
520
521impl TracingOptions {
522 pub fn target(&self) -> &TracingTargetOption {
523 &self.target
524 }
525
526 pub fn level(&self) -> &LogLevel {
527 &self.level
528 }
529}
530
531#[derive(Debug, Copy, Clone, ValueEnum)]
532pub enum TracingTargetOption {
533 File,
534 Stdout,
535}
536
537impl Default for TracingTargetOption {
538 fn default() -> Self {
539 Self::File
540 }
541}
542
543impl TracingTargetOption {
544 pub const FILE: &'static str = "file";
545 pub const STDOUT: &'static str = "stdout";
546}
547
548impl FromStr for TracingTargetOption {
549 type Err = CargoMSRVError;
550
551 fn from_str(s: &str) -> Result<Self, Self::Err> {
552 match s {
553 Self::FILE => Ok(Self::File),
554 Self::STDOUT => Ok(Self::Stdout),
555 unknown => Err(CargoMSRVError::InvalidConfig(format!(
556 "Given log target '{}' is not valid",
557 unknown
558 ))),
559 }
560 }
561}