#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![warn(clippy::cargo)]
use clap::{arg, command, Parser, Subcommand, ValueEnum};
use core::str::FromStr;
use nextver::prelude::*;
#[derive(thiserror::Error, Debug, PartialEq)]
enum NextVerCliError {
#[error(transparent)]
LibraryCompositeError(#[from] CompositeError),
#[error(transparent)]
LibraryFormatError(#[from] FormatError),
#[error(transparent)]
LibraryNextError(#[from] NextError),
#[error("format string was invalid for all schemes")]
NoValidScheme,
#[error("major semantic specifier level should not be used with calsem scheme")]
MajorSpecifierWithCalsem,
#[error("this scheme requires a semantic specifier, use `-l`/`--sem-level`")]
NoSemanticSpecifier,
}
#[derive(Clone, PartialEq, Eq, ValueEnum, Debug)]
enum SchemeArg {
Sem,
Cal,
CalSem,
Guess,
}
type Output = (String, ExitCode);
fn validate(
scheme: &SchemeArg,
format_str: &str,
version_str: &str,
) -> Result<Output, NextVerCliError> {
let is_valid = match scheme {
SchemeArg::Sem => Result::<_, NextVerCliError>::Ok(Sem::is_valid(format_str, version_str)?),
SchemeArg::Cal => Ok(Cal::is_valid(format_str, version_str)?),
SchemeArg::CalSem => Ok(CalSem::is_valid(format_str, version_str)?),
SchemeArg::Guess => {
let any = Sem::is_valid(format_str, version_str).unwrap_or(false)
|| Cal::is_valid(format_str, version_str).unwrap_or(false)
|| CalSem::is_valid(format_str, version_str).unwrap_or(false);
if !any {
return Err(NextVerCliError::NoValidScheme);
}
Ok(true)
}
}?;
if is_valid {
Ok((true.to_string(), ExitCode::Success))
} else {
Ok((false.to_string(), ExitCode::Failure))
}
}
fn next(
scheme: &SchemeArg,
format_str: &str,
version_str: &str,
date: Date,
spec: Option<&SemLevelArg>,
) -> Result<Output, NextVerCliError> {
let sem_spec = || {
spec.map(SemLevelArg::to_sem_level)
.ok_or(NextVerCliError::NoSemanticSpecifier)
};
let cal_sem_spec = || {
spec.map(SemLevelArg::to_calsem_specifier)
.transpose()?
.ok_or(NextVerCliError::NoSemanticSpecifier)
};
let next_version = match scheme {
SchemeArg::Sem => Sem::next_version_string(format_str, version_str, sem_spec()?)?,
SchemeArg::Cal => Cal::next_version_string(format_str, version_str, date)?,
SchemeArg::CalSem => {
CalSem::next_version_string(format_str, version_str, date, cal_sem_spec()?)?
}
SchemeArg::Guess => {
if let Ok(sem_ver) = Sem::new_version(format_str, version_str) {
sem_ver.next(sem_spec()?)?.to_string()
} else if let Ok(cal_ver) = Cal::new_version(format_str, version_str) {
cal_ver.next(date)?.to_string()
} else if let Ok(cal_sem_ver) = CalSem::new_version(format_str, version_str) {
cal_sem_ver.next(date, cal_sem_spec()?)?.to_string()
} else {
return Err(NextVerCliError::NoValidScheme);
}
}
};
Ok((next_version, ExitCode::Success))
}
#[derive(Clone, PartialEq, Eq, ValueEnum, Debug)]
enum SemLevelArg {
Major,
Minor,
Patch,
}
impl SemLevelArg {
fn to_sem_level(&self) -> SemLevel {
use SemLevelArg::{Major, Minor, Patch};
match self {
Major => SemLevel::Major,
Minor => SemLevel::Minor,
Patch => SemLevel::Patch,
}
}
fn to_calsem_specifier(&self) -> Result<CalSemLevel, NextVerCliError> {
use SemLevelArg::{Major, Minor, Patch};
match self {
Major => Err(NextVerCliError::MajorSpecifierWithCalsem),
Minor => Ok(CalSemLevel::Minor),
Patch => Ok(CalSemLevel::Patch),
}
}
}
const UNPARSEABLE_DATE_ERROR: &str = "Could not parse provided date as `utc`, `local`, or `Y-M-D`";
fn parse_date(s: &str) -> Result<Date, &'static str> {
match s {
"utc" => Ok(Date::utc_now()),
"local" => Ok(Date::local_now()),
ymd => Ok(Date::from_str(ymd).map_err(|_| UNPARSEABLE_DATE_ERROR)?),
}
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Option<Subcommands>,
}
#[derive(Subcommand, Debug)]
#[command(arg_required_else_help(true))]
enum Subcommands {
Valid {
version: String,
#[arg(short, long)]
format: String,
#[arg(short, long, value_enum, default_value_t=SchemeArg::Guess)]
scheme: SchemeArg,
},
Next {
version: String,
#[arg(short, long)]
format: String,
#[arg(short = 'l', long, value_enum)]
sem_level: Option<SemLevelArg>,
#[arg(short, long, value_name = "utc|local|Y-M-D", value_parser = parse_date, default_value = "utc")]
date: Date,
#[arg(short, long, value_enum, default_value_t=SchemeArg::Guess)]
scheme: SchemeArg,
},
}
#[derive(Debug, PartialEq)]
#[repr(u8)]
enum ExitCode {
Success = 0,
Failure = 1,
CliUsageError = 2,
}
impl From<&clap::error::Error> for ExitCode {
fn from(e: &clap::error::Error) -> Self {
match e.exit_code() {
0 => ExitCode::Success,
2 => ExitCode::CliUsageError,
_ => panic!("clap should only return exit codes 0-3"),
}
}
}
impl From<ExitCode> for std::process::ExitCode {
fn from(val: ExitCode) -> Self {
std::process::ExitCode::from(val as u8)
}
}
fn main() -> std::process::ExitCode {
let cli = Cli::parse();
match run(cli) {
Ok((output, exit_code)) => {
println!("{output}");
exit_code
}
Err(e) => {
eprintln!("{e}");
if e == NextVerCliError::NoSemanticSpecifier {
ExitCode::CliUsageError
} else {
ExitCode::Failure
}
}
}
.into()
}
fn run(cli: Cli) -> Result<Output, NextVerCliError> {
match cli.command {
Some(Subcommands::Valid {
format: format_str,
version: version_str,
scheme,
}) => validate(&scheme, &format_str, &version_str),
Some(Subcommands::Next {
format,
version,
sem_level: level,
date,
scheme,
}) => next(&scheme, &format, &version, date, level.as_ref()),
None => unreachable!("clap should catch this no-subcommand case"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_basic_calsem_date_diff() {
let res = Cli::try_parse_from([
"nextver",
"next",
"2024.07.0",
"--format",
"<YYYY>.<0W>.<PATCH>",
"--date",
"2024-02-26",
"--sem-level",
"patch",
])
.unwrap();
assert_eq!(Ok(("2024.08.0".to_string(), ExitCode::Success,)), run(res));
}
#[test]
fn test_basic_calsem_date_same() {
let res = Cli::try_parse_from([
"nextver",
"next",
"2024.08.0",
"--format",
"<YYYY>.<0W>.<PATCH>",
"--date",
"2024-02-26",
"--sem-level",
"patch",
])
.unwrap();
assert_eq!(Ok(("2024.08.1".to_string(), ExitCode::Success,)), run(res));
}
}