blob: 18286b05121b7573fece8745d2a96a7221f35dee [file]
use crate::bytes_serializable::BytesSerializable;
use crate::command::CommandPayload;
use crate::error::IggyError;
use crate::identifier::Identifier;
use crate::topics::{MAX_NAME_LENGTH, MAX_PARTITIONS_COUNT};
use crate::utils::byte_size::IggyByteSize;
use crate::utils::text;
use crate::validatable::Validatable;
use bytes::BufMut;
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::from_utf8;
/// `CreateTopic` command is used to create a new topic in a stream.
/// It has additional payload:
/// - `stream_id` - unique stream ID (numeric or name).
/// - `topic_id` - unique topic ID (numeric).
/// - `partitions_count` - number of partitions in the topic, max value is 1000.
/// - `message_expiry` - optional message expiry in seconds, if `None` then messages will never expire.
/// - `max_topic_size` - optional maximum size of the topic, if `None` then topic size is unlimited.
/// Can't be lower than segment size in the config.
/// - `replication_factor` - replication factor for the topic.
/// - `name` - unique topic name, max length is 255 characters.
#[derive(Debug, Serialize, Deserialize, PartialEq)]
pub struct CreateTopic {
/// Unique stream ID (numeric or name).
#[serde(skip)]
pub stream_id: Identifier,
/// Unique topic ID (numeric), if None is provided then the server will automatically assign it.
pub topic_id: Option<u32>,
/// Number of partitions in the topic, max value is 1000.
pub partitions_count: u32,
/// Optional message expiry in seconds, if `None` then messages will never expire.
pub message_expiry: Option<u32>,
/// The optional maximum size of the topic.
pub max_topic_size: Option<IggyByteSize>,
/// Replication factor for the topic.
pub replication_factor: u8,
/// Unique topic name, max length is 255 characters.
pub name: String,
}
impl CommandPayload for CreateTopic {}
impl Default for CreateTopic {
fn default() -> Self {
CreateTopic {
stream_id: Identifier::default(),
topic_id: Some(1),
partitions_count: 1,
message_expiry: None,
max_topic_size: None,
replication_factor: 1,
name: "topic".to_string(),
}
}
}
impl Validatable<IggyError> for CreateTopic {
fn validate(&self) -> Result<(), IggyError> {
if let Some(topic_id) = self.topic_id {
if topic_id == 0 {
return Err(IggyError::InvalidTopicId);
}
}
if self.name.is_empty() || self.name.len() > MAX_NAME_LENGTH {
return Err(IggyError::InvalidTopicName);
}
if !text::is_resource_name_valid(&self.name) {
return Err(IggyError::InvalidTopicName);
}
if !(0..=MAX_PARTITIONS_COUNT).contains(&self.partitions_count) {
return Err(IggyError::TooManyPartitions);
}
if self.replication_factor == 0 {
return Err(IggyError::InvalidReplicationFactor);
}
Ok(())
}
}
impl BytesSerializable for CreateTopic {
fn as_bytes(&self) -> Vec<u8> {
let stream_id_bytes = self.stream_id.as_bytes();
let mut bytes = Vec::with_capacity(22 + stream_id_bytes.len() + self.name.len());
bytes.extend(stream_id_bytes);
bytes.put_u32_le(self.topic_id.unwrap_or(0));
bytes.put_u32_le(self.partitions_count);
match self.message_expiry {
Some(message_expiry) => bytes.put_u32_le(message_expiry),
None => bytes.put_u32_le(0),
}
match self.max_topic_size {
Some(max_topic_size) => bytes.put_u64_le(max_topic_size.as_bytes_u64()),
None => bytes.put_u64_le(0),
}
bytes.put_u8(self.replication_factor);
#[allow(clippy::cast_possible_truncation)]
bytes.put_u8(self.name.len() as u8);
bytes.extend(self.name.as_bytes());
bytes
}
fn from_bytes(bytes: &[u8]) -> std::result::Result<CreateTopic, IggyError> {
if bytes.len() < 18 {
return Err(IggyError::InvalidCommand);
}
let mut position = 0;
let stream_id = Identifier::from_bytes(bytes)?;
position += stream_id.get_size_bytes() as usize;
let topic_id = u32::from_le_bytes(bytes[position..position + 4].try_into()?);
let topic_id = if topic_id == 0 { None } else { Some(topic_id) };
let partitions_count = u32::from_le_bytes(bytes[position + 4..position + 8].try_into()?);
let message_expiry =
match u32::from_le_bytes(bytes[position + 8..position + 12].try_into()?) {
0 => None,
size => Some(size),
};
let max_topic_size =
match u64::from_le_bytes(bytes[position + 12..position + 20].try_into()?) {
0 => None,
size => Some(IggyByteSize::from(size)),
};
let replication_factor = bytes[position + 20];
let name_length = bytes[position + 21];
let name =
from_utf8(&bytes[position + 22..(position + 22 + name_length as usize)])?.to_string();
if name.len() != name_length as usize {
return Err(IggyError::InvalidCommand);
}
let command = CreateTopic {
stream_id,
topic_id,
partitions_count,
message_expiry,
max_topic_size,
replication_factor,
name,
};
command.validate()?;
Ok(command)
}
}
impl Display for CreateTopic {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let max_topic_size = match self.max_topic_size {
Some(max_topic_size) => max_topic_size.to_string(),
None => "unlimited".to_string(),
};
write!(
f,
"{}|{}|{}|{}|{}|{}|{}",
self.stream_id,
self.topic_id.unwrap_or(0),
self.partitions_count,
self.message_expiry.unwrap_or(0),
max_topic_size,
self.replication_factor,
self.name
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::BufMut;
#[test]
fn should_be_serialized_as_bytes() {
let command = CreateTopic {
stream_id: Identifier::numeric(1).unwrap(),
topic_id: Some(2),
partitions_count: 3,
message_expiry: Some(10),
max_topic_size: Some(IggyByteSize::from(100)),
replication_factor: 1,
name: "test".to_string(),
};
let bytes = command.as_bytes();
let mut position = 0;
let stream_id = Identifier::from_bytes(&bytes).unwrap();
position += stream_id.get_size_bytes() as usize;
let topic_id = u32::from_le_bytes(bytes[position..position + 4].try_into().unwrap());
let partitions_count =
u32::from_le_bytes(bytes[position + 4..position + 8].try_into().unwrap());
let message_expiry =
match u32::from_le_bytes(bytes[position + 8..position + 12].try_into().unwrap()) {
0 => None,
secs => Some(secs),
};
let max_topic_size =
match u64::from_le_bytes(bytes[position + 12..position + 20].try_into().unwrap()) {
0 => None,
size => Some(IggyByteSize::from(size)),
};
let replication_factor = bytes[position + 20];
let name_length = bytes[position + 21];
let name = from_utf8(&bytes[position + 22..(position + 22 + name_length as usize)])
.unwrap()
.to_string();
assert!(!bytes.is_empty());
assert_eq!(stream_id, command.stream_id);
assert_eq!(topic_id, command.topic_id.unwrap());
assert_eq!(partitions_count, command.partitions_count);
assert_eq!(message_expiry, command.message_expiry);
assert_eq!(max_topic_size, command.max_topic_size);
assert_eq!(replication_factor, command.replication_factor);
assert_eq!(name.len() as u8, command.name.len() as u8);
assert_eq!(name, command.name);
}
#[test]
fn should_be_deserialized_from_bytes() {
let stream_id = Identifier::numeric(1).unwrap();
let topic_id = 2u32;
let partitions_count = 3u32;
let name = "test".to_string();
let message_expiry = 10;
let max_topic_size = IggyByteSize::from(100);
let replication_factor = 1;
let stream_id_bytes = stream_id.as_bytes();
let mut bytes = Vec::with_capacity(14 + stream_id_bytes.len() + name.len());
bytes.extend(stream_id_bytes);
bytes.put_u32_le(topic_id);
bytes.put_u32_le(partitions_count);
bytes.put_u32_le(message_expiry);
bytes.put_u64_le(max_topic_size.as_bytes_u64());
bytes.put_u8(replication_factor);
#[allow(clippy::cast_possible_truncation)]
bytes.put_u8(name.len() as u8);
bytes.extend(name.as_bytes());
let command = CreateTopic::from_bytes(&bytes);
assert!(command.is_ok());
let command = command.unwrap();
assert_eq!(command.stream_id, stream_id);
assert_eq!(command.topic_id.unwrap(), topic_id);
assert_eq!(command.name, name);
assert_eq!(command.partitions_count, partitions_count);
assert_eq!(command.message_expiry, Some(message_expiry));
assert_eq!(command.max_topic_size, Some(max_topic_size));
assert_eq!(command.replication_factor, replication_factor);
assert_eq!(command.partitions_count, partitions_count);
}
}