#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Semver {
major: u8,
minor: u8,
patch: u8,
pre: u8,
}
impl Semver {
pub const fn pack(self) -> u64 {
(self.major as u64) << 24
| (self.minor as u64) << 16
| (self.patch as u64) << 8
| (self.pre as u64)
}
pub const fn unpack(v: u64) -> Self {
Self {
major: (v >> 24) as u8,
minor: (v >> 16) as u8,
patch: (v >> 8) as u8,
pre: v as u8,
}
}
#[allow(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Option<Self> {
if let Some((main, pre)) = s.split_once('-') {
let mut main_it = main.split('.');
let major: u8 = main_it.next()?.parse().ok()?;
let minor: u8 = main_it.next()?.parse().ok()?;
let patch: u8 = main_it.next()?.parse().ok()?;
if main_it.next().is_some() {
return None;
}
let (pre_text, pre_num) = pre.split_once('.')?;
if pre_text != "pre" {
return None;
}
let pre: u8 = pre_num.parse().ok()?;
Some(Self {
major,
minor,
patch,
pre,
})
} else {
let mut it = s.split('.');
let major: u8 = it.next()?.parse().ok()?;
let minor: u8 = it.next()?.parse().ok()?;
let patch: u8 = it.next()?.parse().ok()?;
if it.next().is_some() {
return None;
}
Some(Self {
major,
minor,
patch,
pre: 0,
})
}
}
}
impl std::cmp::PartialOrd for Semver {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl std::cmp::Ord for Semver {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
let major_cmp = self.major.cmp(&other.major);
let minor_cmp = self.minor.cmp(&other.minor);
let patch_cmp = self.patch.cmp(&other.patch);
let pre_cmp = self.pre.cmp(&other.pre);
if major_cmp != std::cmp::Ordering::Equal {
return major_cmp;
}
if minor_cmp != std::cmp::Ordering::Equal {
return minor_cmp;
}
if patch_cmp != std::cmp::Ordering::Equal {
return patch_cmp;
}
if pre_cmp == std::cmp::Ordering::Equal {
std::cmp::Ordering::Equal
}
else if self.pre == 0 {
std::cmp::Ordering::Greater
} else if other.pre == 0 {
std::cmp::Ordering::Less
} else {
pre_cmp
}
}
}
impl std::fmt::Display for Semver {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)?;
if self.pre > 0 {
write!(f, "-pre.{}", self.pre)?;
}
Ok(())
}
}
#[no_mangle]
#[used]
#[doc(hidden)]
pub static ARK_PACKED_FFI_CRATE_VERSION: u64 = Semver {
major: 0,
minor: 17,
patch: 0,
pre: 15,
}
.pack();
#[cfg(test)]
#[allow(clippy::panic)]
mod test {
use super::*;
#[test]
fn current_version_match() {
let version = Semver::unpack(ARK_PACKED_FFI_CRATE_VERSION);
let version_str = version.to_string();
if version_str != env!("CARGO_PKG_VERSION") {
panic!("Crate version mismatch, please update ARK_PACKED_FFI_CRATE_VERSION (\"{version_str}\") to be the same as the crate version in `api/api-ffi/Cargo.toml` (\"{}\")", env!("CARGO_PKG_VERSION"));
}
}
#[test]
fn compare() {
assert_eq!(
Semver {
major: 0,
minor: 2,
patch: 3,
pre: 4,
},
Semver {
major: 0,
minor: 2,
patch: 3,
pre: 4,
}
);
assert!(
Semver {
major: 0,
minor: 2,
patch: 2,
pre: 0,
} >= Semver {
major: 0,
minor: 2,
patch: 2,
pre: 0,
}
);
assert!(
Semver {
major: 0,
minor: 2,
patch: 2,
pre: 0,
} > Semver {
major: 0,
minor: 1,
patch: 3,
pre: 4,
}
);
assert!(
Semver {
major: 0,
minor: 2,
patch: 3,
pre: 0,
} > Semver {
major: 0,
minor: 2,
patch: 3,
pre: 1,
}
);
assert!(
Semver {
major: 5,
minor: 0,
patch: 0,
pre: 1,
} < Semver {
major: 5,
minor: 0,
patch: 0,
pre: 0,
}
);
assert!(
Semver {
major: 3,
minor: 4,
patch: 5,
pre: 6,
} < Semver {
major: 3,
minor: 4,
patch: 5,
pre: 8,
}
);
assert!(
Semver {
major: 3,
minor: 4,
patch: 5,
pre: 6,
} > Semver {
major: 3,
minor: 4,
patch: 5,
pre: 4,
}
);
assert!(
Semver {
major: 0,
minor: 2,
patch: 2,
pre: 0,
} > Semver {
major: 0,
minor: 2,
patch: 2,
pre: 4,
},
"Pre-release should count as less / older than non-pre-release"
);
}
#[test]
fn string_valid() {
for example in [
Semver {
major: 1,
minor: 2,
patch: 3,
pre: 4,
},
Semver {
major: 0,
minor: 12,
patch: 1,
pre: 0,
},
Semver::unpack(ARK_PACKED_FFI_CRATE_VERSION),
] {
let target = example.to_string();
let converted_string =
if let Some(converted_back) = Semver::from_str(&example.to_string()) {
converted_back.to_string()
} else {
panic!("Failed to convert {example:?} / {target:?} to string");
};
assert_eq!(
target, converted_string,
"Mismatch in semver {example:?} string conversion. Had: {target:?}, got: {converted_string:?}"
);
}
}
#[test]
fn string_invalid() {
for example in ["", "0.0.", "1", "0.3.1.", "0.3.1.alpha1", "0.3.1-pre"] {
assert!(
Semver::from_str(example).is_none(),
"Succeeded converting {example:?}, even though that should have failed"
);
}
}
}