Skip to main content

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}