1use crate::codec::{Decoder, Encoder};
2use crate::error::Result;
3use crate::header::RequestHeader;
4
5pub const API_KEY: i16 = 8;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct OffsetCommitRequestV2 {
9 pub correlation_id: i32,
10 pub client_id: Option<String>,
11 pub group_id: String,
12 pub generation_id_or_member_epoch: i32,
13 pub member_id: String,
14 pub retention_time_ms: i64,
15 pub topics: Vec<OffsetCommitTopic>,
16}
17
18impl OffsetCommitRequestV2 {
19 pub fn encode(&self) -> Result<Vec<u8>> {
20 let mut encoder = Encoder::new();
21 RequestHeader {
22 api_key: API_KEY,
23 api_version: 2,
24 correlation_id: self.correlation_id,
25 client_id: self.client_id.clone(),
26 }
27 .encode_v1(&mut encoder)?;
28 encoder.write_string(&self.group_id)?;
29 encoder.write_i32(self.generation_id_or_member_epoch);
30 encoder.write_string(&self.member_id)?;
31 encoder.write_i64(self.retention_time_ms);
32 encoder.write_array(Some(self.topics.as_slice()), |encoder, topic| {
33 topic.encode(encoder)
34 })?;
35 Ok(encoder.into_bytes())
36 }
37}
38
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct OffsetCommitRequestV7 {
41 pub correlation_id: i32,
42 pub client_id: Option<String>,
43 pub group_id: String,
44 pub generation_id_or_member_epoch: i32,
45 pub member_id: String,
46 pub group_instance_id: Option<String>,
47 pub topics: Vec<OffsetCommitTopicV7>,
48}
49
50impl OffsetCommitRequestV7 {
51 pub fn encode(&self) -> Result<Vec<u8>> {
52 let mut encoder = Encoder::new();
53 RequestHeader {
54 api_key: API_KEY,
55 api_version: 7,
56 correlation_id: self.correlation_id,
57 client_id: self.client_id.clone(),
58 }
59 .encode_v1(&mut encoder)?;
60 encoder.write_string(&self.group_id)?;
61 encoder.write_i32(self.generation_id_or_member_epoch);
62 encoder.write_string(&self.member_id)?;
63 encoder.write_nullable_string(self.group_instance_id.as_deref())?;
64 encoder.write_array(Some(self.topics.as_slice()), |encoder, topic| {
65 topic.encode(encoder)
66 })?;
67 Ok(encoder.into_bytes())
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
77pub struct OffsetCommitRequestV9 {
78 pub correlation_id: i32,
79 pub client_id: Option<String>,
80 pub group_id: String,
81 pub generation_id_or_member_epoch: i32,
82 pub member_id: String,
83 pub group_instance_id: Option<String>,
84 pub topics: Vec<OffsetCommitTopicV9>,
85}
86
87impl OffsetCommitRequestV9 {
88 pub fn encode(&self) -> Result<Vec<u8>> {
89 let mut encoder = Encoder::new();
90 RequestHeader {
91 api_key: API_KEY,
92 api_version: 9,
93 correlation_id: self.correlation_id,
94 client_id: self.client_id.clone(),
95 }
96 .encode_v2(&mut encoder)?;
97 encoder.write_compact_string(&self.group_id)?;
98 encoder.write_i32(self.generation_id_or_member_epoch);
99 encoder.write_compact_string(&self.member_id)?;
100 encoder.write_compact_nullable_string(self.group_instance_id.as_deref())?;
101 encoder.write_compact_array(Some(self.topics.as_slice()), |encoder, topic| {
102 topic.encode(encoder)
103 })?;
104 encoder.write_empty_tagged_fields();
105 Ok(encoder.into_bytes())
106 }
107}
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub struct OffsetCommitTopic {
111 pub name: String,
112 pub partitions: Vec<OffsetCommitPartition>,
113}
114
115impl OffsetCommitTopic {
116 fn encode(&self, encoder: &mut Encoder) -> Result<()> {
117 encoder.write_string(&self.name)?;
118 encoder.write_array(Some(self.partitions.as_slice()), |encoder, partition| {
119 partition.encode(encoder)
120 })
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct OffsetCommitTopicV7 {
126 pub name: String,
127 pub partitions: Vec<OffsetCommitPartitionV7>,
128}
129
130impl OffsetCommitTopicV7 {
131 fn encode(&self, encoder: &mut Encoder) -> Result<()> {
132 encoder.write_string(&self.name)?;
133 encoder.write_array(Some(self.partitions.as_slice()), |encoder, partition| {
134 partition.encode(encoder)
135 })
136 }
137}
138
139#[derive(Debug, Clone, PartialEq, Eq)]
140pub struct OffsetCommitTopicV9 {
141 pub name: String,
142 pub partitions: Vec<OffsetCommitPartitionV9>,
143}
144
145impl OffsetCommitTopicV9 {
146 fn encode(&self, encoder: &mut Encoder) -> Result<()> {
147 encoder.write_compact_string(&self.name)?;
148 encoder.write_compact_array(Some(self.partitions.as_slice()), |encoder, partition| {
149 partition.encode(encoder)
150 })?;
151 encoder.write_empty_tagged_fields();
152 Ok(())
153 }
154}
155
156#[derive(Debug, Clone, PartialEq, Eq)]
157pub struct OffsetCommitPartition {
158 pub partition_index: i32,
159 pub committed_offset: i64,
160 pub committed_metadata: Option<String>,
161}
162
163impl OffsetCommitPartition {
164 fn encode(&self, encoder: &mut Encoder) -> Result<()> {
165 encoder.write_i32(self.partition_index);
166 encoder.write_i64(self.committed_offset);
167 encoder.write_nullable_string(self.committed_metadata.as_deref())
168 }
169}
170
171#[derive(Debug, Clone, PartialEq, Eq)]
172pub struct OffsetCommitPartitionV7 {
173 pub partition_index: i32,
174 pub committed_offset: i64,
175 pub committed_leader_epoch: i32,
176 pub committed_metadata: Option<String>,
177}
178
179impl OffsetCommitPartitionV7 {
180 fn encode(&self, encoder: &mut Encoder) -> Result<()> {
181 encoder.write_i32(self.partition_index);
182 encoder.write_i64(self.committed_offset);
183 encoder.write_i32(self.committed_leader_epoch);
184 encoder.write_nullable_string(self.committed_metadata.as_deref())
185 }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq)]
189pub struct OffsetCommitPartitionV9 {
190 pub partition_index: i32,
191 pub committed_offset: i64,
192 pub committed_leader_epoch: i32,
193 pub committed_metadata: Option<String>,
194}
195
196impl OffsetCommitPartitionV9 {
197 fn encode(&self, encoder: &mut Encoder) -> Result<()> {
198 encoder.write_i32(self.partition_index);
199 encoder.write_i64(self.committed_offset);
200 encoder.write_i32(self.committed_leader_epoch);
201 encoder.write_compact_nullable_string(self.committed_metadata.as_deref())?;
202 encoder.write_empty_tagged_fields();
203 Ok(())
204 }
205}
206
207#[derive(Debug, Clone, PartialEq, Eq)]
208pub struct OffsetCommitResponseV2 {
209 pub topics: Vec<OffsetCommitTopicResponse>,
210}
211
212impl OffsetCommitResponseV2 {
213 pub fn decode_body(decoder: &mut Decoder<'_>) -> Result<Self> {
214 Ok(Self {
215 topics: decoder
216 .read_array(
217 "offset commit topic responses",
218 OffsetCommitTopicResponse::decode,
219 )?
220 .unwrap_or_default(),
221 })
222 }
223}
224
225#[derive(Debug, Clone, PartialEq, Eq)]
226pub struct OffsetCommitResponseV7 {
227 pub throttle_time_ms: i32,
228 pub topics: Vec<OffsetCommitTopicResponse>,
229}
230
231impl OffsetCommitResponseV7 {
232 pub fn decode_body(decoder: &mut Decoder<'_>) -> Result<Self> {
233 Ok(Self {
234 throttle_time_ms: decoder.read_i32()?,
235 topics: decoder
236 .read_array(
237 "offset commit topic responses",
238 OffsetCommitTopicResponse::decode,
239 )?
240 .unwrap_or_default(),
241 })
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
246pub struct OffsetCommitResponseV9 {
247 pub throttle_time_ms: i32,
248 pub topics: Vec<OffsetCommitTopicResponse>,
249}
250
251impl OffsetCommitResponseV9 {
252 pub fn decode_body(decoder: &mut Decoder<'_>) -> Result<Self> {
253 let throttle_time_ms = decoder.read_i32()?;
254 let topics = decoder
255 .read_compact_array("offset commit topic responses", |decoder| {
256 let name = decoder.read_compact_string()?;
257 let partitions = decoder
258 .read_compact_array("offset commit partition responses", |decoder| {
259 let partition_index = decoder.read_i32()?;
260 let error_code = decoder.read_i16()?;
261 decoder.read_tagged_fields()?;
262 Ok(OffsetCommitPartitionResponse {
263 partition_index,
264 error_code,
265 })
266 })?
267 .unwrap_or_default();
268 decoder.read_tagged_fields()?;
269 Ok(OffsetCommitTopicResponse { name, partitions })
270 })?
271 .unwrap_or_default();
272 decoder.read_tagged_fields()?;
273 Ok(Self {
274 throttle_time_ms,
275 topics,
276 })
277 }
278}
279
280#[derive(Debug, Clone, PartialEq, Eq)]
281pub struct OffsetCommitTopicResponse {
282 pub name: String,
283 pub partitions: Vec<OffsetCommitPartitionResponse>,
284}
285
286impl OffsetCommitTopicResponse {
287 fn decode(decoder: &mut Decoder<'_>) -> Result<Self> {
288 Ok(Self {
289 name: decoder.read_string()?,
290 partitions: decoder
291 .read_array(
292 "offset commit partition responses",
293 OffsetCommitPartitionResponse::decode,
294 )?
295 .unwrap_or_default(),
296 })
297 }
298}
299
300#[derive(Debug, Clone, PartialEq, Eq)]
301pub struct OffsetCommitPartitionResponse {
302 pub partition_index: i32,
303 pub error_code: i16,
304}
305
306impl OffsetCommitPartitionResponse {
307 fn decode(decoder: &mut Decoder<'_>) -> Result<Self> {
308 Ok(Self {
309 partition_index: decoder.read_i32()?,
310 error_code: decoder.read_i16()?,
311 })
312 }
313}
314
315#[cfg(test)]
316#[allow(clippy::unwrap_used)]
317mod tests {
318 use super::{
319 OffsetCommitPartition, OffsetCommitPartitionResponse, OffsetCommitPartitionV7,
320 OffsetCommitPartitionV9, OffsetCommitRequestV2, OffsetCommitRequestV7,
321 OffsetCommitRequestV9, OffsetCommitResponseV2, OffsetCommitResponseV7,
322 OffsetCommitResponseV9, OffsetCommitTopic, OffsetCommitTopicResponse, OffsetCommitTopicV7,
323 OffsetCommitTopicV9,
324 };
325 use crate::codec::{Decoder, Encoder};
326
327 #[test]
328 fn encodes_offset_commit_v2_request() {
329 let request = OffsetCommitRequestV2 {
330 correlation_id: 23,
331 client_id: Some("kafrust".to_owned()),
332 group_id: "orders-group".to_owned(),
333 generation_id_or_member_epoch: 7,
334 member_id: "member-a".to_owned(),
335 retention_time_ms: 86_400_000,
336 topics: vec![OffsetCommitTopic {
337 name: "orders".to_owned(),
338 partitions: vec![OffsetCommitPartition {
339 partition_index: 0,
340 committed_offset: 42,
341 committed_metadata: Some("processed".to_owned()),
342 }],
343 }],
344 };
345
346 assert_eq!(
347 request.encode().unwrap(),
348 [
349 0, 8, 0, 2, 0, 0, 0, 23, 0, 7, b'k', b'a', b'f', b'r', b'u', b's', b't', 0, 12, b'o', b'r', b'd', b'e', b'r', b's', b'-', b'g', b'r', b'o', b'u',
354 b'p', 0, 0, 0, 7, 0, 8, b'm', b'e', b'm', b'b', b'e', b'r', b'-', b'a', 0, 0, 0, 0, 5, 38, 92, 0, 0, 0, 0, 1, 0, 6, b'o', b'r', b'd', b'e', b'r', b's', 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 42, 0, 9, b'p', b'r', b'o', b'c', b'e', b's', b's', b'e', b'd', ]
365 );
366 }
367
368 #[test]
369 fn decodes_offset_commit_v2_response() {
370 let mut bytes = Encoder::new();
371 bytes.write_i32(1);
372 bytes.write_string("orders").unwrap();
373 bytes.write_i32(1);
374 bytes.write_i32(0);
375 bytes.write_i16(0);
376 let bytes = bytes.into_bytes();
377
378 let mut decoder = Decoder::new(&bytes);
379 let response = OffsetCommitResponseV2::decode_body(&mut decoder).unwrap();
380
381 assert_eq!(
382 response.topics,
383 vec![OffsetCommitTopicResponse {
384 name: "orders".to_owned(),
385 partitions: vec![OffsetCommitPartitionResponse {
386 partition_index: 0,
387 error_code: 0,
388 }],
389 }]
390 );
391 assert!(decoder.is_empty());
392 }
393
394 #[test]
395 fn encodes_offset_commit_v7_request_with_static_member() {
396 let request = OffsetCommitRequestV7 {
397 correlation_id: 23,
398 client_id: Some("kafrust".to_owned()),
399 group_id: "orders-group".to_owned(),
400 generation_id_or_member_epoch: 7,
401 member_id: "member-a".to_owned(),
402 group_instance_id: Some("orders-reader-1".to_owned()),
403 topics: vec![OffsetCommitTopicV7 {
404 name: "orders".to_owned(),
405 partitions: vec![OffsetCommitPartitionV7 {
406 partition_index: 0,
407 committed_offset: 42,
408 committed_leader_epoch: -1,
409 committed_metadata: None,
410 }],
411 }],
412 };
413
414 let encoded = request.encode().unwrap();
415 assert_eq!(&encoded[0..4], &[0, 8, 0, 7]);
416 assert!(encoded
417 .windows(17)
418 .any(|bytes| bytes == b"\0\x0forders-reader-1"));
419 assert!(encoded.windows(4).any(|bytes| bytes == [u8::MAX; 4]));
420 }
421
422 #[test]
423 fn decodes_offset_commit_v7_response() {
424 let mut bytes = Encoder::new();
425 bytes.write_i32(12);
426 bytes.write_i32(1);
427 bytes.write_string("orders").unwrap();
428 bytes.write_i32(1);
429 bytes.write_i32(0);
430 bytes.write_i16(0);
431 let bytes = bytes.into_bytes();
432
433 let mut decoder = Decoder::new(&bytes);
434 let response = OffsetCommitResponseV7::decode_body(&mut decoder).unwrap();
435
436 assert_eq!(response.throttle_time_ms, 12);
437 assert_eq!(response.topics[0].partitions[0].error_code, 0);
438 assert!(decoder.is_empty());
439 }
440
441 #[test]
442 fn encodes_offset_commit_v9_request_for_consumer_protocol() {
443 let request = OffsetCommitRequestV9 {
444 correlation_id: 23,
445 client_id: Some("kafrust".to_owned()),
446 group_id: "orders-group".to_owned(),
447 generation_id_or_member_epoch: 7,
448 member_id: "member-a".to_owned(),
449 group_instance_id: Some("orders-reader-1".to_owned()),
450 topics: vec![OffsetCommitTopicV9 {
451 name: "orders".to_owned(),
452 partitions: vec![OffsetCommitPartitionV9 {
453 partition_index: 0,
454 committed_offset: 42,
455 committed_leader_epoch: -1,
456 committed_metadata: None,
457 }],
458 }],
459 };
460
461 let encoded = request.encode().unwrap();
462 assert_eq!(&encoded[0..4], &[0, 8, 0, 9]);
463 assert_eq!(
464 &encoded[8..18],
465 &[0, 7, b'k', b'a', b'f', b'r', b'u', b's', b't', 0]
466 );
467
468 let mut decoder = Decoder::new(&encoded[18..]);
469 assert_eq!(decoder.read_compact_string().unwrap(), "orders-group");
470 assert_eq!(decoder.read_i32().unwrap(), 7);
471 assert_eq!(decoder.read_compact_string().unwrap(), "member-a");
472 assert_eq!(
473 decoder.read_compact_nullable_string().unwrap(),
474 Some("orders-reader-1".to_owned())
475 );
476 let topics = decoder
477 .read_compact_array("offset commit topics", |decoder| {
478 let name = decoder.read_compact_string()?;
479 let partitions = decoder
480 .read_compact_array("offset commit partitions", |decoder| {
481 let partition_index = decoder.read_i32()?;
482 let committed_offset = decoder.read_i64()?;
483 let committed_leader_epoch = decoder.read_i32()?;
484 let committed_metadata = decoder.read_compact_nullable_string()?;
485 decoder.read_tagged_fields()?;
486 Ok((
487 partition_index,
488 committed_offset,
489 committed_leader_epoch,
490 committed_metadata,
491 ))
492 })?
493 .unwrap_or_default();
494 decoder.read_tagged_fields()?;
495 Ok((name, partitions))
496 })
497 .unwrap()
498 .unwrap();
499 assert_eq!(topics[0].0, "orders");
500 assert_eq!(topics[0].1[0].0, 0);
501 assert_eq!(topics[0].1[0].1, 42);
502 assert_eq!(topics[0].1[0].2, -1);
503 assert_eq!(topics[0].1[0].3, None);
504 decoder.read_tagged_fields().unwrap();
505 assert!(decoder.is_empty());
506 }
507
508 #[test]
509 fn decodes_offset_commit_v9_response() {
510 let mut bytes = Encoder::new();
511 bytes.write_i32(12);
512 bytes
513 .write_compact_array(Some(&[()]), |encoder, ()| {
514 encoder.write_compact_string("orders")?;
515 encoder.write_compact_array(Some(&[()]), |encoder, ()| {
516 encoder.write_i32(0);
517 encoder.write_i16(0);
518 encoder.write_empty_tagged_fields();
519 Ok(())
520 })?;
521 encoder.write_empty_tagged_fields();
522 Ok(())
523 })
524 .unwrap();
525 bytes.write_empty_tagged_fields();
526
527 let bytes = bytes.into_bytes();
528 let mut decoder = Decoder::new(&bytes);
529 let response = OffsetCommitResponseV9::decode_body(&mut decoder).unwrap();
530
531 assert_eq!(response.throttle_time_ms, 12);
532 assert_eq!(response.topics[0].name, "orders");
533 assert_eq!(response.topics[0].partitions[0].partition_index, 0);
534 assert_eq!(response.topics[0].partitions[0].error_code, 0);
535 assert!(decoder.is_empty());
536 }
537}