#![allow(rustdoc::bare_urls)]
use clap::{Parser, Subcommand, ValueEnum};
use semver::{Version, VersionReq};
use std::error::Error;
use std::io;
mod misc;
mod regex;
mod results;
use misc::*;
use results::*;
#[derive(ValueEnum, Clone, Copy, Debug)]
pub enum SemverComponent {
Major,
Minor,
Patch,
PreRelease,
BuildMetadata,
}
impl From<SemverComponent> for SelectComponent {
fn from(c: SemverComponent) -> Self {
match c {
SemverComponent::Major => SelectComponent::Major,
SemverComponent::Minor => SelectComponent::Minor,
SemverComponent::Patch => SelectComponent::Patch,
SemverComponent::PreRelease => SelectComponent::PreRelease,
SemverComponent::BuildMetadata => SelectComponent::BuildMetadata,
}
}
}
#[derive(Parser, Debug)]
#[command(version, about, long_about = None)]
pub struct Args {
#[command(subcommand)]
cmd: Commands,
#[clap(long, short = 'o', value_enum, default_value_t=OutputFormat::Yaml)]
out: OutputFormat,
}
#[derive(Subcommand, Debug, Clone)]
pub enum Commands {
Explain { semantic_version: Version },
Compare {
#[clap(long, short = 'e', action)]
set_exit_status: bool,
#[clap(long, short = 's', action)]
semantic_exit_status: bool,
a: Version,
b: Version,
},
Sort {
#[clap(long, short = 'f', default_value = None)]
filter: Option<VersionReq>,
#[clap(long, action)]
lexical_sorting: bool,
#[clap(long, short = 'r', action)]
reverse: bool,
#[clap(long, action)]
flatten: bool,
#[clap(long, action)]
fail_if_potentially_ambiguous: bool,
versions: Option<Vec<Version>>,
},
FilterTest {
filter: VersionReq,
semantic_version: Version,
},
Validate {
version: String,
#[clap(long, short = 's', action)]
small: bool,
},
Generate {
#[clap(long, short = 's', action)]
small: bool,
#[clap(default_value_t = 1)]
count: usize,
},
Set {
semantic_version: Version,
#[clap(long, action)]
set_major: Option<u64>,
#[clap(long, action)]
set_minor: Option<u64>,
#[clap(long, action)]
set_patch: Option<u64>,
#[clap(long, action)]
set_pre_release: Option<String>,
#[clap(long, action)]
set_build_metadata: Option<String>,
},
Bump {
semantic_version: Version,
#[clap(long, action)]
bump_major: Option<u64>,
#[clap(long, action)]
bump_minor: Option<u64>,
#[clap(long, action)]
bump_patch: Option<u64>,
},
Select {
component: SemverComponent,
version: String,
#[clap(long, short = 's', action)]
small: bool,
#[clap(long, short = 'F', action)]
fail_if_not_found: bool,
},
}
fn main() -> Result<ExitOutcome, Box<dyn Error>> {
let args = Args::parse();
let mut ignore_exit_status_from_output = false;
let result: SubcommandResult = match args.cmd {
Commands::Explain { semantic_version } => explain(&semantic_version).into(),
Commands::Compare {
set_exit_status,
semantic_exit_status,
a,
b,
} => {
if !set_exit_status {
ignore_exit_status_from_output = true;
}
let res = compare(&a, &b);
if semantic_exit_status && res.semantic_ordering() == &SerializableOrdering::Equal {
ignore_exit_status_from_output = true
}
res.into()
}
Commands::Sort {
versions,
filter,
lexical_sorting,
reverse,
flatten,
fail_if_potentially_ambiguous,
} => {
let mut parsed_versions = Vec::new();
match versions {
Some(versions) => parsed_versions = versions,
None => {
let lines = io::stdin().lines();
for (line_no, line) in lines.enumerate() {
match line {
Ok(line) => {
let line = line.trim();
parsed_versions.push(Version::parse(line)
.map_err(|e| {
eprintln!("unable to parse an enumerated version: line {line_no}: {line}: {e}");
e
})?);
Ok(())
}
Err(e) => {
eprintln!("unable to read from stdin: {e}");
Err(ApplicationError::InvalidArgument {
expected: "to be able to read from stdin".to_string(),
found: e.to_string(),
})
}
}?
}
}
}
let mut ordered_version_list =
sort(&mut parsed_versions, &filter, lexical_sorting, reverse);
if fail_if_potentially_ambiguous && ordered_version_list.potentially_ambiguous() {
return Err(Box::new(misc::ApplicationError::FailedRequirementError {
err: "Potential Ambiguity Detected".to_string(),
}));
}
if flatten {
FlatVersionsList::from(&mut ordered_version_list).into()
} else {
ordered_version_list.into()
}
}
Commands::FilterTest {
filter,
semantic_version,
} => filter_test(&filter, &semantic_version).into(),
Commands::Validate { version, small } => validate(version, small).into(),
Commands::Generate { small, count } => generate(small, count).into(),
Commands::Set {
semantic_version,
set_major,
set_minor,
set_patch,
set_pre_release,
set_build_metadata,
} => set(
&semantic_version,
set_major,
set_minor,
set_patch,
set_pre_release,
set_build_metadata,
)?
.into(),
Commands::Bump {
semantic_version,
bump_major,
bump_minor,
bump_patch,
} => bump(&semantic_version, bump_major, bump_minor, bump_patch)?.into(),
Commands::Select {
component,
version,
small,
fail_if_not_found,
} => select(version.as_str(), component, small, fail_if_not_found)?.into(),
};
match args.out {
OutputFormat::Text => print!("{result}"),
OutputFormat::Yaml => {
println!("---");
let yaml = serde_yaml::to_string(&result)
.map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() })?;
print!("{yaml}");
}
OutputFormat::Json => {
let json = serde_json::to_string(&result)
.map_err(|e| ApplicationError::OutputFormatError { err: e.to_string() })?;
println!("{json}");
}
}
Ok(ExitOutcome::new(result, ignore_exit_status_from_output))
}
fn sort(
versions: &mut Vec<Version>,
filter: &Option<VersionReq>,
lexical_sorting: bool,
reverse: bool,
) -> OrderedVersionMap {
OrderedVersionMap::new(versions, filter, lexical_sorting, reverse)
}
fn compare(a: &Version, b: &Version) -> ComparisonStatement {
ComparisonStatement::new(a, b)
}
fn explain(v: &Version) -> VersionExplanation {
VersionExplanation::from(v)
}
fn filter_test(filter: &VersionReq, semantic_version: &Version) -> FilterTestResult {
FilterTestResult::filter_test(filter, semantic_version)
}
fn validate(semantic_version: String, small: bool) -> ValidateResult {
ValidateResult::validate(semantic_version, small)
}
fn generate(small: bool, count: usize) -> GenerateResult {
GenerateResult::new(small, count)
}
fn set(
version: &Version,
major: Option<u64>,
minor: Option<u64>,
patch: Option<u64>,
pre_release: Option<String>,
build_metadata: Option<String>,
) -> Result<VersionMutationResult, Box<dyn Error>> {
VersionMutationResult::set(version, major, minor, patch, pre_release, build_metadata)
}
fn bump(
version: &Version,
major: Option<u64>,
minor: Option<u64>,
patch: Option<u64>,
) -> Result<VersionMutationResult, Box<dyn Error>> {
VersionMutationResult::bump(version, major, minor, patch)
}
fn select(
version: &str,
component: SemverComponent,
small: bool,
fail_if_not_found: bool,
) -> Result<SelectResult, Box<dyn Error>> {
SelectResult::select(version, component.into(), small, fail_if_not_found)
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use proptest_semver::*;
use crate::{SemverComponent, SerializableOrdering, version_without_build_metadata};
proptest! {
#[test]
fn compare(a in arb_version(), b in arb_version()) {
let comparison = super::compare(&a, &b);
let a_no_build = version_without_build_metadata(&a);
let b_no_build = version_without_build_metadata(&b);
match a.cmp(&b) {
std::cmp::Ordering::Equal => {
prop_assert_eq!(a_no_build, b_no_build);
prop_assert!(comparison.semantic_ordering() == &SerializableOrdering::Equal);
},
std::cmp::Ordering::Greater => {
prop_assert!(comparison.lexical_ordering() == &SerializableOrdering::Greater || comparison.lexical_ordering() == &SerializableOrdering::Equal);
prop_assert_eq!(comparison.semantic_ordering(), &SerializableOrdering::Greater);
},
std::cmp::Ordering::Less => {
prop_assert!(comparison.lexical_ordering() == &SerializableOrdering::Less || comparison.lexical_ordering() == &SerializableOrdering::Equal);
prop_assert_eq!(comparison.semantic_ordering(), &SerializableOrdering::Less);
},
};
}
#[test]
fn explain(version in arb_version()) {
super::explain(&version);
}
#[test]
fn validate(version in arb_semver(), small: bool) {
super::validate(version, small);
}
#[test]
fn filter_test(filter in arb_version_req(MAX_COMPARATORS_IN_VERSION_REQ_STRING), version in arb_version()) {
super::filter_test(&filter, &version);
}
#[test]
fn sort(versions in arb_vec_versions(256), filter in arb_optional_version_req(0.5, MAX_COMPARATORS_IN_VERSION_REQ_STRING), lexical_sorting in any::<bool>(), reverse in any::<bool>()) {
let mut versions = versions.clone();
super::sort(&mut versions, &filter, lexical_sorting, reverse);
}
#[test]
fn generate(small: bool, count: u8) {
super::generate(small, count.into());
}
#[test]
fn select_small(version in arb_version(), component in prop_oneof![
Just(SemverComponent::Major),
Just(SemverComponent::Minor),
Just(SemverComponent::Patch),
Just(SemverComponent::PreRelease),
Just(SemverComponent::BuildMetadata),
]) {
let result = super::select(&version.to_string(), component, true, false).unwrap();
let _ = format!("{result}");
}
}
}