use std::collections::BTreeMap;
use crate::api_key::ApiKey;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct VersionRange {
pub min: i16,
pub max: i16,
}
impl VersionRange {
pub const fn new(min: i16, max: i16) -> Self {
Self { min, max }
}
pub const fn is_empty(&self) -> bool {
self.min > self.max
}
pub const fn intersect(&self, other: &VersionRange) -> VersionRange {
VersionRange {
min: if self.min > other.min {
self.min
} else {
other.min
},
max: if self.max < other.max {
self.max
} else {
other.max
},
}
}
}
impl From<VersionRange> for (i16, i16) {
fn from(r: VersionRange) -> Self {
(r.min, r.max)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BrokerApiVersion {
pub api_key: ApiKey,
pub broker: VersionRange,
pub ours: Option<VersionRange>,
}
impl BrokerApiVersion {
pub fn negotiated(&self) -> Option<i16> {
let ours = self.ours?;
let overlap = self.broker.intersect(&ours);
if overlap.is_empty() {
None
} else {
Some(overlap.max)
}
}
pub fn broker_ahead(&self) -> bool {
self.ours.is_some_and(|ours| self.broker.max > ours.max)
}
}
#[derive(Debug, Clone, Default)]
pub struct ApiVersions {
entries: BTreeMap<i16, BrokerApiVersion>,
}
impl ApiVersions {
pub fn from_triples(triples: impl IntoIterator<Item = (i16, i16, i16)>) -> Self {
let entries = triples
.into_iter()
.map(|(code, min, max)| {
let api_key = ApiKey::from_code(code);
(
code,
BrokerApiVersion {
api_key,
broker: VersionRange::new(min, max),
ours: our_range(api_key),
},
)
})
.collect();
Self { entries }
}
pub fn entries(&self) -> impl Iterator<Item = &BrokerApiVersion> {
self.entries.values()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn get(&self, api_key: ApiKey) -> Option<&BrokerApiVersion> {
self.entries.get(&api_key.code())
}
pub fn supports(&self, api_key: ApiKey) -> bool {
self.get(api_key).is_some_and(|e| e.negotiated().is_some())
}
pub fn negotiate(&self, api_key: ApiKey) -> Result<i16> {
self.negotiate_with(api_key, our_range(api_key))
}
pub fn negotiate_with(&self, api_key: ApiKey, ours: Option<VersionRange>) -> Result<i16> {
let entry = self.entries.get(&api_key.code());
let negotiated = match (entry, ours) {
(Some(entry), Some(ours)) => {
let overlap = entry.broker.intersect(&ours);
if overlap.is_empty() {
None
} else {
Some(overlap.max)
}
}
_ => None,
};
match negotiated {
Some(version) => Ok(version),
None => Err(Error::UnsupportedApi {
api_key,
broker: entry.map(|e| e.broker.into()),
ours: ours.map(Into::into),
}),
}
}
}
pub fn our_range(api_key: ApiKey) -> Option<VersionRange> {
let upstream = kafka_protocol::messages::ApiKey::try_from(api_key.code()).ok()?;
let range = upstream.valid_versions();
Some(VersionRange::new(range.min, range.max))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn we_clamp_to_our_own_ceiling() {
let ours = our_range(ApiKey::Metadata).expect("metadata has a schema");
let table = ApiVersions::from_triples([(ApiKey::Metadata.code(), 0, 99)]);
assert_eq!(table.negotiate(ApiKey::Metadata).ok(), Some(ours.max));
let row = table.get(ApiKey::Metadata).expect("row");
assert!(row.broker_ahead());
}
#[test]
fn we_clamp_to_the_broker_ceiling_when_it_is_lower() {
let table = ApiVersions::from_triples([(ApiKey::Metadata.code(), 0, 2)]);
assert_eq!(table.negotiate(ApiKey::Metadata).ok(), Some(2));
let row = table.get(ApiKey::Metadata).expect("row");
assert!(!row.broker_ahead());
}
#[test]
fn disjoint_ranges_are_an_error_not_a_guess() {
let ours = our_range(ApiKey::Metadata).expect("metadata has a schema");
let table =
ApiVersions::from_triples([(ApiKey::Metadata.code(), ours.max + 1, ours.max + 5)]);
let err = table.negotiate(ApiKey::Metadata).unwrap_err();
assert!(matches!(err, Error::UnsupportedApi { .. }), "{err:?}");
}
#[test]
fn a_key_the_broker_never_mentioned_is_an_error() {
let table = ApiVersions::from_triples([]);
let err = table.negotiate(ApiKey::Metadata).unwrap_err();
match err {
Error::UnsupportedApi { broker, ours, .. } => {
assert!(broker.is_none());
assert!(ours.is_some());
}
other => panic!("{other:?}"),
}
}
#[test]
fn keys_with_no_schema_here_survive_in_the_table() {
let table = ApiVersions::from_triples([(89, 0, 1)]);
let row = table.get(ApiKey::from_code(89)).expect("row survives");
assert_eq!(row.api_key, ApiKey::Unknown(89));
assert!(row.ours.is_none());
assert!(row.negotiated().is_none());
assert!(!row.broker_ahead());
assert!(!table.supports(ApiKey::from_code(89)));
}
}