use crate::WireError;
use crate::codec::{WireDecode, WireEncode, read_u32_le};
use crate::primitives::options::{WireOptions, decode_options_prefixed};
use crate::primitives::partition_assignment::CreatedPartitionAssignment;
use crate::requests::topics::CreateTopicRequest;
use bytes::{BufMut, BytesMut};
fn usize_to_u32(value: usize, context: &str) -> u32 {
u32::try_from(value).unwrap_or_else(|_| panic!("{context} exceeds u32"))
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CreateTopicWithAssignmentsRequest {
pub request: CreateTopicRequest,
pub derived_options: WireOptions,
pub partitions: Vec<CreatedPartitionAssignment>,
pub created_view: u32,
}
impl WireEncode for CreateTopicWithAssignmentsRequest {
fn encoded_size(&self) -> usize {
4 + self.request.encoded_size()
+ 4
+ self.derived_options.encoded_size()
+ 4
+ self
.partitions
.iter()
.map(WireEncode::encoded_size)
.sum::<usize>()
+ 4
}
fn encode(&self, buf: &mut BytesMut) {
buf.put_u32_le(usize_to_u32(
self.request.encoded_size(),
"create topic request size",
));
self.request.encode(buf);
buf.put_u32_le(usize_to_u32(
self.derived_options.encoded_size(),
"create topic derived options size",
));
self.derived_options.encode(buf);
buf.put_u32_le(usize_to_u32(
self.partitions.len(),
"create topic partition count",
));
for partition in &self.partitions {
partition.encode(buf);
}
buf.put_u32_le(self.created_view);
}
}
impl WireDecode for CreateTopicWithAssignmentsRequest {
fn decode(buf: &[u8]) -> Result<(Self, usize), WireError> {
let request_size = read_u32_le(buf, 0)? as usize;
let request_start: usize = 4;
let request_end =
request_start
.checked_add(request_size)
.ok_or_else(|| WireError::UnexpectedEof {
offset: request_start,
need: request_size,
have: buf.len().saturating_sub(request_start),
})?;
if buf.len() < request_end {
return Err(WireError::UnexpectedEof {
offset: request_start,
need: request_size,
have: buf.len().saturating_sub(request_start),
});
}
let request = CreateTopicRequest::decode_from(&buf[request_start..request_end])?;
let (derived_options, derived_consumed) = decode_options_prefixed(buf, request_end)?;
let derived_end = request_end + derived_consumed;
let partitions_count = read_u32_le(buf, derived_end)? as usize;
let mut offset = derived_end + 4;
let mut partitions = Vec::with_capacity(crate::codec::bounded_capacity(
partitions_count,
buf.len().saturating_sub(offset),
CreatedPartitionAssignment::ENCODED_SIZE,
));
for _ in 0..partitions_count {
let (partition, consumed) = CreatedPartitionAssignment::decode(&buf[offset..])?;
offset += consumed;
partitions.push(partition);
}
let created_view = if offset < buf.len() {
let view = read_u32_le(buf, offset)?;
offset += 4;
view
} else {
0
};
Ok((
Self {
request,
derived_options,
partitions,
created_view,
},
offset,
))
}
}
#[cfg(test)]
mod tests {
use super::CreateTopicWithAssignmentsRequest;
use crate::WireIdentifier;
use crate::codec::{WireDecode, WireEncode};
use crate::primitives::identifier::WireName;
use crate::primitives::options::WireOptions;
use crate::primitives::partition_assignment::CreatedPartitionAssignment;
use crate::primitives::user_headers::encode_user_headers;
use crate::requests::topics::CreateTopicRequest;
use bytes::BytesMut;
fn options_block(entries: &[(u8, &[u8], u8, &[u8])]) -> WireOptions {
let mut buf = BytesMut::new();
encode_user_headers(entries, &mut buf);
WireOptions::from_bytes(buf.freeze()).unwrap()
}
#[test]
fn roundtrip() {
let request = CreateTopicWithAssignmentsRequest {
request: CreateTopicRequest {
stream_id: WireIdentifier::numeric(1),
partitions_count: 2,
name: WireName::new("events").unwrap(),
options: options_block(&[(2, b"message_expiry", 2, b"7 days")]),
},
derived_options: options_block(&[(2, b"max_topic_size", 12, &1024u64.to_le_bytes())]),
partitions: vec![
CreatedPartitionAssignment {
partition_id: 0,
consensus_group_id: 1,
},
CreatedPartitionAssignment {
partition_id: 1,
consensus_group_id: 2,
},
],
created_view: 7,
};
let bytes = request.to_bytes();
let (decoded, consumed) = CreateTopicWithAssignmentsRequest::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, request);
}
#[test]
fn decode_without_trailing_view_reads_zero() {
let request = CreateTopicWithAssignmentsRequest {
request: CreateTopicRequest {
stream_id: WireIdentifier::numeric(1),
partitions_count: 1,
name: WireName::new("events").unwrap(),
options: WireOptions::empty(),
},
derived_options: WireOptions::empty(),
partitions: vec![CreatedPartitionAssignment {
partition_id: 0,
consensus_group_id: 1,
}],
created_view: 7,
};
let bytes = request.to_bytes();
let old_bytes = &bytes[..bytes.len() - 4];
let (decoded, consumed) = CreateTopicWithAssignmentsRequest::decode(old_bytes).unwrap();
assert_eq!(consumed, old_bytes.len());
assert_eq!(decoded.created_view, 0);
assert_eq!(decoded.partitions, request.partitions);
assert_eq!(decoded.request, request.request);
}
#[test]
fn roundtrip_with_empty_blocks() {
let request = CreateTopicWithAssignmentsRequest {
request: CreateTopicRequest {
stream_id: WireIdentifier::numeric(7),
partitions_count: 0,
name: WireName::new("bare").unwrap(),
options: WireOptions::empty(),
},
derived_options: WireOptions::empty(),
partitions: vec![],
created_view: 0,
};
let bytes = request.to_bytes();
let (decoded, consumed) = CreateTopicWithAssignmentsRequest::decode(&bytes).unwrap();
assert_eq!(consumed, bytes.len());
assert_eq!(decoded, request);
}
}