use std::collections::HashMap;
use std::sync::{Arc, OnceLock};
use bytes::Bytes;
use ruststream::testing::Coordinator;
use ruststream::{BytesMut, HeaderMap, OutgoingMessage, RawMessage, Str};
use tokio::runtime::Handle;
use crate::error::PulsarError;
use crate::in_process::router::AddressRouter;
use crate::message::{PARTITION_KEY_HEADER, to_pulsar_message};
use crate::topic::PulsarTopic;
#[derive(Debug)]
pub(crate) struct Bus {
router: AddressRouter,
coordinator: OnceLock<Coordinator>,
runtime: Handle,
}
impl Bus {
pub(crate) fn new(runtime: Handle) -> Arc<Self> {
Arc::new(Self {
router: AddressRouter::default(),
coordinator: OnceLock::new(),
runtime,
})
}
pub(crate) const fn runtime(&self) -> &Handle {
&self.runtime
}
pub(crate) const fn router(&self) -> &AddressRouter {
&self.router
}
pub(crate) fn coordinator(&self) -> Option<&Coordinator> {
self.coordinator.get()
}
pub(crate) fn install(&self, coordinator: Coordinator) {
let _ = self.coordinator.set(coordinator);
}
pub(crate) fn publish(
&self,
msg: OutgoingMessage<'_, BytesMut>,
key: Option<&str>,
) -> Result<(), PulsarError> {
let topic = PulsarTopic::parse(msg.name())?;
let message = to_pulsar_message(msg, key)?;
let headers = delivered_headers(message.properties, message.partition_key);
self.router.publish(
topic.as_str(),
Bytes::from(message.payload),
headers,
self.coordinator(),
);
Ok(())
}
pub(crate) fn published(&self, name: &str) -> Vec<RawMessage> {
PulsarTopic::parse(name).map_or_else(
|_| Vec::new(),
|topic| self.router.published(topic.as_str()),
)
}
pub(crate) fn close(&self) {
self.router.clear();
}
}
fn delivered_headers(
properties: HashMap<String, String>,
partition_key: Option<String>,
) -> HeaderMap {
let mut headers = HeaderMap::with_capacity(properties.len() + 1);
for (name, value) in properties {
headers.insert(name, value);
}
if let Some(key) = partition_key {
headers.insert(Str::from_static(PARTITION_KEY_HEADER), key);
}
headers
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn a_publish_hands_the_buffer_to_the_log() {
let bus = Bus::new(Handle::current());
let buffer = BytesMut::from(&b"{\"id\":1}"[..]);
let at = buffer.as_ptr();
bus.publish(OutgoingMessage::produced("orders", buffer), None)
.expect("a topic name is accepted");
let logged = bus.published("orders");
assert_eq!(
logged
.first()
.expect("the publish reached the log")
.payload()
.as_ptr(),
at,
"the log must hold the buffer the publish wrote, not a copy of it",
);
}
#[tokio::test]
async fn a_destination_that_is_no_topic_is_refused() {
let bus = Bus::new(Handle::current());
let refused = bus
.publish(
OutgoingMessage::produced("a/b", BytesMut::from(&b"{}"[..])),
None,
)
.expect_err("a two-part name is no topic");
assert!(matches!(refused, PulsarError::Invalid(_)), "{refused}");
}
#[tokio::test]
async fn a_header_that_is_not_text_is_refused() {
let bus = Bus::new(Handle::current());
let mut headers = HeaderMap::new();
headers.insert("x-raw", vec![0xff, b'a']);
let refused = bus
.publish(
OutgoingMessage::produced("orders", BytesMut::from(&b"{}"[..]))
.with_headers(headers),
None,
)
.expect_err("a binary header is refused");
assert!(matches!(refused, PulsarError::Publish { .. }), "{refused}");
assert_eq!(bus.published("orders"), Vec::<RawMessage>::new());
}
}