use core::fmt;
use crate::domain::ProtocolVersion;
use crate::domain::negotiation::handshake_defined_in;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SupportedVersionsError {
#[error("a peer must support at least one protocol version")]
Empty,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SupportedVersions {
versions: Box<[ProtocolVersion]>,
}
impl SupportedVersions {
pub fn all() -> Result<Self, SupportedVersionsError> {
Self::new(ProtocolVersion::KNOWN)
}
#[must_use]
pub fn as_slice(&self) -> &[ProtocolVersion] {
&self.versions
}
pub fn new(
versions: impl IntoIterator<Item = ProtocolVersion>,
) -> Result<Self, SupportedVersionsError> {
let mut versions: Vec<ProtocolVersion> = versions.into_iter().collect();
if versions.is_empty() {
return Err(SupportedVersionsError::Empty);
}
versions.sort_unstable();
versions.dedup();
Ok(Self {
versions: versions.into_boxed_slice(),
})
}
pub fn from_slice(versions: &[ProtocolVersion]) -> Result<Self, SupportedVersionsError> {
Self::new(versions.iter().copied())
}
#[must_use]
pub fn len(&self) -> usize {
self.versions.len()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
false
}
#[must_use]
pub fn latest(&self) -> ProtocolVersion {
self.versions
.last()
.map_or_else(ProtocolVersion::latest_known, |latest| *latest)
}
#[must_use]
pub fn latest_handshake(&self) -> ProtocolVersion {
self.versions
.iter()
.copied()
.rfind(|version| handshake_defined_in(*version))
.unwrap_or_else(ProtocolVersion::earliest_known)
}
#[must_use]
pub fn earliest(&self) -> ProtocolVersion {
self.versions
.first()
.map_or_else(ProtocolVersion::earliest_known, |earliest| *earliest)
}
#[must_use]
pub fn contains(&self, version: &ProtocolVersion) -> bool {
self.versions.binary_search(version).is_ok()
}
#[must_use]
pub fn best_match(&self, offered: &[ProtocolVersion]) -> Option<ProtocolVersion> {
offered
.iter()
.filter(|version| self.contains(version))
.copied()
.max()
}
pub fn not_offered_by<'a>(
&'a self,
offered: &'a [ProtocolVersion],
) -> impl Iterator<Item = ProtocolVersion> + 'a {
self.versions
.iter()
.filter(|version| !offered.contains(version))
.copied()
}
}
impl fmt::Display for SupportedVersions {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
for (index, version) in self.versions.iter().enumerate() {
if index > 0 {
formatter.write_str(", ")?;
}
write!(formatter, "{version}")?;
}
Ok(())
}
}
impl TryFrom<&[ProtocolVersion]> for SupportedVersions {
type Error = SupportedVersionsError;
fn try_from(versions: &[ProtocolVersion]) -> Result<Self, Self::Error> {
Self::from_slice(versions)
}
}
impl IntoIterator for SupportedVersions {
type Item = ProtocolVersion;
type IntoIter = std::vec::IntoIter<ProtocolVersion>;
fn into_iter(self) -> Self::IntoIter {
self.versions.into_vec().into_iter()
}
}
#[cfg(test)]
mod tests {
#![expect(
clippy::unwrap_used,
clippy::panic,
reason = "tests assert on fallible calls and panic with the failing precondition; propagating errors would replace a clear failure message with noise"
)]
use super::{SupportedVersions, SupportedVersionsError};
use crate::domain::{ProtocolVersion, ReleaseDate};
fn set_of(versions: &[ProtocolVersion]) -> SupportedVersions {
SupportedVersions::from_slice(versions).unwrap_or_else(|e| panic!("{e}"))
}
#[test]
fn rejects_an_empty_set() {
assert_eq!(
SupportedVersions::new([]),
Err(SupportedVersionsError::Empty)
);
assert_eq!(
SupportedVersions::from_slice(&[]),
Err(SupportedVersionsError::Empty)
);
assert_eq!(
SupportedVersions::new(Vec::new()).map(|set| set.len()),
Err(SupportedVersionsError::Empty)
);
}
#[test]
fn sorts_and_deduplicates_at_construction() {
let set = set_of(&[
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_03_26,
]);
assert_eq!(
set.as_slice(),
&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_03_26,
ProtocolVersion::V2025_06_18,
]
);
assert_eq!(set.len(), 3);
}
#[test]
fn latest_and_earliest_read_off_the_sorted_edges() {
let set = set_of(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]);
assert_eq!(set.latest(), ProtocolVersion::V2025_06_18);
assert_eq!(set.earliest(), ProtocolVersion::V2024_11_05);
let single = set_of(&[ProtocolVersion::V2025_11_25]);
assert_eq!(single.latest(), ProtocolVersion::V2025_11_25);
assert_eq!(single.earliest(), ProtocolVersion::V2025_11_25);
assert_eq!(single.len(), 1);
assert!(!single.is_empty());
}
#[test]
fn all_is_every_known_revision() {
let all = SupportedVersions::all().unwrap_or_else(|e| panic!("{e}"));
assert_eq!(all.as_slice(), ProtocolVersion::KNOWN);
assert_eq!(all.latest(), ProtocolVersion::latest_known());
assert_eq!(all.len(), ProtocolVersion::KNOWN.len());
}
#[test]
fn contains_uses_the_sorted_ordering() {
let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
assert!(set.contains(&ProtocolVersion::V2024_11_05));
assert!(set.contains(&ProtocolVersion::V2025_06_18));
assert!(!set.contains(&ProtocolVersion::V2025_03_26));
assert!(!set.contains(&ProtocolVersion::V2026_07_28));
let future = ProtocolVersion::Unknown("2027-01-01".parse::<ReleaseDate>().unwrap());
assert!(!set.contains(&future));
}
#[test]
fn best_match_takes_the_newest_shared_revision() {
let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
assert_eq!(
set.best_match(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]),
Some(ProtocolVersion::V2025_06_18)
);
assert_eq!(
set.best_match(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]),
Some(ProtocolVersion::V2025_06_18)
);
}
#[test]
fn best_match_is_none_when_nothing_is_shared() {
let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_03_26]);
assert_eq!(set.best_match(&[ProtocolVersion::V2026_07_28]), None);
assert_eq!(
set.best_match(&[ProtocolVersion::Unknown("2027-01-01".parse().unwrap())]),
None
);
assert_eq!(set.best_match(&[]), None);
}
#[test]
fn best_match_ignores_duplicate_offers() {
let set = set_of(&[ProtocolVersion::V2025_06_18]);
assert_eq!(
set.best_match(&[
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2025_06_18
]),
Some(ProtocolVersion::V2025_06_18)
);
}
#[test]
fn best_match_can_return_an_unknown_revision() {
let future = ProtocolVersion::Unknown("2027-03-14".parse::<ReleaseDate>().unwrap());
let set = set_of(&[ProtocolVersion::V2025_06_18, future]);
assert_eq!(set.best_match(&[future]), Some(future));
assert_eq!(set.latest(), future);
}
#[test]
fn not_offered_by_is_the_complement() {
let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
let offered = [ProtocolVersion::V2025_06_18, ProtocolVersion::V2026_07_28];
let missing: Vec<ProtocolVersion> = set.not_offered_by(&offered).collect();
assert_eq!(missing, vec![ProtocolVersion::V2024_11_05]);
assert!(set.not_offered_by(&offered).all(|v| !offered.contains(&v)));
}
#[test]
fn display_joins_oldest_first() {
let set = set_of(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]);
assert_eq!(set.to_string(), "2024-11-05, 2025-06-18");
assert_eq!(
set_of(&[ProtocolVersion::V2025_06_18]).to_string(),
"2025-06-18"
);
}
#[test]
fn converts_from_a_slice_and_into_an_iterator() {
let from_try: SupportedVersions = (&[ProtocolVersion::V2024_11_05][..])
.try_into()
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(from_try.len(), 1);
let collected: Vec<ProtocolVersion> = set_of(&ProtocolVersion::KNOWN).into_iter().collect();
assert_eq!(collected, ProtocolVersion::KNOWN.to_vec());
}
#[test]
fn equality_ignores_construction_order() {
let forwards = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
let backwards = set_of(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]);
let with_duplicate = set_of(&[
ProtocolVersion::V2024_11_05,
ProtocolVersion::V2025_06_18,
ProtocolVersion::V2024_11_05,
]);
assert_eq!(forwards, backwards);
assert_eq!(forwards, with_duplicate);
assert_ne!(forwards, set_of(&[ProtocolVersion::V2024_11_05]));
}
}