krabka_kafka_tap/
frame.rs1use std::{collections::HashMap, fmt::Write as _};
5
6use krabka_ids::{ApiKey, ApiVersion};
7
8#[derive(Debug, PartialEq, Eq, Clone, Copy)]
9pub struct RequestPrefix {
10 pub api_key: ApiKey,
11 pub api_version: ApiVersion,
12 pub correlation_id: i32,
13}
14
15#[must_use]
18pub fn parse_request_prefix(body: &[u8]) -> Option<RequestPrefix> {
19 let [k0, k1, v0, v1, c0, c1, c2, c3, ..] = *body else {
20 return None;
21 };
22 Some(RequestPrefix {
23 api_key: ApiKey(i16::from_be_bytes([k0, k1])),
24 api_version: ApiVersion(i16::from_be_bytes([v0, v1])),
25 correlation_id: i32::from_be_bytes([c0, c1, c2, c3]),
26 })
27}
28
29#[must_use]
32pub fn read_correlation_id(body: &[u8]) -> Option<i32> {
33 body.first_chunk().copied().map(i32::from_be_bytes)
34}
35
36#[derive(Default)]
40pub struct Pending {
41 map: HashMap<i32, (ApiKey, ApiVersion)>,
42}
43
44impl Pending {
45 pub fn record(&mut self, correlation_id: i32, api_key: ApiKey, api_version: ApiVersion) {
46 self.map.insert(correlation_id, (api_key, api_version));
47 }
48 #[must_use]
49 pub fn take(&mut self, correlation_id: i32) -> Option<(ApiKey, ApiVersion)> {
50 self.map.remove(&correlation_id)
51 }
52}
53
54#[derive(Debug, Clone, PartialEq, Eq)]
56pub struct CapturedFrame {
57 pub api_key: ApiKey,
58 pub version: ApiVersion,
59 pub is_request: bool,
60 pub body: Vec<u8>,
63}
64
65impl CapturedFrame {
66 #[must_use]
69 pub fn spool_line(&self) -> String {
70 let mut hex = String::with_capacity(self.body.len() * 2);
71 for b in &self.body {
72 let _ = write!(hex, "{b:02x}");
73 }
74 format!(
75 "{{\"api_key\":{},\"version\":{},\"is_request\":{},\"body_hex\":\"{}\"}}\n",
76 self.api_key, self.version, self.is_request, hex
77 )
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 fn req_frame(api_key: i16, api_version: i16, corr: i32, payload: &[u8]) -> Vec<u8> {
86 let mut b = Vec::new();
87 b.extend_from_slice(&api_key.to_be_bytes());
88 b.extend_from_slice(&api_version.to_be_bytes());
89 b.extend_from_slice(&corr.to_be_bytes());
90 b.extend_from_slice(payload);
91 b
92 }
93
94 #[test]
95 fn parses_request_header_prefix() {
96 let body = req_frame(18, 3, 7, &[0xaa, 0xbb]);
97 let p = parse_request_prefix(&body).unwrap();
98 assert2::assert!(
99 p == RequestPrefix {
100 api_key: ApiKey(18),
101 api_version: ApiVersion(3),
102 correlation_id: 7
103 }
104 );
105 }
106
107 #[test]
112 fn correlation_id_needs_exactly_four_bytes() {
113 assert2::check!(read_correlation_id(&7_i32.to_be_bytes()) == Some(7));
114 assert2::check!(read_correlation_id(&[0, 0, 0]) == None);
115 assert2::check!(read_correlation_id(&[]) == None);
116 assert2::check!(read_correlation_id(&[0, 0, 0, 9, 0xaa, 0xbb]) == Some(9));
119 }
120
121 #[test]
125 fn correlation_id_needs_four_bytes_and_four_is_enough() {
126 assert2::check!(read_correlation_id(&7i32.to_be_bytes()) == Some(7));
127 assert2::check!(read_correlation_id(&[0, 0, 0, 1, 0xff]) == Some(1));
128 assert2::check!(read_correlation_id(&[0, 0, 0]) == None);
129 assert2::check!(read_correlation_id(&[]) == None);
130 }
131
132 #[test]
133 fn correlates_response_by_id() {
134 let mut pending = Pending::default();
135 let body = req_frame(1, 11, 42, &[]);
136 let p = parse_request_prefix(&body).unwrap();
137 pending.record(p.correlation_id, p.api_key, p.api_version);
138 let mut resp = Vec::new();
139 resp.extend_from_slice(&42i32.to_be_bytes());
140 resp.extend_from_slice(&[0x01]);
141 let got = pending.take(read_correlation_id(&resp).unwrap()).unwrap();
142 assert2::assert!(got == (ApiKey(1), ApiVersion(11)));
143 }
144}