use bytes::Bytes;
use cheetah_string::CheetahString;
use rocketmq_error::Result;
use crate::common::message::message_body::MessageBody;
use crate::common::message::message_flag::MessageFlag;
use crate::common::message::message_property::MessageProperties;
use crate::common::message::message_property::MessagePropertyKey;
use crate::common::message::MessageConst;
#[derive(Default)]
pub struct MessageBuilder {
topic: Option<CheetahString>,
body: MessageBody,
properties: MessageProperties,
flag: MessageFlag,
transaction_id: Option<CheetahString>,
}
impl MessageBuilder {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn topic(mut self, topic: impl Into<CheetahString>) -> Self {
self.topic = Some(topic.into());
self
}
#[inline]
pub fn body(mut self, body: impl Into<Bytes>) -> Self {
self.body = MessageBody::new(body);
self
}
#[inline]
pub fn body_slice(mut self, body: &[u8]) -> Self {
self.body = MessageBody::new(Bytes::copy_from_slice(body));
self
}
#[inline]
pub fn empty_body(mut self) -> Self {
self.body = MessageBody::empty();
self
}
#[inline]
pub fn tags(mut self, tags: impl Into<CheetahString>) -> Self {
self.properties.insert(MessagePropertyKey::Tags, tags.into());
self
}
#[inline]
pub fn keys(mut self, keys: Vec<String>) -> Self {
if !keys.is_empty() {
let keys_str = keys.join(MessageConst::KEY_SEPARATOR);
self.properties.insert(MessagePropertyKey::Keys, keys_str);
}
self
}
#[inline]
pub fn key(mut self, key: impl Into<CheetahString>) -> Self {
self.properties.insert(MessagePropertyKey::Keys, key.into());
self
}
#[inline]
pub fn flag(mut self, flag: MessageFlag) -> Self {
self.flag = flag;
self
}
#[inline]
pub fn flag_bits(mut self, bits: i32) -> Self {
self.flag = MessageFlag::from_bits(bits);
self
}
#[inline]
pub fn delay_level(mut self, level: i32) -> Self {
self.properties
.insert(MessagePropertyKey::DelayTimeLevel, level.to_string());
self
}
#[inline]
pub fn delay_secs(mut self, secs: u64) -> Self {
self.properties
.insert(MessagePropertyKey::DelayTimeSec, secs.to_string());
self
}
#[inline]
pub fn delay_millis(mut self, millis: u64) -> Self {
self.properties
.insert(MessagePropertyKey::DelayTimeMs, millis.to_string());
self
}
#[inline]
pub fn deliver_time_ms(mut self, time_ms: u64) -> Self {
self.properties
.insert(MessagePropertyKey::DeliverTimeMs, time_ms.to_string());
self
}
#[inline]
pub fn priority(mut self, priority: u32) -> Self {
self.properties
.insert(MessagePropertyKey::Priority, priority.to_string());
self
}
pub fn try_priority(mut self, priority: i32) -> Result<Self> {
if priority < 0 {
return Err(crate::error::invalid_argument(
"The priority must be greater than or equal to 0",
));
}
self.properties
.insert(MessagePropertyKey::Priority, priority.to_string());
Ok(self)
}
#[inline]
pub fn wait_store_msg_ok(mut self, wait: bool) -> Self {
self.properties
.insert(MessagePropertyKey::WaitStoreMsgOk, if wait { "true" } else { "false" });
self
}
#[inline]
pub fn transaction_id(mut self, id: impl Into<CheetahString>) -> Self {
self.transaction_id = Some(id.into());
self
}
#[inline]
pub fn buyer_id(mut self, buyer_id: impl Into<CheetahString>) -> Self {
self.properties.insert(MessagePropertyKey::BuyerId, buyer_id.into());
self
}
#[inline]
pub fn instance_id(mut self, instance_id: impl Into<CheetahString>) -> Self {
self.properties
.insert(MessagePropertyKey::InstanceId, instance_id.into());
self
}
#[inline]
pub fn correlation_id(mut self, correlation_id: impl Into<CheetahString>) -> Self {
self.properties
.insert(MessagePropertyKey::CorrelationId, correlation_id.into());
self
}
#[inline]
pub fn sharding_key(mut self, sharding_key: impl Into<CheetahString>) -> Self {
self.properties
.insert(MessagePropertyKey::ShardingKey, sharding_key.into());
self
}
#[inline]
pub fn trace_switch(mut self, enabled: bool) -> Self {
self.properties
.insert(MessagePropertyKey::TraceSwitch, if enabled { "true" } else { "false" });
self
}
#[inline]
pub fn property(mut self, key: MessagePropertyKey, value: impl Into<CheetahString>) -> Self {
self.properties.insert(key, value.into());
self
}
pub fn raw_property(mut self, key: impl Into<CheetahString>, value: impl Into<CheetahString>) -> Result<Self> {
let key = key.into();
let value = value.into();
if crate::common::message::STRING_HASH_SET.contains(key.as_str()) {
return Err(crate::error::invalid_property(format!(
"The Property<{key}> is used by system, input another please"
)));
}
self.properties.as_map_mut().insert(key, value);
Ok(self)
}
pub fn build(mut self) -> Result<super::message_single::Message> {
let topic = self
.topic
.ok_or_else(|| crate::error::invalid_property("Topic is required for message"))?;
if self.properties.get(MessagePropertyKey::WaitStoreMsgOk).is_none() {
self.properties.insert(MessagePropertyKey::WaitStoreMsgOk, "true");
}
Ok(super::message_single::Message::from_builder(
topic,
self.body,
self.properties,
self.flag,
self.transaction_id,
))
}
#[track_caller]
pub fn build_unchecked(self) -> super::message_single::Message {
self.build().expect("message validation failed")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_builder_basic() {
let msg = MessageBuilder::new()
.topic("test-topic")
.body_slice(b"hello world")
.build_unchecked();
assert_eq!(msg.topic().as_str(), "test-topic");
assert_eq!(msg.body_slice(), b"hello world");
assert_eq!(msg.properties().get(MessagePropertyKey::WaitStoreMsgOk), Some("true"));
}
#[test]
fn test_builder_with_tags_and_keys() {
let msg = MessageBuilder::new()
.topic("test-topic")
.body_slice(b"test")
.tags("tag1")
.keys(vec!["key1".to_string(), "key2".to_string()])
.build_unchecked();
assert_eq!(msg.tags(), Some("tag1"));
assert_eq!(msg.keys(), Some(vec!["key1".to_string(), "key2".to_string()]));
}
#[test]
fn test_builder_with_delay() {
let msg = MessageBuilder::new()
.topic("test-topic")
.body_slice(b"test")
.delay_level(3)
.build_unchecked();
assert_eq!(msg.delay_time_level(), 3);
}
#[test]
fn test_builder_with_priority_matches_java_property() {
let msg = MessageBuilder::new()
.topic("test-topic")
.body_slice(b"test")
.priority(7)
.build_unchecked();
assert_eq!(msg.priority(), 7);
assert_eq!(msg.properties().get(MessagePropertyKey::Priority), Some("7"));
}
#[test]
fn test_builder_try_priority_rejects_negative_like_java() {
match MessageBuilder::new().topic("test-topic").try_priority(-1) {
Ok(_) => panic!("negative priority should be rejected"),
Err(error) => assert_eq!(error.descriptor(), &rocketmq_error::CORE_ARGUMENT_INVALID),
}
}
#[test]
fn test_builder_missing_topic() {
let result = MessageBuilder::new().body_slice(b"test").build();
assert!(result.is_err());
}
#[test]
fn test_builder_with_transaction_id() {
let msg = MessageBuilder::new()
.topic("test-topic")
.body_slice(b"test")
.transaction_id("tx-123")
.build_unchecked();
assert_eq!(msg.transaction_id(), Some("tx-123"));
}
#[test]
fn test_builder_wait_store_msg_ok_writes_java_boolean_property() {
let wait_msg = MessageBuilder::new()
.topic("test-topic")
.body_slice(b"test")
.wait_store_msg_ok(true)
.build_unchecked();
assert!(wait_msg.wait_store_msg_ok());
assert_eq!(
wait_msg.properties().get(MessagePropertyKey::WaitStoreMsgOk),
Some("true")
);
let no_wait_msg = MessageBuilder::new()
.topic("test-topic")
.body_slice(b"test")
.wait_store_msg_ok(false)
.build_unchecked();
assert!(!no_wait_msg.wait_store_msg_ok());
assert_eq!(
no_wait_msg.properties().get(MessagePropertyKey::WaitStoreMsgOk),
Some("false")
);
}
}