use rumqttc::v5::mqttbytes::valid_filter;
use ruststream::SubscriptionSource;
use crate::broker::ConnectedMqttBroker;
use crate::error::MqttError;
use crate::subscriber::MqttSubscriber;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Qos {
AtMostOnce,
#[default]
AtLeastOnce,
ExactlyOnce,
}
impl Qos {
pub(crate) fn to_client(self) -> rumqttc::v5::mqttbytes::QoS {
match self {
Self::AtMostOnce => rumqttc::v5::mqttbytes::QoS::AtMostOnce,
Self::AtLeastOnce => rumqttc::v5::mqttbytes::QoS::AtLeastOnce,
Self::ExactlyOnce => rumqttc::v5::mqttbytes::QoS::ExactlyOnce,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[must_use]
pub struct MqttTopic {
filter: String,
qos: Qos,
shared: Option<String>,
}
impl MqttTopic {
pub fn new(filter: impl Into<String>) -> Self {
Self {
filter: filter.into(),
qos: Qos::default(),
shared: None,
}
}
pub fn qos(mut self, qos: Qos) -> Self {
self.qos = qos;
self
}
pub fn shared(mut self, group: impl Into<String>) -> Self {
self.shared = Some(group.into());
self
}
#[must_use]
pub fn filter(&self) -> &str {
&self.filter
}
pub(crate) fn qos_value(&self) -> Qos {
self.qos
}
pub(crate) fn wire_filter(&self) -> String {
self.shared.as_ref().map_or_else(
|| self.filter.clone(),
|group| format!("$share/{group}/{}", self.filter),
)
}
pub(crate) fn validate(&self) -> Result<(), MqttError> {
if !valid_filter(&self.filter) {
return Err(MqttError::Invalid(format!(
"'{}' is not a valid MQTT topic filter",
self.filter
)));
}
if let Some(group) = &self.shared {
if group.is_empty() || group.contains(['/', '+', '#']) {
return Err(MqttError::Invalid(format!(
"'{group}' is not a valid share group name"
)));
}
}
Ok(())
}
}
impl SubscriptionSource<ConnectedMqttBroker> for MqttTopic {
type Subscriber = MqttSubscriber;
fn name(&self) -> &str {
self.filter()
}
async fn subscribe(self, connected: &ConnectedMqttBroker) -> Result<MqttSubscriber, MqttError> {
connected.subscribe_topic(self).await
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_filters_are_rejected_before_io() {
assert!(MqttTopic::new("a/#/b").validate().is_err());
assert!(MqttTopic::new("").validate().is_err());
}
#[test]
fn invalid_share_groups_are_rejected_before_io() {
assert!(MqttTopic::new("a").shared("g/1").validate().is_err());
assert!(MqttTopic::new("a").shared("").validate().is_err());
}
#[test]
fn shared_filters_wrap_on_the_wire_only() {
let topic = MqttTopic::new("orders/+").shared("workers");
assert_eq!(topic.filter(), "orders/+");
assert_eq!(topic.wire_filter(), "$share/workers/orders/+");
}
}