use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use bytes::Bytes;
use ruststream::{
Broker, ConnectedBroker, DefaultPublish, DescribeServer, OutgoingMessage, RawMessage,
ServerSpec, Subscribe, SubscriptionSource,
testing::{Coordinator, TestableBroker},
};
use crate::{
error::NatsError,
subscribe_options::SubscribeOptions,
testing::{
NatsTestPublisher, NatsTestSubscriber,
publisher::NatsTestPublish,
router::SubjectRouter,
subject::{SubjectPattern, validate_concrete_subject},
},
};
#[derive(Default)]
pub(crate) struct TestBrokerState {
pub(crate) router: SubjectRouter,
closed: AtomicBool,
coordinator: OnceLock<Coordinator>,
}
impl TestBrokerState {
fn install_coordinator(&self, coordinator: Coordinator) {
let _ = self.coordinator.set(coordinator);
}
pub(crate) fn coordinator(&self) -> Option<Coordinator> {
self.coordinator.get().cloned()
}
pub(crate) fn ensure_live(&self, subject: &str) -> Result<(), NatsError> {
if self.closed.load(Ordering::Acquire) {
return Err(NatsError::Closed {
subject: subject.to_owned(),
});
}
Ok(())
}
}
impl std::fmt::Debug for TestBrokerState {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TestBrokerState")
.field("router", &self.router)
.field("closed", &self.closed.load(Ordering::Relaxed))
.finish_non_exhaustive()
}
}
#[derive(Clone, Default, Debug)]
#[must_use]
pub struct NatsTestBroker {
state: Arc<TestBrokerState>,
}
impl NatsTestBroker {
pub fn new() -> Self {
Self::default()
}
}
impl Broker for NatsTestBroker {
type Error = NatsError;
type Connected = ConnectedNatsTestBroker;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
Ok(ConnectedNatsTestBroker { state: self.state })
}
}
impl DescribeServer for NatsTestBroker {
fn describe_server(&self) -> ServerSpec {
ServerSpec::in_process("nats")
}
}
#[derive(Clone, Debug)]
pub struct ConnectedNatsTestBroker {
state: Arc<TestBrokerState>,
}
impl ConnectedNatsTestBroker {
pub(crate) fn state(&self) -> Arc<TestBrokerState> {
Arc::clone(&self.state)
}
#[allow(
clippy::unused_async,
reason = "API parity with ConnectedNatsBroker::subscribe_with"
)]
pub async fn subscribe_with(
&self,
opts: SubscribeOptions,
) -> Result<NatsTestSubscriber, NatsError> {
opts.validate()?;
self.state.ensure_live(opts.subject())?;
let pattern = SubjectPattern::parse(opts.subject()).map_err(|err| {
NatsError::Subscribe(Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
})?;
let (id, requeue, rx) = self.state.router.subscribe(pattern);
Ok(NatsTestSubscriber::new(
Arc::clone(&self.state),
id,
rx,
requeue,
self.state.coordinator(),
))
}
#[must_use]
pub fn publisher(&self, policy: NatsTestPublish) -> NatsTestPublisher {
policy.bind(self)
}
}
impl ConnectedBroker for ConnectedNatsTestBroker {
type Error = NatsError;
type Closed = ();
async fn shutdown(self) -> Result<Self::Closed, Self::Error> {
self.state.closed.store(true, Ordering::Release);
self.state.router.clear();
Ok(())
}
}
impl Subscribe for ConnectedNatsTestBroker {
type Subscriber = NatsTestSubscriber;
async fn subscribe(&self, name: &str) -> Result<Self::Subscriber, Self::Error> {
self.subscribe_with(SubscribeOptions::new(name)).await
}
}
impl SubscriptionSource<ConnectedNatsTestBroker> for SubscribeOptions {
type Subscriber = NatsTestSubscriber;
fn name(&self) -> &str {
self.subject()
}
async fn subscribe(
self,
connected: &ConnectedNatsTestBroker,
) -> Result<Self::Subscriber, NatsError> {
connected.subscribe_with(self).await
}
}
impl DefaultPublish for ConnectedNatsTestBroker {
type Policy = NatsTestPublish;
}
impl TestableBroker for ConnectedNatsTestBroker {
fn install_coordinator(&self, coordinator: Coordinator) {
self.state.install_coordinator(coordinator);
}
fn inject(&self, message: OutgoingMessage<'_>) {
self.state.router.publish(
message.name().to_owned(),
Bytes::copy_from_slice(message.payload()),
message.headers().clone(),
self.state.coordinator().as_ref(),
);
}
fn published(&self, name: &str) -> Vec<RawMessage> {
self.state.router.published(name)
}
}
ruststream::register_testable_broker!(ConnectedNatsTestBroker);
pub(crate) fn validate_publish_subject(subject: &str) -> Result<(), NatsError> {
validate_concrete_subject(subject).map_err(|err| {
NatsError::Publish(Box::new(err) as Box<dyn std::error::Error + Send + Sync>)
})
}