#![cfg_attr(
not(feature = "testing"),
allow(clippy::infallible_destructuring_match)
)]
use std::fmt;
use std::future::{Future, ready};
use bytes::Bytes;
use rumqttc::v5::mqttbytes::v5::PublishProperties;
use rumqttc::v5::mqttbytes::valid_topic;
#[cfg(feature = "asyncapi")]
use ruststream::asyncapi::Bindings;
use ruststream::runtime::{PublishBuilder, PublishSink};
use ruststream::{BytesMut, OutgoingMessage, PairError, PublishPolicy, Publisher, Take};
#[cfg(feature = "asyncapi")]
use crate::asyncapi;
use crate::broker::{ConnectedMqttBroker, CoreCell, Link};
use crate::error::MqttError;
use crate::filter::Qos;
use crate::message::{is_mqtt_text, to_wire_properties};
pub(crate) fn check_topic(topic: &str) -> Result<(), MqttError> {
let reason = if topic.is_empty() {
"an MQTT topic must not be empty"
} else if !valid_topic(topic) {
"not a valid MQTT topic (wildcards are subscribe-only)"
} else if !is_mqtt_text(topic) {
"not a valid MQTT topic (it carries a control character or a Unicode non-character)"
} else {
return Ok(());
};
Err(MqttError::Publish {
topic: topic.to_owned(),
reason: reason.to_owned(),
})
}
async fn send(
cell: &CoreCell,
qos: Qos,
retain: bool,
msg: OutgoingMessage<'_, BytesMut>,
) -> Result<(), MqttError> {
let core = cell.get().ok_or(MqttError::NotConnected)?;
core.shared.ensure_open()?;
check_topic(msg.name())?;
let topic = msg.name();
let (payload, properties) = into_packet(msg)?;
let client = match &core.link {
Link::Wire(client) => client,
#[cfg(feature = "testing")]
Link::InProcess(bus) => {
bus.publish(&core.shared, topic, qos, retain, payload, properties);
return Ok(());
}
};
let outcome = match properties {
Some(properties) => {
client
.publish_bytes_with_properties(topic, qos.to_client(), retain, payload, properties)
.await
}
None => {
client
.publish_bytes(topic, qos.to_client(), retain, payload)
.await
}
};
outcome.map_err(|_| MqttError::Publish {
topic: topic.to_owned(),
reason: "the mqtt connection task has shut down".to_owned(),
})
}
fn into_packet(
msg: OutgoingMessage<'_, BytesMut>,
) -> Result<(Bytes, Option<PublishProperties>), MqttError> {
let properties = to_wire_properties(&msg)?;
Ok((msg.into_payload().freeze(), properties))
}
#[derive(Clone)]
pub struct MqttPublisher {
cell: CoreCell,
qos: Qos,
retain: bool,
}
impl fmt::Debug for MqttPublisher {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MqttPublisher")
.field("qos", &self.qos)
.field("retain", &self.retain)
.finish_non_exhaustive()
}
}
impl MqttPublisher {
pub(crate) fn new(cell: CoreCell, qos: Qos, retain: bool) -> Self {
Self { cell, qos, retain }
}
}
impl Publisher for MqttPublisher {
type Payload = Take;
type Error = MqttError;
type Options = MqttPublishOptions;
async fn publish(
&self,
msg: OutgoingMessage<'_, BytesMut>,
options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
let (qos, retain) = MqttPublishOptions::resolve(options, self.qos, self.retain);
send(&self.cell, qos, retain, msg).await
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
#[must_use]
pub struct MqttPublishOptions {
qos: Option<Qos>,
retain: Option<bool>,
}
impl MqttPublishOptions {
pub const fn qos(mut self, qos: Qos) -> Self {
self.qos = Some(qos);
self
}
pub const fn retain(mut self, retain: bool) -> Self {
self.retain = Some(retain);
self
}
pub(crate) fn resolve(options: Option<&Self>, qos: Qos, retain: bool) -> (Qos, bool) {
let options = options.copied().unwrap_or_default();
(options.qos.unwrap_or(qos), options.retain.unwrap_or(retain))
}
}
pub trait MqttPublishSteps {
#[must_use]
fn qos(self, qos: Qos) -> Self;
#[must_use]
fn retain(self, retain: bool) -> Self;
}
impl<Sink, Body, Enc, Hdrs, Dest> MqttPublishSteps for PublishBuilder<Sink, Body, Enc, Hdrs, Dest>
where
Sink: PublishSink<Options = MqttPublishOptions>,
{
fn qos(mut self, qos: Qos) -> Self {
self.options_mut()
.get_or_insert_with(MqttPublishOptions::default)
.qos = Some(qos);
self
}
fn retain(mut self, retain: bool) -> Self {
self.options_mut()
.get_or_insert_with(MqttPublishOptions::default)
.retain = Some(retain);
self
}
}
#[derive(Debug, Clone, Copy, Default)]
#[must_use]
pub struct MqttPublish {
qos: Qos,
retain: bool,
}
impl MqttPublish {
pub fn qos(mut self, qos: Qos) -> Self {
self.qos = qos;
self
}
pub fn retain(mut self, retain: bool) -> Self {
self.retain = retain;
self
}
}
impl MqttPublish {
pub(crate) fn into_publisher(self, cell: CoreCell) -> MqttPublisher {
MqttPublisher::new(cell, self.qos, self.retain)
}
}
impl PublishPolicy<ConnectedMqttBroker> for MqttPublish {
type Live = MqttPublisher;
fn pair(
self,
connected: &ConnectedMqttBroker,
) -> impl Future<Output = Result<Self::Live, PairError>> {
ready(Ok(connected.publisher_with(self)))
}
#[cfg(feature = "asyncapi")]
fn operation_bindings(&self, _channel: &str) -> Bindings {
asyncapi::send_operation(self.qos, self.retain)
}
#[cfg(feature = "asyncapi")]
fn message_bindings(&self, _channel: &str) -> Bindings {
asyncapi::publish_message()
}
#[cfg(feature = "asyncapi")]
fn reply_address_location(&self) -> Option<&'static str> {
Some(asyncapi::REPLY_ADDRESS_LOCATION)
}
}
#[cfg(test)]
mod tests {
use ruststream::runtime::PublishExt;
use ruststream::{HeaderMap, Outgoing, Serialized};
use super::*;
use crate::broker::MqttBroker;
#[test]
fn a_topic_with_a_control_character_is_refused() {
assert!(check_topic("devices/a\nb").is_err());
assert!(check_topic("devices/\u{fdd0}").is_err());
assert!(check_topic("devices/zürich").is_ok());
}
#[test]
fn the_packet_carries_the_buffer_that_was_handed_in() {
let body = BytesMut::from(&br#"{"id":7}"#[..]);
let written_at = body.as_ptr();
let mut headers = HeaderMap::new();
headers.insert("content-type", "application/json");
let (payload, properties) =
into_packet(OutgoingMessage::produced("orders", body).with_headers(headers))
.expect("the headers are MQTT text");
assert!(properties.is_some(), "the headers still map to properties");
assert_eq!(
payload.as_ptr(),
written_at,
"the packet must carry the buffer the publish wrote, not a copy of it",
);
}
fn publisher() -> MqttPublisher {
MqttBroker::new("mqtt://localhost:1883", "arguments").publisher()
}
#[derive(Outgoing, Serialized)]
#[outgoing(name = "devices/dev42/state")]
struct DeviceState(Vec<u8>);
fn state() -> DeviceState {
DeviceState(b"online".to_vec())
}
#[test]
fn a_call_that_names_nothing_publishes_entirely_under_the_policy() {
assert_eq!(
MqttPublishOptions::resolve(None, Qos::AtLeastOnce, false),
(Qos::AtLeastOnce, false),
"a reply and a deferred redelivery arrive here with no call site at all"
);
}
#[test]
fn a_named_argument_wins_and_an_unnamed_one_keeps_the_policy_value() {
let options = MqttPublishOptions::default().retain(true);
assert_eq!(
MqttPublishOptions::resolve(Some(&options), Qos::ExactlyOnce, false),
(Qos::ExactlyOnce, true),
"the quality of service the call left alone is still the policy's"
);
}
#[test]
fn the_steps_compose_in_either_order() {
let publisher = publisher();
let state = state();
let expected = MqttPublishOptions::default()
.qos(Qos::ExactlyOnce)
.retain(true);
let mut forward = publisher.message(&state).qos(Qos::ExactlyOnce).retain(true);
assert_eq!(*forward.options_mut(), Some(expected));
let mut reversed = publisher.message(&state).retain(true).qos(Qos::ExactlyOnce);
assert_eq!(*reversed.options_mut(), Some(expected));
}
#[test]
fn a_builder_no_step_touched_carries_no_options() {
let publisher = publisher();
let state = state();
let mut plain = publisher.message(&state);
assert_eq!(
*plain.options_mut(),
None,
"nothing to resolve means the policy is the whole answer"
);
}
#[tokio::test]
async fn a_publish_before_connect_reports_the_missing_connection() {
let publisher = publisher();
let msg = OutgoingMessage::new("devices/dev42/state", b"online".as_slice());
let error = publisher
.publish(msg, Some(&MqttPublishOptions::default().retain(true)))
.await
.expect_err("nothing is connected yet");
assert!(matches!(error, MqttError::NotConnected));
}
}