Skip to main content

crabka_client_core/
version.rs

1//! `ApiVersionTable` — broker-advertised version ranges per API key,
2//! plus client-side negotiation.
3
4use std::collections::HashMap;
5
6use crate::error::ClientError;
7use crate::request::ProtocolRequest;
8
9#[derive(Debug, Clone, Default)]
10pub struct ApiVersionTable {
11    by_key: HashMap<i16, (i16, i16)>,
12}
13
14impl ApiVersionTable {
15    /// Build from a sequence of `(api_key, broker_min, broker_max)` tuples.
16    /// Used when seeding the table from a decoded `ApiVersionsResponse`.
17    #[must_use]
18    pub fn from_entries(entries: impl IntoIterator<Item = (i16, i16, i16)>) -> Self {
19        let mut by_key = HashMap::new();
20        for (k, lo, hi) in entries {
21            by_key.insert(k, (lo, hi));
22        }
23        Self { by_key }
24    }
25
26    /// Highest version both sides support for `R`, or
27    /// [`ClientError::IncompatibleVersion`] if the ranges don't overlap.
28    pub fn negotiate<R: ProtocolRequest>(&self) -> Result<i16, ClientError> {
29        let api_key = R::API_KEY;
30        let client_min = R::MIN_VERSION;
31        let client_max = R::MAX_VERSION;
32        let (broker_min, broker_max) = self.by_key.get(&api_key).copied().unwrap_or((0, 0));
33        let chosen = client_max.min(broker_max);
34        if chosen < client_min || chosen < broker_min {
35            return Err(ClientError::IncompatibleVersion {
36                api_key,
37                broker_min,
38                broker_max,
39                client_min,
40                client_max,
41            });
42        }
43        Ok(chosen)
44    }
45
46    /// Return the broker-advertised `(min, max)` version range for `api_key`,
47    /// or `None` if the broker didn't advertise it.
48    #[must_use]
49    pub fn broker_range(&self, api_key: i16) -> Option<(i16, i16)> {
50        self.by_key.get(&api_key).copied()
51    }
52
53    /// Returns `true` if the table contains no entries.
54    #[must_use]
55    pub fn is_empty(&self) -> bool {
56        self.by_key.is_empty()
57    }
58}
59
60#[cfg(test)]
61mod tests {
62    use super::*;
63    use assert2::assert;
64    use crabka_protocol::owned::api_versions_request::ApiVersionsRequest;
65
66    // `ApiVersionsRequest` acts as a sample `ProtocolRequest`. We only
67    // need the trait's constants here; the impl comes from codegen.
68
69    #[test]
70    fn negotiate_takes_min_of_max() {
71        let t = ApiVersionTable::from_entries([(
72            ApiVersionsRequest::API_KEY,
73            0,
74            ApiVersionsRequest::MAX_VERSION,
75        )]);
76        // Sanity: client max wins if broker max is higher.
77        let _ = t.negotiate::<ApiVersionsRequest>().unwrap();
78    }
79
80    #[test]
81    fn negotiate_errors_when_disjoint() {
82        let t = ApiVersionTable::from_entries([(ApiVersionsRequest::API_KEY, 99, 100)]);
83        assert!(matches!(
84            t.negotiate::<ApiVersionsRequest>(),
85            Err(ClientError::IncompatibleVersion { .. })
86        ));
87    }
88
89    #[test]
90    fn negotiate_picks_lowest_supported_when_broker_caps_low() {
91        let t = ApiVersionTable::from_entries([(ApiVersionsRequest::API_KEY, 0, 0)]);
92        // Both sides support 0; that's what's chosen.
93        assert!(t.negotiate::<ApiVersionsRequest>().unwrap() == 0);
94    }
95}