use core::fmt;
use core::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AcpVersion {
V1_25_0,
Unknown {
major: u16,
minor: u16,
patch: u16,
},
}
const fn parts(version: AcpVersion) -> (u16, u16, u16) {
match version {
AcpVersion::V1_25_0 => (1, 25, 0),
AcpVersion::Unknown {
major,
minor,
patch,
} => (major, minor, patch),
}
}
impl PartialOrd for AcpVersion {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for AcpVersion {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
parts(*self).cmp(&parts(*other))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AcpVersionParseError {
input: String,
reason: &'static str,
}
impl fmt::Display for AcpVersionParseError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"ACP version {:?} is invalid: {}",
self.input, self.reason
)
}
}
impl std::error::Error for AcpVersionParseError {}
impl FromStr for AcpVersion {
type Err = AcpVersionParseError;
fn from_str(input: &str) -> Result<Self, Self::Err> {
let version_part = input.strip_prefix("schema-v").unwrap_or(input);
let mut iter = version_part.split('.');
let Some(major) = iter.next() else {
return Err(AcpVersionParseError {
input: input.to_owned(),
reason: "missing the major version",
});
};
let Some(minor) = iter.next() else {
return Err(AcpVersionParseError {
input: input.to_owned(),
reason: "missing the minor version",
});
};
let Some(patch) = iter.next() else {
return Err(AcpVersionParseError {
input: input.to_owned(),
reason: "missing the patch version",
});
};
if iter.next().is_some() {
return Err(AcpVersionParseError {
input: input.to_owned(),
reason: "expected exactly three numeric segments",
});
}
let major = major.parse::<u16>().map_err(|_| AcpVersionParseError {
input: input.to_owned(),
reason: "the major version is not a u16",
})?;
let minor = minor.parse::<u16>().map_err(|_| AcpVersionParseError {
input: input.to_owned(),
reason: "the minor version is not a u16",
})?;
let patch = patch.parse::<u16>().map_err(|_| AcpVersionParseError {
input: input.to_owned(),
reason: "the patch version is not a u16",
})?;
if (1, 25, 0) == (major, minor, patch) {
Ok(Self::V1_25_0)
} else {
Ok(Self::Unknown {
major,
minor,
patch,
})
}
}
}
impl fmt::Display for AcpVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let (major, minor, patch) = parts(*self);
write!(formatter, "{major}.{minor}.{patch}")
}
}
impl Serialize for AcpVersion {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.collect_str(self)
}
}
impl<'de> Deserialize<'de> for AcpVersion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let raw = String::deserialize(deserializer)?;
raw.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use core::cmp::Ordering;
#[test]
fn the_pinned_version_parses_with_or_without_the_prefix() -> Result<(), AcpVersionParseError> {
let with_prefix = "schema-v1.25.0".parse::<AcpVersion>()?;
let without_prefix = "1.25.0".parse::<AcpVersion>()?;
assert_eq!(with_prefix, AcpVersion::V1_25_0);
assert_eq!(without_prefix, AcpVersion::V1_25_0);
Ok(())
}
#[test]
fn an_unknown_version_orders_like_its_known_equivalent() -> Result<(), AcpVersionParseError> {
let unknown: AcpVersion = "1.25.0".parse()?;
assert_eq!(unknown, AcpVersion::V1_25_0);
assert_eq!(unknown.cmp(&AcpVersion::V1_25_0), Ordering::Equal);
let future: AcpVersion = "1.26.0".parse()?;
assert!(future > AcpVersion::V1_25_0);
let older: AcpVersion = "0.9.0".parse()?;
assert!(older < AcpVersion::V1_25_0);
Ok(())
}
#[test]
fn a_malformed_input_carries_the_input_and_the_reason() -> Result<(), String> {
let cases: [(&str, &str); 5] = [
("", "missing the minor version"),
("1", "missing the minor version"),
("1.2", "missing the patch version"),
("1.2.3.4", "expected exactly three numeric segments"),
("a.b.c", "the major version is not a u16"),
];
for (input, expected_reason) in cases {
let Err(error) = input.parse::<AcpVersion>() else {
return Err(format!("{input:?} should be an error"));
};
assert_eq!(error.reason, expected_reason, "input: {input:?}");
let text = format!("{error}");
assert!(
text.contains(input),
"the text should carry the input: {text}"
);
}
Ok(())
}
#[test]
fn display_then_from_str_round_trips_every_known_variant() -> Result<(), AcpVersionParseError> {
let known: AcpVersion = AcpVersion::V1_25_0;
let round_tripped: AcpVersion = format!("{known}").parse()?;
assert_eq!(round_tripped, known);
let future: AcpVersion = "1.27.3".parse()?;
let round_tripped: AcpVersion = format!("{future}").parse()?;
assert_eq!(round_tripped, future);
Ok(())
}
}