use crate::error::MqError;
use crate::queue::{Message, MessageQueue};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Serialize)]
struct SendRequest<'a> {
topic: &'a str,
group: &'a str,
payload: &'a [u8],
}
#[derive(Deserialize)]
struct ConsumeResponse {
id: String,
topic: String,
payload: Vec<u8>,
}
pub struct RocketMqProvider {
base_url: String,
topic: String,
consumer_group: String,
client: reqwest::Client,
}
impl RocketMqProvider {
pub async fn new(name_server: impl Into<String>) -> Result<Self, MqError> {
Self::connect(name_server, "default-topic", "default-group").await
}
pub async fn connect(
base_url: impl Into<String>,
topic: impl Into<String>,
consumer_group: impl Into<String>,
) -> Result<Self, MqError> {
let client = reqwest::Client::builder()
.build()
.map_err(|e| MqError::Connection(format!("reqwest client build failed: {e}")))?;
Ok(Self {
base_url: base_url.into().trim_end_matches('/').to_string(),
topic: topic.into(),
consumer_group: consumer_group.into(),
client,
})
}
pub fn base_url(&self) -> &str {
&self.base_url
}
pub fn topic(&self) -> &str {
&self.topic
}
pub fn consumer_group(&self) -> &str {
&self.consumer_group
}
}
#[async_trait]
impl MessageQueue for RocketMqProvider {
async fn publish(&self, topic: &str, message: &[u8]) -> Result<(), MqError> {
let req = SendRequest {
topic,
group: &self.consumer_group,
payload: message,
};
let url = format!("{}/message", self.base_url);
let resp = self
.client
.post(&url)
.json(&req)
.send()
.await
.map_err(|e| MqError::Publish(format!("RocketMQ HTTP send failed: {e}")))?;
if !resp.status().is_success() {
return Err(MqError::Publish(format!(
"RocketMQ HTTP send status {}",
resp.status()
)));
}
Ok(())
}
async fn consume(&self, topic: &str) -> Result<Option<Message>, MqError> {
let url = format!("{}/message", self.base_url);
let resp = self
.client
.get(&url)
.query(&[("topic", topic), ("group", self.consumer_group.as_str())])
.send()
.await
.map_err(|e| MqError::Connection(format!("RocketMQ HTTP consume failed: {e}")))?;
if resp.status() == reqwest::StatusCode::NO_CONTENT {
return Ok(None);
}
if !resp.status().is_success() {
return Err(MqError::Connection(format!(
"RocketMQ HTTP consume status {}",
resp.status()
)));
}
let body: ConsumeResponse = resp
.json()
.await
.map_err(|e| MqError::Connection(format!("RocketMQ HTTP decode failed: {e}")))?;
Ok(Some(Message {
topic: body.topic,
payload: body.payload,
key: None,
timestamp: current_timestamp_millis(),
headers: HashMap::new(),
id: body.id,
retry_count: 0,
}))
}
async fn ack(&self, message_id: &str) -> Result<(), MqError> {
let url = format!(
"{}/message/{}?group={}",
self.base_url, message_id, self.consumer_group
);
let resp = self
.client
.delete(&url)
.send()
.await
.map_err(|e| MqError::Publish(format!("RocketMQ HTTP ack failed: {e}")))?;
if !resp.status().is_success() {
return Err(MqError::Publish(format!(
"RocketMQ HTTP ack status {}",
resp.status()
)));
}
Ok(())
}
async fn subscribe(&self, _topic: &str) -> Result<(), MqError> {
Ok(())
}
}
fn current_timestamp_millis() -> i64 {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as i64
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rocketmq_send_request_serialize() {
let req = SendRequest {
topic: "t",
group: "g",
payload: &[1, 2, 3],
};
let json = serde_json::to_value(&req).unwrap();
assert_eq!(json["topic"], "t");
assert_eq!(json["group"], "g");
assert_eq!(json["payload"], serde_json::json!([1, 2, 3]));
}
#[test]
fn test_rocketmq_consume_response_deserialize() {
let json = serde_json::json!({ "id": "abc", "topic": "t", "payload": [104, 105] });
let resp: ConsumeResponse = serde_json::from_value(json).unwrap();
assert_eq!(resp.id, "abc");
assert_eq!(resp.topic, "t");
assert_eq!(resp.payload, b"hi");
}
#[tokio::test]
async fn test_rocketmq_connect_builds_client() {
let q = RocketMqProvider::connect("http://127.0.0.1:8081/", "t", "g")
.await
.unwrap();
assert_eq!(q.base_url, "http://127.0.0.1:8081");
assert_eq!(q.topic(), "t");
assert_eq!(q.consumer_group(), "g");
q.subscribe("t").await.unwrap();
}
#[tokio::test]
async fn test_rocketmq_new_uses_defaults() {
let q = RocketMqProvider::new("http://127.0.0.1:8081").await.unwrap();
assert_eq!(q.base_url, "http://127.0.0.1:8081");
assert_eq!(q.topic(), "default-topic");
assert_eq!(q.consumer_group(), "default-group");
}
#[tokio::test]
#[ignore = "需真实 RocketMQ 5.x Proxy HTTP 端点"]
async fn test_rocketmq_publish_consume_ack() {
let q = RocketMqProvider::connect("http://127.0.0.1:8081", "test-topic", "test-group")
.await
.unwrap();
q.publish("test-topic", b"hello-rocket").await.unwrap();
let msg = q
.consume("test-topic")
.await
.unwrap()
.expect("应有消息");
assert_eq!(msg.payload, b"hello-rocket");
q.ack(&msg.id).await.unwrap();
}
}