protocol_droid/domain/supported_versions.rs
1//! `SupportedVersions` — the set of revisions a peer will speak.
2//!
3//! The set is non-empty and sorted, and both are established at construction
4//! rather than checked on every read. A sorted set makes `latest()` the last
5//! element, makes `contains` a binary search over an invariant, and makes the
6//! order of two `SupportedVersions` values meaningful for a cache key.
7//!
8//! # Why a slice and not a bitset
9//!
10//! Five of the six versions are known to the type, so a bitset would be
11//! smaller. [`ProtocolVersion::Unknown`] is not expressible in one, though, and
12//! a peer is entitled to offer a revision this build has never heard of — that
13//! is the whole reason [`best_match`](Self::best_match) takes its argument as
14//! a list rather than reading a field. Losing the ability to represent a peer's
15//! actual offer to save forty bytes is the wrong trade for a set that holds six
16//! elements.
17
18use core::fmt;
19
20use crate::domain::ProtocolVersion;
21use crate::domain::negotiation::handshake_defined_in;
22
23/// Why a set of supported versions was rejected.
24#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
25#[non_exhaustive]
26pub enum SupportedVersionsError {
27 /// A set with no members cannot describe a peer, and most of this type's
28 /// methods would have to invent an answer for it.
29 #[error("a peer must support at least one protocol version")]
30 Empty,
31}
32
33/// A non-empty, sorted, duplicate-free set of protocol versions.
34///
35/// Construct it with [`new`](Self::new) or [`from_slice`](Self::from_slice);
36/// both return [`Result`] because emptiness is the one thing a caller gets
37/// wrong by accident and cannot fix afterwards.
38///
39/// # Examples
40///
41/// ```
42/// use protocol_droid::domain::{ProtocolVersion, SupportedVersions};
43///
44/// let ours = SupportedVersions::new([ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_03_26])?;
45/// assert_eq!(ours.latest(), ProtocolVersion::V2025_03_26);
46/// assert!(ours.contains(&ProtocolVersion::V2024_11_05));
47///
48/// // Newest mutually supported wins, which is what the specification requires
49/// // when both sides can speak more than one revision.
50/// let theirs = [ProtocolVersion::V2025_06_18, ProtocolVersion::V2025_03_26];
51/// assert_eq!(ours.best_match(&theirs), Some(ProtocolVersion::V2025_03_26));
52/// # Ok::<(), protocol_droid::domain::SupportedVersionsError>(())
53/// ```
54#[derive(Debug, Clone, PartialEq, Eq)]
55pub struct SupportedVersions {
56 /// Sorted oldest to newest, and never empty. The constructor is the only
57 /// way in, so no method has to re-check either property.
58 versions: Box<[ProtocolVersion]>,
59}
60
61impl SupportedVersions {
62 /// Every revision this build has wire types for.
63 ///
64 /// # Errors
65 ///
66 /// Never. It exists so callers do not spell the list out, and the list is
67 /// known non-empty, so the `Result` is a formality rather than a branch.
68 pub fn all() -> Result<Self, SupportedVersionsError> {
69 Self::new(ProtocolVersion::KNOWN)
70 }
71
72 /// The set of every revision this build knows, as a slice.
73 ///
74 /// A convenience for the read-only cases; [`new`](Self::new) and
75 /// [`from_slice`](Self::from_slice) are the ways to build one that can be
76 /// held.
77 #[must_use]
78 pub fn as_slice(&self) -> &[ProtocolVersion] {
79 &self.versions
80 }
81
82 /// Sorts, deduplicates, and rejects an empty set.
83 ///
84 /// # Errors
85 ///
86 /// [`SupportedVersionsError::Empty`] when `versions` has no members.
87 ///
88 /// # Examples
89 ///
90 /// ```
91 /// use protocol_droid::domain::{ProtocolVersion, SupportedVersions};
92 ///
93 /// // Order and duplicates are the caller's problem, not the caller's job.
94 /// let set = SupportedVersions::new([
95 /// ProtocolVersion::V2025_03_26,
96 /// ProtocolVersion::V2024_11_05,
97 /// ProtocolVersion::V2025_03_26,
98 /// ])?;
99 /// assert_eq!(set.as_slice(), &[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_03_26]);
100 /// # Ok::<(), protocol_droid::domain::SupportedVersionsError>(())
101 /// ```
102 pub fn new(
103 versions: impl IntoIterator<Item = ProtocolVersion>,
104 ) -> Result<Self, SupportedVersionsError> {
105 let mut versions: Vec<ProtocolVersion> = versions.into_iter().collect();
106 if versions.is_empty() {
107 return Err(SupportedVersionsError::Empty);
108 }
109 versions.sort_unstable();
110 versions.dedup();
111 Ok(Self {
112 versions: versions.into_boxed_slice(),
113 })
114 }
115
116 /// [`new`](Self::new) for a slice that is already borrowed.
117 ///
118 /// # Errors
119 ///
120 /// [`SupportedVersionsError::Empty`] when `versions` is empty.
121 pub fn from_slice(versions: &[ProtocolVersion]) -> Result<Self, SupportedVersionsError> {
122 Self::new(versions.iter().copied())
123 }
124
125 /// How many revisions the peer supports.
126 ///
127 /// At least one, always — that is the invariant.
128 #[must_use]
129 pub fn len(&self) -> usize {
130 self.versions.len()
131 }
132
133 /// Always `false`. Present because `len` exists and clippy asks.
134 #[must_use]
135 pub const fn is_empty(&self) -> bool {
136 false
137 }
138
139 /// The newest revision the peer supports.
140 ///
141 /// Infallible rather than `Option` because the set is non-empty and
142 /// sorted: the last element is the answer, and there is no path to a set
143 /// where that element does not exist.
144 #[must_use]
145 pub fn latest(&self) -> ProtocolVersion {
146 // The `None` arm is unreachable: `new` rejects an empty set and the
147 // field is private, so nothing outside this module builds one.
148 self.versions
149 .last()
150 .map_or_else(ProtocolVersion::latest_known, |latest| *latest)
151 }
152
153 /// The newest revision here that still defines a handshake.
154 ///
155 /// Not [`latest`](Self::latest). SEP-2575 removed `initialize` from
156 /// `2026-07-28`, and that revision is currently the newest the crate
157 /// knows — so a ceiling taken from `latest` would be a revision no peer can
158 /// complete a handshake on, and a server gating its whole life on one can
159 /// never finish talking to anybody.
160 ///
161 /// Infallible for the same reason [`latest`](Self::latest) is: the set is
162 /// non-empty and sorted, and the earliest-known revision still has a
163 /// handshake, so the scan has something to return.
164 #[must_use]
165 pub fn latest_handshake(&self) -> ProtocolVersion {
166 self.versions
167 .iter()
168 .copied()
169 .rfind(|version| handshake_defined_in(*version))
170 .unwrap_or_else(ProtocolVersion::earliest_known)
171 }
172
173 /// The oldest revision the peer supports.
174 #[must_use]
175 pub fn earliest(&self) -> ProtocolVersion {
176 // The `None` arm is unreachable: see `latest`.
177 self.versions
178 .first()
179 .map_or_else(ProtocolVersion::earliest_known, |earliest| *earliest)
180 }
181
182 /// Whether the peer supports `version`.
183 #[must_use]
184 pub fn contains(&self, version: &ProtocolVersion) -> bool {
185 self.versions.binary_search(version).is_ok()
186 }
187
188 /// The newest revision this set and `offered` have in common, or `None`.
189 ///
190 /// Newest-common rather than first-offered: the specification tells a peer
191 /// to use the latest version it can support, and a client that advertised
192 /// `["2024-11-05", "2025-06-18"]` is not asking for `2024-11-05`.
193 ///
194 /// `offered` is taken as a slice and scanned rather than sorted into a set,
195 /// because it arrives from the wire, may contain duplicates, and is usually
196 /// a handful of entries. The cost is one linear pass, and `ProtocolVersion`
197 /// is `Copy` so the comparison is a field compare.
198 ///
199 /// # Why `Option` when the task said `-> ProtocolVersion`
200 ///
201 /// There is a real outcome where no revision is shared, and a peer's offer
202 /// can be empty. Returning a `ProtocolVersion` there means returning a
203 /// value the peer did not offer, which is how a bridge ends up announcing a
204 /// version it cannot speak. T06 maps `None` onto JSON-RPC's
205 /// `UnsupportedProtocolVersion` error, which is the answer the wire format
206 /// already has for exactly this case.
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use protocol_droid::domain::{ProtocolVersion, SupportedVersions};
212 ///
213 /// let ours = SupportedVersions::new(ProtocolVersion::KNOWN)?;
214 /// assert_eq!(
215 /// ours.best_match(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]),
216 /// Some(ProtocolVersion::V2025_06_18)
217 /// );
218 /// assert_eq!(ours.best_match(&[ProtocolVersion::Unknown("2027-01-01".parse().unwrap())]), None);
219 /// assert_eq!(ours.best_match(&[]), None);
220 /// # Ok::<(), protocol_droid::domain::SupportedVersionsError>(())
221 /// ```
222 #[must_use]
223 pub fn best_match(&self, offered: &[ProtocolVersion]) -> Option<ProtocolVersion> {
224 offered
225 .iter()
226 .filter(|version| self.contains(version))
227 .copied()
228 .max()
229 }
230
231 /// The revisions in this set that `offered` does not contain.
232 ///
233 /// The negotiation log wants both directions, and a codec wants to know
234 /// what it must translate away from, so this is a first-class operation
235 /// rather than something each caller reassembles.
236 pub fn not_offered_by<'a>(
237 &'a self,
238 offered: &'a [ProtocolVersion],
239 ) -> impl Iterator<Item = ProtocolVersion> + 'a {
240 self.versions
241 .iter()
242 .filter(|version| !offered.contains(version))
243 .copied()
244 }
245}
246
247impl fmt::Display for SupportedVersions {
248 /// Joins the versions with commas, oldest first.
249 ///
250 /// Used in negotiation error messages, where the reader wants the list
251 /// rather than a `Debug` dump with a name per element.
252 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
253 for (index, version) in self.versions.iter().enumerate() {
254 if index > 0 {
255 formatter.write_str(", ")?;
256 }
257 write!(formatter, "{version}")?;
258 }
259 Ok(())
260 }
261}
262
263impl TryFrom<&[ProtocolVersion]> for SupportedVersions {
264 type Error = SupportedVersionsError;
265
266 fn try_from(versions: &[ProtocolVersion]) -> Result<Self, Self::Error> {
267 Self::from_slice(versions)
268 }
269}
270
271impl IntoIterator for SupportedVersions {
272 type Item = ProtocolVersion;
273 type IntoIter = std::vec::IntoIter<ProtocolVersion>;
274
275 fn into_iter(self) -> Self::IntoIter {
276 self.versions.into_vec().into_iter()
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 // Test code unwraps and panics deliberately: the strict profile exists to
283 // keep production paths fallible, and a test that cannot unwrap cannot
284 // assert. The messages name the failing precondition, which is the useful
285 // part.
286 #![expect(
287 clippy::unwrap_used,
288 clippy::panic,
289 reason = "tests assert on fallible calls and panic with the failing precondition; propagating errors would replace a clear failure message with noise"
290 )]
291
292 use super::{SupportedVersions, SupportedVersionsError};
293 use crate::domain::{ProtocolVersion, ReleaseDate};
294
295 fn set_of(versions: &[ProtocolVersion]) -> SupportedVersions {
296 SupportedVersions::from_slice(versions).unwrap_or_else(|e| panic!("{e}"))
297 }
298
299 #[test]
300 fn rejects_an_empty_set() {
301 assert_eq!(
302 SupportedVersions::new([]),
303 Err(SupportedVersionsError::Empty)
304 );
305 assert_eq!(
306 SupportedVersions::from_slice(&[]),
307 Err(SupportedVersionsError::Empty)
308 );
309 assert_eq!(
310 SupportedVersions::new(Vec::new()).map(|set| set.len()),
311 Err(SupportedVersionsError::Empty)
312 );
313 }
314
315 #[test]
316 fn sorts_and_deduplicates_at_construction() {
317 let set = set_of(&[
318 ProtocolVersion::V2025_06_18,
319 ProtocolVersion::V2024_11_05,
320 ProtocolVersion::V2025_06_18,
321 ProtocolVersion::V2025_03_26,
322 ]);
323 assert_eq!(
324 set.as_slice(),
325 &[
326 ProtocolVersion::V2024_11_05,
327 ProtocolVersion::V2025_03_26,
328 ProtocolVersion::V2025_06_18,
329 ]
330 );
331 assert_eq!(set.len(), 3);
332 }
333
334 #[test]
335 fn latest_and_earliest_read_off_the_sorted_edges() {
336 let set = set_of(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]);
337 assert_eq!(set.latest(), ProtocolVersion::V2025_06_18);
338 assert_eq!(set.earliest(), ProtocolVersion::V2024_11_05);
339
340 let single = set_of(&[ProtocolVersion::V2025_11_25]);
341 assert_eq!(single.latest(), ProtocolVersion::V2025_11_25);
342 assert_eq!(single.earliest(), ProtocolVersion::V2025_11_25);
343 assert_eq!(single.len(), 1);
344 assert!(!single.is_empty());
345 }
346
347 #[test]
348 fn all_is_every_known_revision() {
349 let all = SupportedVersions::all().unwrap_or_else(|e| panic!("{e}"));
350 assert_eq!(all.as_slice(), ProtocolVersion::KNOWN);
351 assert_eq!(all.latest(), ProtocolVersion::latest_known());
352 assert_eq!(all.len(), ProtocolVersion::KNOWN.len());
353 }
354
355 #[test]
356 fn contains_uses_the_sorted_ordering() {
357 let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
358 assert!(set.contains(&ProtocolVersion::V2024_11_05));
359 assert!(set.contains(&ProtocolVersion::V2025_06_18));
360 assert!(!set.contains(&ProtocolVersion::V2025_03_26));
361 assert!(!set.contains(&ProtocolVersion::V2026_07_28));
362 let future = ProtocolVersion::Unknown("2027-01-01".parse::<ReleaseDate>().unwrap());
363 assert!(!set.contains(&future));
364 }
365
366 /// Newest-common, not first-offered: the specification tells a peer to
367 /// use the latest version it can support, and a client that advertised
368 /// oldest-first is not asking for the oldest.
369 #[test]
370 fn best_match_takes_the_newest_shared_revision() {
371 let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
372 assert_eq!(
373 set.best_match(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]),
374 Some(ProtocolVersion::V2025_06_18)
375 );
376 assert_eq!(
377 set.best_match(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]),
378 Some(ProtocolVersion::V2025_06_18)
379 );
380 }
381
382 /// The failure the task's `-> ProtocolVersion` signature could not express.
383 #[test]
384 fn best_match_is_none_when_nothing_is_shared() {
385 let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_03_26]);
386 assert_eq!(set.best_match(&[ProtocolVersion::V2026_07_28]), None);
387 assert_eq!(
388 set.best_match(&[ProtocolVersion::Unknown("2027-01-01".parse().unwrap())]),
389 None
390 );
391 assert_eq!(set.best_match(&[]), None);
392 }
393
394 #[test]
395 fn best_match_ignores_duplicate_offers() {
396 let set = set_of(&[ProtocolVersion::V2025_06_18]);
397 assert_eq!(
398 set.best_match(&[
399 ProtocolVersion::V2025_06_18,
400 ProtocolVersion::V2025_06_18,
401 ProtocolVersion::V2025_06_18
402 ]),
403 Some(ProtocolVersion::V2025_06_18)
404 );
405 }
406
407 /// An unknown revision on both sides is a real match: the peer is the only
408 /// thing that can speak it, and refusing to negotiate with it would leave
409 /// no path forward.
410 #[test]
411 fn best_match_can_return_an_unknown_revision() {
412 let future = ProtocolVersion::Unknown("2027-03-14".parse::<ReleaseDate>().unwrap());
413 let set = set_of(&[ProtocolVersion::V2025_06_18, future]);
414 assert_eq!(set.best_match(&[future]), Some(future));
415 assert_eq!(set.latest(), future);
416 }
417
418 #[test]
419 fn not_offered_by_is_the_complement() {
420 let set = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
421 let offered = [ProtocolVersion::V2025_06_18, ProtocolVersion::V2026_07_28];
422 let missing: Vec<ProtocolVersion> = set.not_offered_by(&offered).collect();
423 assert_eq!(missing, vec![ProtocolVersion::V2024_11_05]);
424 assert!(set.not_offered_by(&offered).all(|v| !offered.contains(&v)));
425 }
426
427 #[test]
428 fn display_joins_oldest_first() {
429 let set = set_of(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]);
430 assert_eq!(set.to_string(), "2024-11-05, 2025-06-18");
431 assert_eq!(
432 set_of(&[ProtocolVersion::V2025_06_18]).to_string(),
433 "2025-06-18"
434 );
435 }
436
437 #[test]
438 fn converts_from_a_slice_and_into_an_iterator() {
439 let from_try: SupportedVersions = (&[ProtocolVersion::V2024_11_05][..])
440 .try_into()
441 .unwrap_or_else(|e| panic!("{e}"));
442 assert_eq!(from_try.len(), 1);
443
444 let collected: Vec<ProtocolVersion> = set_of(&ProtocolVersion::KNOWN).into_iter().collect();
445 assert_eq!(collected, ProtocolVersion::KNOWN.to_vec());
446 }
447
448 #[test]
449 fn equality_ignores_construction_order() {
450 let forwards = set_of(&[ProtocolVersion::V2024_11_05, ProtocolVersion::V2025_06_18]);
451 let backwards = set_of(&[ProtocolVersion::V2025_06_18, ProtocolVersion::V2024_11_05]);
452 let with_duplicate = set_of(&[
453 ProtocolVersion::V2024_11_05,
454 ProtocolVersion::V2025_06_18,
455 ProtocolVersion::V2024_11_05,
456 ]);
457 assert_eq!(forwards, backwards);
458 assert_eq!(forwards, with_duplicate);
459 assert_ne!(forwards, set_of(&[ProtocolVersion::V2024_11_05]));
460 }
461}