use std::fmt;
use std::future::{Future, ready};
use std::sync::Arc;
use std::time::Duration;
use fe2o3_amqp_types::messaging::{Data, Message, Outcome, Properties};
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::{OutgoingFor, PairError, PublishPolicy, Publisher, RequestReply, Take};
#[cfg(feature = "asyncapi")]
use crate::bindings;
use crate::broker::{AmqpCore, ConnectedAmqpBroker, CoreCell, Link, SenderLink};
use crate::error::{AmqpError, box_err};
#[cfg(feature = "testing")]
use crate::in_process;
use crate::message::{AmqpMessage, headers_from_amqp, payload_from_body, to_amqp_message};
#[derive(Clone)]
pub struct AmqpPublisher {
cell: CoreCell,
}
impl fmt::Debug for AmqpPublisher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AmqpPublisher").finish_non_exhaustive()
}
}
impl AmqpPublisher {
pub(crate) fn new(cell: CoreCell) -> Self {
Self { cell }
}
fn link(&self) -> Result<&Link, AmqpError> {
self.cell.get().ok_or(AmqpError::NotConnected)
}
}
fn open(core: &Arc<AmqpCore>) -> Result<&Arc<AmqpCore>, AmqpError> {
core.ensure_open()?;
Ok(core)
}
pub(crate) async fn send_message(
link: &SenderLink,
address: &str,
message: Message<Data>,
) -> Result<(), AmqpError> {
let outcome = link
.with(async |sender| {
sender.send(message).await.map_err(|e| AmqpError::Publish {
address: address.to_owned(),
source: box_err(e),
})
})
.await?;
accepted(outcome, address)
}
pub(crate) fn accepted(outcome: Outcome, address: &str) -> Result<(), AmqpError> {
outcome
.accepted_or_else(|outcome| AmqpError::PublishNotAccepted {
address: address.to_owned(),
outcome: format!("{outcome:?}"),
})
.map(|_| ())
}
impl Publisher for AmqpPublisher {
type Payload = Take;
type Error = AmqpError;
type Options = ();
async fn publish(
&self,
msg: OutgoingFor<'_, Take>,
_options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
let core = match self.link()? {
Link::Amqp(core) => open(core)?,
#[cfg(feature = "testing")]
Link::InProcess(bus) => return in_process::publish(bus, msg),
};
let address = msg.name();
let sender = core.sender_for(address).await?;
send_message(&sender, address, to_amqp_message(msg)).await
}
}
impl RequestReply for AmqpPublisher {
type Reply = AmqpMessage;
async fn request(
&self,
msg: OutgoingFor<'_, Take>,
timeout: Duration,
) -> Result<Self::Reply, Self::Error> {
let core = match self.link()? {
Link::Amqp(core) => open(core)?,
#[cfg(feature = "testing")]
Link::InProcess(bus) => return in_process::request(bus, msg, timeout).await,
};
let mut receiver = ConnectedAmqpBroker::attach_dynamic_receiver(core).await?;
let reply_to = receiver
.source()
.as_ref()
.and_then(|source| source.address.clone())
.ok_or_else(|| AmqpError::Attach {
address: "(dynamic)".to_owned(),
source: Box::from("the broker did not assign a dynamic reply address"),
})?;
let correlation_id = core.correlation_id();
let address = msg.name();
let exchange = async {
let mut message = to_amqp_message(msg);
let properties = message.properties.get_or_insert_with(Properties::default);
properties.reply_to = Some(reply_to);
properties.correlation_id = Some(correlation_id.clone().into());
let sender = core.sender_for(address).await?;
send_message(&sender, address, message).await?;
loop {
let delivery = receiver
.recv::<fe2o3_amqp_types::messaging::Body<fe2o3_amqp_types::primitives::Value>>(
)
.await
.map_err(|e| AmqpError::Receive {
address: "(dynamic)".to_owned(),
source: box_err(e),
})?;
let message = delivery.into_message();
let headers = headers_from_amqp(&message);
if headers.correlation_id() == Some(correlation_id.as_str()) {
let payload = payload_from_body(message.body, "(dynamic)")?;
return Ok(AmqpMessage::settled(payload, headers, None));
}
}
};
let result = tokio::time::timeout(timeout, exchange)
.await
.unwrap_or(Err(AmqpError::RequestTimeout));
if let Err((_, err)) = receiver.detach().await {
tracing::debug!(error = %err, "amqp reply receiver detach failed");
}
result
}
}
#[derive(Debug, Clone, Copy, Default)]
#[must_use]
pub struct AmqpPublish;
impl PublishPolicy<ConnectedAmqpBroker> for AmqpPublish {
type Live = AmqpPublisher;
fn pair(
self,
connected: &ConnectedAmqpBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
ready(Ok(connected.publisher()))
}
#[cfg(feature = "asyncapi")]
fn channel_bindings(&self, channel: &str) -> Bindings {
bindings::target(channel)
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self, _channel: &str) -> Bindings {
bindings::confirmed_posting()
}
#[cfg(feature = "asyncapi")]
fn reply_address_location(&self) -> Option<&'static str> {
Some(bindings::REPLY_ADDRESS_LOCATION)
}
}