use crate::errors::{CargoMSRVError, TResult};
use clap::ArgMatches;
use rust_releases::semver;
use std::convert::TryFrom;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Copy)]
pub enum OutputFormat {
Human,
Json,
None,
TestSuccesses,
}
impl Default for OutputFormat {
fn default() -> Self {
Self::Human
}
}
pub fn test_config_from_matches<'a>(matches: &'a ArgMatches<'a>) -> TResult<Config<'a>> {
let mut config = Config::try_from(matches)?;
config.output_format = OutputFormat::None;
Ok(config)
}
#[derive(Debug, Clone, Copy)]
pub enum ModeIntent {
DetermineMSRV,
VerifyMSRV,
}
impl From<ModeIntent> for &'static str {
fn from(action: ModeIntent) -> Self {
match action {
ModeIntent::DetermineMSRV => "determine-msrv",
ModeIntent::VerifyMSRV => "verify-msrv",
}
}
}
#[derive(Debug, Clone, Copy)]
pub enum ReleaseSource {
RustChangelog,
RustDist,
}
impl TryFrom<&str> for ReleaseSource {
type Error = CargoMSRVError;
fn try_from(source: &str) -> Result<Self, Self::Error> {
match source {
"rust-changelog" => Ok(Self::RustChangelog),
"rust-dist" => Ok(Self::RustDist),
s => Err(CargoMSRVError::RustReleasesSourceParseError(s.to_string())),
}
}
}
#[derive(Debug, Clone)]
pub struct Config<'a> {
mode_intent: ModeIntent,
target: String,
check_command: Vec<&'a str>,
crate_path: Option<PathBuf>,
include_all_patch_releases: bool,
minimum_version: Option<semver::Version>,
maximum_version: Option<semver::Version>,
bisect: bool,
output_toolchain_file: bool,
ignore_lockfile: bool,
output_format: OutputFormat,
release_source: ReleaseSource,
no_tracing: bool,
no_read_min_edition: Option<semver::Version>,
}
impl<'a> Config<'a> {
pub fn new(mode_intent: ModeIntent, target: String) -> Self {
Self {
mode_intent,
target,
check_command: vec!["cargo", "check", "--all"],
crate_path: None,
include_all_patch_releases: false,
minimum_version: None,
maximum_version: None,
bisect: false,
output_toolchain_file: false,
ignore_lockfile: false,
output_format: OutputFormat::Human,
release_source: ReleaseSource::RustChangelog,
no_tracing: false,
no_read_min_edition: None,
}
}
pub fn action_intent(&self) -> ModeIntent {
self.mode_intent
}
pub fn target(&self) -> &String {
&self.target
}
pub fn check_command(&self) -> &Vec<&'a str> {
&self.check_command
}
pub fn check_command_string(&self) -> String {
self.check_command.join(" ")
}
pub fn crate_path(&self) -> Option<&Path> {
self.crate_path.as_deref()
}
pub fn include_all_patch_releases(&self) -> bool {
self.include_all_patch_releases
}
pub fn minimum_version(&self) -> Option<&semver::Version> {
self.minimum_version.as_ref()
}
pub fn maximum_version(&self) -> Option<&semver::Version> {
self.maximum_version.as_ref()
}
pub fn bisect(&self) -> bool {
self.bisect
}
pub fn output_toolchain_file(&self) -> bool {
self.output_toolchain_file
}
pub fn ignore_lockfile(&self) -> bool {
self.ignore_lockfile
}
pub fn output_format(&self) -> OutputFormat {
self.output_format
}
pub fn release_source(&self) -> ReleaseSource {
self.release_source
}
pub fn no_tracing(&self) -> bool {
self.no_tracing
}
pub fn no_read_min_version(&self) -> Option<&semver::Version> {
self.no_read_min_edition.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct ConfigBuilder<'a> {
inner: Config<'a>,
}
impl<'a> ConfigBuilder<'a> {
pub fn new(action_intent: ModeIntent, default_target: &str) -> Self {
Self {
inner: Config::new(action_intent, default_target.to_string()),
}
}
pub fn mode_intent(mut self, mode_intent: ModeIntent) -> Self {
self.inner.mode_intent = mode_intent;
self
}
pub fn target(mut self, target: &str) -> Self {
self.inner.target = target.to_string();
self
}
pub fn check_command(mut self, cmd: Vec<&'a str>) -> Self {
self.inner.check_command = cmd;
self
}
pub fn crate_path<P: AsRef<Path>>(mut self, path: Option<P>) -> Self {
self.inner.crate_path = path.map(|p| PathBuf::from(p.as_ref()));
self
}
pub fn include_all_patch_releases(mut self, answer: bool) -> Self {
self.inner.include_all_patch_releases = answer;
self
}
pub fn minimum_version(mut self, version: semver::Version) -> Self {
self.inner.minimum_version = Some(version);
self
}
pub fn maximum_version(mut self, version: semver::Version) -> Self {
self.inner.maximum_version = Some(version);
self
}
pub fn bisect(mut self, answer: bool) -> Self {
self.inner.bisect = answer;
self
}
pub fn output_toolchain_file(mut self, choice: bool) -> Self {
self.inner.output_toolchain_file = choice;
self
}
pub fn ignore_lockfile(mut self, choice: bool) -> Self {
self.inner.ignore_lockfile = choice;
self
}
pub fn output_format(mut self, output_format: OutputFormat) -> Self {
self.inner.output_format = output_format;
self
}
pub fn release_source(mut self, release_source: ReleaseSource) -> Self {
self.inner.release_source = release_source;
self
}
pub fn no_tracing(mut self, choice: bool) -> Self {
self.inner.no_tracing = choice;
self
}
pub fn no_read_min_edition(mut self, version: semver::Version) -> Self {
self.inner.no_read_min_edition = Some(version);
self
}
pub fn build(self) -> Config<'a> {
self.inner
}
}
impl<'config> TryFrom<&'config ArgMatches<'config>> for Config<'config> {
type Error = CargoMSRVError;
fn try_from(matches: &'config ArgMatches<'config>) -> Result<Self, Self::Error> {
use crate::cli::id;
use crate::fetch::default_target;
let arg_matches = matches
.subcommand_matches(id::SUB_COMMAND_MSRV)
.ok_or(CargoMSRVError::UnableToParseCliArgs)?;
let action_intent = if arg_matches.is_present(id::ARG_VERIFY) {
ModeIntent::VerifyMSRV
} else {
ModeIntent::DetermineMSRV
};
let target = default_target()?;
let mut builder = ConfigBuilder::new(action_intent, &target);
let check_cmd = arg_matches.values_of(id::ARG_CUSTOM_CHECK);
if let Some(cmd) = check_cmd {
builder = builder.check_command(cmd.collect());
}
let crate_path = arg_matches.value_of(id::ARG_SEEK_PATH);
builder = builder.crate_path(crate_path);
let custom_target = arg_matches.value_of(id::ARG_SEEK_CUSTOM_TARGET);
if let Some(target) = custom_target {
builder = builder.target(target);
}
match arg_matches.value_of(id::ARG_MIN) {
Some(min) => builder = builder.minimum_version(parse_version(min)?),
None if arg_matches.is_present(id::ARG_NO_READ_MIN_EDITION) => {}
None => {
let crate_folder = if let Some(ref path) = builder.inner.crate_path {
Ok(path.to_path_buf())
} else {
std::env::current_dir().map_err(CargoMSRVError::Io)
}?;
let cargo_toml = crate_folder.join("Cargo.toml");
let contents = std::fs::read_to_string(&cargo_toml).map_err(CargoMSRVError::Io)?;
let document = decent_toml_rs_alternative::parse_toml(&contents)
.map_err(CargoMSRVError::ParseToml)?;
if let Some(edition) = document
.get("package")
.and_then(|field| field.get("edition"))
.and_then(|value| value.as_string())
{
builder = builder.minimum_version(parse_version(edition.as_str())?)
}
}
}
if let Some(max) = arg_matches.value_of(id::ARG_MAX) {
builder = builder.maximum_version(rust_releases::semver::Version::parse(max)?)
}
builder = builder.bisect(arg_matches.is_present(id::ARG_BISECT));
builder = builder
.include_all_patch_releases(arg_matches.is_present(id::ARG_INCLUDE_ALL_PATCH_RELEASES));
builder = builder.output_toolchain_file(arg_matches.is_present(id::ARG_TOOLCHAIN_FILE));
builder = builder.ignore_lockfile(arg_matches.is_present(id::ARG_IGNORE_LOCKFILE));
let output_format = arg_matches.value_of(id::ARG_OUTPUT_FORMAT);
if let Some(output_format) = output_format {
let output_format = match output_format {
"json" => OutputFormat::Json,
_ => unreachable!(),
};
builder = builder.output_format(output_format);
}
let release_source = arg_matches.value_of(id::ARG_RELEASE_SOURCE);
if let Some(release_source) = release_source {
let release_source = ReleaseSource::try_from(release_source)?;
builder = builder.release_source(release_source);
}
builder = builder.no_tracing(arg_matches.is_present(id::ARG_NO_LOG));
Ok(builder.build())
}
}
fn parse_version(input: &str) -> Result<semver::Version, semver::Error> {
match input {
"2015" => Ok(semver::Version::new(1, 0, 0)),
"2018" => Ok(semver::Version::new(1, 31, 0)),
"2021" => Ok(semver::Version::new(1, 56, 0)),
s => semver::Version::parse(s),
}
}
#[cfg(test)]
mod tests {
use parameterized::parameterized;
use rust_releases::semver::Version;
#[parameterized(
input = {
"1.35.0",
"2015",
"2018",
"2021",
},
expected_version = {
Version::new(1,35,0),
Version::new(1,0,0),
Version::new(1,31,0),
Version::new(1,56,0),
}
)]
fn parse_version(input: &str, expected_version: Version) {
let version = super::super::parse_version(input).unwrap();
assert_eq!(version, expected_version)
}
}