use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct CanonVersion {
pub major: u64,
pub minor: u64,
pub patch: u64,
}
impl CanonVersion {
#[must_use]
pub fn current() -> Self {
Self::from_str(env!("CARGO_PKG_VERSION")).unwrap_or(Self {
major: 0,
minor: 0,
patch: 0,
})
}
}
impl fmt::Display for CanonVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("'{0}' is not a semantic version")]
pub struct VersionError(String);
impl FromStr for CanonVersion {
type Err = VersionError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = || VersionError(s.to_string());
let mut parts = s.split('.');
let mut next = || -> Result<u64, VersionError> {
let part = parts.next().ok_or_else(err)?;
if part.is_empty() || !part.bytes().all(|b| b.is_ascii_digit()) {
return Err(err());
}
part.parse().map_err(|_| err())
};
let version = Self {
major: next()?,
minor: next()?,
patch: next()?,
};
if parts.next().is_some() {
return Err(err());
}
Ok(version)
}
}
impl Serialize for CanonVersion {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for CanonVersion {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_a_triple() {
let v: CanonVersion = "0.2.0".parse().unwrap();
assert_eq!((v.major, v.minor, v.patch), (0, 2, 0));
assert_eq!(v.to_string(), "0.2.0");
}
#[test]
fn rejects_non_triples() {
for bad in ["", "1.2", "1.2.3.4", "v1.2.3", "1.2.x", "1..3", "1.2.3-rc1"] {
assert!(bad.parse::<CanonVersion>().is_err(), "accepted {bad:?}");
}
}
#[test]
fn orders_numerically() {
let a: CanonVersion = "0.9.0".parse().unwrap();
let b: CanonVersion = "0.10.0".parse().unwrap();
assert!(a < b);
}
#[test]
fn current_matches_cargo_version() {
assert_eq!(
CanonVersion::current().to_string(),
env!("CARGO_PKG_VERSION")
);
}
#[test]
fn serde_round_trips_as_a_string() {
let v: CanonVersion = "1.4.2".parse().unwrap();
let json = serde_json::to_string(&v).unwrap();
assert_eq!(json, "\"1.4.2\"");
assert_eq!(serde_json::from_str::<CanonVersion>(&json).unwrap(), v);
}
}