use core::fmt;
use core::str::FromStr;
use crate::domain::ReleaseDate;
use crate::domain::release_date::ParseReleaseDateError;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ProtocolVersion {
V2024_11_05,
V2025_03_26,
V2025_06_18,
V2025_11_25,
V2026_07_28,
Unknown(ReleaseDate),
}
impl ProtocolVersion {
pub const KNOWN: [Self; 5] = [
Self::V2024_11_05,
Self::V2025_03_26,
Self::V2025_06_18,
Self::V2025_11_25,
Self::V2026_07_28,
];
#[must_use]
pub const fn latest_known() -> Self {
Self::V2026_07_28
}
#[must_use]
pub const fn earliest_known() -> Self {
Self::V2024_11_05
}
#[must_use]
pub const fn date(self) -> ReleaseDate {
match self {
Self::V2024_11_05 => ReleaseDate::V2024_11_05,
Self::V2025_03_26 => ReleaseDate::V2025_03_26,
Self::V2025_06_18 => ReleaseDate::V2025_06_18,
Self::V2025_11_25 => ReleaseDate::V2025_11_25,
Self::V2026_07_28 => ReleaseDate::V2026_07_28,
Self::Unknown(date) => date,
}
}
#[must_use]
pub fn as_wire(self) -> std::borrow::Cow<'static, str> {
match self {
Self::V2024_11_05 => "2024-11-05".into(),
Self::V2025_03_26 => "2025-03-26".into(),
Self::V2025_06_18 => "2025-06-18".into(),
Self::V2025_11_25 => "2025-11-25".into(),
Self::V2026_07_28 => "2026-07-28".into(),
Self::Unknown(date) => date.to_string().into(),
}
}
#[must_use]
pub const fn is_known(self) -> bool {
!matches!(self, Self::Unknown(_))
}
#[must_use]
pub fn is_newer_than_all_known(self) -> bool {
self > Self::latest_known()
}
pub fn unknown_after_known(date: ReleaseDate) -> Result<Self, UnknownVersionError> {
if date <= Self::latest_known().date() {
return Err(UnknownVersionError::NotAfterKnown { date });
}
Ok(Self::Unknown(date))
}
}
impl fmt::Display for ProtocolVersion {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.as_wire())
}
}
impl FromStr for ProtocolVersion {
type Err = ParseProtocolVersionError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
match text {
"2024-11-05" => return Ok(Self::V2024_11_05),
"2025-03-26" => return Ok(Self::V2025_03_26),
"2025-06-18" => return Ok(Self::V2025_06_18),
"2025-11-25" => return Ok(Self::V2025_11_25),
"2026-07-28" => return Ok(Self::V2026_07_28),
_ => {}
}
let date = text.parse::<ReleaseDate>()?;
if !date.is_at_or_after_first_revision() {
return Err(ParseProtocolVersionError::ReleaseDate(
ParseReleaseDateError::BeforeFirstRevision { date },
));
}
Self::unknown_after_known(date).map_err(ParseProtocolVersionError::UnknownVersion)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ParseProtocolVersionError {
#[error(transparent)]
ReleaseDate(#[from] ParseReleaseDateError),
#[error("a date this build already knows cannot become an unknown revision: {0}")]
UnknownVersion(#[from] UnknownVersionError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum UnknownVersionError {
#[error("{date} is not after 2026-07-28, so it cannot be an unknown revision")]
NotAfterKnown {
date: ReleaseDate,
},
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
reason = "tests assert on fallible calls and panic with the failing precondition; propagating errors would replace a clear failure message with noise"
)]
use super::{ParseProtocolVersionError, ProtocolVersion, ReleaseDate, UnknownVersionError};
use std::collections::BTreeSet;
fn corpus() -> Vec<ProtocolVersion> {
let mut versions = ProtocolVersion::KNOWN.to_vec();
versions.push(ProtocolVersion::Unknown("2027-01-01".parse().unwrap()));
versions.push(ProtocolVersion::Unknown("2030-12-31".parse().unwrap()));
versions
}
#[test]
fn derived_ordering_agrees_in_both_directions() {
for left in corpus() {
for right in corpus() {
let total = left.cmp(&right);
assert_eq!(
left.partial_cmp(&right),
Some(total),
"cmp and partial_cmp disagree for {left} against {right}"
);
assert_eq!(left < right, total.is_lt());
assert_eq!(left > right, total.is_gt());
assert_eq!(left <= right, total.is_le());
assert_eq!(left >= right, total.is_ge());
}
}
}
#[test]
fn unknown_dates_sort_after_every_known_revision() {
let future = ProtocolVersion::Unknown("2027-01-01".parse().unwrap());
for known in ProtocolVersion::KNOWN {
assert!(
known < future,
"{known} must sort below an unknown revision"
);
assert!(
future > known,
"{known} must sort below an unknown revision"
);
}
}
#[test]
fn declaration_order_is_chronological() {
let sorted: BTreeSet<ProtocolVersion> = ProtocolVersion::KNOWN.into_iter().collect();
let ascending: Vec<ProtocolVersion> = sorted.into_iter().collect();
assert_eq!(ascending, ProtocolVersion::KNOWN.to_vec());
assert_eq!(
ProtocolVersion::earliest_known(),
ProtocolVersion::V2024_11_05
);
assert_eq!(
ProtocolVersion::latest_known(),
ProtocolVersion::V2026_07_28
);
}
#[test]
fn parses_each_known_revision() {
let cases = [
("2024-11-05", ProtocolVersion::V2024_11_05),
("2025-03-26", ProtocolVersion::V2025_03_26),
("2025-06-18", ProtocolVersion::V2025_06_18),
("2025-11-25", ProtocolVersion::V2025_11_25),
("2026-07-28", ProtocolVersion::V2026_07_28),
];
for (text, expected) in cases {
assert_eq!(text.parse::<ProtocolVersion>(), Ok(expected), "{text}");
}
}
#[test]
fn accepts_an_unknown_future_date() {
let parsed = "2027-03-14".parse::<ProtocolVersion>().unwrap();
assert_eq!(
parsed,
ProtocolVersion::Unknown("2027-03-14".parse::<ReleaseDate>().unwrap())
);
assert!(!parsed.is_known());
assert!(parsed.is_newer_than_all_known());
assert_eq!(parsed.as_wire(), "2027-03-14");
}
#[test]
fn rejects_a_date_from_before_the_first_revision() {
let error = "2020-01-01".parse::<ProtocolVersion>().unwrap_err();
assert!(matches!(error, ParseProtocolVersionError::ReleaseDate(_)));
assert!(error.to_string().contains("predates the first revision"));
}
#[test]
fn rejects_malformed_strings() {
for text in ["", "latest", "2025-3-26", "2025-03-26 ", " DRAFT-2025-v3"] {
assert!(text.parse::<ProtocolVersion>().is_err(), "{text:?} parsed");
}
}
#[test]
fn the_draft_string_upstream_ships_is_not_a_version() {
assert!("DRAFT-2025-v3".parse::<ProtocolVersion>().is_err());
}
#[test]
fn constructing_an_unknown_requires_a_later_date() {
let unknown = ProtocolVersion::unknown_after_known;
let error = unknown(ReleaseDate::V2025_06_18).unwrap_err();
assert_eq!(
error,
UnknownVersionError::NotAfterKnown {
date: ReleaseDate::V2025_06_18
}
);
assert!(unknown(ReleaseDate::V2026_07_28).is_err());
assert!(unknown("2026-07-29".parse().unwrap()).is_ok());
}
#[test]
fn known_versions_never_report_as_unknown() {
for version in ProtocolVersion::KNOWN {
assert!(version.is_known());
assert!(!version.is_newer_than_all_known());
assert!(version <= ProtocolVersion::latest_known());
}
}
#[test]
fn round_trips_through_its_display_form() {
for version in corpus() {
assert_eq!(version.to_string().parse::<ProtocolVersion>(), Ok(version));
}
}
#[test]
fn as_wire_borrows_when_it_can() {
for version in ProtocolVersion::KNOWN {
assert!(matches!(version.as_wire(), std::borrow::Cow::Borrowed(_)));
assert_eq!(version.as_wire(), version.to_string());
}
let future = ProtocolVersion::Unknown("2027-03-14".parse().unwrap());
assert!(matches!(future.as_wire(), std::borrow::Cow::Owned(_)));
assert_eq!(future.as_wire(), "2027-03-14");
}
#[test]
fn dates_match_the_variants() {
assert_eq!(
ProtocolVersion::V2024_11_05.date(),
ReleaseDate::V2024_11_05
);
assert_eq!(
ProtocolVersion::V2025_03_26.date(),
ReleaseDate::V2025_03_26
);
assert_eq!(
ProtocolVersion::V2025_06_18.date(),
ReleaseDate::V2025_06_18
);
assert_eq!(
ProtocolVersion::V2025_11_25.date(),
ReleaseDate::V2025_11_25
);
assert_eq!(
ProtocolVersion::V2026_07_28.date(),
ReleaseDate::V2026_07_28
);
}
}