use std::borrow::Borrow;
use std::fmt;
use std::str::FromStr;
use anyhow::anyhow;
use serde::Serialize;
use crate::Result;
#[derive(Clone, Debug, Default)]
pub struct VersionInvalidPositionValueParser {}
impl clap::builder::TypedValueParser for VersionInvalidPositionValueParser {
type Value = Version;
fn parse_ref(
&self,
cmd: &clap::Command,
arg: Option<&clap::Arg>,
value: &std::ffi::OsStr,
) -> std::result::Result<Self::Value, clap::error::Error> {
let mut err = clap::Error::new(clap::error::ErrorKind::UnknownArgument)
.with_cmd(cmd);
if let Some(arg) = arg {
err.insert(
clap::error::ContextKind::InvalidArg,
clap::error::ContextValue::String(arg.to_string()));
}
err.insert(
clap::error::ContextKind::InvalidValue,
clap::error::ContextValue::String(
String::from_utf8_lossy(value.as_encoded_bytes()).to_string()));
err.insert(
clap::error::ContextKind::Suggested,
clap::error::ContextValue::StyledStrs(vec![
"--cli-version must be the very first argument".to_string().into()
]));
Err(err)
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize)]
pub struct Version {
major: usize,
minor: usize,
patch: usize,
}
impl Version {
pub const fn new(major: usize, minor: usize, patch: usize) -> Self {
Self {
major,
minor,
patch,
}
}
pub fn is_acceptable_for<V>(&self, wanted: V) -> bool
where V: Borrow<Self>
{
let wanted = wanted.borrow();
self.major == wanted.major &&
(self.minor > wanted.minor ||
(self.minor == wanted.minor && self.patch >= wanted.patch))
}
}
impl FromStr for Version {
type Err = anyhow::Error;
fn from_str(v: &str) -> Result<Self> {
let ints = parse_ints(v)?;
match ints.len() {
0 => Err(anyhow!("doesn't look like a version: {}", v)),
1 => Ok(Self::new(ints[0], 0, 0)),
2 => Ok(Self::new(ints[0], ints[1], 0)),
3 => Ok(Self::new(ints[0], ints[1], ints[2])),
_ => Err(anyhow!("too many components in version (at most three allowed): {}", v)),
}
}
}
impl fmt::Display for Version {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl PartialEq<Version> for &Version {
fn eq(&self, other: &Version) -> bool {
self.major == other.major &&
self.minor == other.minor &&
self.patch == other.patch
}
}
fn parse_ints(s: &str) -> Result<Vec<usize>> {
let mut ints = vec![];
let mut v = s;
while !v.is_empty() {
if let Some(i) = v.find('.') {
ints.push(parse_component(&v[..i])?);
v = &v[i+1..];
if v.is_empty() {
return Err(anyhow!("trailing dot in version: {}", s));
}
} else {
ints.push(parse_component(v)?);
v = "";
}
}
Ok(ints)
}
fn parse_component(s: &str) -> Result<usize> {
Ok(FromStr::from_str(s)?)
}
#[cfg(test)]
mod test {
use std::str::FromStr;
use super::Version;
#[test]
fn empty_string() {
assert!(Version::from_str("").is_err());
}
#[test]
fn not_int() {
assert!(Version::from_str("foo").is_err());
}
#[test]
fn not_int2() {
assert!(Version::from_str("1.foo").is_err());
}
#[test]
fn leading_dot() {
assert!(Version::from_str(".1").is_err());
}
#[test]
fn trailing_dot() {
assert!(Version::from_str("1.").is_err());
}
#[test]
fn one_int() {
assert_eq!(Version::from_str("1").unwrap(), Version::new(1, 0, 0));
}
#[test]
fn two_ints() {
assert_eq!(Version::from_str("1.2").unwrap(), Version::new(1, 2, 0));
}
#[test]
fn three_ints() {
assert_eq!(Version::from_str("1.2.3").unwrap(), Version::new(1, 2, 3));
}
#[test]
fn four_ints() {
assert!(Version::from_str("1.2.3.4").is_err());
}
#[test]
fn acceptable_if_same() {
let a = Version::new(0, 0, 0);
assert!(a.is_acceptable_for(a));
}
#[test]
fn acceptable_if_newer_patch() {
let wanted = Version::new(0, 0, 0);
let actual = Version::new(0, 0, 1);
assert!(actual.is_acceptable_for(wanted));
}
#[test]
fn not_acceptable_if_older_patch() {
let wanted = Version::new(0, 0, 1);
let actual = Version::new(0, 0, 0);
assert!(!actual.is_acceptable_for(wanted));
}
#[test]
fn acceptable_if_newer_minor() {
let wanted = Version::new(0, 0, 0);
let actual = Version::new(0, 1, 0);
assert!(actual.is_acceptable_for(wanted));
}
#[test]
fn not_acceptable_if_older_minor() {
let wanted = Version::new(0, 1, 0);
let actual = Version::new(0, 0, 0);
assert!(!actual.is_acceptable_for(wanted));
}
#[test]
fn not_acceptable_if_newer_major() {
let wanted = Version::new(0, 0, 0);
let actual = Version::new(1, 0, 0);
assert!(!actual.is_acceptable_for(wanted));
}
#[test]
fn not_acceptable_if_older_major() {
let wanted = Version::new(1, 0, 0);
let actual = Version::new(0, 0, 0);
assert!(!actual.is_acceptable_for(wanted));
}
}