use serde::{Deserialize, Serialize};
use crate::__compat::wire::{DescribeWire, WireSchema};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct FrameworkVersion {
major: u16,
minor: u16,
patch: u16,
}
impl FrameworkVersion {
pub const CURRENT_SPELLING: &'static str = env!("CARGO_PKG_VERSION");
pub const CURRENT: Self = match Self::parse(Self::CURRENT_SPELLING.as_bytes()) {
Some(version) => version,
None => panic!("the crate version is not a canonical <major>.<minor>.<patch> version"),
};
#[must_use]
pub const fn new(major: u16, minor: u16, patch: u16) -> Self {
Self {
major,
minor,
patch,
}
}
#[must_use]
pub const fn major(self) -> u16 {
self.major
}
#[must_use]
pub const fn minor(self) -> u16 {
self.minor
}
#[must_use]
pub const fn patch(self) -> u16 {
self.patch
}
#[must_use]
pub const fn compatibility_line(self) -> CompatibilityLine {
if self.major == 0 {
CompatibilityLine::PreV1 { minor: self.minor }
} else {
CompatibilityLine::Stable { major: self.major }
}
}
#[must_use]
pub const fn is_compatible_with(self, other: Self) -> bool {
match (self.compatibility_line(), other.compatibility_line()) {
(
CompatibilityLine::PreV1 { minor },
CompatibilityLine::PreV1 { minor: other_minor },
) => minor == other_minor,
(
CompatibilityLine::Stable { major },
CompatibilityLine::Stable { major: other_major },
) => major == other_major,
_ => false,
}
}
const fn parse(bytes: &[u8]) -> Option<Self> {
let (major, index) = match Self::parse_segment(bytes, 0) {
Some(parsed) => parsed,
None => return None,
};
if index >= bytes.len() || bytes[index] != b'.' {
return None;
}
let (minor, index) = match Self::parse_segment(bytes, index + 1) {
Some(parsed) => parsed,
None => return None,
};
if index >= bytes.len() || bytes[index] != b'.' {
return None;
}
let (patch, index) = match Self::parse_segment(bytes, index + 1) {
Some(parsed) => parsed,
None => return None,
};
if index != bytes.len() {
return None;
}
Some(Self::new(major, minor, patch))
}
const fn parse_segment(bytes: &[u8], start: usize) -> Option<(u16, usize)> {
let mut index = start;
let mut value: u16 = 0;
while index < bytes.len() && bytes[index].is_ascii_digit() {
let digit = (bytes[index] - b'0') as u16;
value = match value.checked_mul(10) {
Some(scaled) => scaled,
None => return None,
};
value = match value.checked_add(digit) {
Some(added) => added,
None => return None,
};
index += 1;
}
if index == start {
return None;
}
if bytes[start] == b'0' && index - start > 1 {
return None;
}
Some((value, index))
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum CompatibilityLine {
PreV1 { minor: u16 },
Stable { major: u16 },
}
impl std::fmt::Display for CompatibilityLine {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PreV1 { minor } => write!(formatter, "0.{minor}.x"),
Self::Stable { major } => write!(formatter, "{major}.x"),
}
}
}
impl std::fmt::Display for FrameworkVersion {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(formatter, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
impl std::str::FromStr for FrameworkVersion {
type Err = FrameworkVersionError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value.as_bytes()).ok_or_else(|| FrameworkVersionError {
value: value.to_owned(),
})
}
}
#[derive(Clone, Debug, thiserror::Error)]
#[error("invalid framework version '{value}'; expected <major>.<minor>.<patch>")]
pub struct FrameworkVersionError {
value: String,
}
impl Serialize for FrameworkVersion {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for FrameworkVersion {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let value = String::deserialize(deserializer)?;
Self::parse(value.as_bytes())
.ok_or_else(|| serde::de::Error::custom(FrameworkVersionError { value }))
}
}
impl DescribeWire for FrameworkVersion {
fn wire_schema() -> WireSchema {
WireSchema::opaque("FrameworkVersion", WireSchema::String)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_wire_spelling_is_the_semver_string_and_round_trips() {
let version = FrameworkVersion::new(0, 57, 2);
assert_eq!(version.to_string(), "0.57.2");
assert_eq!(
serde_json::to_string(&version).expect("a framework version serializes"),
"\"0.57.2\""
);
assert_eq!(
serde_json::from_str::<FrameworkVersion>("\"0.57.2\"").expect("the spelling parses"),
version
);
assert_eq!(
"0.57.2"
.parse::<FrameworkVersion>()
.expect("the spelling parses"),
version
);
assert_eq!(
(version.major(), version.minor(), version.patch()),
(0, 57, 2)
);
}
#[test]
fn every_non_canonical_spelling_is_rejected() {
for value in [
"\"v0.57.2\"",
"\"0.57\"",
"\"0.57.2.1\"",
"\"0.57.2-rc.1\"",
"\"0.57.2+build.5\"",
"\"0.057.2\"",
"\"00.57.2\"",
"\"0.57.2 \"",
"\" 0.57.2\"",
"\"\"",
r#"{"major":0,"minor":57,"patch":2}"#,
] {
assert!(
serde_json::from_str::<FrameworkVersion>(value).is_err(),
"{value} must not parse as a framework version"
);
}
assert!("0.57.2-rc.1".parse::<FrameworkVersion>().is_err());
assert!("65536.0.0".parse::<FrameworkVersion>().is_err());
}
#[test]
fn the_declared_wire_shape_is_the_shape_the_serializer_writes() {
let value = serde_json::to_value(FrameworkVersion::new(0, 57, 2))
.expect("a framework version serializes");
assert_eq!(FrameworkVersion::wire_schema().conforms(&value), Ok(()));
assert_eq!(
FrameworkVersion::wire_schema().canonical_json(),
r#"{"kind":"opaque","name":"FrameworkVersion","wire":{"kind":"string"}}"#
);
}
#[test]
fn current_is_the_crate_version() {
assert_eq!(
FrameworkVersion::CURRENT.to_string(),
env!("CARGO_PKG_VERSION")
);
assert_eq!(
env!("CARGO_PKG_VERSION")
.parse::<FrameworkVersion>()
.expect("the crate version is a canonical framework version"),
FrameworkVersion::CURRENT
);
assert_eq!(
FrameworkVersion::CURRENT_SPELLING,
FrameworkVersion::CURRENT.to_string()
);
}
#[test]
fn the_compatibility_line_is_the_minor_before_v1_and_the_major_after() {
assert_eq!(
FrameworkVersion::new(0, 57, 2).compatibility_line(),
CompatibilityLine::PreV1 { minor: 57 }
);
assert_eq!(
FrameworkVersion::new(0, 58, 0).compatibility_line(),
CompatibilityLine::PreV1 { minor: 58 }
);
assert_eq!(
FrameworkVersion::new(1, 4, 9).compatibility_line(),
CompatibilityLine::Stable { major: 1 }
);
assert_eq!(
FrameworkVersion::new(2, 0, 0).compatibility_line(),
CompatibilityLine::Stable { major: 2 }
);
}
#[test]
fn a_line_is_spelled_as_the_release_it_names() {
assert_eq!(
FrameworkVersion::new(0, 58, 2)
.compatibility_line()
.to_string(),
"0.58.x"
);
assert_eq!(
FrameworkVersion::new(1, 4, 9)
.compatibility_line()
.to_string(),
"1.x"
);
}
#[test]
fn versions_on_the_same_line_are_not_equal_but_are_compatible() {
let earlier = FrameworkVersion::new(0, 57, 0);
let later = FrameworkVersion::new(0, 57, 1);
assert_eq!(earlier.compatibility_line(), later.compatibility_line());
assert_ne!(earlier, later);
assert!(earlier.is_compatible_with(later));
assert!(later.is_compatible_with(earlier));
}
#[test]
fn compatibility_is_the_line_and_the_line_is_the_break() {
let pre_v1 = FrameworkVersion::new(0, 58, 0);
assert!(pre_v1.is_compatible_with(FrameworkVersion::new(0, 58, 7)));
assert!(!pre_v1.is_compatible_with(FrameworkVersion::new(0, 59, 0)));
assert!(!pre_v1.is_compatible_with(FrameworkVersion::new(0, 57, 9)));
let stable = FrameworkVersion::new(1, 4, 2);
assert!(stable.is_compatible_with(FrameworkVersion::new(1, 9, 0)));
assert!(!stable.is_compatible_with(FrameworkVersion::new(2, 0, 0)));
assert!(!stable.is_compatible_with(pre_v1));
assert!(!pre_v1.is_compatible_with(stable));
assert!(FrameworkVersion::CURRENT.is_compatible_with(FrameworkVersion::CURRENT));
}
}