use crate::{Chunk, Chunks, Error, MChunk, Mess, Release, Sep};
use nom::character::complete::char;
use nom::combinator::opt;
use nom::{IResult, Parser};
use std::cmp::Ordering;
use std::cmp::Ordering::{Equal, Greater, Less};
use std::hash::Hash;
use std::str::FromStr;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
pub struct Version {
pub epoch: Option<u32>,
pub chunks: Chunks,
pub release: Option<Release>,
pub meta: Option<String>,
}
impl Version {
pub fn new<S>(s: S) -> Option<Version>
where
S: AsRef<str>,
{
match Version::parse(s.as_ref()) {
Ok(("", v)) => Some(v),
_ => None,
}
}
pub fn nth(&self, n: usize) -> Option<u32> {
self.chunks.0.get(n).and_then(Chunk::single_digit)
}
pub fn nth_lenient(&self, n: usize) -> Option<u32> {
self.chunks.0.get(n).and_then(Chunk::single_digit_lenient)
}
pub fn to_mess(&self) -> Mess {
match self.epoch {
None => self.to_mess_continued(),
Some(e) => {
let chunks = vec![MChunk::Digits(e, e.to_string())];
let next = Some((Sep::Colon, Box::new(self.to_mess_continued())));
Mess { chunks, next }
}
}
}
fn to_mess_continued(&self) -> Mess {
let chunks = self.chunks.0.iter().map(|c| c.mchunk()).collect();
let next = self.release.as_ref().map(|cs| {
let chunks = cs.0.iter().map(|c| c.mchunk()).collect();
(Sep::Hyphen, Box::new(Mess { chunks, next: None }))
});
Mess { chunks, next }
}
pub(crate) fn cmp_mess(&self, other: &Mess) -> Ordering {
match self.epoch {
Some(e) if e > 0 && other.chunks.len() == 1 => match &other.next {
None => Greater,
Some((Sep::Colon, m)) => match other.nth(0) {
None => Greater,
Some(me) => match e.cmp(&me) {
Equal => Version::cmp_mess_continued(self, m),
ord => ord,
},
},
Some(_) => Greater,
},
Some(e) if e > 0 => Greater,
_ => Version::cmp_mess_continued(self, other),
}
}
fn cmp_mess_continued(&self, other: &Mess) -> Ordering {
(0..)
.find_map(
|n| match self.nth(n).and_then(|x| other.nth(n).map(|y| x.cmp(&y))) {
None => Some(self.to_mess().cmp(other)),
Some(Greater) => Some(Greater),
Some(Less) => Some(Less),
Some(Equal) => None,
},
)
.unwrap_or_else(|| self.to_mess().cmp(other))
}
pub fn parse(i: &str) -> IResult<&str, Version> {
let (i, epoch) = opt(Version::epoch).parse(i)?;
let (i, chunks) = Chunks::parse(i)?;
let (i, release) = opt(Release::parse).parse(i)?;
let (i, meta) = opt(crate::parsers::meta).parse(i)?;
let v = Version {
epoch,
chunks,
meta,
release,
};
Ok((i, v))
}
fn epoch(i: &str) -> IResult<&str, u32> {
let (i, epoch) = crate::parsers::unsigned(i)?;
let (i, _) = char(':')(i)?;
Ok((i, epoch))
}
pub(crate) fn matches_tilde(&self, other: &Version) -> bool {
if self.chunks.0.len() != other.chunks.0.len() {
false
} else {
let inits_equal = self
.chunks
.0
.iter()
.rev()
.skip(1)
.rev()
.zip(other.chunks.0.iter().rev().skip(1).rev())
.all(|(a, b)| a == b);
let last_good = match (self.chunks.0.last(), other.chunks.0.last()) {
(Some(Chunk::Alphanum(_)), Some(Chunk::Alphanum(_))) => true,
(Some(Chunk::Numeric(n1)), Some(Chunk::Numeric(n2))) => n2 >= n1,
_ => false,
};
inits_equal && last_good
}
}
pub(crate) fn matches_caret(&self, other: &Version) -> bool {
let mut got_first_nonzero = false;
for (v1_chunk, v2_chunk) in self.chunks.0.iter().zip(other.chunks.0.iter()) {
if !got_first_nonzero {
if !v1_chunk.single_digit().is_some_and(|n| n == 0) {
got_first_nonzero = true;
if v1_chunk != v2_chunk {
return false;
}
}
} else if v2_chunk.cmp_lenient(v1_chunk).is_lt() {
return false;
}
}
true
}
}
impl PartialOrd for Version {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Version {
fn cmp(&self, other: &Self) -> Ordering {
let ae = self.epoch.unwrap_or(0);
let be = other.epoch.unwrap_or(0);
match ae.cmp(&be) {
Equal => match self.chunks.cmp(&other.chunks) {
Equal => self.release.cmp(&other.release),
ord => ord,
},
ord => ord,
}
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
if let Some(e) = self.epoch {
write!(f, "{}:", e)?;
}
write!(f, "{}", self.chunks)?;
if let Some(r) = &self.release {
write!(f, "-{}", r)?;
}
if let Some(m) = &self.meta {
write!(f, "+{}", m)?;
}
Ok(())
}
}
impl FromStr for Version {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Version::new(s).ok_or_else(|| Error::IllegalVersion(s.to_string()))
}
}
impl TryFrom<&str> for Version {
type Error = Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Version::from_str(value)
}
}