use std::sync::Arc;
use std::sync::Mutex;
#[cfg(feature = "asyncapi")]
use crate::asyncapi::Bindings;
use crate::runtime::lifecycle::ConnectedSlot;
#[cfg(feature = "testing")]
use crate::testing::coordinator::pairs_against;
use crate::{Broker, Connected, ConnectedBroker, PairError, PublishPolicy};
pub struct Bindable<B: Broker> {
pub(crate) broker: B,
pub(crate) slot: ConnectedSlot<B>,
}
impl<B: Broker + std::fmt::Debug> std::fmt::Debug for Bindable<B> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bindable")
.field("broker", &self.broker)
.finish_non_exhaustive()
}
}
impl<B: Broker + 'static> Bindable<B> {
#[must_use]
pub fn new(broker: B) -> Self {
Self {
broker,
slot: Arc::new(Mutex::new(None)),
}
}
#[must_use]
pub fn broker(&self) -> &B {
&self.broker
}
#[must_use]
pub fn bind<S>(&self, source: S) -> Bound<B, S>
where
S: PublishPolicy<Connected<B>>,
{
Bound {
slot: Arc::clone(&self.slot),
source,
}
}
}
pub trait BrokerRegistration {
type Broker: Broker + 'static;
#[doc(hidden)]
fn into_parts(self) -> (Self::Broker, ConnectedSlot<Self::Broker>);
}
impl<B: Broker + 'static> BrokerRegistration for B {
type Broker = B;
fn into_parts(self) -> (B, ConnectedSlot<B>) {
(self, Arc::new(Mutex::new(None)))
}
}
impl<B: Broker + 'static> BrokerRegistration for Bindable<B> {
type Broker = B;
fn into_parts(self) -> (B, ConnectedSlot<B>) {
(self.broker, self.slot)
}
}
pub struct Bound<B2: Broker, S> {
pub(crate) slot: ConnectedSlot<B2>,
pub(crate) source: S,
}
impl<B2: Broker, S: std::fmt::Debug> std::fmt::Debug for Bound<B2, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Bound")
.field("source", &self.source)
.finish_non_exhaustive()
}
}
impl<B2: Broker + 'static, S: Clone> Clone for Bound<B2, S> {
fn clone(&self) -> Self {
Self {
slot: Arc::clone(&self.slot),
source: self.source.clone(),
}
}
}
impl<B2, S> Bound<B2, S>
where
B2: Broker + 'static,
S: PublishPolicy<Connected<B2>>,
{
pub async fn live(self) -> Result<S::Live, PairError> {
pair_bound::<B2, S>(&self.slot, self.source).await
}
}
pub(crate) async fn pair_bound<B2, S>(
slot: &ConnectedSlot<B2>,
source: S,
) -> Result<S::Live, PairError>
where
B2: Broker + 'static,
S: PublishPolicy<Connected<B2>>,
{
let connected = slot
.lock()
.expect("connected slot mutex poisoned")
.clone()
.ok_or_else(|| {
PairError::from_boxed(Box::from(
"the token's broker is not connected: pairing happens after startup connects \
every registered broker",
))
})?;
#[cfg(feature = "testing")]
pairs_against(connected.as_ref());
source.pair(connected.as_ref()).await
}
impl<C, B2, S> PublishPolicy<C> for Bound<B2, S>
where
C: ConnectedBroker,
B2: Broker + 'static,
S: PublishPolicy<Connected<B2>> + Send,
{
type Live = S::Live;
async fn pair(self, _connected: &C) -> Result<Self::Live, PairError> {
pair_bound::<B2, S>(&self.slot, self.source).await
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self, channel: &str) -> Bindings {
<S as PublishPolicy<Connected<B2>>>::channel_bindings(&self.source, channel)
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self, channel: &str) -> Bindings {
<S as PublishPolicy<Connected<B2>>>::operation_bindings(&self.source, channel)
}
#[cfg(feature = "asyncapi")]
fn message_bindings(&self, channel: &str) -> Bindings {
<S as PublishPolicy<Connected<B2>>>::message_bindings(&self.source, channel)
}
#[cfg(feature = "asyncapi")]
fn reply_address_location(&self) -> Option<&'static str> {
<S as PublishPolicy<Connected<B2>>>::reply_address_location(&self.source)
}
}
#[cfg(all(test, feature = "memory", feature = "json"))]
mod tests {
use futures::StreamExt;
use crate::memory::{MemoryBroker, MemoryPublish};
use crate::runtime::{AppInfo, RustStream};
use crate::{Broker, IncomingMessage, OutgoingMessage, Publisher, Subscriber};
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_cloned_token_pairs_against_the_same_broker() {
let bindable = MemoryBroker::new().bindable();
let mut inbox = bindable.broker().subscribe("bound.out");
let token = bindable.bind(MemoryPublish);
let spare = token.clone();
assert!(
format!("{bindable:?}").contains("MemoryBroker"),
"the wrapper names the broker it holds",
);
assert!(
format!("{token:?}").contains("MemoryPublish"),
"the token names the policy it carries",
);
let err = spare
.clone()
.live()
.await
.expect_err("a token cannot pair before startup connects its broker");
assert!(
err.to_string().contains("not connected"),
"the error must name the reason: {err}",
);
let app =
RustStream::new(AppInfo::new("bound", "0.1.0")).with_broker(bindable, |_scope| {});
let running = app.start().await.expect("startup failed");
for (token, payload) in [(token, b"one"), (spare, b"two")] {
let publisher = token.live().await.expect("pairing after startup failed");
publisher
.publish(OutgoingMessage::new("bound.out", payload), None)
.await
.expect("publish through the paired token failed");
}
let mut stream = std::pin::pin!(inbox.stream());
let mut seen = Vec::new();
for _ in 0..2 {
let msg = stream
.next()
.await
.expect("delivery missing")
.expect("memory subscriber never errors");
seen.push(msg.payload().to_vec());
msg.ack().await.expect("ack failed");
}
assert_eq!(
seen,
vec![b"one".to_vec(), b"two".to_vec()],
"both clones reach the broker the wrapper registered",
);
running.shutdown().await.expect("shutdown failed");
}
}