From f86a2b9829767aa3d3c540df50db494cf6ed2975 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 11:33:11 +0200 Subject: [PATCH 01/21] more notes --- Tether4.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tether4.md b/Tether4.md index 490d518..d659834 100644 --- a/Tether4.md +++ b/Tether4.md @@ -12,10 +12,10 @@ For Output Plugs (publishing) NOW CHANNEL SENDERS, the topic will be constructed - agentRole/chanelName/optionalID For Input Plugs (subscribing) NOW CHANNEL RECEIVERS, the topic will be constructed as follows: -- agentRole/chanelName/# (matches "no ID" part and "ID part included") +- agentRole/chanelName/# (matches "no ID" part and "ID part(s) included") - agentRole/chanelName/optionalID (will only match when ID part is matched) -The main practical difference between a "topic" and a "Channel" (previously "plug") is simply that a Channel is expected to match ONLY ONE TYPE OF MESSAGE. So, a single MQTT Client may have multiple subscriptions, but we ensure that the correct messages are matched with the correct Channel when received, by applying our additional Tether Complaint Topic (TCT) matching pattern. +The main practical difference between a "topic" and a "Channel" (previously "plug") is simply that a Channel is expected to match ONLY ONE TYPE OF MESSAGE. So, a single MQTT Client may have multiple subscriptions, but we ensure that the correct messages are matched with the correct Channel when received, by applying our additional Tether Complaint Topic (TCT) matching pattern. The libraries (particularly typed languages such as TypeScript and Rust) should try to encourage (if not enforce) this practice. ## Cleaning up Unused "utilities" and the "explorer" will be removed. From 8d47fd543c66d740a0173bc165dc0a243b83084c Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 12:13:24 +0200 Subject: [PATCH 02/21] POC: the channel owns an Rc ref to the Tether Agent, which means it can send, without passing Tether Agent ref to channel or channel to Tether Agent --- examples/rust/send.rs | 50 +- lib/rust/src/agent/mod.rs | 139 ++--- lib/rust/src/channels/definitions.rs | 122 +++-- lib/rust/src/channels/options.rs | 785 +++++++++++++-------------- 4 files changed, 544 insertions(+), 552 deletions(-) diff --git a/examples/rust/send.rs b/examples/rust/send.rs index 1e11d3e..879b469 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -25,46 +25,12 @@ fn main() { let (role, id, _) = tether_agent.description(); info!("Created agent OK: {}, {}", role, id); - let empty_message_output = ChannelOptionsBuilder::create_sender("nothing") - .build(&mut tether_agent) - .expect("failed to create output"); - let boolean_message_output = ChannelOptionsBuilder::create_sender("one") - .build(&mut tether_agent) - .expect("failed to create output"); - let custom_output = ChannelOptionsBuilder::create_sender("two") - .topic(Some("custom/custom/two")) - .build(&mut tether_agent) - .expect("failed to create output"); - let grouped_output_1 = ChannelOptionsBuilder::create_sender("one") - .id(Some("groupMessages")) - .build(&mut tether_agent) - .expect("failed to create output"); - let grouped_output_2 = ChannelOptionsBuilder::create_sender("two") - .id(Some("groupMessages")) - .build(&mut tether_agent) - .expect("failed to create output"); - - for i in 1..=10 { - info!("#{i}: Sending empty message..."); - tether_agent.send_raw(&empty_message_output, None).unwrap(); - - let just_a_boolean = i % 2 == 0; - info!("#{i}: Sending boolean message..."); - tether_agent - .send(&boolean_message_output, just_a_boolean) - .unwrap(); - - info!("#{i}: Sending custom struct message..."); - let custom_message = CustomStruct { - id: i, - name: "hello".into(), - }; - tether_agent.send(&custom_output, custom_message).unwrap(); - - info!("#{i}: Sending grouped messages..."); - tether_agent.send_empty(&grouped_output_1).unwrap(); - tether_agent.send_empty(&grouped_output_2).unwrap(); - - thread::sleep(Duration::from_millis(1000)) - } + let sender = tether_agent.create_sender("values"); + + let test_struct = CustomStruct { + id: 101, + name: "something".into(), + }; + let payload = rmp_serde::to_vec_named(&test_struct).expect("failed to serialize"); + sender.send_raw(&payload).expect("failed to send"); } diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index cfe6bee..5c17dd1 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -4,14 +4,15 @@ use rmp_serde::to_vec_named; use rumqttc::tokio_rustls::rustls::ClientConfig; use rumqttc::{Client, Event, MqttOptions, Packet, QoS, Transport}; use serde::Serialize; +use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use uuid::Uuid; -use crate::ChannelDefinition; +use crate::ChannelSender; use crate::{ tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}, - ChannelDefinitionCommon, + ChannelCommon, }; const TIMEOUT_SECONDS: u64 = 3; @@ -169,6 +170,18 @@ impl TetherAgentOptionsBuilder { } impl TetherAgent { + pub fn create_sender(&self, name: &str) -> ChannelSender { + ChannelSender::new( + name, + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( + self, name, None, None, + )), + None, + None, + Rc::new(&self), + ) + } + pub fn is_connected(&self) -> bool { self.client.is_some() } @@ -386,67 +399,67 @@ impl TetherAgent { } } - /// Unlike .send, this function does NOT serialize the data before publishing. - /// - /// Given a channel definition and a raw (u8 buffer) payload, publishes a message - /// using an appropriate topic and with the QOS specified in the Channel Definition - pub fn send_raw( - &self, - channel_definition: &ChannelDefinition, - payload: Option<&[u8]>, - ) -> anyhow::Result<()> { - match channel_definition { - ChannelDefinition::ChannelReceiver(_) => { - panic!("You cannot publish using a Channel Receiver") - } - ChannelDefinition::ChannelSender(channel_sender_definition) => { - let topic = channel_sender_definition.generated_topic(); - let qos = match channel_sender_definition.qos() { - 0 => QoS::AtMostOnce, - 1 => QoS::AtLeastOnce, - 2 => QoS::ExactlyOnce, - _ => QoS::AtMostOnce, - }; - - if let Some(client) = &self.client { - let res = client - .publish( - topic, - qos, - channel_sender_definition.retain(), - payload.unwrap_or_default(), - ) - .map_err(anyhow::Error::msg); - debug!("Published OK"); - res - } else { - Err(anyhow!("Client not ready for publish")) - } - } - } - } - - /// Serializes the data automatically before publishing. - /// - /// Given a channel definition and serializeable data payload, publishes a message - /// using an appropriate topic and with the QOS specified in the Channel Definition - pub fn send( - &self, - channel_definition: &ChannelDefinition, - data: T, - ) -> anyhow::Result<()> { - match to_vec_named(&data) { - Ok(payload) => self.send_raw(channel_definition, Some(&payload)), - Err(e) => { - error!("Failed to encode: {e:?}"); - Err(e.into()) - } - } - } - - pub fn send_empty(&self, channel_definition: &ChannelDefinition) -> anyhow::Result<()> { - self.send_raw(channel_definition, None) - } + // /// Unlike .send, this function does NOT serialize the data before publishing. + // /// + // /// Given a channel definition and a raw (u8 buffer) payload, publishes a message + // /// using an appropriate topic and with the QOS specified in the Channel Definition + // pub fn send_raw( + // &self, + // channel_definition: &TetherChannel, + // payload: Option<&[u8]>, + // ) -> anyhow::Result<()> { + // match channel_definition { + // TetherChannel::ChannelReceiver(_) => { + // panic!("You cannot publish using a Channel Receiver") + // } + // TetherChannel::ChannelSender(channel_sender_definition) => { + // let topic = channel_sender_definition.generated_topic(); + // let qos = match channel_sender_definition.qos() { + // 0 => QoS::AtMostOnce, + // 1 => QoS::AtLeastOnce, + // 2 => QoS::ExactlyOnce, + // _ => QoS::AtMostOnce, + // }; + + // if let Some(client) = &self.client { + // let res = client + // .publish( + // topic, + // qos, + // channel_sender_definition.retain(), + // payload.unwrap_or_default(), + // ) + // .map_err(anyhow::Error::msg); + // debug!("Published OK"); + // res + // } else { + // Err(anyhow!("Client not ready for publish")) + // } + // } + // } + // } + + // /// Serializes the data automatically before publishing. + // /// + // /// Given a channel definition and serializeable data payload, publishes a message + // /// using an appropriate topic and with the QOS specified in the Channel Definition + // pub fn send( + // &self, + // channel_definition: &TetherChannel, + // data: T, + // ) -> anyhow::Result<()> { + // match to_vec_named(&data) { + // Ok(payload) => self.send_raw(channel_definition, Some(&payload)), + // Err(e) => { + // error!("Failed to encode: {e:?}"); + // Err(e.into()) + // } + // } + // } + + // pub fn send_empty(&self, channel_definition: &TetherChannel) -> anyhow::Result<()> { + // self.send_raw(channel_definition, None) + // } pub fn publish_raw( &self, diff --git a/lib/rust/src/channels/definitions.rs b/lib/rust/src/channels/definitions.rs index a45985b..8d543a2 100644 --- a/lib/rust/src/channels/definitions.rs +++ b/lib/rust/src/channels/definitions.rs @@ -1,9 +1,14 @@ +use anyhow::anyhow; +use std::rc::Rc; + use log::{debug, error, warn}; use serde::{Deserialize, Serialize}; +use crate::TetherAgent; + use super::tether_compliant_topic::TetherOrCustomTopic; -pub trait ChannelDefinitionCommon<'a> { +pub trait ChannelCommon<'a> { fn name(&'a self) -> &'a str; /// Return the generated topic string actually used by the Channel fn generated_topic(&'a self) -> &'a str; @@ -13,13 +18,13 @@ pub trait ChannelDefinitionCommon<'a> { } #[derive(Serialize, Deserialize, Debug)] -pub struct ChannelReceiverDefinition { +pub struct ChannelReceiver { name: String, topic: TetherOrCustomTopic, qos: i32, } -impl ChannelDefinitionCommon<'_> for ChannelReceiverDefinition { +impl ChannelCommon<'_> for ChannelReceiver { fn name(&self) -> &str { &self.name } @@ -52,13 +57,9 @@ impl ChannelDefinitionCommon<'_> for ChannelReceiverDefinition { } } -impl ChannelReceiverDefinition { - pub fn new( - name: &str, - topic: TetherOrCustomTopic, - qos: Option, - ) -> ChannelReceiverDefinition { - ChannelReceiverDefinition { +impl ChannelReceiver { + pub fn new(name: &str, topic: TetherOrCustomTopic, qos: Option) -> ChannelReceiver { + ChannelReceiver { name: String::from(name), topic, qos: qos.unwrap_or(1), @@ -129,15 +130,15 @@ impl ChannelReceiverDefinition { } } -#[derive(Serialize, Deserialize, Debug)] -pub struct ChannelSenderDefinition { +pub struct ChannelSender<'a> { name: String, topic: TetherOrCustomTopic, qos: i32, retain: bool, + tether_agent: Rc<&'a TetherAgent>, } -impl ChannelDefinitionCommon<'_> for ChannelSenderDefinition { +impl<'a> ChannelCommon<'a> for ChannelSender<'a> { fn name(&'_ self) -> &'_ str { &self.name } @@ -158,69 +159,86 @@ impl ChannelDefinitionCommon<'_> for ChannelSenderDefinition { } } -impl ChannelSenderDefinition { +impl<'a> ChannelSender<'a> { pub fn new( name: &str, topic: TetherOrCustomTopic, qos: Option, retain: Option, - ) -> ChannelSenderDefinition { - ChannelSenderDefinition { + tether_agent: Rc<&'a TetherAgent>, + ) -> ChannelSender<'a> { + ChannelSender { name: String::from(name), topic, qos: qos.unwrap_or(1), retain: retain.unwrap_or(false), + tether_agent, } } pub fn retain(&self) -> bool { self.retain } -} -#[derive(Serialize, Deserialize, Debug)] -pub enum ChannelDefinition { - ChannelReceiver(ChannelReceiverDefinition), - ChannelSender(ChannelSenderDefinition), -} - -impl ChannelDefinition { - pub fn name(&self) -> &str { - match self { - ChannelDefinition::ChannelReceiver(p) => p.name(), - ChannelDefinition::ChannelSender(p) => p.name(), - } - } - - pub fn generated_topic(&self) -> &str { - match self { - ChannelDefinition::ChannelReceiver(p) => p.generated_topic(), - ChannelDefinition::ChannelSender(p) => p.generated_topic(), - } - } - - pub fn matches(&self, topic: &TetherOrCustomTopic) -> bool { - match self { - ChannelDefinition::ChannelReceiver(p) => p.matches(topic), - ChannelDefinition::ChannelSender(_) => { - error!("We don't check matches for Channel Senders"); - false - } + pub fn send_raw(&self, payload: &[u8]) -> anyhow::Result<()> { + if let Some(client) = &self.tether_agent.client { + let res = client + .publish( + self.generated_topic(), + rumqttc::QoS::AtLeastOnce, + false, + payload, + ) + .map_err(anyhow::Error::msg); + res + } else { + Err(anyhow!("no client")) } } } +// pub enum TetherChannel { +// ChannelReceiver(ChannelReceiver), +// ChannelSender(ChannelSender), +// } + +// impl TetherChannel { +// pub fn name(&self) -> &str { +// match self { +// TetherChannel::ChannelReceiver(p) => p.name(), +// TetherChannel::ChannelSender(p) => p.name(), +// } +// } + +// pub fn generated_topic(&self) -> &str { +// match self { +// TetherChannel::ChannelReceiver(p) => p.generated_topic(), +// TetherChannel::ChannelSender(p) => p.generated_topic(), +// } +// } + +// pub fn matches(&self, topic: &TetherOrCustomTopic) -> bool { +// match self { +// TetherChannel::ChannelReceiver(p) => p.matches(topic), +// TetherChannel::ChannelSender(_) => { +// error!("We don't check matches for Channel Senders"); +// false +// } +// } +// } +// } + #[cfg(test)] mod tests { use crate::{ tether_compliant_topic::{parse_channel_name, TetherCompliantTopic, TetherOrCustomTopic}, - ChannelDefinitionCommon, ChannelReceiverDefinition, + ChannelCommon, ChannelReceiver, }; #[test] fn receiver_match_tpt() { - let channel_def = ChannelReceiverDefinition::new( + let channel_def = ChannelReceiver::new( "testChannel", TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( "testChannel", @@ -250,7 +268,7 @@ mod tests { #[test] fn receiver_match_tpt_custom_role() { - let channel_def = ChannelReceiverDefinition::new( + let channel_def = ChannelReceiver::new( "customChanel", TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( "customChanel", @@ -278,7 +296,7 @@ mod tests { #[test] fn receiver_match_custom_id() { - let channel_def = ChannelReceiverDefinition::new( + let channel_def = ChannelReceiver::new( "customChanel", TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( "customChanel", @@ -306,7 +324,7 @@ mod tests { #[test] fn receiver_match_both() { - let channel_def = ChannelReceiverDefinition::new( + let channel_def = ChannelReceiver::new( "customChanel", TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( "customChanel", @@ -337,7 +355,7 @@ mod tests { #[test] fn receiver_match_custom_topic() { - let channel_def = ChannelReceiverDefinition::new( + let channel_def = ChannelReceiver::new( "customChanel", TetherOrCustomTopic::Custom("one/two/three/four/five".into()), // not a standard Tether Three Part Topic None, @@ -355,7 +373,7 @@ mod tests { #[test] fn receiver_match_wildcard() { - let channel_def = ChannelReceiverDefinition::new( + let channel_def = ChannelReceiver::new( "everything", TetherOrCustomTopic::Custom("#".into()), // fully legal, but not a standard Three Part Topic None, diff --git a/lib/rust/src/channels/options.rs b/lib/rust/src/channels/options.rs index e779b11..e8c9cf7 100644 --- a/lib/rust/src/channels/options.rs +++ b/lib/rust/src/channels/options.rs @@ -2,13 +2,10 @@ use anyhow::anyhow; use log::{debug, error, info, warn}; use crate::{ - definitions::ChannelDefinitionCommon, tether_compliant_topic::TetherCompliantTopic, - ChannelDefinition, TetherAgent, + definitions::ChannelCommon, tether_compliant_topic::TetherCompliantTopic, TetherAgent, }; -use super::{ - tether_compliant_topic::TetherOrCustomTopic, ChannelReceiverDefinition, ChannelSenderDefinition, -}; +use super::{tether_compliant_topic::TetherOrCustomTopic, ChannelReceiver, ChannelSender}; pub struct ChannelReceiverOptions { channel_name: String, @@ -228,395 +225,393 @@ impl ChannelOptionsBuilder { self } - /// Finalise the options (substituting suitable defaults if no custom values have been - /// provided) and return a valid ChannelDefinition that you can actually use. - pub fn build(self, tether_agent: &mut TetherAgent) -> anyhow::Result { - match self { - Self::ChannelReceiver(channel_options) => { - let tpt: TetherOrCustomTopic = match channel_options.override_topic { - Some(custom) => TetherOrCustomTopic::Custom(custom), - None => { - debug!("Not a custom topic; provided overrides: role = {:?}, id = {:?}, name = {:?}", channel_options.override_subscribe_role, channel_options.override_subscribe_id, channel_options.override_subscribe_channel_name); - - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - &channel_options - .override_subscribe_channel_name - .unwrap_or(channel_options.channel_name.clone()), - channel_options.override_subscribe_role.as_deref(), - channel_options.override_subscribe_id.as_deref(), - )) - } - }; - let channel_definition = ChannelReceiverDefinition::new( - &channel_options.channel_name, - tpt, - channel_options.qos, - ); - - // This is really only useful for testing purposes. - if !tether_agent.auto_connect_enabled() { - warn!("Auto-connect is disabled, skipping subscription"); - return Ok(ChannelDefinition::ChannelReceiver(channel_definition)); - } - - if let Some(client) = &tether_agent.client { - match client.subscribe( - channel_definition.generated_topic(), - match channel_definition.qos() { - 0 => rumqttc::QoS::AtMostOnce, - 1 => rumqttc::QoS::AtLeastOnce, - 2 => rumqttc::QoS::ExactlyOnce, - _ => rumqttc::QoS::AtLeastOnce, - }, - ) { - Ok(res) => { - debug!( - "This topic was fine: \"{}\"", - channel_definition.generated_topic() - ); - debug!("Server respond OK for subscribe: {res:?}"); - Ok(ChannelDefinition::ChannelReceiver(channel_definition)) - } - Err(_e) => Err(anyhow!("ClientError")), - } - } else { - Err(anyhow!("Client not available for subscription")) - } - } - Self::ChannelSender(channel_options) => { - let tpt: TetherOrCustomTopic = match channel_options.override_topic { - Some(custom) => { - warn!( - "Custom topic override: \"{}\" - all other options ignored", - custom - ); - TetherOrCustomTopic::Custom(custom) - } - None => { - let optional_id_part = match channel_options.override_publish_id { - Some(id) => { - debug!("Publish ID was overriden at Channel options level. The Agent ID will be ignored."); - Some(id) - } - None => { - debug!("Publish ID was not overriden at Channel options level. The Agent ID will be used instead, if specified in Agent creation."); - tether_agent.id().map(String::from) - } - }; - - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( - tether_agent, - &channel_options.channel_name, - channel_options.override_publish_role.as_deref(), - optional_id_part.as_deref(), - )) - } - }; - - let channel_definition = ChannelSenderDefinition::new( - &channel_options.channel_name, - tpt, - channel_options.qos, - channel_options.retain, - ); - Ok(ChannelDefinition::ChannelSender(channel_definition)) - } - } - } -} - -#[cfg(test)] -mod tests { - - use crate::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; - - // fn verbose_logging() { - // use env_logger::{Builder, Env}; - // let mut logger_builder = Builder::from_env(Env::default().default_filter_or("debug")); - // logger_builder.init(); + // /// Finalise the options (substituting suitable defaults if no custom values have been + // /// provided) and return a valid ChannelDefinition that you can actually use. + // pub fn build(self, tether_agent: &mut TetherAgent) -> anyhow::Result { + // todo!(); + // // match self { + // // Self::ChannelReceiver(channel_options) => { + // // let tpt: TetherOrCustomTopic = match channel_options.override_topic { + // // Some(custom) => TetherOrCustomTopic::Custom(custom), + // // None => { + // // debug!("Not a custom topic; provided overrides: role = {:?}, id = {:?}, name = {:?}", channel_options.override_subscribe_role, channel_options.override_subscribe_id, channel_options.override_subscribe_channel_name); + + // // TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( + // // &channel_options + // // .override_subscribe_channel_name + // // .unwrap_or(channel_options.channel_name.clone()), + // // channel_options.override_subscribe_role.as_deref(), + // // channel_options.override_subscribe_id.as_deref(), + // // )) + // // } + // // }; + // // let channel_definition = + // // ChannelReceiver::new(&channel_options.channel_name, tpt, channel_options.qos); + + // // // This is really only useful for testing purposes. + // // if !tether_agent.auto_connect_enabled() { + // // warn!("Auto-connect is disabled, skipping subscription"); + // // return Ok(TetherChannel::ChannelReceiver(channel_definition)); + // // } + + // // if let Some(client) = &tether_agent.client { + // // match client.subscribe( + // // channel_definition.generated_topic(), + // // match channel_definition.qos() { + // // 0 => rumqttc::QoS::AtMostOnce, + // // 1 => rumqttc::QoS::AtLeastOnce, + // // 2 => rumqttc::QoS::ExactlyOnce, + // // _ => rumqttc::QoS::AtLeastOnce, + // // }, + // // ) { + // // Ok(res) => { + // // debug!( + // // "This topic was fine: \"{}\"", + // // channel_definition.generated_topic() + // // ); + // // debug!("Server respond OK for subscribe: {res:?}"); + // // Ok(TetherChannel::ChannelReceiver(channel_definition)) + // // } + // // Err(_e) => Err(anyhow!("ClientError")), + // // } + // // } else { + // // Err(anyhow!("Client not available for subscription")) + // // } + // // } + // // Self::ChannelSender(channel_options) => { + // // let tpt: TetherOrCustomTopic = match channel_options.override_topic { + // // Some(custom) => { + // // warn!( + // // "Custom topic override: \"{}\" - all other options ignored", + // // custom + // // ); + // // TetherOrCustomTopic::Custom(custom) + // // } + // // None => { + // // let optional_id_part = match channel_options.override_publish_id { + // // Some(id) => { + // // debug!("Publish ID was overriden at Channel options level. The Agent ID will be ignored."); + // // Some(id) + // // } + // // None => { + // // debug!("Publish ID was not overriden at Channel options level. The Agent ID will be used instead, if specified in Agent creation."); + // // tether_agent.id().map(String::from) + // // } + // // }; + + // // TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( + // // tether_agent, + // // &channel_options.channel_name, + // // channel_options.override_publish_role.as_deref(), + // // optional_id_part.as_deref(), + // // )) + // // } + // // }; + + // // let channel_definition = ChannelSender::new( + // // &channel_options.channel_name, + // // tpt, + // // channel_options.qos, + // // channel_options.retain, + // // ); + // // Ok(TetherChannel::ChannelSender(channel_definition)) + // // } + // // } // } - - #[test] - fn default_receiver_channel() { - // verbose_logging(); - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - let receiver = ChannelOptionsBuilder::create_receiver("one") - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver.name(), "one"); - assert_eq!(receiver.generated_topic(), "+/one/#"); - } - - #[test] - /// This is a fairly trivial example, but contrast with the test - /// `sender_channel_default_but_agent_id_custom`: although a custom ID was set for the - /// Agent, this does not affect the Topic for a Channel Receiver created without any - /// explicit overrides. - fn default_channel_receiver_with_agent_custom_id() { - // verbose_logging(); - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .id(Some("verySpecialGroup")) - .build() - .expect("sorry, these tests require working localhost Broker"); - let receiver = ChannelOptionsBuilder::create_receiver("one") - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver.name(), "one"); - assert_eq!(receiver.generated_topic(), "+/one/#"); - } - - #[test] - fn default_channel_sender() { - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - let channel = ChannelOptionsBuilder::create_sender("two") - .build(&mut tether_agent) - .unwrap(); - assert_eq!(channel.name(), "two"); - assert_eq!(channel.generated_topic(), "tester/two"); - } - - #[test] - /// This is identical to the case in which a Channel Sender is created with defaults (no overrides), - /// BUT the Agent had a custom ID set, which means that the final topic includes this custom - /// ID/Group value. - fn sender_channel_default_but_agent_id_custom() { - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .id(Some("specialCustomGrouping")) - .build() - .expect("sorry, these tests require working localhost Broker"); - let channel = ChannelOptionsBuilder::create_sender("somethingStandard") - .build(&mut tether_agent) - .unwrap(); - assert_eq!(channel.name(), "somethingStandard"); - assert_eq!( - channel.generated_topic(), - "tester/somethingStandard/specialCustomGrouping" - ); - } - - #[test] - fn receiver_id_andor_role() { - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - - let receive_role_only = ChannelOptionsBuilder::create_receiver("theChannel") - .role(Some("specificRole")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receive_role_only.name(), "theChannel"); - assert_eq!( - receive_role_only.generated_topic(), - "specificRole/theChannel/#" - ); - - let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") - .id(Some("specificID")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_id_only.name(), "theChannel"); - assert_eq!( - receiver_id_only.generated_topic(), - "+/theChannel/specificID" - ); - - let receiver_both_custom = ChannelOptionsBuilder::create_receiver("theChannel") - .id(Some("specificID")) - .role(Some("specificRole")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_both_custom.name(), "theChannel"); - assert_eq!( - receiver_both_custom.generated_topic(), - "specificRole/theChannel/specificID" - ); - } - - #[test] - /// If the end-user implicitly specifies the chanel name part (does not set it to Some(_) - /// or None) then the ID and/or Role parts will change but the Channel Name part will - /// remain the "original" / default - /// Contrast with receiver_specific_id_andor_role_no_chanel_name below. - fn receiver_specific_id_andor_role_with_channel_name() { - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - - let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") - .role(Some("specificRole")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_role_only.name(), "theChannel"); - assert_eq!( - receiver_role_only.generated_topic(), - "specificRole/theChannel/#" - ); - - let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") - .id(Some("specificID")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_id_only.name(), "theChannel"); - assert_eq!( - receiver_id_only.generated_topic(), - "+/theChannel/specificID" - ); - - let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") - .id(Some("specificID")) - .role(Some("specificRole")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_both.name(), "theChannel"); - assert_eq!( - receiver_both.generated_topic(), - "specificRole/theChannel/specificID" - ); - } - - #[test] - /// Unlike receiver_specific_id_andor_role_with_channel_name, this tests the situation where - /// the end-user (possibly) specifies the ID and/or Role, but also explicitly - /// sets the Channel Name to Some("+"), ie. "use a wildcard at this - /// position instead" - and NOT the original channel name. - fn receiver_specific_id_andor_role_no_channel_name() { - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - - let receiver_only_chanel_name_none = ChannelOptionsBuilder::create_receiver("theChannel") - .name(Some("+")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_only_chanel_name_none.name(), "theChannel"); - assert_eq!(receiver_only_chanel_name_none.generated_topic(), "+/+/#"); - - let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") - .name(Some("+")) - .role(Some("specificRole")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_role_only.name(), "theChannel"); - assert_eq!(receiver_role_only.generated_topic(), "specificRole/+/#"); - - let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") - // .name(Some("+")) - .any_channel() // equivalent to Some("+") - .id(Some("specificID")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_id_only.name(), "theChannel"); - assert_eq!(receiver_id_only.generated_topic(), "+/+/specificID"); - - let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") - .name(Some("+")) - .id(Some("specificID")) - .role(Some("specificRole")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_both.name(), "theChannel"); - assert_eq!(receiver_both.generated_topic(), "specificRole/+/specificID"); - } - - #[test] - fn any_name_but_specify_role() { - // Some fairly niche cases here - - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - - let receiver_any_channel = ChannelOptionsBuilder::create_receiver("aTest") - .any_channel() - .build(&mut tether_agent) - .unwrap(); - - assert_eq!(receiver_any_channel.name(), "aTest"); - assert_eq!(receiver_any_channel.generated_topic(), "+/+/#"); - - let receiver_specify_role = ChannelOptionsBuilder::create_receiver("aTest") - .any_channel() - .role(Some("brain")) - .build(&mut tether_agent) - .unwrap(); - - assert_eq!(receiver_specify_role.name(), "aTest"); - assert_eq!(receiver_specify_role.generated_topic(), "brain/+/#"); - } - - #[test] - fn sender_custom() { - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - - let sender_custom_role = ChannelOptionsBuilder::create_sender("theChannelSender") - .role(Some("customRole")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(sender_custom_role.name(), "theChannelSender"); - assert_eq!( - sender_custom_role.generated_topic(), - "customRole/theChannelSender" - ); - - let sender_custom_id = ChannelOptionsBuilder::create_sender("theChannelSender") - .id(Some("customID")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(sender_custom_id.name(), "theChannelSender"); - assert_eq!( - sender_custom_id.generated_topic(), - "tester/theChannelSender/customID" - ); - - let sender_custom_both = ChannelOptionsBuilder::create_sender("theChannelSender") - .role(Some("customRole")) - .id(Some("customID")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(sender_custom_both.name(), "theChannelSender"); - assert_eq!( - sender_custom_both.generated_topic(), - "customRole/theChannelSender/customID" - ); - } - - #[test] - fn receiver_manual_topics() { - let mut tether_agent = TetherAgentOptionsBuilder::new("tester") - .auto_connect(false) - .build() - .expect("sorry, these tests require working localhost Broker"); - - let receiver_all = ChannelOptionsBuilder::create_receiver("everything") - .topic(Some("#")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_all.name(), "everything"); - assert_eq!(receiver_all.generated_topic(), "#"); - - let receiver_nontether = ChannelOptionsBuilder::create_receiver("weird") - .topic(Some("foo/bar/baz/one/two/three")) - .build(&mut tether_agent) - .unwrap(); - assert_eq!(receiver_nontether.name(), "weird"); - assert_eq!( - receiver_nontether.generated_topic(), - "foo/bar/baz/one/two/three" - ); - } } + +// #[cfg(test)] +// mod tests { + +// use crate::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; + +// // fn verbose_logging() { +// // use env_logger::{Builder, Env}; +// // let mut logger_builder = Builder::from_env(Env::default().default_filter_or("debug")); +// // logger_builder.init(); +// // } + +// #[test] +// fn default_receiver_channel() { +// // verbose_logging(); +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let receiver = ChannelOptionsBuilder::create_receiver("one") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver.name(), "one"); +// assert_eq!(receiver.generated_topic(), "+/one/#"); +// } + +// #[test] +// /// This is a fairly trivial example, but contrast with the test +// /// `sender_channel_default_but_agent_id_custom`: although a custom ID was set for the +// /// Agent, this does not affect the Topic for a Channel Receiver created without any +// /// explicit overrides. +// fn default_channel_receiver_with_agent_custom_id() { +// // verbose_logging(); +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .id(Some("verySpecialGroup")) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let receiver = ChannelOptionsBuilder::create_receiver("one") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver.name(), "one"); +// assert_eq!(receiver.generated_topic(), "+/one/#"); +// } + +// #[test] +// fn default_channel_sender() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let channel = ChannelOptionsBuilder::create_sender("two") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(channel.name(), "two"); +// assert_eq!(channel.generated_topic(), "tester/two"); +// } + +// #[test] +// /// This is identical to the case in which a Channel Sender is created with defaults (no overrides), +// /// BUT the Agent had a custom ID set, which means that the final topic includes this custom +// /// ID/Group value. +// fn sender_channel_default_but_agent_id_custom() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .id(Some("specialCustomGrouping")) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let channel = ChannelOptionsBuilder::create_sender("somethingStandard") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(channel.name(), "somethingStandard"); +// assert_eq!( +// channel.generated_topic(), +// "tester/somethingStandard/specialCustomGrouping" +// ); +// } + +// #[test] +// fn receiver_id_andor_role() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receive_role_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receive_role_only.name(), "theChannel"); +// assert_eq!( +// receive_role_only.generated_topic(), +// "specificRole/theChannel/#" +// ); + +// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_id_only.name(), "theChannel"); +// assert_eq!( +// receiver_id_only.generated_topic(), +// "+/theChannel/specificID" +// ); + +// let receiver_both_custom = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_both_custom.name(), "theChannel"); +// assert_eq!( +// receiver_both_custom.generated_topic(), +// "specificRole/theChannel/specificID" +// ); +// } + +// #[test] +// /// If the end-user implicitly specifies the chanel name part (does not set it to Some(_) +// /// or None) then the ID and/or Role parts will change but the Channel Name part will +// /// remain the "original" / default +// /// Contrast with receiver_specific_id_andor_role_no_chanel_name below. +// fn receiver_specific_id_andor_role_with_channel_name() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_role_only.name(), "theChannel"); +// assert_eq!( +// receiver_role_only.generated_topic(), +// "specificRole/theChannel/#" +// ); + +// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_id_only.name(), "theChannel"); +// assert_eq!( +// receiver_id_only.generated_topic(), +// "+/theChannel/specificID" +// ); + +// let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_both.name(), "theChannel"); +// assert_eq!( +// receiver_both.generated_topic(), +// "specificRole/theChannel/specificID" +// ); +// } + +// #[test] +// /// Unlike receiver_specific_id_andor_role_with_channel_name, this tests the situation where +// /// the end-user (possibly) specifies the ID and/or Role, but also explicitly +// /// sets the Channel Name to Some("+"), ie. "use a wildcard at this +// /// position instead" - and NOT the original channel name. +// fn receiver_specific_id_andor_role_no_channel_name() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_only_chanel_name_none = ChannelOptionsBuilder::create_receiver("theChannel") +// .name(Some("+")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_only_chanel_name_none.name(), "theChannel"); +// assert_eq!(receiver_only_chanel_name_none.generated_topic(), "+/+/#"); + +// let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .name(Some("+")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_role_only.name(), "theChannel"); +// assert_eq!(receiver_role_only.generated_topic(), "specificRole/+/#"); + +// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") +// // .name(Some("+")) +// .any_channel() // equivalent to Some("+") +// .id(Some("specificID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_id_only.name(), "theChannel"); +// assert_eq!(receiver_id_only.generated_topic(), "+/+/specificID"); + +// let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") +// .name(Some("+")) +// .id(Some("specificID")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_both.name(), "theChannel"); +// assert_eq!(receiver_both.generated_topic(), "specificRole/+/specificID"); +// } + +// #[test] +// fn any_name_but_specify_role() { +// // Some fairly niche cases here + +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_any_channel = ChannelOptionsBuilder::create_receiver("aTest") +// .any_channel() +// .build(&mut tether_agent) +// .unwrap(); + +// assert_eq!(receiver_any_channel.name(), "aTest"); +// assert_eq!(receiver_any_channel.generated_topic(), "+/+/#"); + +// let receiver_specify_role = ChannelOptionsBuilder::create_receiver("aTest") +// .any_channel() +// .role(Some("brain")) +// .build(&mut tether_agent) +// .unwrap(); + +// assert_eq!(receiver_specify_role.name(), "aTest"); +// assert_eq!(receiver_specify_role.generated_topic(), "brain/+/#"); +// } + +// #[test] +// fn sender_custom() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let sender_custom_role = ChannelOptionsBuilder::create_sender("theChannelSender") +// .role(Some("customRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(sender_custom_role.name(), "theChannelSender"); +// assert_eq!( +// sender_custom_role.generated_topic(), +// "customRole/theChannelSender" +// ); + +// let sender_custom_id = ChannelOptionsBuilder::create_sender("theChannelSender") +// .id(Some("customID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(sender_custom_id.name(), "theChannelSender"); +// assert_eq!( +// sender_custom_id.generated_topic(), +// "tester/theChannelSender/customID" +// ); + +// let sender_custom_both = ChannelOptionsBuilder::create_sender("theChannelSender") +// .role(Some("customRole")) +// .id(Some("customID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(sender_custom_both.name(), "theChannelSender"); +// assert_eq!( +// sender_custom_both.generated_topic(), +// "customRole/theChannelSender/customID" +// ); +// } + +// #[test] +// fn receiver_manual_topics() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_all = ChannelOptionsBuilder::create_receiver("everything") +// .topic(Some("#")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_all.name(), "everything"); +// assert_eq!(receiver_all.generated_topic(), "#"); + +// let receiver_nontether = ChannelOptionsBuilder::create_receiver("weird") +// .topic(Some("foo/bar/baz/one/two/three")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_nontether.name(), "weird"); +// assert_eq!( +// receiver_nontether.generated_topic(), +// "foo/bar/baz/one/two/three" +// ); +// } +// } From abb885d9d4860263c04dc72e95479e9fc9f844e7 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 12:16:43 +0200 Subject: [PATCH 03/21] Event better: no need for Rc, just ref with lifetime --- examples/rust/send.rs | 2 +- lib/rust/src/agent/mod.rs | 2 +- lib/rust/src/channels/definitions.rs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/rust/send.rs b/examples/rust/send.rs index 879b469..c09b34c 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -19,7 +19,7 @@ fn main() { debug!("Debugging is enabled; could be verbose"); - let mut tether_agent = TetherAgentOptionsBuilder::new("rustExample") + let tether_agent = TetherAgentOptionsBuilder::new("rustExample") .build() .expect("failed to connect Tether"); let (role, id, _) = tether_agent.description(); diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index 5c17dd1..068ef47 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -178,7 +178,7 @@ impl TetherAgent { )), None, None, - Rc::new(&self), + self, ) } diff --git a/lib/rust/src/channels/definitions.rs b/lib/rust/src/channels/definitions.rs index 8d543a2..9fb214c 100644 --- a/lib/rust/src/channels/definitions.rs +++ b/lib/rust/src/channels/definitions.rs @@ -135,7 +135,7 @@ pub struct ChannelSender<'a> { topic: TetherOrCustomTopic, qos: i32, retain: bool, - tether_agent: Rc<&'a TetherAgent>, + tether_agent: &'a TetherAgent, } impl<'a> ChannelCommon<'a> for ChannelSender<'a> { @@ -165,7 +165,7 @@ impl<'a> ChannelSender<'a> { topic: TetherOrCustomTopic, qos: Option, retain: Option, - tether_agent: Rc<&'a TetherAgent>, + tether_agent: &'a TetherAgent, ) -> ChannelSender<'a> { ChannelSender { name: String::from(name), From 00275eb0492182ea09c2ad798e5482d67284be1d Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 12:41:20 +0200 Subject: [PATCH 04/21] Restructured sender module for ChannelSender, demo auto-encoding (but not typed at ChannelSender level!) --- examples/rust/send.rs | 11 +- lib/rust/src/agent/mod.rs | 10 +- lib/rust/src/channels/definitions.rs | 394 --------------------------- lib/rust/src/channels/mod.rs | 330 +++++++++++++++++++++- lib/rust/src/channels/options.rs | 7 +- lib/rust/src/channels/sender.rs | 81 ++++++ 6 files changed, 422 insertions(+), 411 deletions(-) delete mode 100644 lib/rust/src/channels/definitions.rs create mode 100644 lib/rust/src/channels/sender.rs diff --git a/examples/rust/send.rs b/examples/rust/send.rs index c09b34c..9bda525 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -1,4 +1,4 @@ -use std::{thread, time::Duration}; +use std::time::Duration; use env_logger::{Builder, Env}; use log::{debug, info}; @@ -33,4 +33,13 @@ fn main() { }; let payload = rmp_serde::to_vec_named(&test_struct).expect("failed to serialize"); sender.send_raw(&payload).expect("failed to send"); + + let another_struct = CustomStruct { + id: 202, + name: "auto encoded".into(), + }; + + sender.send(&another_struct).expect("failed to encode+send"); + + std::thread::sleep(Duration::from_millis(3000)); } diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index 068ef47..b4e2f5d 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -1,19 +1,13 @@ use anyhow::anyhow; use log::{debug, error, info, trace, warn}; -use rmp_serde::to_vec_named; use rumqttc::tokio_rustls::rustls::ClientConfig; use rumqttc::{Client, Event, MqttOptions, Packet, QoS, Transport}; -use serde::Serialize; -use std::rc::Rc; use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use uuid::Uuid; -use crate::ChannelSender; -use crate::{ - tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}, - ChannelCommon, -}; +use crate::sender::ChannelSender; +use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; const TIMEOUT_SECONDS: u64 = 3; const DEFAULT_USERNAME: &str = "tether"; diff --git a/lib/rust/src/channels/definitions.rs b/lib/rust/src/channels/definitions.rs deleted file mode 100644 index 9fb214c..0000000 --- a/lib/rust/src/channels/definitions.rs +++ /dev/null @@ -1,394 +0,0 @@ -use anyhow::anyhow; -use std::rc::Rc; - -use log::{debug, error, warn}; -use serde::{Deserialize, Serialize}; - -use crate::TetherAgent; - -use super::tether_compliant_topic::TetherOrCustomTopic; - -pub trait ChannelCommon<'a> { - fn name(&'a self) -> &'a str; - /// Return the generated topic string actually used by the Channel - fn generated_topic(&'a self) -> &'a str; - /// Return the custom or Tether-compliant topic - fn topic(&'a self) -> &'a TetherOrCustomTopic; - fn qos(&'a self) -> i32; -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct ChannelReceiver { - name: String, - topic: TetherOrCustomTopic, - qos: i32, -} - -impl ChannelCommon<'_> for ChannelReceiver { - fn name(&self) -> &str { - &self.name - } - - fn generated_topic(&self) -> &str { - match &self.topic { - TetherOrCustomTopic::Custom(s) => { - debug!( - "Channel named \"{}\" has custom topic \"{}\"", - &self.name, &s - ); - s - } - TetherOrCustomTopic::Tether(t) => { - debug!( - "Channel named \"{}\" has Tether-compliant topic \"{:?}\"", - &self.name, t - ); - t.topic() - } - } - } - - fn topic(&'_ self) -> &'_ TetherOrCustomTopic { - &self.topic - } - - fn qos(&self) -> i32 { - self.qos - } -} - -impl ChannelReceiver { - pub fn new(name: &str, topic: TetherOrCustomTopic, qos: Option) -> ChannelReceiver { - ChannelReceiver { - name: String::from(name), - topic, - qos: qos.unwrap_or(1), - } - } - - /// Use the topic of an incoming message to check against the definition of an Channel Receiver. - /// - /// Due to the use of wildcard subscriptions, multiple topic strings might match a given - /// Channel Receiver definition. e.g. `someRole/channelMessages` and `anotherRole/channelMessages` and `someRole/channelMessages/specificID` - /// should ALL match on a Channel Receiver named `channelMessages` unless more specific Role and/or ID - /// parts were specified in the Channel Receiver Definition. - /// - /// In the case where a Channel Receiver was defined with a completely manually-specified topic string, - /// this function returns a warning and marks ANY incoming message as a valid match; the end-user - /// developer is expected to match against topic strings themselves. - pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool { - match incoming_topic { - TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic { - TetherOrCustomTopic::Tether(my_tpt) => { - let matches_role = - my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role()); - let matches_channel_name = my_tpt.channel_name() == "+" - || my_tpt - .channel_name() - .eq(incoming_three_parts.channel_name()); - let matches_id = match my_tpt.id() { - Some(specified_id) => match incoming_three_parts.id() { - Some(incoming_id) => specified_id == incoming_id, - None => false, - }, - None => true, - }; - - debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_channel_name); - matches_role && matches_id && matches_channel_name - } - TetherOrCustomTopic::Custom(my_custom_topic) => { - debug!( - "Custom/manual topic \"{}\" on Channel \"{}\" cannot be matched automatically; please filter manually for this", - &my_custom_topic, - self.name() - ); - my_custom_topic.as_str() == "#" - || my_custom_topic.as_str() == incoming_three_parts.topic() - } - }, - TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic { - TetherOrCustomTopic::Custom(my_custom_topic) => { - if my_custom_topic.as_str() == "#" - || my_custom_topic.as_str() == incoming_custom.as_str() - { - true - } else { - warn!( - "Incoming topic \"{}\" is not a Tether-Compliant topic", - &incoming_custom - ); - false - } - } - TetherOrCustomTopic::Tether(_) => { - error!("Incoming is NOT Tether Compliant Topic but this Channel DOES have Tether Compliant Topic; cannot decide match"); - false - } - }, - } - } -} - -pub struct ChannelSender<'a> { - name: String, - topic: TetherOrCustomTopic, - qos: i32, - retain: bool, - tether_agent: &'a TetherAgent, -} - -impl<'a> ChannelCommon<'a> for ChannelSender<'a> { - fn name(&'_ self) -> &'_ str { - &self.name - } - - fn generated_topic(&self) -> &str { - match &self.topic { - TetherOrCustomTopic::Custom(s) => s, - TetherOrCustomTopic::Tether(t) => t.topic(), - } - } - - fn topic(&'_ self) -> &'_ TetherOrCustomTopic { - &self.topic - } - - fn qos(&'_ self) -> i32 { - self.qos - } -} - -impl<'a> ChannelSender<'a> { - pub fn new( - name: &str, - topic: TetherOrCustomTopic, - qos: Option, - retain: Option, - tether_agent: &'a TetherAgent, - ) -> ChannelSender<'a> { - ChannelSender { - name: String::from(name), - topic, - qos: qos.unwrap_or(1), - retain: retain.unwrap_or(false), - tether_agent, - } - } - - pub fn retain(&self) -> bool { - self.retain - } - - pub fn send_raw(&self, payload: &[u8]) -> anyhow::Result<()> { - if let Some(client) = &self.tether_agent.client { - let res = client - .publish( - self.generated_topic(), - rumqttc::QoS::AtLeastOnce, - false, - payload, - ) - .map_err(anyhow::Error::msg); - res - } else { - Err(anyhow!("no client")) - } - } -} - -// pub enum TetherChannel { -// ChannelReceiver(ChannelReceiver), -// ChannelSender(ChannelSender), -// } - -// impl TetherChannel { -// pub fn name(&self) -> &str { -// match self { -// TetherChannel::ChannelReceiver(p) => p.name(), -// TetherChannel::ChannelSender(p) => p.name(), -// } -// } - -// pub fn generated_topic(&self) -> &str { -// match self { -// TetherChannel::ChannelReceiver(p) => p.generated_topic(), -// TetherChannel::ChannelSender(p) => p.generated_topic(), -// } -// } - -// pub fn matches(&self, topic: &TetherOrCustomTopic) -> bool { -// match self { -// TetherChannel::ChannelReceiver(p) => p.matches(topic), -// TetherChannel::ChannelSender(_) => { -// error!("We don't check matches for Channel Senders"); -// false -// } -// } -// } -// } - -#[cfg(test)] -mod tests { - - use crate::{ - tether_compliant_topic::{parse_channel_name, TetherCompliantTopic, TetherOrCustomTopic}, - ChannelCommon, ChannelReceiver, - }; - - #[test] - fn receiver_match_tpt() { - let channel_def = ChannelReceiver::new( - "testChannel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "testChannel", - None, - None, - )), - None, - ); - - assert_eq!(&channel_def.name, "testChannel"); - assert_eq!(channel_def.generated_topic(), "+/testChannel/#"); - assert_eq!( - parse_channel_name("someRole/testChannel"), - Some("testChannel") - ); - assert_eq!( - parse_channel_name("someRole/testChannel/something"), - Some("testChannel") - ); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("dummy", "testChannel", "#") - ))); - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("dummy", "anotherChannel", "#") - ))); - } - - #[test] - fn receiver_match_tpt_custom_role() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "customChanel", - Some("customRole"), - None, - )), - None, - ); - - assert_eq!(&channel_def.name, "customChanel"); - assert_eq!(channel_def.generated_topic(), "customRole/customChanel/#"); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("customRole", "customChanel", "#") - ))); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("customRole", "customChanel", "andAnythingElse") - ))); - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("customRole", "notMyChannel", "#") - ))); // wrong incoming Channel Name - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("someOtherRole", "customChanel", "#") - ))); // wrong incoming Role - } - - #[test] - fn receiver_match_custom_id() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "customChanel", - None, - Some("specificID"), - )), - None, - ); - - assert_eq!(&channel_def.name, "customChanel"); - assert_eq!(channel_def.generated_topic(), "+/customChanel/specificID"); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anyRole", "customChanel", "specificID",) - ))); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anotherRole", "customChanel", "specificID",) - ))); // wrong incoming Role - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anyRole", "notMyChannel", "specificID",) - ))); // wrong incoming Channel Name - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anyRole", "customChanel", "anotherID",) - ))); // wrong incoming ID - } - - #[test] - fn receiver_match_both() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "customChanel", - Some("specificRole"), - Some("specificID"), - )), - None, - ); - - assert_eq!(&channel_def.name, "customChanel"); - assert_eq!( - channel_def.generated_topic(), - "specificRole/customChanel/specificID" - ); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("specificRole", "customChanel", "specificID",) - ))); - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( - "specificRole/notMyChannel/specificID".into() - ))); // wrong incoming Channel Name - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( - "specificRole/customChanel/anotherID".into() - ))); // wrong incoming ID - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( - "anotherRole/customChanel/anotherID".into() - ))); // wrong incoming Role - } - - #[test] - fn receiver_match_custom_topic() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Custom("one/two/three/four/five".into()), // not a standard Tether Three Part Topic - None, - ); - - assert_eq!(channel_def.name(), "customChanel"); - // it will match on exactly the same topic: - assert!(channel_def.matches(&TetherOrCustomTopic::Custom( - "one/two/three/four/five".into() - ))); - - // it will NOT match on anything else: - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom("one/one/one/one/one".into()))); - } - - #[test] - fn receiver_match_wildcard() { - let channel_def = ChannelReceiver::new( - "everything", - TetherOrCustomTopic::Custom("#".into()), // fully legal, but not a standard Three Part Topic - None, - ); - - assert_eq!(channel_def.name(), "everything"); - - // Standard TPT will match - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("any", "chanelName", "#") - ))); - - // Anything will match, even custom incoming - assert!(channel_def.matches(&TetherOrCustomTopic::Custom( - "one/two/three/four/five".into() - ))); - } -} diff --git a/lib/rust/src/channels/mod.rs b/lib/rust/src/channels/mod.rs index 0c14210..1204da2 100644 --- a/lib/rust/src/channels/mod.rs +++ b/lib/rust/src/channels/mod.rs @@ -1,6 +1,332 @@ -pub mod definitions; pub mod options; +pub mod sender; pub mod tether_compliant_topic; -pub use definitions::*; pub use options::*; + +use anyhow::anyhow; + +use log::{debug, error, warn}; +use serde::{Deserialize, Serialize}; + +use crate::TetherAgent; + +use super::tether_compliant_topic::TetherOrCustomTopic; + +pub trait ChannelCommon<'a> { + fn name(&'a self) -> &'a str; + /// Return the generated topic string actually used by the Channel + fn generated_topic(&'a self) -> &'a str; + /// Return the custom or Tether-compliant topic + fn topic(&'a self) -> &'a TetherOrCustomTopic; + fn qos(&'a self) -> i32; +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct ChannelReceiver { + name: String, + topic: TetherOrCustomTopic, + qos: i32, +} + +impl ChannelCommon<'_> for ChannelReceiver { + fn name(&self) -> &str { + &self.name + } + + fn generated_topic(&self) -> &str { + match &self.topic { + TetherOrCustomTopic::Custom(s) => { + debug!( + "Channel named \"{}\" has custom topic \"{}\"", + &self.name, &s + ); + s + } + TetherOrCustomTopic::Tether(t) => { + debug!( + "Channel named \"{}\" has Tether-compliant topic \"{:?}\"", + &self.name, t + ); + t.topic() + } + } + } + + fn topic(&'_ self) -> &'_ TetherOrCustomTopic { + &self.topic + } + + fn qos(&self) -> i32 { + self.qos + } +} + +impl ChannelReceiver { + pub fn new(name: &str, topic: TetherOrCustomTopic, qos: Option) -> ChannelReceiver { + ChannelReceiver { + name: String::from(name), + topic, + qos: qos.unwrap_or(1), + } + } + + /// Use the topic of an incoming message to check against the definition of an Channel Receiver. + /// + /// Due to the use of wildcard subscriptions, multiple topic strings might match a given + /// Channel Receiver definition. e.g. `someRole/channelMessages` and `anotherRole/channelMessages` and `someRole/channelMessages/specificID` + /// should ALL match on a Channel Receiver named `channelMessages` unless more specific Role and/or ID + /// parts were specified in the Channel Receiver Definition. + /// + /// In the case where a Channel Receiver was defined with a completely manually-specified topic string, + /// this function returns a warning and marks ANY incoming message as a valid match; the end-user + /// developer is expected to match against topic strings themselves. + pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool { + match incoming_topic { + TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic { + TetherOrCustomTopic::Tether(my_tpt) => { + let matches_role = + my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role()); + let matches_channel_name = my_tpt.channel_name() == "+" + || my_tpt + .channel_name() + .eq(incoming_three_parts.channel_name()); + let matches_id = match my_tpt.id() { + Some(specified_id) => match incoming_three_parts.id() { + Some(incoming_id) => specified_id == incoming_id, + None => false, + }, + None => true, + }; + + debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_channel_name); + matches_role && matches_id && matches_channel_name + } + TetherOrCustomTopic::Custom(my_custom_topic) => { + debug!( + "Custom/manual topic \"{}\" on Channel \"{}\" cannot be matched automatically; please filter manually for this", + &my_custom_topic, + self.name() + ); + my_custom_topic.as_str() == "#" + || my_custom_topic.as_str() == incoming_three_parts.topic() + } + }, + TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic { + TetherOrCustomTopic::Custom(my_custom_topic) => { + if my_custom_topic.as_str() == "#" + || my_custom_topic.as_str() == incoming_custom.as_str() + { + true + } else { + warn!( + "Incoming topic \"{}\" is not a Tether-Compliant topic", + &incoming_custom + ); + false + } + } + TetherOrCustomTopic::Tether(_) => { + error!("Incoming is NOT Tether Compliant Topic but this Channel DOES have Tether Compliant Topic; cannot decide match"); + false + } + }, + } + } +} + +// pub enum TetherChannel { +// ChannelReceiver(ChannelReceiver), +// ChannelSender(ChannelSender), +// } + +// impl TetherChannel { +// pub fn name(&self) -> &str { +// match self { +// TetherChannel::ChannelReceiver(p) => p.name(), +// TetherChannel::ChannelSender(p) => p.name(), +// } +// } + +// pub fn generated_topic(&self) -> &str { +// match self { +// TetherChannel::ChannelReceiver(p) => p.generated_topic(), +// TetherChannel::ChannelSender(p) => p.generated_topic(), +// } +// } + +// pub fn matches(&self, topic: &TetherOrCustomTopic) -> bool { +// match self { +// TetherChannel::ChannelReceiver(p) => p.matches(topic), +// TetherChannel::ChannelSender(_) => { +// error!("We don't check matches for Channel Senders"); +// false +// } +// } +// } +// } + +#[cfg(test)] +mod tests { + + use crate::{ + tether_compliant_topic::{parse_channel_name, TetherCompliantTopic, TetherOrCustomTopic}, + ChannelCommon, ChannelReceiver, + }; + + #[test] + fn receiver_match_tpt() { + let channel_def = ChannelReceiver::new( + "testChannel", + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( + "testChannel", + None, + None, + )), + None, + ); + + assert_eq!(&channel_def.name, "testChannel"); + assert_eq!(channel_def.generated_topic(), "+/testChannel/#"); + assert_eq!( + parse_channel_name("someRole/testChannel"), + Some("testChannel") + ); + assert_eq!( + parse_channel_name("someRole/testChannel/something"), + Some("testChannel") + ); + assert!(channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("dummy", "testChannel", "#") + ))); + assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("dummy", "anotherChannel", "#") + ))); + } + + #[test] + fn receiver_match_tpt_custom_role() { + let channel_def = ChannelReceiver::new( + "customChanel", + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( + "customChanel", + Some("customRole"), + None, + )), + None, + ); + + assert_eq!(&channel_def.name, "customChanel"); + assert_eq!(channel_def.generated_topic(), "customRole/customChanel/#"); + assert!(channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("customRole", "customChanel", "#") + ))); + assert!(channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("customRole", "customChanel", "andAnythingElse") + ))); + assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("customRole", "notMyChannel", "#") + ))); // wrong incoming Channel Name + assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("someOtherRole", "customChanel", "#") + ))); // wrong incoming Role + } + + #[test] + fn receiver_match_custom_id() { + let channel_def = ChannelReceiver::new( + "customChanel", + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( + "customChanel", + None, + Some("specificID"), + )), + None, + ); + + assert_eq!(&channel_def.name, "customChanel"); + assert_eq!(channel_def.generated_topic(), "+/customChanel/specificID"); + assert!(channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("anyRole", "customChanel", "specificID",) + ))); + assert!(channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("anotherRole", "customChanel", "specificID",) + ))); // wrong incoming Role + assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("anyRole", "notMyChannel", "specificID",) + ))); // wrong incoming Channel Name + assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("anyRole", "customChanel", "anotherID",) + ))); // wrong incoming ID + } + + #[test] + fn receiver_match_both() { + let channel_def = ChannelReceiver::new( + "customChanel", + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( + "customChanel", + Some("specificRole"), + Some("specificID"), + )), + None, + ); + + assert_eq!(&channel_def.name, "customChanel"); + assert_eq!( + channel_def.generated_topic(), + "specificRole/customChanel/specificID" + ); + assert!(channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("specificRole", "customChanel", "specificID",) + ))); + assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( + "specificRole/notMyChannel/specificID".into() + ))); // wrong incoming Channel Name + assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( + "specificRole/customChanel/anotherID".into() + ))); // wrong incoming ID + assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( + "anotherRole/customChanel/anotherID".into() + ))); // wrong incoming Role + } + + #[test] + fn receiver_match_custom_topic() { + let channel_def = ChannelReceiver::new( + "customChanel", + TetherOrCustomTopic::Custom("one/two/three/four/five".into()), // not a standard Tether Three Part Topic + None, + ); + + assert_eq!(channel_def.name(), "customChanel"); + // it will match on exactly the same topic: + assert!(channel_def.matches(&TetherOrCustomTopic::Custom( + "one/two/three/four/five".into() + ))); + + // it will NOT match on anything else: + assert!(!channel_def.matches(&TetherOrCustomTopic::Custom("one/one/one/one/one".into()))); + } + + #[test] + fn receiver_match_wildcard() { + let channel_def = ChannelReceiver::new( + "everything", + TetherOrCustomTopic::Custom("#".into()), // fully legal, but not a standard Three Part Topic + None, + ); + + assert_eq!(channel_def.name(), "everything"); + + // Standard TPT will match + assert!(channel_def.matches(&TetherOrCustomTopic::Tether( + TetherCompliantTopic::new_three("any", "chanelName", "#") + ))); + + // Anything will match, even custom incoming + assert!(channel_def.matches(&TetherOrCustomTopic::Custom( + "one/two/three/four/five".into() + ))); + } +} diff --git a/lib/rust/src/channels/options.rs b/lib/rust/src/channels/options.rs index e8c9cf7..0b5490e 100644 --- a/lib/rust/src/channels/options.rs +++ b/lib/rust/src/channels/options.rs @@ -1,11 +1,6 @@ -use anyhow::anyhow; use log::{debug, error, info, warn}; -use crate::{ - definitions::ChannelCommon, tether_compliant_topic::TetherCompliantTopic, TetherAgent, -}; - -use super::{tether_compliant_topic::TetherOrCustomTopic, ChannelReceiver, ChannelSender}; +use crate::tether_compliant_topic::TetherCompliantTopic; pub struct ChannelReceiverOptions { channel_name: String, diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs new file mode 100644 index 0000000..25d6cb0 --- /dev/null +++ b/lib/rust/src/channels/sender.rs @@ -0,0 +1,81 @@ +use crate::TetherAgent; +use anyhow::anyhow; +use log::*; +use rmp_serde::to_vec_named; +use serde::Serialize; + +use super::{tether_compliant_topic::TetherOrCustomTopic, ChannelCommon}; + +pub struct ChannelSender<'a> { + name: String, + topic: TetherOrCustomTopic, + qos: i32, + retain: bool, + tether_agent: &'a TetherAgent, +} + +impl<'a> ChannelCommon<'a> for ChannelSender<'a> { + fn name(&'_ self) -> &'_ str { + &self.name + } + + fn generated_topic(&self) -> &str { + match &self.topic { + TetherOrCustomTopic::Custom(s) => s, + TetherOrCustomTopic::Tether(t) => t.topic(), + } + } + + fn topic(&'_ self) -> &'_ TetherOrCustomTopic { + &self.topic + } + + fn qos(&'_ self) -> i32 { + self.qos + } +} + +impl<'a> ChannelSender<'a> { + pub fn new( + name: &str, + topic: TetherOrCustomTopic, + qos: Option, + retain: Option, + tether_agent: &'a TetherAgent, + ) -> ChannelSender<'a> { + ChannelSender { + name: String::from(name), + topic, + qos: qos.unwrap_or(1), + retain: retain.unwrap_or(false), + tether_agent, + } + } + + pub fn retain(&self) -> bool { + self.retain + } + + pub fn send_raw(&self, payload: &[u8]) -> anyhow::Result<()> { + if let Some(client) = &self.tether_agent.client { + let res = client + .publish( + self.generated_topic(), + rumqttc::QoS::AtLeastOnce, + false, + payload, + ) + .map_err(anyhow::Error::msg); + res + } else { + Err(anyhow!("no client")) + } + } + + pub fn send(&self, payload: T) -> anyhow::Result<()> { + match to_vec_named(&payload) { + Ok(data) => self.send_raw(&data), + Err(e) => Err(e.into()), + } + } +} From f349e862e745654a8e47912e744bfe8abd13728c Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 12:55:38 +0200 Subject: [PATCH 05/21] POC strictly typed senders --- examples/rust/send.rs | 4 ++++ lib/rust/src/agent/mod.rs | 3 ++- lib/rust/src/channels/sender.rs | 12 +++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/rust/send.rs b/examples/rust/send.rs index 9bda525..b3a32b6 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -41,5 +41,9 @@ fn main() { sender.send(&another_struct).expect("failed to encode+send"); + let number_sender = tether_agent.create_sender::("numbersOnly"); + + number_sender.send(8).expect("failed to send"); + std::thread::sleep(Duration::from_millis(3000)); } diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index b4e2f5d..b5d1427 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -2,6 +2,7 @@ use anyhow::anyhow; use log::{debug, error, info, trace, warn}; use rumqttc::tokio_rustls::rustls::ClientConfig; use rumqttc::{Client, Event, MqttOptions, Packet, QoS, Transport}; +use serde::Serialize; use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use uuid::Uuid; @@ -164,7 +165,7 @@ impl TetherAgentOptionsBuilder { } impl TetherAgent { - pub fn create_sender(&self, name: &str) -> ChannelSender { + pub fn create_sender(&self, name: &str) -> ChannelSender { ChannelSender::new( name, TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index 25d6cb0..aebb6f5 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -6,15 +6,16 @@ use serde::Serialize; use super::{tether_compliant_topic::TetherOrCustomTopic, ChannelCommon}; -pub struct ChannelSender<'a> { +pub struct ChannelSender<'a, T: Serialize> { name: String, topic: TetherOrCustomTopic, qos: i32, retain: bool, tether_agent: &'a TetherAgent, + marker: std::marker::PhantomData, } -impl<'a> ChannelCommon<'a> for ChannelSender<'a> { +impl<'a, T: Serialize> ChannelCommon<'a> for ChannelSender<'a, T> { fn name(&'_ self) -> &'_ str { &self.name } @@ -35,20 +36,21 @@ impl<'a> ChannelCommon<'a> for ChannelSender<'a> { } } -impl<'a> ChannelSender<'a> { +impl<'a, T: Serialize> ChannelSender<'a, T> { pub fn new( name: &str, topic: TetherOrCustomTopic, qos: Option, retain: Option, tether_agent: &'a TetherAgent, - ) -> ChannelSender<'a> { + ) -> ChannelSender<'a, T> { ChannelSender { name: String::from(name), topic, qos: qos.unwrap_or(1), retain: retain.unwrap_or(false), tether_agent, + marker: std::marker::PhantomData, } } @@ -72,7 +74,7 @@ impl<'a> ChannelSender<'a> { } } - pub fn send(&self, payload: T) -> anyhow::Result<()> { + pub fn send(&self, payload: T) -> anyhow::Result<()> { match to_vec_named(&payload) { Ok(data) => self.send_raw(&data), Err(e) => Err(e.into()), From 97d27a693c7884505b2e4321a150b0bc47e4779a Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 13:31:03 +0200 Subject: [PATCH 06/21] Enable some agent-level sending/publishing as well, for convenience --- lib/rust/src/agent/mod.rs | 123 ++++++++++++++++---------------- lib/rust/src/channels/sender.rs | 1 - 2 files changed, 62 insertions(+), 62 deletions(-) diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index b5d1427..1dfd1e3 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use log::{debug, error, info, trace, warn}; +use rmp_serde::to_vec_named; use rumqttc::tokio_rustls::rustls::ClientConfig; use rumqttc::{Client, Event, MqttOptions, Packet, QoS, Transport}; use serde::Serialize; @@ -9,6 +10,7 @@ use uuid::Uuid; use crate::sender::ChannelSender; use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; +use crate::ChannelCommon; const TIMEOUT_SECONDS: u64 = 3; const DEFAULT_USERNAME: &str = "tether"; @@ -394,68 +396,67 @@ impl TetherAgent { } } - // /// Unlike .send, this function does NOT serialize the data before publishing. - // /// - // /// Given a channel definition and a raw (u8 buffer) payload, publishes a message - // /// using an appropriate topic and with the QOS specified in the Channel Definition - // pub fn send_raw( - // &self, - // channel_definition: &TetherChannel, - // payload: Option<&[u8]>, - // ) -> anyhow::Result<()> { - // match channel_definition { - // TetherChannel::ChannelReceiver(_) => { - // panic!("You cannot publish using a Channel Receiver") - // } - // TetherChannel::ChannelSender(channel_sender_definition) => { - // let topic = channel_sender_definition.generated_topic(); - // let qos = match channel_sender_definition.qos() { - // 0 => QoS::AtMostOnce, - // 1 => QoS::AtLeastOnce, - // 2 => QoS::ExactlyOnce, - // _ => QoS::AtMostOnce, - // }; - - // if let Some(client) = &self.client { - // let res = client - // .publish( - // topic, - // qos, - // channel_sender_definition.retain(), - // payload.unwrap_or_default(), - // ) - // .map_err(anyhow::Error::msg); - // debug!("Published OK"); - // res - // } else { - // Err(anyhow!("Client not ready for publish")) - // } - // } - // } - // } - - // /// Serializes the data automatically before publishing. - // /// - // /// Given a channel definition and serializeable data payload, publishes a message - // /// using an appropriate topic and with the QOS specified in the Channel Definition - // pub fn send( - // &self, - // channel_definition: &TetherChannel, - // data: T, - // ) -> anyhow::Result<()> { - // match to_vec_named(&data) { - // Ok(payload) => self.send_raw(channel_definition, Some(&payload)), - // Err(e) => { - // error!("Failed to encode: {e:?}"); - // Err(e.into()) - // } - // } - // } - - // pub fn send_empty(&self, channel_definition: &TetherChannel) -> anyhow::Result<()> { - // self.send_raw(channel_definition, None) - // } + /// Unlike .send, this function does NOT serialize the data before publishing. + /// + /// Given a channel definition and a raw (u8 buffer) payload, publishes a message + /// using an appropriate topic and with the QOS specified in the Channel Definition + pub fn send_raw( + &self, + channel_definition: &ChannelSender, + payload: Option<&[u8]>, + ) -> anyhow::Result<()> { + let topic = channel_definition.generated_topic(); + let qos = match channel_definition.qos() { + 0 => QoS::AtMostOnce, + 1 => QoS::AtLeastOnce, + 2 => QoS::ExactlyOnce, + _ => QoS::AtMostOnce, + }; + + if let Some(client) = &self.client { + let res = client + .publish( + topic, + qos, + channel_definition.retain(), + payload.unwrap_or_default(), + ) + .map_err(anyhow::Error::msg); + debug!("Published OK"); + res + } else { + Err(anyhow!("Client not ready for publish")) + } + } + + /// Serializes the data automatically before publishing. + /// + /// Given a channel definition and serializeable data payload, publishes a message + /// using an appropriate topic and with the QOS specified in the Channel Definition + pub fn send( + &self, + channel_definition: &ChannelSender, + data: T, + ) -> anyhow::Result<()> { + match to_vec_named(&data) { + Ok(payload) => self.send_raw(channel_definition, Some(&payload)), + Err(e) => { + error!("Failed to encode: {e:?}"); + Err(e.into()) + } + } + } + + pub fn send_empty( + &self, + channel_definition: &ChannelSender, + ) -> anyhow::Result<()> { + self.send_raw(channel_definition, None) + } + /// Publish an already-encoded payload using a + /// full topic string - no need for passing a ChannelSender + /// reference pub fn publish_raw( &self, topic: &str, diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index aebb6f5..0b7a75e 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -1,6 +1,5 @@ use crate::TetherAgent; use anyhow::anyhow; -use log::*; use rmp_serde::to_vec_named; use serde::Serialize; From 35080254519f8a916b90ee8b919224cac1a36937 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 14:10:46 +0200 Subject: [PATCH 07/21] POC: typed receiver that can auto-subscribe; but not emit messages (yet) --- examples/rust/receive.rs | 116 +------- lib/rust/src/agent/mod.rs | 17 +- lib/rust/src/channels/mod.rs | 439 +++++++++++------------------- lib/rust/src/channels/receiver.rs | 159 +++++++++++ lib/rust/src/channels/sender.rs | 2 +- 5 files changed, 339 insertions(+), 394 deletions(-) create mode 100644 lib/rust/src/channels/receiver.rs diff --git a/examples/rust/receive.rs b/examples/rust/receive.rs index 0286555..665f9ed 100644 --- a/examples/rust/receive.rs +++ b/examples/rust/receive.rs @@ -24,120 +24,12 @@ fn main() { debug!("Debugging is enabled; could be verbose"); - let mut tether_agent = TetherAgentOptionsBuilder::new("RustDemo") + let tether_agent = TetherAgentOptionsBuilder::new("RustDemo") .id(Some("example")) .build() .expect("failed to init Tether agent"); - let input_one = ChannelOptionsBuilder::create_receiver("one") - .build(&mut tether_agent) - .expect("failed to create input"); - info!( - "input one {} = {}", - input_one.name(), - input_one.generated_topic() - ); - let input_two = ChannelOptionsBuilder::create_receiver("two") - .role(Some("specific")) - .build(&mut tether_agent) - .expect("failed to create input"); - info!( - "input two {} = {}", - input_two.name(), - input_two.generated_topic() - ); - let input_empty = ChannelOptionsBuilder::create_receiver("nothing") - .build(&mut tether_agent) - .expect("failed to create input"); - - let input_everything = ChannelOptionsBuilder::create_receiver("everything") - .topic(Some("#")) - .build(&mut tether_agent) - .expect("failed to create input"); - - let input_specify_id = ChannelOptionsBuilder::create_receiver("groupMessages") - .id(Some("someGroup")) - .name(None) - .build(&mut tether_agent) - .expect("failed to create input"); - - debug!( - "input everything {} = {}", - input_everything.name(), - input_everything.generated_topic() - ); - - info!("Checking messages every 1s, 10x..."); - - loop { - debug!("Checking for messages..."); - while let Some((topic, payload)) = tether_agent.check_messages() { - // debug!( - // "........ Received a message topic {:?} => topic parts {:?}", - // topic, topic - // ); - - if input_one.matches(&topic) { - info!( - "******** INPUT ONE:\n Received a message for plug named \"{}\" on topic {:?} with length {} bytes", - input_one.name(), - topic, - payload.len() - ); - // assert_eq!(parse_plug_name(topic.un), Some("one")); - } - if input_two.matches(&topic) { - info!( - "******** INPUT TWO:\n Received a message for plug named \"{}\" on topic {:?} with length {} bytes", - input_two.name(), - topic, - payload.len() - ); - // assert_eq!(parse_plug_name(message.topic()), Some("two")); - // assert_ne!(parse_plug_name(message.topic()), Some("one")); - - // Notice how you must give the from_slice function a type so it knows what to expect - let decoded = from_slice::(&payload); - match decoded { - Ok(d) => { - info!("Yes, we decoded the MessagePack payload as: {:?}", d); - let CustomMessage { name, id } = d; - debug!("Name is {} and ID is {}", name, id); - } - Err(e) => { - warn!("Failed to decode the payload: {}", e) - } - }; - } - if input_empty.matches(&topic) { - info!( - "******** EMPTY MESSAGE:\n Received a message for plug named \"{}\" on topic {:?} with length {} bytes", - input_empty.name(), - topic, - payload.len() - ); - // assert_eq!(parse_plug_name(topic), Some("nothing")); - } - if input_everything.matches(&topic) { - info!( - "******** EVERYTHING MATCHES HERE:\n Received a message for plug named \"{}\" on topic {:?} with length {} bytes", - input_everything.name(), - topic, - payload.len() - ); - } - if input_specify_id.matches(&topic) { - info!("******** ID MATCH:\n Should match any role and plug name, but only messages with ID \"groupMessages\""); - info!( - "\n Received a message from plug named \"{}\" on topic {:?} with length {} bytes", - input_specify_id.name(), - topic, - payload.len() - ); - // assert_eq!(parse_agent_id(message.topic()), Some("groupMessages")); - } - } - - thread::sleep(Duration::from_millis(1000)) - } + let receiver = tether_agent + .create_receiver::("numbersOnly") + .expect("failed to create receiver"); } diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index 1dfd1e3..5db9a2d 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -3,11 +3,12 @@ use log::{debug, error, info, trace, warn}; use rmp_serde::to_vec_named; use rumqttc::tokio_rustls::rustls::ClientConfig; use rumqttc::{Client, Event, MqttOptions, Packet, QoS, Transport}; -use serde::Serialize; +use serde::{Deserialize, Serialize}; use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use uuid::Uuid; +use crate::receiver::ChannelReceiver; use crate::sender::ChannelSender; use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; use crate::ChannelCommon; @@ -166,16 +167,28 @@ impl TetherAgentOptionsBuilder { } } -impl TetherAgent { +impl<'a> TetherAgent { pub fn create_sender(&self, name: &str) -> ChannelSender { ChannelSender::new( + self, name, TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( self, name, None, None, )), None, None, + ) + } + + pub fn create_receiver>( + &'a self, + name: &str, + ) -> anyhow::Result> { + ChannelReceiver::new( self, + name, + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe(name, None, None)), + None, ) } diff --git a/lib/rust/src/channels/mod.rs b/lib/rust/src/channels/mod.rs index 1204da2..6841f52 100644 --- a/lib/rust/src/channels/mod.rs +++ b/lib/rust/src/channels/mod.rs @@ -1,16 +1,10 @@ pub mod options; +pub mod receiver; pub mod sender; pub mod tether_compliant_topic; pub use options::*; -use anyhow::anyhow; - -use log::{debug, error, warn}; -use serde::{Deserialize, Serialize}; - -use crate::TetherAgent; - use super::tether_compliant_topic::TetherOrCustomTopic; pub trait ChannelCommon<'a> { @@ -22,119 +16,6 @@ pub trait ChannelCommon<'a> { fn qos(&'a self) -> i32; } -#[derive(Serialize, Deserialize, Debug)] -pub struct ChannelReceiver { - name: String, - topic: TetherOrCustomTopic, - qos: i32, -} - -impl ChannelCommon<'_> for ChannelReceiver { - fn name(&self) -> &str { - &self.name - } - - fn generated_topic(&self) -> &str { - match &self.topic { - TetherOrCustomTopic::Custom(s) => { - debug!( - "Channel named \"{}\" has custom topic \"{}\"", - &self.name, &s - ); - s - } - TetherOrCustomTopic::Tether(t) => { - debug!( - "Channel named \"{}\" has Tether-compliant topic \"{:?}\"", - &self.name, t - ); - t.topic() - } - } - } - - fn topic(&'_ self) -> &'_ TetherOrCustomTopic { - &self.topic - } - - fn qos(&self) -> i32 { - self.qos - } -} - -impl ChannelReceiver { - pub fn new(name: &str, topic: TetherOrCustomTopic, qos: Option) -> ChannelReceiver { - ChannelReceiver { - name: String::from(name), - topic, - qos: qos.unwrap_or(1), - } - } - - /// Use the topic of an incoming message to check against the definition of an Channel Receiver. - /// - /// Due to the use of wildcard subscriptions, multiple topic strings might match a given - /// Channel Receiver definition. e.g. `someRole/channelMessages` and `anotherRole/channelMessages` and `someRole/channelMessages/specificID` - /// should ALL match on a Channel Receiver named `channelMessages` unless more specific Role and/or ID - /// parts were specified in the Channel Receiver Definition. - /// - /// In the case where a Channel Receiver was defined with a completely manually-specified topic string, - /// this function returns a warning and marks ANY incoming message as a valid match; the end-user - /// developer is expected to match against topic strings themselves. - pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool { - match incoming_topic { - TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic { - TetherOrCustomTopic::Tether(my_tpt) => { - let matches_role = - my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role()); - let matches_channel_name = my_tpt.channel_name() == "+" - || my_tpt - .channel_name() - .eq(incoming_three_parts.channel_name()); - let matches_id = match my_tpt.id() { - Some(specified_id) => match incoming_three_parts.id() { - Some(incoming_id) => specified_id == incoming_id, - None => false, - }, - None => true, - }; - - debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_channel_name); - matches_role && matches_id && matches_channel_name - } - TetherOrCustomTopic::Custom(my_custom_topic) => { - debug!( - "Custom/manual topic \"{}\" on Channel \"{}\" cannot be matched automatically; please filter manually for this", - &my_custom_topic, - self.name() - ); - my_custom_topic.as_str() == "#" - || my_custom_topic.as_str() == incoming_three_parts.topic() - } - }, - TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic { - TetherOrCustomTopic::Custom(my_custom_topic) => { - if my_custom_topic.as_str() == "#" - || my_custom_topic.as_str() == incoming_custom.as_str() - { - true - } else { - warn!( - "Incoming topic \"{}\" is not a Tether-Compliant topic", - &incoming_custom - ); - false - } - } - TetherOrCustomTopic::Tether(_) => { - error!("Incoming is NOT Tether Compliant Topic but this Channel DOES have Tether Compliant Topic; cannot decide match"); - false - } - }, - } - } -} - // pub enum TetherChannel { // ChannelReceiver(ChannelReceiver), // ChannelSender(ChannelSender), @@ -166,167 +47,167 @@ impl ChannelReceiver { // } // } -#[cfg(test)] -mod tests { - - use crate::{ - tether_compliant_topic::{parse_channel_name, TetherCompliantTopic, TetherOrCustomTopic}, - ChannelCommon, ChannelReceiver, - }; - - #[test] - fn receiver_match_tpt() { - let channel_def = ChannelReceiver::new( - "testChannel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "testChannel", - None, - None, - )), - None, - ); - - assert_eq!(&channel_def.name, "testChannel"); - assert_eq!(channel_def.generated_topic(), "+/testChannel/#"); - assert_eq!( - parse_channel_name("someRole/testChannel"), - Some("testChannel") - ); - assert_eq!( - parse_channel_name("someRole/testChannel/something"), - Some("testChannel") - ); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("dummy", "testChannel", "#") - ))); - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("dummy", "anotherChannel", "#") - ))); - } - - #[test] - fn receiver_match_tpt_custom_role() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "customChanel", - Some("customRole"), - None, - )), - None, - ); - - assert_eq!(&channel_def.name, "customChanel"); - assert_eq!(channel_def.generated_topic(), "customRole/customChanel/#"); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("customRole", "customChanel", "#") - ))); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("customRole", "customChanel", "andAnythingElse") - ))); - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("customRole", "notMyChannel", "#") - ))); // wrong incoming Channel Name - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("someOtherRole", "customChanel", "#") - ))); // wrong incoming Role - } - - #[test] - fn receiver_match_custom_id() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "customChanel", - None, - Some("specificID"), - )), - None, - ); - - assert_eq!(&channel_def.name, "customChanel"); - assert_eq!(channel_def.generated_topic(), "+/customChanel/specificID"); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anyRole", "customChanel", "specificID",) - ))); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anotherRole", "customChanel", "specificID",) - ))); // wrong incoming Role - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anyRole", "notMyChannel", "specificID",) - ))); // wrong incoming Channel Name - assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("anyRole", "customChanel", "anotherID",) - ))); // wrong incoming ID - } - - #[test] - fn receiver_match_both() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - "customChanel", - Some("specificRole"), - Some("specificID"), - )), - None, - ); - - assert_eq!(&channel_def.name, "customChanel"); - assert_eq!( - channel_def.generated_topic(), - "specificRole/customChanel/specificID" - ); - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("specificRole", "customChanel", "specificID",) - ))); - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( - "specificRole/notMyChannel/specificID".into() - ))); // wrong incoming Channel Name - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( - "specificRole/customChanel/anotherID".into() - ))); // wrong incoming ID - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( - "anotherRole/customChanel/anotherID".into() - ))); // wrong incoming Role - } - - #[test] - fn receiver_match_custom_topic() { - let channel_def = ChannelReceiver::new( - "customChanel", - TetherOrCustomTopic::Custom("one/two/three/four/five".into()), // not a standard Tether Three Part Topic - None, - ); - - assert_eq!(channel_def.name(), "customChanel"); - // it will match on exactly the same topic: - assert!(channel_def.matches(&TetherOrCustomTopic::Custom( - "one/two/three/four/five".into() - ))); +// #[cfg(test)] +// mod tests { + +// use crate::{ +// tether_compliant_topic::{parse_channel_name, TetherCompliantTopic, TetherOrCustomTopic}, +// ChannelCommon, ChannelReceiver, +// }; + +// #[test] +// fn receiver_match_tpt() { +// let channel_def = ChannelReceiver::new( +// "testChannel", +// TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( +// "testChannel", +// None, +// None, +// )), +// None, +// ); + +// assert_eq!(&channel_def.name, "testChannel"); +// assert_eq!(channel_def.generated_topic(), "+/testChannel/#"); +// assert_eq!( +// parse_channel_name("someRole/testChannel"), +// Some("testChannel") +// ); +// assert_eq!( +// parse_channel_name("someRole/testChannel/something"), +// Some("testChannel") +// ); +// assert!(channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("dummy", "testChannel", "#") +// ))); +// assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("dummy", "anotherChannel", "#") +// ))); +// } - // it will NOT match on anything else: - assert!(!channel_def.matches(&TetherOrCustomTopic::Custom("one/one/one/one/one".into()))); - } +// #[test] +// fn receiver_match_tpt_custom_role() { +// let channel_def = ChannelReceiver::new( +// "customChanel", +// TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( +// "customChanel", +// Some("customRole"), +// None, +// )), +// None, +// ); + +// assert_eq!(&channel_def.name, "customChanel"); +// assert_eq!(channel_def.generated_topic(), "customRole/customChanel/#"); +// assert!(channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("customRole", "customChanel", "#") +// ))); +// assert!(channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("customRole", "customChanel", "andAnythingElse") +// ))); +// assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("customRole", "notMyChannel", "#") +// ))); // wrong incoming Channel Name +// assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("someOtherRole", "customChanel", "#") +// ))); // wrong incoming Role +// } - #[test] - fn receiver_match_wildcard() { - let channel_def = ChannelReceiver::new( - "everything", - TetherOrCustomTopic::Custom("#".into()), // fully legal, but not a standard Three Part Topic - None, - ); +// #[test] +// fn receiver_match_custom_id() { +// let channel_def = ChannelReceiver::new( +// "customChanel", +// TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( +// "customChanel", +// None, +// Some("specificID"), +// )), +// None, +// ); + +// assert_eq!(&channel_def.name, "customChanel"); +// assert_eq!(channel_def.generated_topic(), "+/customChanel/specificID"); +// assert!(channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("anyRole", "customChanel", "specificID",) +// ))); +// assert!(channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("anotherRole", "customChanel", "specificID",) +// ))); // wrong incoming Role +// assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("anyRole", "notMyChannel", "specificID",) +// ))); // wrong incoming Channel Name +// assert!(!channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("anyRole", "customChanel", "anotherID",) +// ))); // wrong incoming ID +// } - assert_eq!(channel_def.name(), "everything"); +// #[test] +// fn receiver_match_both() { +// let channel_def = ChannelReceiver::new( +// "customChanel", +// TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( +// "customChanel", +// Some("specificRole"), +// Some("specificID"), +// )), +// None, +// ); + +// assert_eq!(&channel_def.name, "customChanel"); +// assert_eq!( +// channel_def.generated_topic(), +// "specificRole/customChanel/specificID" +// ); +// assert!(channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("specificRole", "customChanel", "specificID",) +// ))); +// assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( +// "specificRole/notMyChannel/specificID".into() +// ))); // wrong incoming Channel Name +// assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( +// "specificRole/customChanel/anotherID".into() +// ))); // wrong incoming ID +// assert!(!channel_def.matches(&TetherOrCustomTopic::Custom( +// "anotherRole/customChanel/anotherID".into() +// ))); // wrong incoming Role +// } - // Standard TPT will match - assert!(channel_def.matches(&TetherOrCustomTopic::Tether( - TetherCompliantTopic::new_three("any", "chanelName", "#") - ))); +// #[test] +// fn receiver_match_custom_topic() { +// let channel_def = ChannelReceiver::new( +// "customChanel", +// TetherOrCustomTopic::Custom("one/two/three/four/five".into()), // not a standard Tether Three Part Topic +// None, +// ); + +// assert_eq!(channel_def.name(), "customChanel"); +// // it will match on exactly the same topic: +// assert!(channel_def.matches(&TetherOrCustomTopic::Custom( +// "one/two/three/four/five".into() +// ))); + +// // it will NOT match on anything else: +// assert!(!channel_def.matches(&TetherOrCustomTopic::Custom("one/one/one/one/one".into()))); +// } - // Anything will match, even custom incoming - assert!(channel_def.matches(&TetherOrCustomTopic::Custom( - "one/two/three/four/five".into() - ))); - } -} +// #[test] +// fn receiver_match_wildcard() { +// let channel_def = ChannelReceiver::new( +// "everything", +// TetherOrCustomTopic::Custom("#".into()), // fully legal, but not a standard Three Part Topic +// None, +// ); + +// assert_eq!(channel_def.name(), "everything"); + +// // Standard TPT will match +// assert!(channel_def.matches(&TetherOrCustomTopic::Tether( +// TetherCompliantTopic::new_three("any", "chanelName", "#") +// ))); + +// // Anything will match, even custom incoming +// assert!(channel_def.matches(&TetherOrCustomTopic::Custom( +// "one/two/three/four/five".into() +// ))); +// } +// } diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs new file mode 100644 index 0000000..2c71784 --- /dev/null +++ b/lib/rust/src/channels/receiver.rs @@ -0,0 +1,159 @@ +use anyhow::anyhow; +use log::*; +use serde::Deserialize; + +use crate::TetherAgent; + +use super::{tether_compliant_topic::TetherOrCustomTopic, ChannelCommon}; + +pub struct ChannelReceiver<'a, T: Deserialize<'a>> { + name: String, + topic: TetherOrCustomTopic, + qos: i32, + tether_agent: &'a TetherAgent, + marker: std::marker::PhantomData, +} + +impl<'a, T: Deserialize<'a>> ChannelCommon<'a> for ChannelReceiver<'a, T> { + fn name(&self) -> &str { + &self.name + } + + fn generated_topic(&self) -> &str { + match &self.topic { + TetherOrCustomTopic::Custom(s) => { + debug!( + "Channel named \"{}\" has custom topic \"{}\"", + &self.name, &s + ); + s + } + TetherOrCustomTopic::Tether(t) => { + debug!( + "Channel named \"{}\" has Tether-compliant topic \"{:?}\"", + &self.name, t + ); + t.topic() + } + } + } + + fn topic(&'_ self) -> &'_ TetherOrCustomTopic { + &self.topic + } + + fn qos(&self) -> i32 { + self.qos + } +} + +impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { + pub fn new( + tether_agent: &'a TetherAgent, + name: &str, + topic: TetherOrCustomTopic, + qos: Option, + ) -> anyhow::Result> { + let topic_string = topic.full_topic_string(); + + let channel = ChannelReceiver { + name: String::from(name), + topic, + qos: qos.unwrap_or(1), + tether_agent, + marker: std::marker::PhantomData, + }; + + // This is really only useful for testing purposes. + if !tether_agent.auto_connect_enabled() { + warn!("Auto-connect is disabled, skipping subscription"); + return Ok(channel); + } + + if let Some(client) = &tether_agent.client { + match client.subscribe(&topic_string, { + match qos { + Some(0) => rumqttc::QoS::AtMostOnce, + Some(1) => rumqttc::QoS::AtLeastOnce, + Some(2) => rumqttc::QoS::ExactlyOnce, + _ => rumqttc::QoS::AtLeastOnce, + } + }) { + Ok(res) => { + debug!("This topic was fine: \"{}\"", &topic_string); + debug!("Server respond OK for subscribe: {res:?}"); + Ok(channel) + } + Err(_e) => Err(anyhow!("ClientError")), + } + } else { + Err(anyhow!("Client not available for subscription")) + } + } + + // /// Use the topic of an incoming message to check against the definition of an Channel Receiver. + // /// + // /// Due to the use of wildcard subscriptions, multiple topic strings might match a given + // /// Channel Receiver definition. e.g. `someRole/channelMessages` and `anotherRole/channelMessages` and `someRole/channelMessages/specificID` + // /// should ALL match on a Channel Receiver named `channelMessages` unless more specific Role and/or ID + // /// parts were specified in the Channel Receiver Definition. + // /// + // /// In the case where a Channel Receiver was defined with a completely manually-specified topic string, + // /// this function returns a warning and marks ANY incoming message as a valid match; the end-user + // /// developer is expected to match against topic strings themselves. + // pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool { + // match incoming_topic { + // TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic { + // TetherOrCustomTopic::Tether(my_tpt) => { + // let matches_role = + // my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role()); + // let matches_channel_name = my_tpt.channel_name() == "+" + // || my_tpt + // .channel_name() + // .eq(incoming_three_parts.channel_name()); + // let matches_id = match my_tpt.id() { + // Some(specified_id) => match incoming_three_parts.id() { + // Some(incoming_id) => specified_id == incoming_id, + // None => false, + // }, + // None => true, + // }; + + // debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_channel_name); + // matches_role && matches_id && matches_channel_name + // } + // TetherOrCustomTopic::Custom(my_custom_topic) => { + // debug!( + // "Custom/manual topic \"{}\" on Channel \"{}\" cannot be matched automatically; please filter manually for this", + // &my_custom_topic, + // self.name() + // ); + // my_custom_topic.as_str() == "#" + // || my_custom_topic.as_str() == incoming_three_parts.topic() + // } + // }, + // TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic { + // TetherOrCustomTopic::Custom(my_custom_topic) => { + // if my_custom_topic.as_str() == "#" + // || my_custom_topic.as_str() == incoming_custom.as_str() + // { + // true + // } else { + // warn!( + // "Incoming topic \"{}\" is not a Tether-Compliant topic", + // &incoming_custom + // ); + // false + // } + // } + // TetherOrCustomTopic::Tether(_) => { + // error!("Incoming is NOT Tether Compliant Topic but this Channel DOES have Tether Compliant Topic; cannot decide match"); + // false + // } + // }, + // } + // } + // + // + // +} diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index 0b7a75e..6e610bc 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -37,11 +37,11 @@ impl<'a, T: Serialize> ChannelCommon<'a> for ChannelSender<'a, T> { impl<'a, T: Serialize> ChannelSender<'a, T> { pub fn new( + tether_agent: &'a TetherAgent, name: &str, topic: TetherOrCustomTopic, qos: Option, retain: Option, - tether_agent: &'a TetherAgent, ) -> ChannelSender<'a, T> { ChannelSender { name: String::from(name), From 96fc7d74dd5aaeb8dff0a376ca9d932346ccfd1c Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 14:41:51 +0200 Subject: [PATCH 08/21] POC: the ChannelReceiver checks and parses messages, "emitting" any matching results in correct type --- examples/rust/receive.rs | 24 +++++- lib/rust/src/channels/receiver.rs | 139 ++++++++++++++++-------------- 2 files changed, 98 insertions(+), 65 deletions(-) diff --git a/examples/rust/receive.rs b/examples/rust/receive.rs index 665f9ed..5e1bde4 100644 --- a/examples/rust/receive.rs +++ b/examples/rust/receive.rs @@ -17,7 +17,7 @@ struct CustomMessage { fn main() { println!("Rust Tether Agent subscribe example"); - let mut builder = Builder::from_env(Env::default().default_filter_or("debug")); + let mut builder = Builder::from_env(Env::default().default_filter_or("info")); builder.filter_module("tether_agent", log::LevelFilter::Warn); builder.filter_module("rumqttc", log::LevelFilter::Warn); builder.init(); @@ -32,4 +32,26 @@ fn main() { let receiver = tether_agent .create_receiver::("numbersOnly") .expect("failed to create receiver"); + + loop { + debug!("Checking for messages..."); + while let Some((topic, payload)) = tether_agent.check_messages() { + if let Some(decoded_message) = receiver.parse(&topic, &payload) { + info!("Decoded a message for our Channel: {:?}", decoded_message); + } + // if receiver.matches(&topic) { + // let decoded = from_slice::(&payload); + // match decoded { + // Ok(d) => { + // info!("Yes, we decoded the MessagePack payload as: {:?}", d); + // let CustomMessage { name, id } = d; + // debug!("Name is {} and ID is {}", name, id); + // } + // Err(e) => { + // warn!("Failed to decode the payload: {}", e) + // } + // }; + // } + } + } } diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index 2c71784..24f5e28 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -1,5 +1,6 @@ use anyhow::anyhow; use log::*; +use rmp_serde::from_slice; use serde::Deserialize; use crate::TetherAgent; @@ -91,69 +92,79 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { } } - // /// Use the topic of an incoming message to check against the definition of an Channel Receiver. - // /// - // /// Due to the use of wildcard subscriptions, multiple topic strings might match a given - // /// Channel Receiver definition. e.g. `someRole/channelMessages` and `anotherRole/channelMessages` and `someRole/channelMessages/specificID` - // /// should ALL match on a Channel Receiver named `channelMessages` unless more specific Role and/or ID - // /// parts were specified in the Channel Receiver Definition. - // /// - // /// In the case where a Channel Receiver was defined with a completely manually-specified topic string, - // /// this function returns a warning and marks ANY incoming message as a valid match; the end-user - // /// developer is expected to match against topic strings themselves. - // pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool { - // match incoming_topic { - // TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic { - // TetherOrCustomTopic::Tether(my_tpt) => { - // let matches_role = - // my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role()); - // let matches_channel_name = my_tpt.channel_name() == "+" - // || my_tpt - // .channel_name() - // .eq(incoming_three_parts.channel_name()); - // let matches_id = match my_tpt.id() { - // Some(specified_id) => match incoming_three_parts.id() { - // Some(incoming_id) => specified_id == incoming_id, - // None => false, - // }, - // None => true, - // }; + /// Use the topic of an incoming message to check against the definition of an Channel Receiver. + /// + /// Due to the use of wildcard subscriptions, multiple topic strings might match a given + /// Channel Receiver definition. e.g. `someRole/channelMessages` and `anotherRole/channelMessages` and `someRole/channelMessages/specificID` + /// should ALL match on a Channel Receiver named `channelMessages` unless more specific Role and/or ID + /// parts were specified in the Channel Receiver Definition. + /// + /// In the case where a Channel Receiver was defined with a completely manually-specified topic string, + /// this function returns a warning and marks ANY incoming message as a valid match; the end-user + /// developer is expected to match against topic strings themselves. + pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool { + match incoming_topic { + TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic { + TetherOrCustomTopic::Tether(my_tpt) => { + let matches_role = + my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role()); + let matches_channel_name = my_tpt.channel_name() == "+" + || my_tpt + .channel_name() + .eq(incoming_three_parts.channel_name()); + let matches_id = match my_tpt.id() { + Some(specified_id) => match incoming_three_parts.id() { + Some(incoming_id) => specified_id == incoming_id, + None => false, + }, + None => true, + }; - // debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_channel_name); - // matches_role && matches_id && matches_channel_name - // } - // TetherOrCustomTopic::Custom(my_custom_topic) => { - // debug!( - // "Custom/manual topic \"{}\" on Channel \"{}\" cannot be matched automatically; please filter manually for this", - // &my_custom_topic, - // self.name() - // ); - // my_custom_topic.as_str() == "#" - // || my_custom_topic.as_str() == incoming_three_parts.topic() - // } - // }, - // TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic { - // TetherOrCustomTopic::Custom(my_custom_topic) => { - // if my_custom_topic.as_str() == "#" - // || my_custom_topic.as_str() == incoming_custom.as_str() - // { - // true - // } else { - // warn!( - // "Incoming topic \"{}\" is not a Tether-Compliant topic", - // &incoming_custom - // ); - // false - // } - // } - // TetherOrCustomTopic::Tether(_) => { - // error!("Incoming is NOT Tether Compliant Topic but this Channel DOES have Tether Compliant Topic; cannot decide match"); - // false - // } - // }, - // } - // } - // - // - // + debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_channel_name); + matches_role && matches_id && matches_channel_name + } + TetherOrCustomTopic::Custom(my_custom_topic) => { + debug!( + "Custom/manual topic \"{}\" cannot be matched automatically; please filter manually for this", + &my_custom_topic, + ); + my_custom_topic.as_str() == "#" + || my_custom_topic.as_str() == incoming_three_parts.topic() + } + }, + TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic { + TetherOrCustomTopic::Custom(my_custom_topic) => { + if my_custom_topic.as_str() == "#" + || my_custom_topic.as_str() == incoming_custom.as_str() + { + true + } else { + warn!( + "Incoming topic \"{}\" is not a Tether-Compliant topic", + &incoming_custom + ); + false + } + } + TetherOrCustomTopic::Tether(_) => { + error!("Incoming is NOT Tether Compliant Topic but this Channel DOES have Tether Compliant Topic; cannot decide match"); + false + } + }, + } + } + + pub fn parse(&self, incoming_topic: &TetherOrCustomTopic, payload: &'a [u8]) -> Option { + if self.matches(incoming_topic) { + match from_slice::(payload) { + Ok(msg) => Some(msg), + Err(e) => { + error!("Failed to parse message: {}", e); + None + } + } + } else { + None + } + } } From eae96283ef294aa8591d20d0f3d4612afa568fa6 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 14:56:06 +0200 Subject: [PATCH 09/21] demonstrate multiple matches --- examples/rust/receive.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/examples/rust/receive.rs b/examples/rust/receive.rs index 5e1bde4..98734e8 100644 --- a/examples/rust/receive.rs +++ b/examples/rust/receive.rs @@ -29,15 +29,25 @@ fn main() { .build() .expect("failed to init Tether agent"); - let receiver = tether_agent + let receiver_of_numbers = tether_agent .create_receiver::("numbersOnly") .expect("failed to create receiver"); + let receiver_of_custom_structs = tether_agent + .create_receiver::("values") + .expect("failed to create receiver"); + loop { debug!("Checking for messages..."); while let Some((topic, payload)) = tether_agent.check_messages() { - if let Some(decoded_message) = receiver.parse(&topic, &payload) { - info!("Decoded a message for our Channel: {:?}", decoded_message); + if let Some(m) = receiver_of_numbers.parse(&topic, &payload) { + info!("Decoded a message for our 'numbers' Channel: {:?}", m); + } + if let Some(m) = receiver_of_custom_structs.parse(&topic, &payload) { + info!( + "Decoded a message for our 'custom structs' Channel: {:?}", + m + ); } // if receiver.matches(&topic) { // let decoded = from_slice::(&payload); From 04dd158e7be53f6e6dd95e4b842a3c0e71e44132 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Fri, 11 Apr 2025 18:28:22 +0200 Subject: [PATCH 10/21] WIP must make OptionsBuilder work again --- examples/rust/custom_options.rs | 6 +- examples/rust/receive.rs | 21 +--- lib/rust/src/channels/options.rs | 187 +++++++++++++++---------------- 3 files changed, 98 insertions(+), 116 deletions(-) diff --git a/examples/rust/custom_options.rs b/examples/rust/custom_options.rs index fe2f54a..20906ce 100644 --- a/examples/rust/custom_options.rs +++ b/examples/rust/custom_options.rs @@ -1,8 +1,6 @@ use std::time::Duration; -use tether_agent::{ - ChannelDefinition, ChannelDefinitionCommon, ChannelOptionsBuilder, TetherAgentOptionsBuilder, -}; +use tether_agent::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; fn main() { let mut tether_agent = TetherAgentOptionsBuilder::new("example") @@ -18,7 +16,7 @@ fn main() { .role(Some("pretendingToBeSomethingElse")) .qos(Some(2)) .retain(Some(true)) - .build(&mut tether_agent) + .build() .expect("failed to create sender channel"); let input_wildcard_channel = ChannelOptionsBuilder::create_receiver("everything") .topic(Some("#")) diff --git a/examples/rust/receive.rs b/examples/rust/receive.rs index 98734e8..73686a1 100644 --- a/examples/rust/receive.rs +++ b/examples/rust/receive.rs @@ -1,11 +1,9 @@ -use std::{thread, time::Duration}; - use env_logger::{Builder, Env}; -use log::{debug, info, warn}; -use rmp_serde::from_slice; +use log::*; use serde::Deserialize; -use tether_agent::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; +use tether_agent::TetherAgentOptionsBuilder; +#[allow(dead_code)] #[derive(Deserialize, Debug)] struct CustomMessage { id: usize, @@ -49,19 +47,6 @@ fn main() { m ); } - // if receiver.matches(&topic) { - // let decoded = from_slice::(&payload); - // match decoded { - // Ok(d) => { - // info!("Yes, we decoded the MessagePack payload as: {:?}", d); - // let CustomMessage { name, id } = d; - // debug!("Name is {} and ID is {}", name, id); - // } - // Err(e) => { - // warn!("Failed to decode the payload: {}", e) - // } - // }; - // } } } } diff --git a/lib/rust/src/channels/options.rs b/lib/rust/src/channels/options.rs index 0b5490e..c8f592e 100644 --- a/lib/rust/src/channels/options.rs +++ b/lib/rust/src/channels/options.rs @@ -1,6 +1,6 @@ use log::{debug, error, info, warn}; -use crate::tether_compliant_topic::TetherCompliantTopic; +use crate::{tether_compliant_topic::TetherCompliantTopic, TetherAgent}; pub struct ChannelReceiverOptions { channel_name: String, @@ -220,99 +220,98 @@ impl ChannelOptionsBuilder { self } - // /// Finalise the options (substituting suitable defaults if no custom values have been - // /// provided) and return a valid ChannelDefinition that you can actually use. - // pub fn build(self, tether_agent: &mut TetherAgent) -> anyhow::Result { - // todo!(); - // // match self { - // // Self::ChannelReceiver(channel_options) => { - // // let tpt: TetherOrCustomTopic = match channel_options.override_topic { - // // Some(custom) => TetherOrCustomTopic::Custom(custom), - // // None => { - // // debug!("Not a custom topic; provided overrides: role = {:?}, id = {:?}, name = {:?}", channel_options.override_subscribe_role, channel_options.override_subscribe_id, channel_options.override_subscribe_channel_name); - - // // TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - // // &channel_options - // // .override_subscribe_channel_name - // // .unwrap_or(channel_options.channel_name.clone()), - // // channel_options.override_subscribe_role.as_deref(), - // // channel_options.override_subscribe_id.as_deref(), - // // )) - // // } - // // }; - // // let channel_definition = - // // ChannelReceiver::new(&channel_options.channel_name, tpt, channel_options.qos); - - // // // This is really only useful for testing purposes. - // // if !tether_agent.auto_connect_enabled() { - // // warn!("Auto-connect is disabled, skipping subscription"); - // // return Ok(TetherChannel::ChannelReceiver(channel_definition)); - // // } - - // // if let Some(client) = &tether_agent.client { - // // match client.subscribe( - // // channel_definition.generated_topic(), - // // match channel_definition.qos() { - // // 0 => rumqttc::QoS::AtMostOnce, - // // 1 => rumqttc::QoS::AtLeastOnce, - // // 2 => rumqttc::QoS::ExactlyOnce, - // // _ => rumqttc::QoS::AtLeastOnce, - // // }, - // // ) { - // // Ok(res) => { - // // debug!( - // // "This topic was fine: \"{}\"", - // // channel_definition.generated_topic() - // // ); - // // debug!("Server respond OK for subscribe: {res:?}"); - // // Ok(TetherChannel::ChannelReceiver(channel_definition)) - // // } - // // Err(_e) => Err(anyhow!("ClientError")), - // // } - // // } else { - // // Err(anyhow!("Client not available for subscription")) - // // } - // // } - // // Self::ChannelSender(channel_options) => { - // // let tpt: TetherOrCustomTopic = match channel_options.override_topic { - // // Some(custom) => { - // // warn!( - // // "Custom topic override: \"{}\" - all other options ignored", - // // custom - // // ); - // // TetherOrCustomTopic::Custom(custom) - // // } - // // None => { - // // let optional_id_part = match channel_options.override_publish_id { - // // Some(id) => { - // // debug!("Publish ID was overriden at Channel options level. The Agent ID will be ignored."); - // // Some(id) - // // } - // // None => { - // // debug!("Publish ID was not overriden at Channel options level. The Agent ID will be used instead, if specified in Agent creation."); - // // tether_agent.id().map(String::from) - // // } - // // }; - - // // TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( - // // tether_agent, - // // &channel_options.channel_name, - // // channel_options.override_publish_role.as_deref(), - // // optional_id_part.as_deref(), - // // )) - // // } - // // }; - - // // let channel_definition = ChannelSender::new( - // // &channel_options.channel_name, - // // tpt, - // // channel_options.qos, - // // channel_options.retain, - // // ); - // // Ok(TetherChannel::ChannelSender(channel_definition)) - // // } - // // } - // } + /// Finalise the options (substituting suitable defaults if no custom values have been + /// provided) and return a valid ChannelDefinition that you can actually use. + pub fn build(self, tether_agent: &mut TetherAgent) -> anyhow::Result { + match self { + Self::ChannelReceiver(channel_options) => { + let tpt: TetherOrCustomTopic = match channel_options.override_topic { + Some(custom) => TetherOrCustomTopic::Custom(custom), + None => { + debug!("Not a custom topic; provided overrides: role = {:?}, id = {:?}, name = {:?}", channel_options.override_subscribe_role, channel_options.override_subscribe_id, channel_options.override_subscribe_channel_name); + + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( + &channel_options + .override_subscribe_channel_name + .unwrap_or(channel_options.channel_name.clone()), + channel_options.override_subscribe_role.as_deref(), + channel_options.override_subscribe_id.as_deref(), + )) + } + }; + let channel_definition = + ChannelReceiver::new(&channel_options.channel_name, tpt, channel_options.qos); + + // This is really only useful for testing purposes. + if !tether_agent.auto_connect_enabled() { + warn!("Auto-connect is disabled, skipping subscription"); + return Ok(TetherChannel::ChannelReceiver(channel_definition)); + } + + if let Some(client) = &tether_agent.client { + match client.subscribe( + channel_definition.generated_topic(), + match channel_definition.qos() { + 0 => rumqttc::QoS::AtMostOnce, + 1 => rumqttc::QoS::AtLeastOnce, + 2 => rumqttc::QoS::ExactlyOnce, + _ => rumqttc::QoS::AtLeastOnce, + }, + ) { + Ok(res) => { + debug!( + "This topic was fine: \"{}\"", + channel_definition.generated_topic() + ); + debug!("Server respond OK for subscribe: {res:?}"); + Ok(TetherChannel::ChannelReceiver(channel_definition)) + } + Err(_e) => Err(anyhow!("ClientError")), + } + } else { + Err(anyhow!("Client not available for subscription")) + } + } + Self::ChannelSender(channel_options) => { + let tpt: TetherOrCustomTopic = match channel_options.override_topic { + Some(custom) => { + warn!( + "Custom topic override: \"{}\" - all other options ignored", + custom + ); + TetherOrCustomTopic::Custom(custom) + } + None => { + let optional_id_part = match channel_options.override_publish_id { + Some(id) => { + debug!("Publish ID was overriden at Channel options level. The Agent ID will be ignored."); + Some(id) + } + None => { + debug!("Publish ID was not overriden at Channel options level. The Agent ID will be used instead, if specified in Agent creation."); + tether_agent.id().map(String::from) + } + }; + + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( + tether_agent, + &channel_options.channel_name, + channel_options.override_publish_role.as_deref(), + optional_id_part.as_deref(), + )) + } + }; + + let channel_definition = ChannelSender::new( + &channel_options.channel_name, + tpt, + channel_options.qos, + channel_options.retain, + ); + Ok(TetherChannel::ChannelSender(channel_definition)) + } + } + } } // #[cfg(test)] From 503674f1164ff86371033d24bb9ce4db822bed5f Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Mon, 14 Apr 2025 11:35:34 +0200 Subject: [PATCH 11/21] When creating via OptionsBuilder, borrow checking is again a problem for TetherAgent --- examples/rust/custom_options.rs | 39 +- lib/rust/src/channels/mod.rs | 5 +- lib/rust/src/channels/options.rs | 611 ------------------ lib/rust/src/channels/options/mod.rs | 316 +++++++++ .../src/channels/options/receiver_options.rs | 138 ++++ .../src/channels/options/sender_options.rs | 145 +++++ 6 files changed, 624 insertions(+), 630 deletions(-) delete mode 100644 lib/rust/src/channels/options.rs create mode 100644 lib/rust/src/channels/options/mod.rs create mode 100644 lib/rust/src/channels/options/receiver_options.rs create mode 100644 lib/rust/src/channels/options/sender_options.rs diff --git a/examples/rust/custom_options.rs b/examples/rust/custom_options.rs index 20906ce..8793880 100644 --- a/examples/rust/custom_options.rs +++ b/examples/rust/custom_options.rs @@ -1,6 +1,9 @@ use std::time::Duration; -use tether_agent::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; +use tether_agent::{ + channels::options::ChannelOptions, receiver_options::ChannelReceiverOptionsBuilder, + ChannelCommon, ChannelSenderOptionsBuilder, TetherAgentOptionsBuilder, +}; fn main() { let mut tether_agent = TetherAgentOptionsBuilder::new("example") @@ -12,36 +15,36 @@ fn main() { .build() .expect("failed to create Tether Agent"); - let sender_channel = ChannelOptionsBuilder::create_sender("anOutput") + let sender_channel = ChannelSenderOptionsBuilder::new("anOutput") .role(Some("pretendingToBeSomethingElse")) .qos(Some(2)) .retain(Some(true)) - .build() + .build(&mut tether_agent) .expect("failed to create sender channel"); - let input_wildcard_channel = ChannelOptionsBuilder::create_receiver("everything") - .topic(Some("#")) - .build(&mut tether_agent); + let input_wildcard_channel = ChannelReceiverOptionsBuilder::new("everything") + .override_topic(Some("#")) + .build::(&mut tether_agent) + .expect("failed to create receiver channel"); - let input_customid_channel = ChannelOptionsBuilder::create_receiver("someData") + let input_customid_channel = ChannelReceiverOptionsBuilder::new("someData") .role(None) // i.e., just use default .id(Some("specificIDonly")) - .build(&mut tether_agent); + .build::(&mut tether_agent) + .expect("failed to create receiver channel"); println!("Agent looks like this: {:?}", tether_agent.description()); let (role, id, _) = tether_agent.description(); assert_eq!(role, "example"); assert_eq!(id, "any"); // because we set None - if let ChannelDefinition::ChannelSender(p) = &sender_channel { - println!("sender channel: {:?}", p); - assert_eq!( - p.generated_topic(), - "pretendingToBeSomethingElse/any/anOutput" - ); - } - - println!("wildcard input channel: {:?}", input_wildcard_channel); - println!("speific ID input channel: {:?}", input_customid_channel); + println!( + "wildcard input channel: {:?}", + input_wildcard_channel.generated_topic() + ); + println!( + "speific ID input channel: {:?}", + input_customid_channel.generated_topic() + ); let payload = rmp_serde::to_vec::(&String::from("boo")).expect("failed to serialise payload"); diff --git a/lib/rust/src/channels/mod.rs b/lib/rust/src/channels/mod.rs index 6841f52..9a7e20f 100644 --- a/lib/rust/src/channels/mod.rs +++ b/lib/rust/src/channels/mod.rs @@ -21,7 +21,10 @@ pub trait ChannelCommon<'a> { // ChannelSender(ChannelSender), // } -// impl TetherChannel { +// impl ChannelCommon< for T +// where +// T: ChannelCommon, +// { // pub fn name(&self) -> &str { // match self { // TetherChannel::ChannelReceiver(p) => p.name(), diff --git a/lib/rust/src/channels/options.rs b/lib/rust/src/channels/options.rs deleted file mode 100644 index c8f592e..0000000 --- a/lib/rust/src/channels/options.rs +++ /dev/null @@ -1,611 +0,0 @@ -use log::{debug, error, info, warn}; - -use crate::{tether_compliant_topic::TetherCompliantTopic, TetherAgent}; - -pub struct ChannelReceiverOptions { - channel_name: String, - qos: Option, - override_subscribe_role: Option, - override_subscribe_id: Option, - override_subscribe_channel_name: Option, - override_topic: Option, -} - -pub struct ChannelSenderOptions { - channel_name: String, - qos: Option, - override_publish_role: Option, - override_publish_id: Option, - override_topic: Option, - retain: Option, -} - -/// This is the definition of a Channel Receiver or Sender. -/// -/// You typically don't use an instance of this directly; call `.build()` at the -/// end of the chain to get a usable **ChannelDefinition** -pub enum ChannelOptionsBuilder { - ChannelReceiver(ChannelReceiverOptions), - ChannelSender(ChannelSenderOptions), -} - -impl ChannelOptionsBuilder { - pub fn create_receiver(name: &str) -> ChannelOptionsBuilder { - ChannelOptionsBuilder::ChannelReceiver(ChannelReceiverOptions { - channel_name: String::from(name), - override_subscribe_id: None, - override_subscribe_role: None, - override_subscribe_channel_name: None, - override_topic: None, - qos: None, - }) - } - - pub fn create_sender(name: &str) -> ChannelOptionsBuilder { - ChannelOptionsBuilder::ChannelSender(ChannelSenderOptions { - channel_name: String::from(name), - override_publish_id: None, - override_publish_role: None, - override_topic: None, - qos: None, - retain: None, - }) - } - - pub fn qos(mut self, qos: Option) -> Self { - match &mut self { - ChannelOptionsBuilder::ChannelReceiver(s) => s.qos = qos, - ChannelOptionsBuilder::ChannelSender(s) => s.qos = qos, - }; - self - } - - /** - Override the "role" part of the topic that gets generated for this Channel. - - For Channel Receivers, this means you want to be specific about the Role part - of the topic, instead of using the default wildcard `+` at this location - - For Channel Senders, this means you want to override the Role part instead - of using your Agent's "own" Role with which you created the Tether Agent - - If you override the entire topic using `.topic` this will be ignored. - */ - pub fn role(mut self, role: Option<&str>) -> Self { - match &mut self { - ChannelOptionsBuilder::ChannelReceiver(s) => { - if s.override_topic.is_some() { - error!("Override topic was also provided; this will take precedence"); - } else { - s.override_subscribe_role = role.map(|s| s.into()); - } - } - ChannelOptionsBuilder::ChannelSender(s) => { - if s.override_topic.is_some() { - error!("Override topic was also provided; this will take precedence"); - } else { - s.override_publish_role = role.map(|s| s.into()); - } - } - }; - self - } - - /** - Override the "id" part of the topic that gets generated for this Channel. - - For Channel Receivers, this means you want to be specific about the ID part - of the topic, instead of using the default wildcard `+` at this location - - For Channel Senders, this means you want to override the ID part instead - of using your Agent's "own" ID which you specified (or left blank, i.e. "any") - when creating the Tether Agent - - If you override the entire topic using `.topic` this will be ignored. - */ - pub fn id(mut self, id: Option<&str>) -> Self { - match &mut self { - ChannelOptionsBuilder::ChannelReceiver(s) => { - if s.override_topic.is_some() { - error!("Override topic was also provided; this will take precedence"); - } else { - s.override_subscribe_id = id.map(|s| s.into()); - } - } - ChannelOptionsBuilder::ChannelSender(s) => { - if s.override_topic.is_some() { - error!("Override topic was also provided; this will take precedence"); - } else { - s.override_publish_id = id.map(|s| s.into()); - } - } - }; - self - } - - /// Override the "name" part of the topic that gets generated for this Channel. - /// This is mainly to facilitate wildcard subscriptions such as - /// `someRole/+` instead of `someRole/originalChannelName`. - /// - /// In the case of Receiver Topics, a wildcard `+` can be used to substitute - /// the last part of the topic as in `role/id/+` - /// - /// Channel Senders will ignore (with an error) any attempt to change the name after - /// instantiation. - pub fn name(mut self, override_channel_name: Option<&str>) -> Self { - match &mut self { - ChannelOptionsBuilder::ChannelReceiver(opt) => { - if opt.override_topic.is_some() { - error!("Override topic was also provided; this will take precedence"); - } - if override_channel_name.is_some() { - opt.override_subscribe_channel_name = override_channel_name.map(|s| s.into()); - } else { - debug!("Override Channel name set to None; will use original name \"{}\" given in ::create_receiver constructor", opt.channel_name); - } - } - ChannelOptionsBuilder::ChannelSender(_) => { - error!( - "Channel Senders cannot change their name part after ::create_sender constructor" - ); - } - }; - self - } - - /// Call this if you would like your Channel Receiver to match **any channel**. - /// This is equivalent to `.name(Some("+"))` but is provided for convenience - /// since it does not require you to remember the wildcard string. - /// - /// This also does not prevent you from further restricting the topic - /// subscription match by Role and/or ID. So, for example, if you are - /// interested in **all messages** from an Agent with the role `"brain"`, - /// it is valid to create a channel with `.role("brain").any_channel()` and this - /// will subscribe to `"brain/+/#"` as expected. - pub fn any_channel(mut self) -> Self { - match &mut self { - ChannelOptionsBuilder::ChannelReceiver(opt) => { - opt.override_subscribe_channel_name = Some("+".into()); - } - ChannelOptionsBuilder::ChannelSender(_) => { - error!( - "Channel Senders cannot change their name part after ::create_sender constructor" - ); - } - } - self - } - - /// Override the final topic to use for publishing or subscribing. The provided topic **will** be checked - /// against the Tether Compliant Topic (TCT) convention, but the function **will not** reject topic strings - just - /// produce a warning. It's therefore valid to use a wildcard such as "#", for Receivers (subscribing). - /// - /// Any customisations specified using `.role(...)` or `.id(...)` will be ignored if this function is called - /// after these. - /// - /// By default, the override_topic is None, but you can specify None explicitly using this function. - pub fn topic(mut self, override_topic: Option<&str>) -> Self { - match override_topic { - Some(t) => { - if TryInto::::try_into(t).is_ok() { - info!("Custom topic passes Tether Compliant Topic validation"); - } else if t == "#" { - info!("Wildcard \"#\" custom topics are not Tether Compliant Topics but are valid"); - } else { - warn!( - "Could not convert \"{}\" into Tether Compliant Topic; presumably you know what you're doing!", - t - ); - } - match &mut self { - ChannelOptionsBuilder::ChannelReceiver(s) => s.override_topic = Some(t.into()), - ChannelOptionsBuilder::ChannelSender(s) => s.override_topic = Some(t.into()), - }; - } - None => { - match &mut self { - ChannelOptionsBuilder::ChannelReceiver(s) => s.override_topic = None, - ChannelOptionsBuilder::ChannelSender(s) => s.override_topic = None, - }; - } - } - self - } - - pub fn retain(mut self, should_retain: Option) -> Self { - match &mut self { - Self::ChannelReceiver(_) => { - error!("Cannot set retain flag on Receiver / subscription"); - } - Self::ChannelSender(s) => { - s.retain = should_retain; - } - } - self - } - - /// Finalise the options (substituting suitable defaults if no custom values have been - /// provided) and return a valid ChannelDefinition that you can actually use. - pub fn build(self, tether_agent: &mut TetherAgent) -> anyhow::Result { - match self { - Self::ChannelReceiver(channel_options) => { - let tpt: TetherOrCustomTopic = match channel_options.override_topic { - Some(custom) => TetherOrCustomTopic::Custom(custom), - None => { - debug!("Not a custom topic; provided overrides: role = {:?}, id = {:?}, name = {:?}", channel_options.override_subscribe_role, channel_options.override_subscribe_id, channel_options.override_subscribe_channel_name); - - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( - &channel_options - .override_subscribe_channel_name - .unwrap_or(channel_options.channel_name.clone()), - channel_options.override_subscribe_role.as_deref(), - channel_options.override_subscribe_id.as_deref(), - )) - } - }; - let channel_definition = - ChannelReceiver::new(&channel_options.channel_name, tpt, channel_options.qos); - - // This is really only useful for testing purposes. - if !tether_agent.auto_connect_enabled() { - warn!("Auto-connect is disabled, skipping subscription"); - return Ok(TetherChannel::ChannelReceiver(channel_definition)); - } - - if let Some(client) = &tether_agent.client { - match client.subscribe( - channel_definition.generated_topic(), - match channel_definition.qos() { - 0 => rumqttc::QoS::AtMostOnce, - 1 => rumqttc::QoS::AtLeastOnce, - 2 => rumqttc::QoS::ExactlyOnce, - _ => rumqttc::QoS::AtLeastOnce, - }, - ) { - Ok(res) => { - debug!( - "This topic was fine: \"{}\"", - channel_definition.generated_topic() - ); - debug!("Server respond OK for subscribe: {res:?}"); - Ok(TetherChannel::ChannelReceiver(channel_definition)) - } - Err(_e) => Err(anyhow!("ClientError")), - } - } else { - Err(anyhow!("Client not available for subscription")) - } - } - Self::ChannelSender(channel_options) => { - let tpt: TetherOrCustomTopic = match channel_options.override_topic { - Some(custom) => { - warn!( - "Custom topic override: \"{}\" - all other options ignored", - custom - ); - TetherOrCustomTopic::Custom(custom) - } - None => { - let optional_id_part = match channel_options.override_publish_id { - Some(id) => { - debug!("Publish ID was overriden at Channel options level. The Agent ID will be ignored."); - Some(id) - } - None => { - debug!("Publish ID was not overriden at Channel options level. The Agent ID will be used instead, if specified in Agent creation."); - tether_agent.id().map(String::from) - } - }; - - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( - tether_agent, - &channel_options.channel_name, - channel_options.override_publish_role.as_deref(), - optional_id_part.as_deref(), - )) - } - }; - - let channel_definition = ChannelSender::new( - &channel_options.channel_name, - tpt, - channel_options.qos, - channel_options.retain, - ); - Ok(TetherChannel::ChannelSender(channel_definition)) - } - } - } -} - -// #[cfg(test)] -// mod tests { - -// use crate::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; - -// // fn verbose_logging() { -// // use env_logger::{Builder, Env}; -// // let mut logger_builder = Builder::from_env(Env::default().default_filter_or("debug")); -// // logger_builder.init(); -// // } - -// #[test] -// fn default_receiver_channel() { -// // verbose_logging(); -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); -// let receiver = ChannelOptionsBuilder::create_receiver("one") -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver.name(), "one"); -// assert_eq!(receiver.generated_topic(), "+/one/#"); -// } - -// #[test] -// /// This is a fairly trivial example, but contrast with the test -// /// `sender_channel_default_but_agent_id_custom`: although a custom ID was set for the -// /// Agent, this does not affect the Topic for a Channel Receiver created without any -// /// explicit overrides. -// fn default_channel_receiver_with_agent_custom_id() { -// // verbose_logging(); -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .id(Some("verySpecialGroup")) -// .build() -// .expect("sorry, these tests require working localhost Broker"); -// let receiver = ChannelOptionsBuilder::create_receiver("one") -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver.name(), "one"); -// assert_eq!(receiver.generated_topic(), "+/one/#"); -// } - -// #[test] -// fn default_channel_sender() { -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); -// let channel = ChannelOptionsBuilder::create_sender("two") -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(channel.name(), "two"); -// assert_eq!(channel.generated_topic(), "tester/two"); -// } - -// #[test] -// /// This is identical to the case in which a Channel Sender is created with defaults (no overrides), -// /// BUT the Agent had a custom ID set, which means that the final topic includes this custom -// /// ID/Group value. -// fn sender_channel_default_but_agent_id_custom() { -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .id(Some("specialCustomGrouping")) -// .build() -// .expect("sorry, these tests require working localhost Broker"); -// let channel = ChannelOptionsBuilder::create_sender("somethingStandard") -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(channel.name(), "somethingStandard"); -// assert_eq!( -// channel.generated_topic(), -// "tester/somethingStandard/specialCustomGrouping" -// ); -// } - -// #[test] -// fn receiver_id_andor_role() { -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); - -// let receive_role_only = ChannelOptionsBuilder::create_receiver("theChannel") -// .role(Some("specificRole")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receive_role_only.name(), "theChannel"); -// assert_eq!( -// receive_role_only.generated_topic(), -// "specificRole/theChannel/#" -// ); - -// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") -// .id(Some("specificID")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_id_only.name(), "theChannel"); -// assert_eq!( -// receiver_id_only.generated_topic(), -// "+/theChannel/specificID" -// ); - -// let receiver_both_custom = ChannelOptionsBuilder::create_receiver("theChannel") -// .id(Some("specificID")) -// .role(Some("specificRole")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_both_custom.name(), "theChannel"); -// assert_eq!( -// receiver_both_custom.generated_topic(), -// "specificRole/theChannel/specificID" -// ); -// } - -// #[test] -// /// If the end-user implicitly specifies the chanel name part (does not set it to Some(_) -// /// or None) then the ID and/or Role parts will change but the Channel Name part will -// /// remain the "original" / default -// /// Contrast with receiver_specific_id_andor_role_no_chanel_name below. -// fn receiver_specific_id_andor_role_with_channel_name() { -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); - -// let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") -// .role(Some("specificRole")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_role_only.name(), "theChannel"); -// assert_eq!( -// receiver_role_only.generated_topic(), -// "specificRole/theChannel/#" -// ); - -// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") -// .id(Some("specificID")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_id_only.name(), "theChannel"); -// assert_eq!( -// receiver_id_only.generated_topic(), -// "+/theChannel/specificID" -// ); - -// let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") -// .id(Some("specificID")) -// .role(Some("specificRole")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_both.name(), "theChannel"); -// assert_eq!( -// receiver_both.generated_topic(), -// "specificRole/theChannel/specificID" -// ); -// } - -// #[test] -// /// Unlike receiver_specific_id_andor_role_with_channel_name, this tests the situation where -// /// the end-user (possibly) specifies the ID and/or Role, but also explicitly -// /// sets the Channel Name to Some("+"), ie. "use a wildcard at this -// /// position instead" - and NOT the original channel name. -// fn receiver_specific_id_andor_role_no_channel_name() { -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); - -// let receiver_only_chanel_name_none = ChannelOptionsBuilder::create_receiver("theChannel") -// .name(Some("+")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_only_chanel_name_none.name(), "theChannel"); -// assert_eq!(receiver_only_chanel_name_none.generated_topic(), "+/+/#"); - -// let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") -// .name(Some("+")) -// .role(Some("specificRole")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_role_only.name(), "theChannel"); -// assert_eq!(receiver_role_only.generated_topic(), "specificRole/+/#"); - -// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") -// // .name(Some("+")) -// .any_channel() // equivalent to Some("+") -// .id(Some("specificID")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_id_only.name(), "theChannel"); -// assert_eq!(receiver_id_only.generated_topic(), "+/+/specificID"); - -// let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") -// .name(Some("+")) -// .id(Some("specificID")) -// .role(Some("specificRole")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_both.name(), "theChannel"); -// assert_eq!(receiver_both.generated_topic(), "specificRole/+/specificID"); -// } - -// #[test] -// fn any_name_but_specify_role() { -// // Some fairly niche cases here - -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); - -// let receiver_any_channel = ChannelOptionsBuilder::create_receiver("aTest") -// .any_channel() -// .build(&mut tether_agent) -// .unwrap(); - -// assert_eq!(receiver_any_channel.name(), "aTest"); -// assert_eq!(receiver_any_channel.generated_topic(), "+/+/#"); - -// let receiver_specify_role = ChannelOptionsBuilder::create_receiver("aTest") -// .any_channel() -// .role(Some("brain")) -// .build(&mut tether_agent) -// .unwrap(); - -// assert_eq!(receiver_specify_role.name(), "aTest"); -// assert_eq!(receiver_specify_role.generated_topic(), "brain/+/#"); -// } - -// #[test] -// fn sender_custom() { -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); - -// let sender_custom_role = ChannelOptionsBuilder::create_sender("theChannelSender") -// .role(Some("customRole")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(sender_custom_role.name(), "theChannelSender"); -// assert_eq!( -// sender_custom_role.generated_topic(), -// "customRole/theChannelSender" -// ); - -// let sender_custom_id = ChannelOptionsBuilder::create_sender("theChannelSender") -// .id(Some("customID")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(sender_custom_id.name(), "theChannelSender"); -// assert_eq!( -// sender_custom_id.generated_topic(), -// "tester/theChannelSender/customID" -// ); - -// let sender_custom_both = ChannelOptionsBuilder::create_sender("theChannelSender") -// .role(Some("customRole")) -// .id(Some("customID")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(sender_custom_both.name(), "theChannelSender"); -// assert_eq!( -// sender_custom_both.generated_topic(), -// "customRole/theChannelSender/customID" -// ); -// } - -// #[test] -// fn receiver_manual_topics() { -// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") -// .auto_connect(false) -// .build() -// .expect("sorry, these tests require working localhost Broker"); - -// let receiver_all = ChannelOptionsBuilder::create_receiver("everything") -// .topic(Some("#")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_all.name(), "everything"); -// assert_eq!(receiver_all.generated_topic(), "#"); - -// let receiver_nontether = ChannelOptionsBuilder::create_receiver("weird") -// .topic(Some("foo/bar/baz/one/two/three")) -// .build(&mut tether_agent) -// .unwrap(); -// assert_eq!(receiver_nontether.name(), "weird"); -// assert_eq!( -// receiver_nontether.generated_topic(), -// "foo/bar/baz/one/two/three" -// ); -// } -// } diff --git a/lib/rust/src/channels/options/mod.rs b/lib/rust/src/channels/options/mod.rs new file mode 100644 index 0000000..7426699 --- /dev/null +++ b/lib/rust/src/channels/options/mod.rs @@ -0,0 +1,316 @@ +pub mod receiver_options; +pub mod sender_options; + +pub trait ChannelOptions { + fn new(name: &str) -> Self; + fn qos(self, qos: Option) -> Self; + fn role(self, role: Option<&str>) -> Self; + fn id(self, id: Option<&str>) -> Self; + fn override_name(self, override_channel_name: Option<&str>) -> Self; + fn override_topic(self, override_topic: Option<&str>) -> Self; +} + +pub struct ChannelSenderOptionsBuilder { + channel_name: String, + qos: Option, + override_publish_role: Option, + override_publish_id: Option, + override_topic: Option, + retain: Option, +} + +// #[cfg(test)] +// mod tests { + +// use crate::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; + +// // fn verbose_logging() { +// // use env_logger::{Builder, Env}; +// // let mut logger_builder = Builder::from_env(Env::default().default_filter_or("debug")); +// // logger_builder.init(); +// // } + +// #[test] +// fn default_receiver_channel() { +// // verbose_logging(); +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let receiver = ChannelOptionsBuilder::create_receiver("one") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver.name(), "one"); +// assert_eq!(receiver.generated_topic(), "+/one/#"); +// } + +// #[test] +// /// This is a fairly trivial example, but contrast with the test +// /// `sender_channel_default_but_agent_id_custom`: although a custom ID was set for the +// /// Agent, this does not affect the Topic for a Channel Receiver created without any +// /// explicit overrides. +// fn default_channel_receiver_with_agent_custom_id() { +// // verbose_logging(); +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .id(Some("verySpecialGroup")) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let receiver = ChannelOptionsBuilder::create_receiver("one") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver.name(), "one"); +// assert_eq!(receiver.generated_topic(), "+/one/#"); +// } + +// #[test] +// fn default_channel_sender() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let channel = ChannelOptionsBuilder::create_sender("two") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(channel.name(), "two"); +// assert_eq!(channel.generated_topic(), "tester/two"); +// } + +// #[test] +// /// This is identical to the case in which a Channel Sender is created with defaults (no overrides), +// /// BUT the Agent had a custom ID set, which means that the final topic includes this custom +// /// ID/Group value. +// fn sender_channel_default_but_agent_id_custom() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .id(Some("specialCustomGrouping")) +// .build() +// .expect("sorry, these tests require working localhost Broker"); +// let channel = ChannelOptionsBuilder::create_sender("somethingStandard") +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(channel.name(), "somethingStandard"); +// assert_eq!( +// channel.generated_topic(), +// "tester/somethingStandard/specialCustomGrouping" +// ); +// } + +// #[test] +// fn receiver_id_andor_role() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receive_role_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receive_role_only.name(), "theChannel"); +// assert_eq!( +// receive_role_only.generated_topic(), +// "specificRole/theChannel/#" +// ); + +// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_id_only.name(), "theChannel"); +// assert_eq!( +// receiver_id_only.generated_topic(), +// "+/theChannel/specificID" +// ); + +// let receiver_both_custom = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_both_custom.name(), "theChannel"); +// assert_eq!( +// receiver_both_custom.generated_topic(), +// "specificRole/theChannel/specificID" +// ); +// } + +// #[test] +// /// If the end-user implicitly specifies the chanel name part (does not set it to Some(_) +// /// or None) then the ID and/or Role parts will change but the Channel Name part will +// /// remain the "original" / default +// /// Contrast with receiver_specific_id_andor_role_no_chanel_name below. +// fn receiver_specific_id_andor_role_with_channel_name() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_role_only.name(), "theChannel"); +// assert_eq!( +// receiver_role_only.generated_topic(), +// "specificRole/theChannel/#" +// ); + +// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_id_only.name(), "theChannel"); +// assert_eq!( +// receiver_id_only.generated_topic(), +// "+/theChannel/specificID" +// ); + +// let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") +// .id(Some("specificID")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_both.name(), "theChannel"); +// assert_eq!( +// receiver_both.generated_topic(), +// "specificRole/theChannel/specificID" +// ); +// } + +// #[test] +// /// Unlike receiver_specific_id_andor_role_with_channel_name, this tests the situation where +// /// the end-user (possibly) specifies the ID and/or Role, but also explicitly +// /// sets the Channel Name to Some("+"), ie. "use a wildcard at this +// /// position instead" - and NOT the original channel name. +// fn receiver_specific_id_andor_role_no_channel_name() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_only_chanel_name_none = ChannelOptionsBuilder::create_receiver("theChannel") +// .name(Some("+")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_only_chanel_name_none.name(), "theChannel"); +// assert_eq!(receiver_only_chanel_name_none.generated_topic(), "+/+/#"); + +// let receiver_role_only = ChannelOptionsBuilder::create_receiver("theChannel") +// .name(Some("+")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_role_only.name(), "theChannel"); +// assert_eq!(receiver_role_only.generated_topic(), "specificRole/+/#"); + +// let receiver_id_only = ChannelOptionsBuilder::create_receiver("theChannel") +// // .name(Some("+")) +// .any_channel() // equivalent to Some("+") +// .id(Some("specificID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_id_only.name(), "theChannel"); +// assert_eq!(receiver_id_only.generated_topic(), "+/+/specificID"); + +// let receiver_both = ChannelOptionsBuilder::create_receiver("theChannel") +// .name(Some("+")) +// .id(Some("specificID")) +// .role(Some("specificRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_both.name(), "theChannel"); +// assert_eq!(receiver_both.generated_topic(), "specificRole/+/specificID"); +// } + +// #[test] +// fn any_name_but_specify_role() { +// // Some fairly niche cases here + +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_any_channel = ChannelOptionsBuilder::create_receiver("aTest") +// .any_channel() +// .build(&mut tether_agent) +// .unwrap(); + +// assert_eq!(receiver_any_channel.name(), "aTest"); +// assert_eq!(receiver_any_channel.generated_topic(), "+/+/#"); + +// let receiver_specify_role = ChannelOptionsBuilder::create_receiver("aTest") +// .any_channel() +// .role(Some("brain")) +// .build(&mut tether_agent) +// .unwrap(); + +// assert_eq!(receiver_specify_role.name(), "aTest"); +// assert_eq!(receiver_specify_role.generated_topic(), "brain/+/#"); +// } + +// #[test] +// fn sender_custom() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let sender_custom_role = ChannelOptionsBuilder::create_sender("theChannelSender") +// .role(Some("customRole")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(sender_custom_role.name(), "theChannelSender"); +// assert_eq!( +// sender_custom_role.generated_topic(), +// "customRole/theChannelSender" +// ); + +// let sender_custom_id = ChannelOptionsBuilder::create_sender("theChannelSender") +// .id(Some("customID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(sender_custom_id.name(), "theChannelSender"); +// assert_eq!( +// sender_custom_id.generated_topic(), +// "tester/theChannelSender/customID" +// ); + +// let sender_custom_both = ChannelOptionsBuilder::create_sender("theChannelSender") +// .role(Some("customRole")) +// .id(Some("customID")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(sender_custom_both.name(), "theChannelSender"); +// assert_eq!( +// sender_custom_both.generated_topic(), +// "customRole/theChannelSender/customID" +// ); +// } + +// #[test] +// fn receiver_manual_topics() { +// let mut tether_agent = TetherAgentOptionsBuilder::new("tester") +// .auto_connect(false) +// .build() +// .expect("sorry, these tests require working localhost Broker"); + +// let receiver_all = ChannelOptionsBuilder::create_receiver("everything") +// .topic(Some("#")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_all.name(), "everything"); +// assert_eq!(receiver_all.generated_topic(), "#"); + +// let receiver_nontether = ChannelOptionsBuilder::create_receiver("weird") +// .topic(Some("foo/bar/baz/one/two/three")) +// .build(&mut tether_agent) +// .unwrap(); +// assert_eq!(receiver_nontether.name(), "weird"); +// assert_eq!( +// receiver_nontether.generated_topic(), +// "foo/bar/baz/one/two/three" +// ); +// } +// } diff --git a/lib/rust/src/channels/options/receiver_options.rs b/lib/rust/src/channels/options/receiver_options.rs new file mode 100644 index 0000000..305bbbf --- /dev/null +++ b/lib/rust/src/channels/options/receiver_options.rs @@ -0,0 +1,138 @@ +use crate::{ + receiver::ChannelReceiver, + tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}, + TetherAgent, +}; + +use super::ChannelOptions; +use log::*; +use serde::Deserialize; + +pub struct ChannelReceiverOptionsBuilder { + channel_name: String, + qos: Option, + override_subscribe_role: Option, + override_subscribe_id: Option, + override_subscribe_channel_name: Option, + override_topic: Option, +} + +impl ChannelOptions for ChannelReceiverOptionsBuilder { + fn new(name: &str) -> Self { + ChannelReceiverOptionsBuilder { + channel_name: String::from(name), + override_subscribe_id: None, + override_subscribe_role: None, + override_subscribe_channel_name: None, + override_topic: None, + qos: None, + } + } + + fn qos(self, qos: Option) -> Self { + ChannelReceiverOptionsBuilder { qos, ..self } + } + + fn role(self, role: Option<&str>) -> Self { + if self.override_topic.is_some() { + error!("Override topic was also provided; this will take precedence"); + self + } else { + let override_subscribe_role = role.map(|s| s.into()); + ChannelReceiverOptionsBuilder { + override_subscribe_role, + ..self + } + } + } + + fn id(self, id: Option<&str>) -> Self { + if self.override_topic.is_some() { + error!("Override topic was also provided; this will take precedence"); + self + } else { + let override_subscribe_id = id.map(|s| s.into()); + ChannelReceiverOptionsBuilder { + override_subscribe_id, + ..self + } + } + } + + fn override_name(self, override_channel_name: Option<&str>) -> Self { + if self.override_topic.is_some() { + error!("Override topic was also provided; this will take precedence"); + return self; + } + if override_channel_name.is_some() { + let override_subscribe_channel_name = override_channel_name.map(|s| s.into()); + ChannelReceiverOptionsBuilder { + override_subscribe_channel_name, + ..self + } + } else { + debug!("Override Channel name set to None; will use original name \"{}\" given in ::create_receiver constructor", self.channel_name); + self + } + } + + fn override_topic(self, override_topic: Option<&str>) -> Self { + match override_topic { + Some(t) => { + if TryInto::::try_into(t).is_ok() { + info!("Custom topic passes Tether Compliant Topic validation"); + } else if t == "#" { + info!("Wildcard \"#\" custom topics are not Tether Compliant Topics but are valid"); + } else { + warn!( + "Could not convert \"{}\" into Tether Compliant Topic; presumably you know what you're doing!", + t + ); + } + ChannelReceiverOptionsBuilder { + override_topic: Some(t.into()), + ..self + } + } + None => ChannelReceiverOptionsBuilder { + override_topic: None, + ..self + }, + } + } +} + +impl ChannelReceiverOptionsBuilder { + pub fn any_channel(self) -> Self { + ChannelReceiverOptionsBuilder { + override_subscribe_channel_name: Some("+".into()), + ..self + } + } + + pub fn build<'a, T: Deserialize<'a>>( + self, + tether_agent: &'a mut TetherAgent, + ) -> anyhow::Result> { + let tpt: TetherOrCustomTopic = match self.override_topic { + Some(custom) => TetherOrCustomTopic::Custom(custom), + None => { + debug!( + "Not a custom topic; provided overrides: role = {:?}, id = {:?}, name = {:?}", + self.override_subscribe_role, + self.override_subscribe_id, + self.override_subscribe_channel_name + ); + + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe( + &self + .override_subscribe_channel_name + .unwrap_or(self.channel_name.clone()), + self.override_subscribe_role.as_deref(), + self.override_subscribe_id.as_deref(), + )) + } + }; + ChannelReceiver::new(tether_agent, &self.channel_name, tpt, self.qos) + } +} diff --git a/lib/rust/src/channels/options/sender_options.rs b/lib/rust/src/channels/options/sender_options.rs new file mode 100644 index 0000000..8079c3d --- /dev/null +++ b/lib/rust/src/channels/options/sender_options.rs @@ -0,0 +1,145 @@ +use crate::{ + sender::ChannelSender, + tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}, + TetherAgent, +}; + +use super::{ChannelOptions, ChannelSenderOptionsBuilder}; +use log::*; +use serde::Serialize; + +impl ChannelOptions for ChannelSenderOptionsBuilder { + fn new(name: &str) -> Self { + ChannelSenderOptionsBuilder { + channel_name: String::from(name), + override_publish_id: None, + override_publish_role: None, + override_topic: None, + retain: None, + qos: None, + } + } + + fn qos(self, qos: Option) -> Self { + ChannelSenderOptionsBuilder { qos, ..self } + } + + fn role(self, role: Option<&str>) -> Self { + if self.override_topic.is_some() { + error!("Override topic was also provided; this will take precedence"); + self + } else { + let override_publish_role = role.map(|s| s.into()); + ChannelSenderOptionsBuilder { + override_publish_role, + ..self + } + } + } + + fn id(self, id: Option<&str>) -> Self { + if self.override_topic.is_some() { + error!("Override topic was also provided; this will take precedence"); + self + } else { + let override_publish_id = id.map(|s| s.into()); + ChannelSenderOptionsBuilder { + override_publish_id, + ..self + } + } + } + + fn override_name(self, override_channel_name: Option<&str>) -> Self { + if self.override_topic.is_some() { + error!("Override topic was also provided; this will take precedence"); + return self; + } + match override_channel_name { + Some(n) => ChannelSenderOptionsBuilder { + channel_name: n.into(), + ..self + }, + None => { + debug!("Override Channel name set to None; will use original name \"{}\" given in ::create_receiver constructor", self.channel_name); + self + } + } + } + + fn override_topic(self, override_topic: Option<&str>) -> Self { + match override_topic { + Some(t) => { + if TryInto::::try_into(t).is_ok() { + info!("Custom topic passes Tether Compliant Topic validation"); + } else if t == "#" { + info!("Wildcard \"#\" custom topics are not Tether Compliant Topics but are valid"); + } else { + warn!( + "Could not convert \"{}\" into Tether Compliant Topic; presumably you know what you're doing!", + t + ); + } + ChannelSenderOptionsBuilder { + override_topic: Some(t.into()), + ..self + } + } + None => ChannelSenderOptionsBuilder { + override_topic: None, + ..self + }, + } + } +} + +impl ChannelSenderOptionsBuilder { + pub fn retain(self, should_retain: Option) -> Self { + ChannelSenderOptionsBuilder { + retain: should_retain, + ..self + } + } + + pub fn build( + self, + tether_agent: &TetherAgent, + ) -> anyhow::Result> { + let tpt: TetherOrCustomTopic = match self.override_topic { + Some(custom) => { + warn!( + "Custom topic override: \"{}\" - all other options ignored", + custom + ); + TetherOrCustomTopic::Custom(custom) + } + None => { + let optional_id_part = match self.override_publish_id { + Some(id) => { + debug!("Publish ID was overriden at Channel options level. The Agent ID will be ignored."); + Some(id) + } + None => { + debug!("Publish ID was not overriden at Channel options level. The Agent ID will be used instead, if specified in Agent creation."); + tether_agent.id().map(String::from) + } + }; + + TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( + tether_agent, + &self.channel_name, + self.override_publish_role.as_deref(), + optional_id_part.as_deref(), + )) + } + }; + + Ok(ChannelSender::new( + tether_agent, + &self.channel_name, + tpt, + self.qos, + self.retain, + )) + } +} From b332e680c053eff30b35b3ddf5e6022c36e7084a Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Mon, 14 Apr 2025 12:47:44 +0200 Subject: [PATCH 12/21] Creating Definitions separately from Channels --- examples/rust/custom_options.rs | 12 ++-- examples/rust/send.rs | 11 ++- lib/rust/src/agent/mod.rs | 28 +++----- .../src/channels/definitions/definitions.rs | 69 +++++++++++++++++++ .../channels/{options => definitions}/mod.rs | 10 +-- .../receiver_options.rs | 44 ++++++------ .../sender_options.rs | 52 +++++++------- lib/rust/src/channels/mod.rs | 15 +--- lib/rust/src/channels/receiver.rs | 27 ++++---- lib/rust/src/channels/sender.rs | 20 +++--- .../src/channels/tether_compliant_topic.rs | 4 +- 11 files changed, 170 insertions(+), 122 deletions(-) create mode 100644 lib/rust/src/channels/definitions/definitions.rs rename lib/rust/src/channels/{options => definitions}/mod.rs (98%) rename lib/rust/src/channels/{options => definitions}/receiver_options.rs (80%) rename lib/rust/src/channels/{options => definitions}/sender_options.rs (79%) diff --git a/examples/rust/custom_options.rs b/examples/rust/custom_options.rs index 8793880..940d465 100644 --- a/examples/rust/custom_options.rs +++ b/examples/rust/custom_options.rs @@ -1,8 +1,8 @@ use std::time::Duration; use tether_agent::{ - channels::options::ChannelOptions, receiver_options::ChannelReceiverOptionsBuilder, - ChannelCommon, ChannelSenderOptionsBuilder, TetherAgentOptionsBuilder, + channels::options::ChannelOptions, receiver_options::ChannelReceiverOptions, ChannelDefinition, + ChannelSenderOptionsBuilder, TetherAgentOptionsBuilder, }; fn main() { @@ -21,15 +21,15 @@ fn main() { .retain(Some(true)) .build(&mut tether_agent) .expect("failed to create sender channel"); - let input_wildcard_channel = ChannelReceiverOptionsBuilder::new("everything") + let input_wildcard_channel = ChannelReceiverOptions::new("everything") .override_topic(Some("#")) - .build::(&mut tether_agent) + .build() .expect("failed to create receiver channel"); - let input_customid_channel = ChannelReceiverOptionsBuilder::new("someData") + let input_customid_channel = ChannelReceiverOptions::new("someData") .role(None) // i.e., just use default .id(Some("specificIDonly")) - .build::(&mut tether_agent) + .build() .expect("failed to create receiver channel"); println!("Agent looks like this: {:?}", tether_agent.description()); diff --git a/examples/rust/send.rs b/examples/rust/send.rs index b3a32b6..c64de96 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -3,7 +3,10 @@ use std::time::Duration; use env_logger::{Builder, Env}; use log::{debug, info}; use serde::Serialize; -use tether_agent::{ChannelOptionsBuilder, TetherAgentOptionsBuilder}; +use tether_agent::{ + definitions::{sender_options::ChannelSenderOptions, ChannelOptions}, + TetherAgentOptionsBuilder, +}; #[derive(Serialize)] #[serde(rename_all = "camelCase")] @@ -25,7 +28,8 @@ fn main() { let (role, id, _) = tether_agent.description(); info!("Created agent OK: {}, {}", role, id); - let sender = tether_agent.create_sender("values"); + let sender_definition = ChannelSenderOptions::new("values").build(&tether_agent); + let sender = tether_agent.create_sender(&sender_definition); let test_struct = CustomStruct { id: 101, @@ -41,7 +45,8 @@ fn main() { sender.send(&another_struct).expect("failed to encode+send"); - let number_sender = tether_agent.create_sender::("numbersOnly"); + let number_sender = tether_agent + .create_sender::(&ChannelSenderOptions::new("numbersOnly").build(&tether_agent)); number_sender.send(8).expect("failed to send"); diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index 5db9a2d..203f859 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -8,10 +8,12 @@ use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use uuid::Uuid; +use crate::definitions::definitions::{ + ChannelDefinition, ChannelReceiverDefinition, ChannelSenderDefinition, +}; use crate::receiver::ChannelReceiver; use crate::sender::ChannelSender; use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; -use crate::ChannelCommon; const TIMEOUT_SECONDS: u64 = 3; const DEFAULT_USERNAME: &str = "tether"; @@ -168,28 +170,18 @@ impl TetherAgentOptionsBuilder { } impl<'a> TetherAgent { - pub fn create_sender(&self, name: &str) -> ChannelSender { - ChannelSender::new( - self, - name, - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_publish( - self, name, None, None, - )), - None, - None, - ) + pub fn create_sender( + &self, + definition: &ChannelSenderDefinition, + ) -> ChannelSender { + ChannelSender::new(self, definition) } pub fn create_receiver>( &'a self, - name: &str, + definition: &ChannelReceiverDefinition, ) -> anyhow::Result> { - ChannelReceiver::new( - self, - name, - TetherOrCustomTopic::Tether(TetherCompliantTopic::new_for_subscribe(name, None, None)), - None, - ) + ChannelReceiver::new(&self, definition) } pub fn is_connected(&self) -> bool { diff --git a/lib/rust/src/channels/definitions/definitions.rs b/lib/rust/src/channels/definitions/definitions.rs new file mode 100644 index 0000000..0b096eb --- /dev/null +++ b/lib/rust/src/channels/definitions/definitions.rs @@ -0,0 +1,69 @@ +use crate::tether_compliant_topic::TetherOrCustomTopic; + +pub trait ChannelDefinition<'a> { + fn name(&'a self) -> &'a str; + /// Return the generated topic string actually used by the Channel + fn generated_topic(&'a self) -> &'a str; + /// Return the custom or Tether-compliant topic + fn topic(&'a self) -> &'a TetherOrCustomTopic; + fn qos(&'a self) -> i32; +} + +#[derive(Clone)] +pub struct ChannelSenderDefinition { + pub name: String, + pub generated_topic: String, + pub topic: TetherOrCustomTopic, + pub qos: i32, + pub retain: bool, +} + +impl ChannelSenderDefinition { + pub fn retain(&self) -> bool { + self.retain + } +} + +#[derive(Clone)] +pub struct ChannelReceiverDefinition { + pub name: String, + pub generated_topic: String, + pub topic: TetherOrCustomTopic, + pub qos: i32, +} + +impl<'a> ChannelDefinition<'a> for ChannelSenderDefinition { + fn name(&'a self) -> &'a str { + &self.name + } + + fn generated_topic(&'a self) -> &'a str { + &self.generated_topic + } + + fn topic(&'a self) -> &'a TetherOrCustomTopic { + &self.topic + } + + fn qos(&'a self) -> i32 { + self.qos + } +} + +impl<'a> ChannelDefinition<'a> for ChannelReceiverDefinition { + fn name(&'a self) -> &'a str { + &self.name + } + + fn generated_topic(&'a self) -> &'a str { + &self.generated_topic + } + + fn topic(&'a self) -> &'a TetherOrCustomTopic { + &self.topic + } + + fn qos(&'a self) -> i32 { + self.qos + } +} diff --git a/lib/rust/src/channels/options/mod.rs b/lib/rust/src/channels/definitions/mod.rs similarity index 98% rename from lib/rust/src/channels/options/mod.rs rename to lib/rust/src/channels/definitions/mod.rs index 7426699..4d9dce6 100644 --- a/lib/rust/src/channels/options/mod.rs +++ b/lib/rust/src/channels/definitions/mod.rs @@ -1,3 +1,4 @@ +pub mod definitions; pub mod receiver_options; pub mod sender_options; @@ -10,15 +11,6 @@ pub trait ChannelOptions { fn override_topic(self, override_topic: Option<&str>) -> Self; } -pub struct ChannelSenderOptionsBuilder { - channel_name: String, - qos: Option, - override_publish_role: Option, - override_publish_id: Option, - override_topic: Option, - retain: Option, -} - // #[cfg(test)] // mod tests { diff --git a/lib/rust/src/channels/options/receiver_options.rs b/lib/rust/src/channels/definitions/receiver_options.rs similarity index 80% rename from lib/rust/src/channels/options/receiver_options.rs rename to lib/rust/src/channels/definitions/receiver_options.rs index 305bbbf..205eed0 100644 --- a/lib/rust/src/channels/options/receiver_options.rs +++ b/lib/rust/src/channels/definitions/receiver_options.rs @@ -1,14 +1,9 @@ -use crate::{ - receiver::ChannelReceiver, - tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}, - TetherAgent, -}; +use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; -use super::ChannelOptions; +use super::{definitions::ChannelReceiverDefinition, ChannelOptions}; use log::*; -use serde::Deserialize; -pub struct ChannelReceiverOptionsBuilder { +pub struct ChannelReceiverOptions { channel_name: String, qos: Option, override_subscribe_role: Option, @@ -17,9 +12,9 @@ pub struct ChannelReceiverOptionsBuilder { override_topic: Option, } -impl ChannelOptions for ChannelReceiverOptionsBuilder { +impl ChannelOptions for ChannelReceiverOptions { fn new(name: &str) -> Self { - ChannelReceiverOptionsBuilder { + ChannelReceiverOptions { channel_name: String::from(name), override_subscribe_id: None, override_subscribe_role: None, @@ -30,7 +25,7 @@ impl ChannelOptions for ChannelReceiverOptionsBuilder { } fn qos(self, qos: Option) -> Self { - ChannelReceiverOptionsBuilder { qos, ..self } + ChannelReceiverOptions { qos, ..self } } fn role(self, role: Option<&str>) -> Self { @@ -39,7 +34,7 @@ impl ChannelOptions for ChannelReceiverOptionsBuilder { self } else { let override_subscribe_role = role.map(|s| s.into()); - ChannelReceiverOptionsBuilder { + ChannelReceiverOptions { override_subscribe_role, ..self } @@ -52,7 +47,7 @@ impl ChannelOptions for ChannelReceiverOptionsBuilder { self } else { let override_subscribe_id = id.map(|s| s.into()); - ChannelReceiverOptionsBuilder { + ChannelReceiverOptions { override_subscribe_id, ..self } @@ -66,7 +61,7 @@ impl ChannelOptions for ChannelReceiverOptionsBuilder { } if override_channel_name.is_some() { let override_subscribe_channel_name = override_channel_name.map(|s| s.into()); - ChannelReceiverOptionsBuilder { + ChannelReceiverOptions { override_subscribe_channel_name, ..self } @@ -89,12 +84,12 @@ impl ChannelOptions for ChannelReceiverOptionsBuilder { t ); } - ChannelReceiverOptionsBuilder { + ChannelReceiverOptions { override_topic: Some(t.into()), ..self } } - None => ChannelReceiverOptionsBuilder { + None => ChannelReceiverOptions { override_topic: None, ..self }, @@ -102,18 +97,15 @@ impl ChannelOptions for ChannelReceiverOptionsBuilder { } } -impl ChannelReceiverOptionsBuilder { +impl ChannelReceiverOptions { pub fn any_channel(self) -> Self { - ChannelReceiverOptionsBuilder { + ChannelReceiverOptions { override_subscribe_channel_name: Some("+".into()), ..self } } - pub fn build<'a, T: Deserialize<'a>>( - self, - tether_agent: &'a mut TetherAgent, - ) -> anyhow::Result> { + pub fn build(self) -> ChannelReceiverDefinition { let tpt: TetherOrCustomTopic = match self.override_topic { Some(custom) => TetherOrCustomTopic::Custom(custom), None => { @@ -133,6 +125,12 @@ impl ChannelReceiverOptionsBuilder { )) } }; - ChannelReceiver::new(tether_agent, &self.channel_name, tpt, self.qos) + + ChannelReceiverDefinition { + name: self.channel_name, + generated_topic: tpt.full_topic_string(), + topic: tpt, + qos: self.qos.unwrap_or(1), + } } } diff --git a/lib/rust/src/channels/options/sender_options.rs b/lib/rust/src/channels/definitions/sender_options.rs similarity index 79% rename from lib/rust/src/channels/options/sender_options.rs rename to lib/rust/src/channels/definitions/sender_options.rs index 8079c3d..df60fdb 100644 --- a/lib/rust/src/channels/options/sender_options.rs +++ b/lib/rust/src/channels/definitions/sender_options.rs @@ -1,16 +1,23 @@ use crate::{ - sender::ChannelSender, tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}, TetherAgent, }; -use super::{ChannelOptions, ChannelSenderOptionsBuilder}; +use super::{definitions::ChannelSenderDefinition, ChannelOptions}; use log::*; -use serde::Serialize; -impl ChannelOptions for ChannelSenderOptionsBuilder { +pub struct ChannelSenderOptions { + channel_name: String, + qos: Option, + override_publish_role: Option, + override_publish_id: Option, + override_topic: Option, + retain: Option, +} + +impl ChannelOptions for ChannelSenderOptions { fn new(name: &str) -> Self { - ChannelSenderOptionsBuilder { + ChannelSenderOptions { channel_name: String::from(name), override_publish_id: None, override_publish_role: None, @@ -21,7 +28,7 @@ impl ChannelOptions for ChannelSenderOptionsBuilder { } fn qos(self, qos: Option) -> Self { - ChannelSenderOptionsBuilder { qos, ..self } + ChannelSenderOptions { qos, ..self } } fn role(self, role: Option<&str>) -> Self { @@ -30,7 +37,7 @@ impl ChannelOptions for ChannelSenderOptionsBuilder { self } else { let override_publish_role = role.map(|s| s.into()); - ChannelSenderOptionsBuilder { + ChannelSenderOptions { override_publish_role, ..self } @@ -43,7 +50,7 @@ impl ChannelOptions for ChannelSenderOptionsBuilder { self } else { let override_publish_id = id.map(|s| s.into()); - ChannelSenderOptionsBuilder { + ChannelSenderOptions { override_publish_id, ..self } @@ -56,7 +63,7 @@ impl ChannelOptions for ChannelSenderOptionsBuilder { return self; } match override_channel_name { - Some(n) => ChannelSenderOptionsBuilder { + Some(n) => ChannelSenderOptions { channel_name: n.into(), ..self }, @@ -80,12 +87,12 @@ impl ChannelOptions for ChannelSenderOptionsBuilder { t ); } - ChannelSenderOptionsBuilder { + ChannelSenderOptions { override_topic: Some(t.into()), ..self } } - None => ChannelSenderOptionsBuilder { + None => ChannelSenderOptions { override_topic: None, ..self }, @@ -93,18 +100,15 @@ impl ChannelOptions for ChannelSenderOptionsBuilder { } } -impl ChannelSenderOptionsBuilder { +impl ChannelSenderOptions { pub fn retain(self, should_retain: Option) -> Self { - ChannelSenderOptionsBuilder { + ChannelSenderOptions { retain: should_retain, ..self } } - pub fn build( - self, - tether_agent: &TetherAgent, - ) -> anyhow::Result> { + pub fn build(self, tether_agent: &TetherAgent) -> ChannelSenderDefinition { let tpt: TetherOrCustomTopic = match self.override_topic { Some(custom) => { warn!( @@ -134,12 +138,12 @@ impl ChannelSenderOptionsBuilder { } }; - Ok(ChannelSender::new( - tether_agent, - &self.channel_name, - tpt, - self.qos, - self.retain, - )) + ChannelSenderDefinition { + name: self.channel_name, + generated_topic: tpt.full_topic_string(), + topic: tpt, + qos: self.qos.unwrap_or(1), + retain: self.retain.unwrap_or(false), + } } } diff --git a/lib/rust/src/channels/mod.rs b/lib/rust/src/channels/mod.rs index 9a7e20f..01f38d8 100644 --- a/lib/rust/src/channels/mod.rs +++ b/lib/rust/src/channels/mod.rs @@ -1,21 +1,8 @@ -pub mod options; +pub mod definitions; pub mod receiver; pub mod sender; pub mod tether_compliant_topic; -pub use options::*; - -use super::tether_compliant_topic::TetherOrCustomTopic; - -pub trait ChannelCommon<'a> { - fn name(&'a self) -> &'a str; - /// Return the generated topic string actually used by the Channel - fn generated_topic(&'a self) -> &'a str; - /// Return the custom or Tether-compliant topic - fn topic(&'a self) -> &'a TetherOrCustomTopic; - fn qos(&'a self) -> i32; -} - // pub enum TetherChannel { // ChannelReceiver(ChannelReceiver), // ChannelSender(ChannelSender), diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index 24f5e28..c36cafd 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -5,7 +5,10 @@ use serde::Deserialize; use crate::TetherAgent; -use super::{tether_compliant_topic::TetherOrCustomTopic, ChannelCommon}; +use super::{ + definitions::definitions::{ChannelDefinition, ChannelReceiverDefinition}, + tether_compliant_topic::TetherOrCustomTopic, +}; pub struct ChannelReceiver<'a, T: Deserialize<'a>> { name: String, @@ -15,7 +18,7 @@ pub struct ChannelReceiver<'a, T: Deserialize<'a>> { marker: std::marker::PhantomData, } -impl<'a, T: Deserialize<'a>> ChannelCommon<'a> for ChannelReceiver<'a, T> { +impl<'a, T: Deserialize<'a>> ChannelDefinition<'a> for ChannelReceiver<'a, T> { fn name(&self) -> &str { &self.name } @@ -51,16 +54,14 @@ impl<'a, T: Deserialize<'a>> ChannelCommon<'a> for ChannelReceiver<'a, T> { impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { pub fn new( tether_agent: &'a TetherAgent, - name: &str, - topic: TetherOrCustomTopic, - qos: Option, + definition: &ChannelReceiverDefinition, ) -> anyhow::Result> { - let topic_string = topic.full_topic_string(); + let topic_string = definition.topic().full_topic_string(); let channel = ChannelReceiver { - name: String::from(name), - topic, - qos: qos.unwrap_or(1), + name: String::from(definition.name()), + topic: definition.topic().clone(), + qos: definition.qos(), tether_agent, marker: std::marker::PhantomData, }; @@ -73,10 +74,10 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { if let Some(client) = &tether_agent.client { match client.subscribe(&topic_string, { - match qos { - Some(0) => rumqttc::QoS::AtMostOnce, - Some(1) => rumqttc::QoS::AtLeastOnce, - Some(2) => rumqttc::QoS::ExactlyOnce, + match definition.qos() { + 0 => rumqttc::QoS::AtMostOnce, + 1 => rumqttc::QoS::AtLeastOnce, + 2 => rumqttc::QoS::ExactlyOnce, _ => rumqttc::QoS::AtLeastOnce, } }) { diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index 6e610bc..bf76e37 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -3,7 +3,10 @@ use anyhow::anyhow; use rmp_serde::to_vec_named; use serde::Serialize; -use super::{tether_compliant_topic::TetherOrCustomTopic, ChannelCommon}; +use super::{ + definitions::definitions::{ChannelDefinition, ChannelSenderDefinition}, + tether_compliant_topic::TetherOrCustomTopic, +}; pub struct ChannelSender<'a, T: Serialize> { name: String, @@ -14,7 +17,7 @@ pub struct ChannelSender<'a, T: Serialize> { marker: std::marker::PhantomData, } -impl<'a, T: Serialize> ChannelCommon<'a> for ChannelSender<'a, T> { +impl<'a, T: Serialize> ChannelDefinition<'a> for ChannelSender<'a, T> { fn name(&'_ self) -> &'_ str { &self.name } @@ -38,16 +41,13 @@ impl<'a, T: Serialize> ChannelCommon<'a> for ChannelSender<'a, T> { impl<'a, T: Serialize> ChannelSender<'a, T> { pub fn new( tether_agent: &'a TetherAgent, - name: &str, - topic: TetherOrCustomTopic, - qos: Option, - retain: Option, + definition: &ChannelSenderDefinition, ) -> ChannelSender<'a, T> { ChannelSender { - name: String::from(name), - topic, - qos: qos.unwrap_or(1), - retain: retain.unwrap_or(false), + name: String::from(definition.name()), + topic: definition.topic().clone(), + qos: definition.qos(), + retain: definition.retain(), tether_agent, marker: std::marker::PhantomData, } diff --git a/lib/rust/src/channels/tether_compliant_topic.rs b/lib/rust/src/channels/tether_compliant_topic.rs index aabcf1b..0788292 100644 --- a/lib/rust/src/channels/tether_compliant_topic.rs +++ b/lib/rust/src/channels/tether_compliant_topic.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use crate::TetherAgent; -#[derive(Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub struct TetherCompliantTopic { role: String, id: Option, @@ -12,7 +12,7 @@ pub struct TetherCompliantTopic { full_topic: String, } -#[derive(Serialize, Deserialize, Debug)] +#[derive(Clone, Serialize, Deserialize, Debug)] pub enum TetherOrCustomTopic { Tether(TetherCompliantTopic), Custom(String), From a075f127c3e8b096c0fe558f4eaaa89aec14c95d Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Mon, 14 Apr 2025 16:48:31 +0200 Subject: [PATCH 13/21] options to construct Channels with name only or with proper definitions - both from tether_agent instance --- examples/rust/custom_options.rs | 40 ++++++++++++++++++------------- examples/rust/send.rs | 5 ++-- lib/rust/src/agent/mod.rs | 22 +++++++++++++---- lib/rust/src/channels/receiver.rs | 2 +- lib/rust/src/channels/sender.rs | 2 +- 5 files changed, 45 insertions(+), 26 deletions(-) diff --git a/examples/rust/custom_options.rs b/examples/rust/custom_options.rs index 940d465..f02b67d 100644 --- a/examples/rust/custom_options.rs +++ b/examples/rust/custom_options.rs @@ -1,8 +1,11 @@ use std::time::Duration; use tether_agent::{ - channels::options::ChannelOptions, receiver_options::ChannelReceiverOptions, ChannelDefinition, - ChannelSenderOptionsBuilder, TetherAgentOptionsBuilder, + definitions::{ + definitions::ChannelDefinition, receiver_options::ChannelReceiverOptions, + sender_options::ChannelSenderOptions, ChannelOptions, + }, + TetherAgentOptionsBuilder, }; fn main() { @@ -15,22 +18,25 @@ fn main() { .build() .expect("failed to create Tether Agent"); - let sender_channel = ChannelSenderOptionsBuilder::new("anOutput") + let sender_channel_def = ChannelSenderOptions::new("anOutput") .role(Some("pretendingToBeSomethingElse")) .qos(Some(2)) .retain(Some(true)) - .build(&mut tether_agent) - .expect("failed to create sender channel"); - let input_wildcard_channel = ChannelReceiverOptions::new("everything") + .build(&tether_agent); + + let sender_channel = tether_agent.create_sender_with_definition(sender_channel_def); + + let input_wildcard_channel_def = ChannelReceiverOptions::new("everything") .override_topic(Some("#")) - .build() - .expect("failed to create receiver channel"); + .build(); + let input_wildcard_channel = tether_agent + .create_receiver_with_definition::(input_wildcard_channel_def) + .expect("failed to create Channel Receiver"); - let input_customid_channel = ChannelReceiverOptions::new("someData") - .role(None) // i.e., just use default - .id(Some("specificIDonly")) - .build() - .expect("failed to create receiver channel"); + // let input_customid_channel_def = ChannelReceiverOptions::new("someData") + // .role(None) // i.e., just use default + // .id(Some("specificIDonly")) + // .build(); println!("Agent looks like this: {:?}", tether_agent.description()); let (role, id, _) = tether_agent.description(); @@ -41,10 +47,10 @@ fn main() { "wildcard input channel: {:?}", input_wildcard_channel.generated_topic() ); - println!( - "speific ID input channel: {:?}", - input_customid_channel.generated_topic() - ); + // println!( + // "speific ID input channel: {:?}", + // input_customid_channel_def.generated_topic() + // ); let payload = rmp_serde::to_vec::(&String::from("boo")).expect("failed to serialise payload"); diff --git a/examples/rust/send.rs b/examples/rust/send.rs index c64de96..ac54ce4 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -29,7 +29,7 @@ fn main() { info!("Created agent OK: {}, {}", role, id); let sender_definition = ChannelSenderOptions::new("values").build(&tether_agent); - let sender = tether_agent.create_sender(&sender_definition); + let sender = tether_agent.create_sender_with_definition(sender_definition); let test_struct = CustomStruct { id: 101, @@ -45,8 +45,7 @@ fn main() { sender.send(&another_struct).expect("failed to encode+send"); - let number_sender = tether_agent - .create_sender::(&ChannelSenderOptions::new("numbersOnly").build(&tether_agent)); + let number_sender = tether_agent.create_sender::("numbersOnly"); number_sender.send(8).expect("failed to send"); diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index 203f859..23e4517 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -11,6 +11,9 @@ use uuid::Uuid; use crate::definitions::definitions::{ ChannelDefinition, ChannelReceiverDefinition, ChannelSenderDefinition, }; +use crate::definitions::receiver_options::ChannelReceiverOptions; +use crate::definitions::sender_options::ChannelSenderOptions; +use crate::definitions::ChannelOptions; use crate::receiver::ChannelReceiver; use crate::sender::ChannelSender; use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; @@ -170,20 +173,31 @@ impl TetherAgentOptionsBuilder { } impl<'a> TetherAgent { - pub fn create_sender( + pub fn create_sender_with_definition( &self, - definition: &ChannelSenderDefinition, + definition: ChannelSenderDefinition, ) -> ChannelSender { ChannelSender::new(self, definition) } - pub fn create_receiver>( + pub fn create_sender(&self, name: &str) -> ChannelSender { + ChannelSender::new(self, ChannelSenderOptions::new(name).build(self)) + } + + pub fn create_receiver_with_definition>( &'a self, - definition: &ChannelReceiverDefinition, + definition: ChannelReceiverDefinition, ) -> anyhow::Result> { ChannelReceiver::new(&self, definition) } + pub fn create_receiver>( + &'a self, + name: &str, + ) -> anyhow::Result> { + ChannelReceiver::new(&self, ChannelReceiverOptions::new(name).build()) + } + pub fn is_connected(&self) -> bool { self.client.is_some() } diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index c36cafd..e650144 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -54,7 +54,7 @@ impl<'a, T: Deserialize<'a>> ChannelDefinition<'a> for ChannelReceiver<'a, T> { impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { pub fn new( tether_agent: &'a TetherAgent, - definition: &ChannelReceiverDefinition, + definition: ChannelReceiverDefinition, ) -> anyhow::Result> { let topic_string = definition.topic().full_topic_string(); diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index bf76e37..75bd332 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -41,7 +41,7 @@ impl<'a, T: Serialize> ChannelDefinition<'a> for ChannelSender<'a, T> { impl<'a, T: Serialize> ChannelSender<'a, T> { pub fn new( tether_agent: &'a TetherAgent, - definition: &ChannelSenderDefinition, + definition: ChannelSenderDefinition, ) -> ChannelSender<'a, T> { ChannelSender { name: String::from(definition.name()), From 64bdbdfcb86bf30d3615a2a8f20dfb2cef947f5a Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Mon, 14 Apr 2025 16:50:17 +0200 Subject: [PATCH 14/21] avoiding double-naming modules --- lib/rust/src/agent/mod.rs | 12 ++++++------ lib/rust/src/channels/mod.rs | 2 +- .../channels/{definitions => options}/definitions.rs | 0 .../src/channels/{definitions => options}/mod.rs | 0 .../{definitions => options}/receiver_options.rs | 0 .../{definitions => options}/sender_options.rs | 0 lib/rust/src/channels/receiver.rs | 2 +- lib/rust/src/channels/sender.rs | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) rename lib/rust/src/channels/{definitions => options}/definitions.rs (100%) rename lib/rust/src/channels/{definitions => options}/mod.rs (100%) rename lib/rust/src/channels/{definitions => options}/receiver_options.rs (100%) rename lib/rust/src/channels/{definitions => options}/sender_options.rs (100%) diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index 23e4517..b642ab7 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -8,12 +8,12 @@ use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use uuid::Uuid; -use crate::definitions::definitions::{ +use crate::options::definitions::{ ChannelDefinition, ChannelReceiverDefinition, ChannelSenderDefinition, }; -use crate::definitions::receiver_options::ChannelReceiverOptions; -use crate::definitions::sender_options::ChannelSenderOptions; -use crate::definitions::ChannelOptions; +use crate::options::receiver_options::ChannelReceiverOptions; +use crate::options::sender_options::ChannelSenderOptions; +use crate::options::ChannelOptions; use crate::receiver::ChannelReceiver; use crate::sender::ChannelSender; use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; @@ -188,14 +188,14 @@ impl<'a> TetherAgent { &'a self, definition: ChannelReceiverDefinition, ) -> anyhow::Result> { - ChannelReceiver::new(&self, definition) + ChannelReceiver::new(self, definition) } pub fn create_receiver>( &'a self, name: &str, ) -> anyhow::Result> { - ChannelReceiver::new(&self, ChannelReceiverOptions::new(name).build()) + ChannelReceiver::new(self, ChannelReceiverOptions::new(name).build()) } pub fn is_connected(&self) -> bool { diff --git a/lib/rust/src/channels/mod.rs b/lib/rust/src/channels/mod.rs index 01f38d8..d9a05b8 100644 --- a/lib/rust/src/channels/mod.rs +++ b/lib/rust/src/channels/mod.rs @@ -1,4 +1,4 @@ -pub mod definitions; +pub mod options; pub mod receiver; pub mod sender; pub mod tether_compliant_topic; diff --git a/lib/rust/src/channels/definitions/definitions.rs b/lib/rust/src/channels/options/definitions.rs similarity index 100% rename from lib/rust/src/channels/definitions/definitions.rs rename to lib/rust/src/channels/options/definitions.rs diff --git a/lib/rust/src/channels/definitions/mod.rs b/lib/rust/src/channels/options/mod.rs similarity index 100% rename from lib/rust/src/channels/definitions/mod.rs rename to lib/rust/src/channels/options/mod.rs diff --git a/lib/rust/src/channels/definitions/receiver_options.rs b/lib/rust/src/channels/options/receiver_options.rs similarity index 100% rename from lib/rust/src/channels/definitions/receiver_options.rs rename to lib/rust/src/channels/options/receiver_options.rs diff --git a/lib/rust/src/channels/definitions/sender_options.rs b/lib/rust/src/channels/options/sender_options.rs similarity index 100% rename from lib/rust/src/channels/definitions/sender_options.rs rename to lib/rust/src/channels/options/sender_options.rs diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index e650144..6e2ea4d 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -6,7 +6,7 @@ use serde::Deserialize; use crate::TetherAgent; use super::{ - definitions::definitions::{ChannelDefinition, ChannelReceiverDefinition}, + options::definitions::{ChannelDefinition, ChannelReceiverDefinition}, tether_compliant_topic::TetherOrCustomTopic, }; diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index 75bd332..3eae23c 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -4,7 +4,7 @@ use rmp_serde::to_vec_named; use serde::Serialize; use super::{ - definitions::definitions::{ChannelDefinition, ChannelSenderDefinition}, + options::definitions::{ChannelDefinition, ChannelSenderDefinition}, tether_compliant_topic::TetherOrCustomTopic, }; From 871ad6c0506db5165ed50219c6e9c27a23e3e9b6 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Mon, 14 Apr 2025 17:39:25 +0200 Subject: [PATCH 15/21] WIP trying automatic check messages ON channel --- examples/rust/custom_options.rs | 2 +- examples/rust/send.rs | 2 +- lib/rust/src/agent/mod.rs | 2 +- lib/rust/src/channels/receiver.rs | 42 ++++++++++++++++++++++++++----- tether-utils/Cargo.toml | 2 +- 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/examples/rust/custom_options.rs b/examples/rust/custom_options.rs index f02b67d..468664e 100644 --- a/examples/rust/custom_options.rs +++ b/examples/rust/custom_options.rs @@ -1,7 +1,7 @@ use std::time::Duration; use tether_agent::{ - definitions::{ + options::{ definitions::ChannelDefinition, receiver_options::ChannelReceiverOptions, sender_options::ChannelSenderOptions, ChannelOptions, }, diff --git a/examples/rust/send.rs b/examples/rust/send.rs index ac54ce4..ca9ca85 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -4,7 +4,7 @@ use env_logger::{Builder, Env}; use log::{debug, info}; use serde::Serialize; use tether_agent::{ - definitions::{sender_options::ChannelSenderOptions, ChannelOptions}, + options::{sender_options::ChannelSenderOptions, ChannelOptions}, TetherAgentOptionsBuilder, }; diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index b642ab7..d641d4c 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -34,7 +34,7 @@ pub struct TetherAgent { mqtt_client_id: Option, pub(crate) client: Option, message_sender: mpsc::Sender<(TetherOrCustomTopic, Vec)>, - message_receiver: mpsc::Receiver<(TetherOrCustomTopic, Vec)>, + pub message_receiver: mpsc::Receiver<(TetherOrCustomTopic, Vec)>, is_connected: Arc>, auto_connect_enabled: bool, } diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index 6e2ea4d..5053260 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -10,15 +10,15 @@ use super::{ tether_compliant_topic::TetherOrCustomTopic, }; -pub struct ChannelReceiver<'a, T: Deserialize<'a>> { +pub struct ChannelReceiver<'a, 'de, T: Deserialize<'de>> { name: String, topic: TetherOrCustomTopic, qos: i32, tether_agent: &'a TetherAgent, - marker: std::marker::PhantomData, + marker: std::marker::PhantomData<&'de T>, } -impl<'a, T: Deserialize<'a>> ChannelDefinition<'a> for ChannelReceiver<'a, T> { +impl<'a, 'de, T: Deserialize<'de>> ChannelDefinition<'a> for ChannelReceiver<'a, 'de, T> { fn name(&self) -> &str { &self.name } @@ -51,11 +51,11 @@ impl<'a, T: Deserialize<'a>> ChannelDefinition<'a> for ChannelReceiver<'a, T> { } } -impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { +impl<'a, 'de, T: Deserialize<'de>> ChannelReceiver<'a, 'de, T> { pub fn new( tether_agent: &'a TetherAgent, definition: ChannelReceiverDefinition, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let topic_string = definition.topic().full_topic_string(); let channel = ChannelReceiver { @@ -155,7 +155,7 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { } } - pub fn parse(&self, incoming_topic: &TetherOrCustomTopic, payload: &'a [u8]) -> Option { + pub fn parse(&self, incoming_topic: &TetherOrCustomTopic, payload: &'de [u8]) -> Option { if self.matches(incoming_topic) { match from_slice::(payload) { Ok(msg) => Some(msg), @@ -168,4 +168,34 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { None } } + + pub fn check(&self, on_message: fn(decoded: Option)) { + // if let Ok((topic, payload)) = self.tether_agent.message_receiver.try_recv() { + // let decoded = rmp_serde::from_slice::(&payload); + // // match rmp_serde::from_slice::(&incoming_payload) { + // // Ok(msg) => on_message(Some(msg)), + // // Err(e) => { + // // error!( + // // "Failure parsing message on Channel Receiver \"{}\": {}", + // // &self.name, e + // // ); + // // on_message(None) + // // } + // // } + // } + if let Some((_topic, incoming_payload)) = self.tether_agent.check_messages() { + let cp = incoming_payload.clone(); + let decoded = from_slice::(&cp); + // match rmp_serde::from_slice::(&incoming_payload) { + // Ok(msg) => on_message(Some(msg)), + // Err(e) => { + // error!( + // "Failure parsing message on Channel Receiver \"{}\": {}", + // &self.name, e + // ); + // on_message(None) + // } + // } + } + } } diff --git a/tether-utils/Cargo.toml b/tether-utils/Cargo.toml index a17dbce..18cf334 100644 --- a/tether-utils/Cargo.toml +++ b/tether-utils/Cargo.toml @@ -2,7 +2,7 @@ name = "tether-utils" description = "Utilities for Tether Systems" version = "1.0.3-alpha" -edition = "2021" +edition = "2024" license = "MIT" repository = "https://github.com/RandomStudio/tether" homepage = "https://github.com/RandomStudio/tether" From 16c80f31f0197ca200b0f26c6bc1e0b6b815f74d Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Thu, 17 Apr 2025 11:10:54 +0200 Subject: [PATCH 16/21] Channel Senders are not properly typed (compiler does not catch errors) --- examples/rust/custom_options.rs | 4 +- examples/rust/receive.rs | 4 +- examples/rust/send.rs | 14 +++--- lib/rust/src/agent/mod.rs | 6 +-- lib/rust/src/channels/receiver.rs | 44 +++---------------- .../src/channels/tether_compliant_topic.rs | 4 +- 6 files changed, 22 insertions(+), 54 deletions(-) diff --git a/examples/rust/custom_options.rs b/examples/rust/custom_options.rs index 468664e..499883f 100644 --- a/examples/rust/custom_options.rs +++ b/examples/rust/custom_options.rs @@ -5,11 +5,11 @@ use tether_agent::{ definitions::ChannelDefinition, receiver_options::ChannelReceiverOptions, sender_options::ChannelSenderOptions, ChannelOptions, }, - TetherAgentOptionsBuilder, + TetherAgentBuilder, }; fn main() { - let mut tether_agent = TetherAgentOptionsBuilder::new("example") + let tether_agent = TetherAgentBuilder::new("example") .id(None) .host(Some("localhost")) .port(Some(1883)) diff --git a/examples/rust/receive.rs b/examples/rust/receive.rs index 73686a1..3369075 100644 --- a/examples/rust/receive.rs +++ b/examples/rust/receive.rs @@ -1,7 +1,7 @@ use env_logger::{Builder, Env}; use log::*; use serde::Deserialize; -use tether_agent::TetherAgentOptionsBuilder; +use tether_agent::TetherAgentBuilder; #[allow(dead_code)] #[derive(Deserialize, Debug)] @@ -22,7 +22,7 @@ fn main() { debug!("Debugging is enabled; could be verbose"); - let tether_agent = TetherAgentOptionsBuilder::new("RustDemo") + let tether_agent = TetherAgentBuilder::new("RustDemo") .id(Some("example")) .build() .expect("failed to init Tether agent"); diff --git a/examples/rust/send.rs b/examples/rust/send.rs index ca9ca85..1b8c738 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -5,7 +5,7 @@ use log::{debug, info}; use serde::Serialize; use tether_agent::{ options::{sender_options::ChannelSenderOptions, ChannelOptions}, - TetherAgentOptionsBuilder, + TetherAgentBuilder, }; #[derive(Serialize)] @@ -22,7 +22,7 @@ fn main() { debug!("Debugging is enabled; could be verbose"); - let tether_agent = TetherAgentOptionsBuilder::new("rustExample") + let tether_agent = TetherAgentBuilder::new("rustExample") .build() .expect("failed to connect Tether"); let (role, id, _) = tether_agent.description(); @@ -31,11 +31,11 @@ fn main() { let sender_definition = ChannelSenderOptions::new("values").build(&tether_agent); let sender = tether_agent.create_sender_with_definition(sender_definition); - let test_struct = CustomStruct { - id: 101, - name: "something".into(), - }; - let payload = rmp_serde::to_vec_named(&test_struct).expect("failed to serialize"); + // let test_struct = CustomStruct { + // id: 101, + // name: "something".into(), + // }; + let payload = rmp_serde::to_vec_named(&101).expect("failed to serialize"); sender.send_raw(&payload).expect("failed to send"); let another_struct = CustomStruct { diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index d641d4c..fe82aea 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -40,7 +40,7 @@ pub struct TetherAgent { } #[derive(Clone)] -pub struct TetherAgentOptionsBuilder { +pub struct TetherAgentBuilder { role: String, id: Option, protocol: Option, @@ -53,11 +53,11 @@ pub struct TetherAgentOptionsBuilder { mqtt_client_id: Option, } -impl TetherAgentOptionsBuilder { +impl TetherAgentBuilder { /// Initialise Tether Options struct with default options; call other methods to customise. /// Call `build()` to get the actual TetherAgent instance (and connect automatically, by default) pub fn new(role: &str) -> Self { - TetherAgentOptionsBuilder { + TetherAgentBuilder { role: String::from(role), id: None, protocol: None, diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index 5053260..73e31d3 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -10,15 +10,14 @@ use super::{ tether_compliant_topic::TetherOrCustomTopic, }; -pub struct ChannelReceiver<'a, 'de, T: Deserialize<'de>> { +pub struct ChannelReceiver<'a, T: Deserialize<'a>> { name: String, topic: TetherOrCustomTopic, qos: i32, - tether_agent: &'a TetherAgent, - marker: std::marker::PhantomData<&'de T>, + marker: std::marker::PhantomData<&'a T>, } -impl<'a, 'de, T: Deserialize<'de>> ChannelDefinition<'a> for ChannelReceiver<'a, 'de, T> { +impl<'a, T: Deserialize<'a>> ChannelDefinition<'a> for ChannelReceiver<'a, T> { fn name(&self) -> &str { &self.name } @@ -51,18 +50,17 @@ impl<'a, 'de, T: Deserialize<'de>> ChannelDefinition<'a> for ChannelReceiver<'a, } } -impl<'a, 'de, T: Deserialize<'de>> ChannelReceiver<'a, 'de, T> { +impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { pub fn new( tether_agent: &'a TetherAgent, definition: ChannelReceiverDefinition, - ) -> anyhow::Result> { + ) -> anyhow::Result> { let topic_string = definition.topic().full_topic_string(); let channel = ChannelReceiver { name: String::from(definition.name()), topic: definition.topic().clone(), qos: definition.qos(), - tether_agent, marker: std::marker::PhantomData, }; @@ -155,7 +153,7 @@ impl<'a, 'de, T: Deserialize<'de>> ChannelReceiver<'a, 'de, T> { } } - pub fn parse(&self, incoming_topic: &TetherOrCustomTopic, payload: &'de [u8]) -> Option { + pub fn parse(&self, incoming_topic: &TetherOrCustomTopic, payload: &'a [u8]) -> Option { if self.matches(incoming_topic) { match from_slice::(payload) { Ok(msg) => Some(msg), @@ -168,34 +166,4 @@ impl<'a, 'de, T: Deserialize<'de>> ChannelReceiver<'a, 'de, T> { None } } - - pub fn check(&self, on_message: fn(decoded: Option)) { - // if let Ok((topic, payload)) = self.tether_agent.message_receiver.try_recv() { - // let decoded = rmp_serde::from_slice::(&payload); - // // match rmp_serde::from_slice::(&incoming_payload) { - // // Ok(msg) => on_message(Some(msg)), - // // Err(e) => { - // // error!( - // // "Failure parsing message on Channel Receiver \"{}\": {}", - // // &self.name, e - // // ); - // // on_message(None) - // // } - // // } - // } - if let Some((_topic, incoming_payload)) = self.tether_agent.check_messages() { - let cp = incoming_payload.clone(); - let decoded = from_slice::(&cp); - // match rmp_serde::from_slice::(&incoming_payload) { - // Ok(msg) => on_message(Some(msg)), - // Err(e) => { - // error!( - // "Failure parsing message on Channel Receiver \"{}\": {}", - // &self.name, e - // ); - // on_message(None) - // } - // } - } - } } diff --git a/lib/rust/src/channels/tether_compliant_topic.rs b/lib/rust/src/channels/tether_compliant_topic.rs index 0788292..36f24c5 100644 --- a/lib/rust/src/channels/tether_compliant_topic.rs +++ b/lib/rust/src/channels/tether_compliant_topic.rs @@ -189,7 +189,7 @@ pub fn parse_agent_role(topic: &str) -> Option<&str> { mod tests { use crate::{ tether_compliant_topic::{parse_agent_id, parse_agent_role, parse_channel_name}, - TetherAgentOptionsBuilder, + TetherAgentBuilder, }; use super::TetherCompliantTopic; @@ -204,7 +204,7 @@ mod tests { #[test] fn build_full_topic() { - let agent = TetherAgentOptionsBuilder::new("testingRole") + let agent = TetherAgentBuilder::new("testingRole") .auto_connect(false) .build() .expect("failed to construct agent"); From 7ed5a7dfbdb611a4f143538c0778cadef22b8a87 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Thu, 17 Apr 2025 11:28:09 +0200 Subject: [PATCH 17/21] Sending channels are properly type-checked --- examples/rust/receive.rs | 2 +- examples/rust/send.rs | 10 +++++++--- lib/rust/src/channels/receiver.rs | 5 ++++- lib/rust/src/channels/sender.rs | 2 +- 4 files changed, 13 insertions(+), 6 deletions(-) diff --git a/examples/rust/receive.rs b/examples/rust/receive.rs index 3369075..e034433 100644 --- a/examples/rust/receive.rs +++ b/examples/rust/receive.rs @@ -32,7 +32,7 @@ fn main() { .expect("failed to create receiver"); let receiver_of_custom_structs = tether_agent - .create_receiver::("values") + .create_receiver::("customStructs") .expect("failed to create receiver"); loop { diff --git a/examples/rust/send.rs b/examples/rust/send.rs index 1b8c738..997961d 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -28,8 +28,8 @@ fn main() { let (role, id, _) = tether_agent.description(); info!("Created agent OK: {}, {}", role, id); - let sender_definition = ChannelSenderOptions::new("values").build(&tether_agent); - let sender = tether_agent.create_sender_with_definition(sender_definition); + let sender_definition = ChannelSenderOptions::new("customStructs").build(&tether_agent); + let sender = tether_agent.create_sender_with_definition::(sender_definition); // let test_struct = CustomStruct { // id: 101, @@ -43,11 +43,15 @@ fn main() { name: "auto encoded".into(), }; + // Notice how the line below will produce a compiler error, whereas sender.send_raw for the + // exact same payload (101) is fine, because .send_raw is not type-checked. + // sender.send(&101).expect("failed to encode+send"); + sender.send(&another_struct).expect("failed to encode+send"); let number_sender = tether_agent.create_sender::("numbersOnly"); - number_sender.send(8).expect("failed to send"); + number_sender.send(&8).expect("failed to send"); std::thread::sleep(Duration::from_millis(3000)); } diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index 73e31d3..78eac05 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -158,7 +158,10 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { match from_slice::(payload) { Ok(msg) => Some(msg), Err(e) => { - error!("Failed to parse message: {}", e); + error!( + "Failed to parse message on channel \"{}\": {}", + &self.name, e + ); None } } diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index 3eae23c..3bcab37 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -73,7 +73,7 @@ impl<'a, T: Serialize> ChannelSender<'a, T> { } } - pub fn send(&self, payload: T) -> anyhow::Result<()> { + pub fn send(&self, payload: &T) -> anyhow::Result<()> { match to_vec_named(&payload) { Ok(data) => self.send_raw(&data), Err(e) => Err(e.into()), From c617d094a03bcc7ad00891f60c8ccacc15b4ef6b Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Thu, 17 Apr 2025 13:08:21 +0200 Subject: [PATCH 18/21] Removed a lot of redundancy between "definitions", "channels" and the TetherAgent itself --- Tether4.md | 15 ++ examples/rust/custom_options.rs | 16 +- examples/rust/send.rs | 7 +- lib/rust/src/agent/mod.rs | 158 +++++++++++++----- .../channels/{options => definitions}/mod.rs | 88 +++++++++- .../receiver_builder.rs} | 25 +-- .../sender_builder.rs} | 25 +-- lib/rust/src/channels/mod.rs | 2 +- lib/rust/src/channels/options/definitions.rs | 69 -------- lib/rust/src/channels/receiver.rs | 63 ++----- lib/rust/src/channels/sender.rs | 65 +------ lib/rust/src/lib.rs | 1 + 12 files changed, 273 insertions(+), 261 deletions(-) rename lib/rust/src/channels/{options => definitions}/mod.rs (86%) rename lib/rust/src/channels/{options/receiver_options.rs => definitions/receiver_builder.rs} (89%) rename lib/rust/src/channels/{options/sender_options.rs => definitions/sender_builder.rs} (90%) delete mode 100644 lib/rust/src/channels/options/definitions.rs diff --git a/Tether4.md b/Tether4.md index d659834..7a60bce 100644 --- a/Tether4.md +++ b/Tether4.md @@ -49,3 +49,18 @@ Apart from the terminology changes, the following are important to note: ## Rust changes Apart from the terminology changes, the following are important to note: - `agent.send` used to assume an already-encoded payload, while `.encode_and_send` did auto-encoding. Now, `.send` is the auto-encoding version and additional `.send_raw` and `.send_empty` functions are provided. It is VERY important that the new `.send` will actually (incorrectly!) accept already-encoded payloads, because `&[u8]` is ALSO `T: Serialize`! So applications using the new version must be carefully checked to ensure that things are not (double) encoded before sending! + +The term "OptionsBuilder" suffix has now been replaced with the much simpler "Builder", so we have simply: +- TetherAgentBuilder +- ChannelSenderBuilder +- ChannelReceiverBuilder + +Even better, the ChannelSenderBuilder/ChannelReceiver builder do not **have** to be used in all cases, since both ChannelSender and ChannelReceiver objects can be constructed via the Tether Agent object itself, i.e. + +- `tether_agent::create_sender` +- `tether_agent::create_receiver` + +All that needs to be provided, in the default cases, is the name and the type. For example: +- `tether_agent.create_sender::("numbersOnly")` creates a ChannelSender called "numbersOnly" which will automatically expect (require) u8 payloads + +Arguably, the TypeScript library should work in a similar way! diff --git a/examples/rust/custom_options.rs b/examples/rust/custom_options.rs index 499883f..8362d27 100644 --- a/examples/rust/custom_options.rs +++ b/examples/rust/custom_options.rs @@ -1,12 +1,6 @@ use std::time::Duration; -use tether_agent::{ - options::{ - definitions::ChannelDefinition, receiver_options::ChannelReceiverOptions, - sender_options::ChannelSenderOptions, ChannelOptions, - }, - TetherAgentBuilder, -}; +use tether_agent::{definitions::*, TetherAgentBuilder}; fn main() { let tether_agent = TetherAgentBuilder::new("example") @@ -18,7 +12,7 @@ fn main() { .build() .expect("failed to create Tether Agent"); - let sender_channel_def = ChannelSenderOptions::new("anOutput") + let sender_channel_def = ChannelSenderBuilder::new("anOutput") .role(Some("pretendingToBeSomethingElse")) .qos(Some(2)) .retain(Some(true)) @@ -26,7 +20,7 @@ fn main() { let sender_channel = tether_agent.create_sender_with_definition(sender_channel_def); - let input_wildcard_channel_def = ChannelReceiverOptions::new("everything") + let input_wildcard_channel_def = ChannelReceiverBuilder::new("everything") .override_topic(Some("#")) .build(); let input_wildcard_channel = tether_agent @@ -45,7 +39,7 @@ fn main() { println!( "wildcard input channel: {:?}", - input_wildcard_channel.generated_topic() + input_wildcard_channel.definition().generated_topic() ); // println!( // "speific ID input channel: {:?}", @@ -55,7 +49,7 @@ fn main() { let payload = rmp_serde::to_vec::(&String::from("boo")).expect("failed to serialise payload"); tether_agent - .send(&sender_channel, Some(&payload)) + .send(&sender_channel, &payload) .expect("failed to publish"); std::thread::sleep(Duration::from_millis(4000)); diff --git a/examples/rust/send.rs b/examples/rust/send.rs index 997961d..8df5af0 100644 --- a/examples/rust/send.rs +++ b/examples/rust/send.rs @@ -3,10 +3,7 @@ use std::time::Duration; use env_logger::{Builder, Env}; use log::{debug, info}; use serde::Serialize; -use tether_agent::{ - options::{sender_options::ChannelSenderOptions, ChannelOptions}, - TetherAgentBuilder, -}; +use tether_agent::*; #[derive(Serialize)] #[serde(rename_all = "camelCase")] @@ -28,7 +25,7 @@ fn main() { let (role, id, _) = tether_agent.description(); info!("Created agent OK: {}, {}", role, id); - let sender_definition = ChannelSenderOptions::new("customStructs").build(&tether_agent); + let sender_definition = ChannelSenderBuilder::new("customStructs").build(&tether_agent); let sender = tether_agent.create_sender_with_definition::(sender_definition); // let test_struct = CustomStruct { diff --git a/lib/rust/src/agent/mod.rs b/lib/rust/src/agent/mod.rs index fe82aea..ff2dd20 100644 --- a/lib/rust/src/agent/mod.rs +++ b/lib/rust/src/agent/mod.rs @@ -8,12 +8,10 @@ use std::sync::{Arc, Mutex}; use std::{sync::mpsc, thread, time::Duration}; use uuid::Uuid; -use crate::options::definitions::{ - ChannelDefinition, ChannelReceiverDefinition, ChannelSenderDefinition, -}; -use crate::options::receiver_options::ChannelReceiverOptions; -use crate::options::sender_options::ChannelSenderOptions; -use crate::options::ChannelOptions; +use crate::definitions::receiver_builder::ChannelReceiverBuilder; +use crate::definitions::sender_builder::ChannelSenderBuilder; +use crate::definitions::ChannelBuilder; +use crate::definitions::{ChannelDefinition, ChannelReceiverDefinition, ChannelSenderDefinition}; use crate::receiver::ChannelReceiver; use crate::sender::ChannelSender; use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; @@ -22,6 +20,22 @@ const TIMEOUT_SECONDS: u64 = 3; const DEFAULT_USERNAME: &str = "tether"; const DEFAULT_PASSWORD: &str = "sp_ceB0ss!"; +/** +A Tether Agent struct encapsulates everything required to set up a single +"Agent" as part of your Tether-based system. The only thing absolutely required is +a "role" - everything else is optional and sensible defaults will be used when +not explicitly specified. + +By default, the Agent will connect (automatically) to an MQTT Broker on localhost:1883 + +It will **not** have an ID, and therefore publishing/subscribing topics will not append anything +this into the topic string when ChannelSender and ChannelReceiver instances are created using +this Tether Agent instance, unless explicitly provided on creation. + +Note that you should typically not construct a new TetherAgent instance yourself; rather +use the provided TetherAgentBuilder to specify any options you might need, and call +.build to get a well-configured TetherAgent. +*/ pub struct TetherAgent { role: String, id: Option, @@ -39,6 +53,13 @@ pub struct TetherAgent { auto_connect_enabled: bool, } +/** +Typically, you will use this to construct a well-configured TetherAgent with a combination +of sensible defaults and custom overrides. + +Make a new instance of TetherAgentBuilder with `TetherAgentBuilder::new()`, chain whatever +overrides you might need, and finally call `build()` to get the actual TetherAgent instance. +*/ #[derive(Clone)] pub struct TetherAgentBuilder { role: String, @@ -123,11 +144,20 @@ impl TetherAgentBuilder { self } + /// Specify explicitly whether to attempt auto-connection on build; + /// if set to `false` you will need to connect the TetherAgent (and therefore + /// its underlying MQTT client) yourself after creating the instance. pub fn auto_connect(mut self, should_auto_connect: bool) -> Self { self.auto_connect = should_auto_connect; self } + /// Using a combination of sensible defaults and any overrides you might + /// have provided using other functions called on TetherAgentOptions, this + /// function returns a well-configured TetherAgent instance. + /// + /// Unless you set `.auto_connect(false)`, the TetherAgent will attempt to + /// connect to the MQTT broker automatically upon creation. pub fn build(self) -> anyhow::Result { let protocol = self.protocol.clone().unwrap_or("mqtt".into()); let host = self.host.clone().unwrap_or("localhost".into()); @@ -173,6 +203,17 @@ impl TetherAgentBuilder { } impl<'a> TetherAgent { + /// The simplest way to create a ChannelSender. + /// + /// You provide only a Channel Name; + /// configuration derived from your Tether Agent instance is used to construct + /// the appropriate publishing topics. + pub fn create_sender(&self, name: &str) -> ChannelSender { + ChannelSender::new(self, ChannelSenderBuilder::new(name).build(self)) + } + + /// Create a ChannelSender instance using a ChannelSenderDefinition already constructed + /// elsewhere. pub fn create_sender_with_definition( &self, definition: ChannelSenderDefinition, @@ -180,10 +221,22 @@ impl<'a> TetherAgent { ChannelSender::new(self, definition) } - pub fn create_sender(&self, name: &str) -> ChannelSender { - ChannelSender::new(self, ChannelSenderOptions::new(name).build(self)) + /// The simplest way to create a Channel Receiver. + /// + /// You provide only a Channel Name; + /// configuration derived from your Tether Agent instance is used to construct + /// the appropriate subscribing topics. + /// + /// The actual subscription is also initiated automatically. + pub fn create_receiver>( + &'a self, + name: &str, + ) -> anyhow::Result> { + ChannelReceiver::new(self, ChannelReceiverBuilder::new(name).build()) } + /// Create a ChannelReceiver instance using a ChannelReceiverDefinition already constructed + /// elsewhere. pub fn create_receiver_with_definition>( &'a self, definition: ChannelReceiverDefinition, @@ -191,13 +244,6 @@ impl<'a> TetherAgent { ChannelReceiver::new(self, definition) } - pub fn create_receiver>( - &'a self, - name: &str, - ) -> anyhow::Result> { - ChannelReceiver::new(self, ChannelReceiverOptions::new(name).build()) - } - pub fn is_connected(&self) -> bool { self.client.is_some() } @@ -235,15 +281,25 @@ impl<'a> TetherAgent { ) } + /// Change the role, even if it was set before. Be careful _when_ you call this, + /// as it could affect any new Channel Senders/Receivers created after that point. pub fn set_role(&mut self, role: &str) { self.role = role.into(); } + /// Change the ID, even if it was set (or left empty) before. + /// Be careful _when_ you call this, + /// as it could affect any new Channel Senders/Receivers created after that point. pub fn set_id(&mut self, id: &str) { self.id = Some(id.into()); } - /// Self must be mutable in order to create and assign new Client (with Connection) + /// Use this function yourself **only if you explicitly disallowed auto connection**. + /// Otherwise, this function is called automatically as part of the `.build` process. + /// + /// This function spawns a separate thread for polling the MQTT broker. Any events + /// and messages are relayed via mpsc channels internally; for example, you will call + /// `.check_messages()` to see if any messages were received and are waiting to be parsed. pub fn connect(&mut self) -> anyhow::Result<()> { info!( "Make new connection to the MQTT server at {}://{}:{}...", @@ -400,7 +456,9 @@ impl<'a> TetherAgent { Ok(()) } - /// If a message is waiting return ThreePartTopic, Message (String, Message) + /// If a message is waiting to be parsed by your application, + /// this function will return Topic, Message, i.e. `(TetherOrCustomTopic, Message)` + /// /// Messages received on topics that are not parseable as Tether Three Part Topics will be returned with /// the complete Topic string instead pub fn check_messages(&self) -> Option<(TetherOrCustomTopic, Vec)> { @@ -415,13 +473,39 @@ impl<'a> TetherAgent { } } - /// Unlike .send, this function does NOT serialize the data before publishing. + /// Typically called via the Channel Sender itself. + /// + /// This function serializes the data (using Serde/MessagePack) automatically before publishing. /// - /// Given a channel definition and a raw (u8 buffer) payload, publishes a message + /// Given a Channel Sender and serializeable data payload, publishes a message + /// using an appropriate topic and with the QOS specified in the Channel Definition. + /// + /// Note that this function requires that the data payload be the same type as + /// the Channel Sender, so it will return an Error if the types do not match. + pub fn send( + &self, + channel_sender: &ChannelSender, + data: &T, + ) -> anyhow::Result<()> { + match to_vec_named(&data) { + Ok(payload) => self.send_raw(channel_sender.definition(), Some(&payload)), + Err(e) => { + error!("Failed to encode: {e:?}"); + Err(e.into()) + } + } + } + + /// Typically called via the Channel Sender itself. + /// + /// Unlike .send, this function does NOT serialize the data before publishing. It therefore + /// does no type checking of the payload. + /// + /// Given a Channel Sender and a raw (u8 buffer) payload, publishes a message /// using an appropriate topic and with the QOS specified in the Channel Definition - pub fn send_raw( + pub fn send_raw( &self, - channel_definition: &ChannelSender, + channel_definition: &ChannelSenderDefinition, payload: Option<&[u8]>, ) -> anyhow::Result<()> { let topic = channel_definition.generated_topic(); @@ -448,34 +532,16 @@ impl<'a> TetherAgent { } } - /// Serializes the data automatically before publishing. - /// - /// Given a channel definition and serializeable data payload, publishes a message - /// using an appropriate topic and with the QOS specified in the Channel Definition - pub fn send( - &self, - channel_definition: &ChannelSender, - data: T, - ) -> anyhow::Result<()> { - match to_vec_named(&data) { - Ok(payload) => self.send_raw(channel_definition, Some(&payload)), - Err(e) => { - error!("Failed to encode: {e:?}"); - Err(e.into()) - } - } - } - - pub fn send_empty( - &self, - channel_definition: &ChannelSender, - ) -> anyhow::Result<()> { + pub fn send_empty(&self, channel_definition: &ChannelSenderDefinition) -> anyhow::Result<()> { self.send_raw(channel_definition, None) } - /// Publish an already-encoded payload using a - /// full topic string - no need for passing a ChannelSender - /// reference + /// Publish an already-encoded payload using a provided + /// **full topic string** - no need for passing a ChannelSender or + /// ChannelSenderDefinition reference. + /// + /// **WARNING:** This is a back door to using MQTT directly, without any + /// guarrantees of correctedness in a Tether-based system! pub fn publish_raw( &self, topic: &str, diff --git a/lib/rust/src/channels/options/mod.rs b/lib/rust/src/channels/definitions/mod.rs similarity index 86% rename from lib/rust/src/channels/options/mod.rs rename to lib/rust/src/channels/definitions/mod.rs index 4d9dce6..4152d17 100644 --- a/lib/rust/src/channels/options/mod.rs +++ b/lib/rust/src/channels/definitions/mod.rs @@ -1,8 +1,15 @@ -pub mod definitions; -pub mod receiver_options; -pub mod sender_options; +use super::tether_compliant_topic::TetherOrCustomTopic; -pub trait ChannelOptions { +pub mod receiver_builder; +pub mod sender_builder; + +pub use receiver_builder::ChannelReceiverBuilder; +pub use sender_builder::ChannelSenderBuilder; + +/** +A Channel Builder is used for creating a Channel Definition. +*/ +pub trait ChannelBuilder { fn new(name: &str) -> Self; fn qos(self, qos: Option) -> Self; fn role(self, role: Option<&str>) -> Self; @@ -11,6 +18,79 @@ pub trait ChannelOptions { fn override_topic(self, override_topic: Option<&str>) -> Self; } +/** +A Channel Definition is intended to encapsulate only the essential metadata +and configuration needed to describe a Channel. In contrast with a Channel Sender/Receiver, +it is **not** responsible for actually sending or receiving messages on that Channel. +*/ +pub trait ChannelDefinition<'a> { + fn name(&'a self) -> &'a str; + /// Return the generated topic string actually used by the Channel + fn generated_topic(&'a self) -> &'a str; + /// Return the custom or Tether-compliant topic + fn topic(&'a self) -> &'a TetherOrCustomTopic; + fn qos(&'a self) -> i32; +} + +#[derive(Clone)] +pub struct ChannelSenderDefinition { + pub name: String, + pub generated_topic: String, + pub topic: TetherOrCustomTopic, + pub qos: i32, + pub retain: bool, +} + +impl ChannelSenderDefinition { + pub fn retain(&self) -> bool { + self.retain + } +} + +#[derive(Clone)] +pub struct ChannelReceiverDefinition { + pub name: String, + pub generated_topic: String, + pub topic: TetherOrCustomTopic, + pub qos: i32, +} + +impl<'a> ChannelDefinition<'a> for ChannelSenderDefinition { + fn name(&'a self) -> &'a str { + &self.name + } + + fn generated_topic(&'a self) -> &'a str { + &self.generated_topic + } + + fn topic(&'a self) -> &'a TetherOrCustomTopic { + &self.topic + } + + fn qos(&'a self) -> i32 { + self.qos + } +} + +impl<'a> ChannelDefinition<'a> for ChannelReceiverDefinition { + fn name(&'a self) -> &'a str { + &self.name + } + + fn generated_topic(&'a self) -> &'a str { + &self.generated_topic + } + + fn topic(&'a self) -> &'a TetherOrCustomTopic { + &self.topic + } + + fn qos(&'a self) -> i32 { + self.qos + } +} + // #[cfg(test)] // mod tests { diff --git a/lib/rust/src/channels/options/receiver_options.rs b/lib/rust/src/channels/definitions/receiver_builder.rs similarity index 89% rename from lib/rust/src/channels/options/receiver_options.rs rename to lib/rust/src/channels/definitions/receiver_builder.rs index 205eed0..a5cb7d0 100644 --- a/lib/rust/src/channels/options/receiver_options.rs +++ b/lib/rust/src/channels/definitions/receiver_builder.rs @@ -1,9 +1,10 @@ use crate::tether_compliant_topic::{TetherCompliantTopic, TetherOrCustomTopic}; -use super::{definitions::ChannelReceiverDefinition, ChannelOptions}; use log::*; -pub struct ChannelReceiverOptions { +use super::{ChannelBuilder, ChannelReceiverDefinition}; + +pub struct ChannelReceiverBuilder { channel_name: String, qos: Option, override_subscribe_role: Option, @@ -12,9 +13,9 @@ pub struct ChannelReceiverOptions { override_topic: Option, } -impl ChannelOptions for ChannelReceiverOptions { +impl ChannelBuilder for ChannelReceiverBuilder { fn new(name: &str) -> Self { - ChannelReceiverOptions { + ChannelReceiverBuilder { channel_name: String::from(name), override_subscribe_id: None, override_subscribe_role: None, @@ -25,7 +26,7 @@ impl ChannelOptions for ChannelReceiverOptions { } fn qos(self, qos: Option) -> Self { - ChannelReceiverOptions { qos, ..self } + ChannelReceiverBuilder { qos, ..self } } fn role(self, role: Option<&str>) -> Self { @@ -34,7 +35,7 @@ impl ChannelOptions for ChannelReceiverOptions { self } else { let override_subscribe_role = role.map(|s| s.into()); - ChannelReceiverOptions { + ChannelReceiverBuilder { override_subscribe_role, ..self } @@ -47,7 +48,7 @@ impl ChannelOptions for ChannelReceiverOptions { self } else { let override_subscribe_id = id.map(|s| s.into()); - ChannelReceiverOptions { + ChannelReceiverBuilder { override_subscribe_id, ..self } @@ -61,7 +62,7 @@ impl ChannelOptions for ChannelReceiverOptions { } if override_channel_name.is_some() { let override_subscribe_channel_name = override_channel_name.map(|s| s.into()); - ChannelReceiverOptions { + ChannelReceiverBuilder { override_subscribe_channel_name, ..self } @@ -84,12 +85,12 @@ impl ChannelOptions for ChannelReceiverOptions { t ); } - ChannelReceiverOptions { + ChannelReceiverBuilder { override_topic: Some(t.into()), ..self } } - None => ChannelReceiverOptions { + None => ChannelReceiverBuilder { override_topic: None, ..self }, @@ -97,9 +98,9 @@ impl ChannelOptions for ChannelReceiverOptions { } } -impl ChannelReceiverOptions { +impl ChannelReceiverBuilder { pub fn any_channel(self) -> Self { - ChannelReceiverOptions { + ChannelReceiverBuilder { override_subscribe_channel_name: Some("+".into()), ..self } diff --git a/lib/rust/src/channels/options/sender_options.rs b/lib/rust/src/channels/definitions/sender_builder.rs similarity index 90% rename from lib/rust/src/channels/options/sender_options.rs rename to lib/rust/src/channels/definitions/sender_builder.rs index df60fdb..95a1715 100644 --- a/lib/rust/src/channels/options/sender_options.rs +++ b/lib/rust/src/channels/definitions/sender_builder.rs @@ -3,10 +3,11 @@ use crate::{ TetherAgent, }; -use super::{definitions::ChannelSenderDefinition, ChannelOptions}; use log::*; -pub struct ChannelSenderOptions { +use super::{ChannelBuilder, ChannelSenderDefinition}; + +pub struct ChannelSenderBuilder { channel_name: String, qos: Option, override_publish_role: Option, @@ -15,9 +16,9 @@ pub struct ChannelSenderOptions { retain: Option, } -impl ChannelOptions for ChannelSenderOptions { +impl ChannelBuilder for ChannelSenderBuilder { fn new(name: &str) -> Self { - ChannelSenderOptions { + ChannelSenderBuilder { channel_name: String::from(name), override_publish_id: None, override_publish_role: None, @@ -28,7 +29,7 @@ impl ChannelOptions for ChannelSenderOptions { } fn qos(self, qos: Option) -> Self { - ChannelSenderOptions { qos, ..self } + ChannelSenderBuilder { qos, ..self } } fn role(self, role: Option<&str>) -> Self { @@ -37,7 +38,7 @@ impl ChannelOptions for ChannelSenderOptions { self } else { let override_publish_role = role.map(|s| s.into()); - ChannelSenderOptions { + ChannelSenderBuilder { override_publish_role, ..self } @@ -50,7 +51,7 @@ impl ChannelOptions for ChannelSenderOptions { self } else { let override_publish_id = id.map(|s| s.into()); - ChannelSenderOptions { + ChannelSenderBuilder { override_publish_id, ..self } @@ -63,7 +64,7 @@ impl ChannelOptions for ChannelSenderOptions { return self; } match override_channel_name { - Some(n) => ChannelSenderOptions { + Some(n) => ChannelSenderBuilder { channel_name: n.into(), ..self }, @@ -87,12 +88,12 @@ impl ChannelOptions for ChannelSenderOptions { t ); } - ChannelSenderOptions { + ChannelSenderBuilder { override_topic: Some(t.into()), ..self } } - None => ChannelSenderOptions { + None => ChannelSenderBuilder { override_topic: None, ..self }, @@ -100,9 +101,9 @@ impl ChannelOptions for ChannelSenderOptions { } } -impl ChannelSenderOptions { +impl ChannelSenderBuilder { pub fn retain(self, should_retain: Option) -> Self { - ChannelSenderOptions { + ChannelSenderBuilder { retain: should_retain, ..self } diff --git a/lib/rust/src/channels/mod.rs b/lib/rust/src/channels/mod.rs index d9a05b8..01f38d8 100644 --- a/lib/rust/src/channels/mod.rs +++ b/lib/rust/src/channels/mod.rs @@ -1,4 +1,4 @@ -pub mod options; +pub mod definitions; pub mod receiver; pub mod sender; pub mod tether_compliant_topic; diff --git a/lib/rust/src/channels/options/definitions.rs b/lib/rust/src/channels/options/definitions.rs deleted file mode 100644 index 0b096eb..0000000 --- a/lib/rust/src/channels/options/definitions.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::tether_compliant_topic::TetherOrCustomTopic; - -pub trait ChannelDefinition<'a> { - fn name(&'a self) -> &'a str; - /// Return the generated topic string actually used by the Channel - fn generated_topic(&'a self) -> &'a str; - /// Return the custom or Tether-compliant topic - fn topic(&'a self) -> &'a TetherOrCustomTopic; - fn qos(&'a self) -> i32; -} - -#[derive(Clone)] -pub struct ChannelSenderDefinition { - pub name: String, - pub generated_topic: String, - pub topic: TetherOrCustomTopic, - pub qos: i32, - pub retain: bool, -} - -impl ChannelSenderDefinition { - pub fn retain(&self) -> bool { - self.retain - } -} - -#[derive(Clone)] -pub struct ChannelReceiverDefinition { - pub name: String, - pub generated_topic: String, - pub topic: TetherOrCustomTopic, - pub qos: i32, -} - -impl<'a> ChannelDefinition<'a> for ChannelSenderDefinition { - fn name(&'a self) -> &'a str { - &self.name - } - - fn generated_topic(&'a self) -> &'a str { - &self.generated_topic - } - - fn topic(&'a self) -> &'a TetherOrCustomTopic { - &self.topic - } - - fn qos(&'a self) -> i32 { - self.qos - } -} - -impl<'a> ChannelDefinition<'a> for ChannelReceiverDefinition { - fn name(&'a self) -> &'a str { - &self.name - } - - fn generated_topic(&'a self) -> &'a str { - &self.generated_topic - } - - fn topic(&'a self) -> &'a TetherOrCustomTopic { - &self.topic - } - - fn qos(&'a self) -> i32 { - self.qos - } -} diff --git a/lib/rust/src/channels/receiver.rs b/lib/rust/src/channels/receiver.rs index 78eac05..fc4f99c 100644 --- a/lib/rust/src/channels/receiver.rs +++ b/lib/rust/src/channels/receiver.rs @@ -6,50 +6,15 @@ use serde::Deserialize; use crate::TetherAgent; use super::{ - options::definitions::{ChannelDefinition, ChannelReceiverDefinition}, + definitions::{ChannelDefinition, ChannelReceiverDefinition}, tether_compliant_topic::TetherOrCustomTopic, }; pub struct ChannelReceiver<'a, T: Deserialize<'a>> { - name: String, - topic: TetherOrCustomTopic, - qos: i32, + definition: ChannelReceiverDefinition, marker: std::marker::PhantomData<&'a T>, } -impl<'a, T: Deserialize<'a>> ChannelDefinition<'a> for ChannelReceiver<'a, T> { - fn name(&self) -> &str { - &self.name - } - - fn generated_topic(&self) -> &str { - match &self.topic { - TetherOrCustomTopic::Custom(s) => { - debug!( - "Channel named \"{}\" has custom topic \"{}\"", - &self.name, &s - ); - s - } - TetherOrCustomTopic::Tether(t) => { - debug!( - "Channel named \"{}\" has Tether-compliant topic \"{:?}\"", - &self.name, t - ); - t.topic() - } - } - } - - fn topic(&'_ self) -> &'_ TetherOrCustomTopic { - &self.topic - } - - fn qos(&self) -> i32 { - self.qos - } -} - impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { pub fn new( tether_agent: &'a TetherAgent, @@ -58,9 +23,7 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { let topic_string = definition.topic().full_topic_string(); let channel = ChannelReceiver { - name: String::from(definition.name()), - topic: definition.topic().clone(), - qos: definition.qos(), + definition: definition.clone(), marker: std::marker::PhantomData, }; @@ -91,7 +54,14 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { } } - /// Use the topic of an incoming message to check against the definition of an Channel Receiver. + pub fn definition(&self) -> &ChannelReceiverDefinition { + &self.definition + } + + /// Typically, you do not need to call this function yourself - rather use `.parse` which will + /// both check if the message belongs this channel AND, if so, decode it as well. + /// + /// Uses the topic of an incoming message to check against the definition of an Channel Receiver. /// /// Due to the use of wildcard subscriptions, multiple topic strings might match a given /// Channel Receiver definition. e.g. `someRole/channelMessages` and `anotherRole/channelMessages` and `someRole/channelMessages/specificID` @@ -103,7 +73,7 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { /// developer is expected to match against topic strings themselves. pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool { match incoming_topic { - TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic { + TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.definition().topic() { TetherOrCustomTopic::Tether(my_tpt) => { let matches_role = my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role()); @@ -119,7 +89,9 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { None => true, }; - debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_channel_name); + debug!("Test match for Channel named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_channel_name? {}", + &self.definition().name(), &self.definition().topic(), + &incoming_three_parts, matches_role, matches_id, matches_channel_name); matches_role && matches_id && matches_channel_name } TetherOrCustomTopic::Custom(my_custom_topic) => { @@ -131,7 +103,7 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { || my_custom_topic.as_str() == incoming_three_parts.topic() } }, - TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic { + TetherOrCustomTopic::Custom(incoming_custom) => match &self.definition().topic { TetherOrCustomTopic::Custom(my_custom_topic) => { if my_custom_topic.as_str() == "#" || my_custom_topic.as_str() == incoming_custom.as_str() @@ -160,7 +132,8 @@ impl<'a, T: Deserialize<'a>> ChannelReceiver<'a, T> { Err(e) => { error!( "Failed to parse message on channel \"{}\": {}", - &self.name, e + &self.definition().name, + e ); None } diff --git a/lib/rust/src/channels/sender.rs b/lib/rust/src/channels/sender.rs index 3bcab37..c5008f7 100644 --- a/lib/rust/src/channels/sender.rs +++ b/lib/rust/src/channels/sender.rs @@ -1,82 +1,35 @@ use crate::TetherAgent; -use anyhow::anyhow; -use rmp_serde::to_vec_named; use serde::Serialize; -use super::{ - options::definitions::{ChannelDefinition, ChannelSenderDefinition}, - tether_compliant_topic::TetherOrCustomTopic, -}; +use super::definitions::ChannelSenderDefinition; pub struct ChannelSender<'a, T: Serialize> { - name: String, - topic: TetherOrCustomTopic, - qos: i32, - retain: bool, + definition: ChannelSenderDefinition, tether_agent: &'a TetherAgent, marker: std::marker::PhantomData, } -impl<'a, T: Serialize> ChannelDefinition<'a> for ChannelSender<'a, T> { - fn name(&'_ self) -> &'_ str { - &self.name - } - - fn generated_topic(&self) -> &str { - match &self.topic { - TetherOrCustomTopic::Custom(s) => s, - TetherOrCustomTopic::Tether(t) => t.topic(), - } - } - - fn topic(&'_ self) -> &'_ TetherOrCustomTopic { - &self.topic - } - - fn qos(&'_ self) -> i32 { - self.qos - } -} - impl<'a, T: Serialize> ChannelSender<'a, T> { pub fn new( tether_agent: &'a TetherAgent, definition: ChannelSenderDefinition, ) -> ChannelSender<'a, T> { ChannelSender { - name: String::from(definition.name()), - topic: definition.topic().clone(), - qos: definition.qos(), - retain: definition.retain(), + definition, tether_agent, marker: std::marker::PhantomData, } } - pub fn retain(&self) -> bool { - self.retain + pub fn definition(&self) -> &ChannelSenderDefinition { + &self.definition } - pub fn send_raw(&self, payload: &[u8]) -> anyhow::Result<()> { - if let Some(client) = &self.tether_agent.client { - let res = client - .publish( - self.generated_topic(), - rumqttc::QoS::AtLeastOnce, - false, - payload, - ) - .map_err(anyhow::Error::msg); - res - } else { - Err(anyhow!("no client")) - } + pub fn send(&self, payload: &T) -> anyhow::Result<()> { + self.tether_agent.send(self, payload) } - pub fn send(&self, payload: &T) -> anyhow::Result<()> { - match to_vec_named(&payload) { - Ok(data) => self.send_raw(&data), - Err(e) => Err(e.into()), - } + pub fn send_raw(&self, payload: &[u8]) -> anyhow::Result<()> { + self.tether_agent.send_raw(self.definition(), Some(payload)) } } diff --git a/lib/rust/src/lib.rs b/lib/rust/src/lib.rs index 3ef37ad..5b8686c 100644 --- a/lib/rust/src/lib.rs +++ b/lib/rust/src/lib.rs @@ -2,5 +2,6 @@ pub mod agent; pub mod channels; pub use agent::*; +pub use channels::definitions::*; pub use channels::*; pub use rumqttc as mqtt; From af8850434fcc0ec6d1353af17b03c7baec5d540e Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Thu, 17 Apr 2025 13:30:53 +0200 Subject: [PATCH 19/21] Following the logic of the Rust library, allow option to create channels via TetherAgent instances --- examples/nodejs-ts/src/index.ts | 6 +- lib/js/src/Agent.ts | 11 +- .../ChannelReceiver.ts} | 172 +++--------------- lib/js/src/Channel/ChannelSender.ts | 110 +++++++++++ lib/js/src/Channel/index.ts | 25 +++ lib/js/src/index.test.ts | 88 ++++----- lib/js/src/index.ts | 3 +- 7 files changed, 224 insertions(+), 191 deletions(-) rename lib/js/src/{Channel.ts => Channel/ChannelReceiver.ts} (58%) create mode 100644 lib/js/src/Channel/ChannelSender.ts create mode 100644 lib/js/src/Channel/index.ts diff --git a/examples/nodejs-ts/src/index.ts b/examples/nodejs-ts/src/index.ts index a83d726..e720e2c 100644 --- a/examples/nodejs-ts/src/index.ts +++ b/examples/nodejs-ts/src/index.ts @@ -17,7 +17,11 @@ logger.debug("Debug logging enabled; output could be verbose!"); const main = async () => { const agent = await TetherAgent.create("brain"); - const sender = new ChannelSender(agent, "randomValues"); + // Note the alternative syntax for doing the same thing, below: + // ... + // const sender = new ChannelSender(agent, "randomValues"); + const sender = agent.createSender("randomValues"); + sender.send({ value: Math.random(), timestamp: Date.now(), diff --git a/lib/js/src/Agent.ts b/lib/js/src/Agent.ts index bc11255..fb803e2 100644 --- a/lib/js/src/Agent.ts +++ b/lib/js/src/Agent.ts @@ -1,7 +1,8 @@ import mqtt, { AsyncMqttClient } from "async-mqtt"; import { TetherAgentConfig, TetherOptions } from "./types"; +import { ChannelReceiver } from "./Channel/ChannelReceiver"; import { LogLevelDesc } from "loglevel"; -import { logger } from "./"; +import { ChannelSender, logger } from "./"; import defaults from "./defaults"; enum State { @@ -153,4 +154,12 @@ export class TetherAgent { }; public getConfig = () => this.config; + + public async createReceiver(name: string): Promise> { + return ChannelReceiver.create(this, name); + } + + public createSender(name: string): ChannelSender { + return new ChannelSender(this, name); + } } diff --git a/lib/js/src/Channel.ts b/lib/js/src/Channel/ChannelReceiver.ts similarity index 58% rename from lib/js/src/Channel.ts rename to lib/js/src/Channel/ChannelReceiver.ts index e0ceee4..950663f 100644 --- a/lib/js/src/Channel.ts +++ b/lib/js/src/Channel/ChannelReceiver.ts @@ -1,29 +1,13 @@ -import { IClientPublishOptions, IClientSubscribeOptions } from "async-mqtt"; -import { TetherAgent, decode, encode, logger } from "."; -import { ChannelDefinition } from "./types"; -import { Buffer } from "buffer"; - -// declare interface ChannelReceiver { -// on( -// event: "message", -// listener: (payload: Buffer, topic: string) => void -// ): this; -// on(event: string, listener: Function): this; -// } - -class Channel { - protected definition: ChannelDefinition; - protected agent: TetherAgent; - - constructor(agent: TetherAgent, definition: ChannelDefinition) { - this.agent = agent; - - logger.debug("Channel super definition:", JSON.stringify(definition)); - this.definition = definition; - } - - public getDefinition = () => this.definition; -} +import { IClientSubscribeOptions } from "async-mqtt"; +import { Channel, containsWildcards } from "."; +import { + decode, + logger, + parseAgentIdOrGroup, + parseAgentRole, + parseChannelName, + TetherAgent, +} from ".."; type ReceiverCallback = (payload: T, topic: string) => void; export class ChannelReceiver extends Channel { @@ -41,6 +25,13 @@ export class ChannelReceiver extends Channel { ) { const instance = new ChannelReceiver(agent, channelName, options || {}); + if (agent.getConfig().autoConnect === false) { + logger.warn( + "Agent had autoConnect set to FALSE; will not attempt subscription - fine for testing, bad for anything else!" + ); + return instance; + } + try { await instance.subscribe(options?.subscribeOptions || { qos: 1 }); logger.info("subscribed OK to", instance.definition.topic); @@ -125,92 +116,18 @@ export class ChannelReceiver extends Channel { }; } -export class ChannelSender extends Channel { - private publishOptions: IClientPublishOptions; - - constructor( - agent: TetherAgent, - channelName: string, - options?: { - overrideTopic?: string; - id?: string; - publishOptions?: IClientPublishOptions; - } - ) { - super(agent, { - name: channelName, - topic: - options?.overrideTopic || - buildOutputTopic( - channelName, - agent.getConfig().role, - options?.id || agent.getConfig().id - ), - }); - this.publishOptions = options?.publishOptions || { - retain: false, - qos: 1, - }; - if (channelName === undefined) { - throw Error("No name provided for Output"); - } - if (agent.getConfig().autoConnect === true && !agent.getIsConnected()) { - throw Error("trying to create an Output before client is connected"); - } +const buildInputTopic = ( + channelName: string, + specifyRole?: string, + specifyID?: string +): string => { + const role = specifyRole || "+"; + if (specifyID) { + return `${role}/${channelName}/${specifyID}`; + } else { + return `${role}/${channelName}/#`; } - - /** Do NOT encode the content (assume it's already encoded), and then publish */ - sendRaw = async (content?: Uint8Array) => { - if (!this.agent.getIsConnected()) { - throw Error( - "trying to send without connection; not possible until connected" - ); - } - try { - logger.debug("Sending on topic", this.definition.topic, "with options", { - ...this.publishOptions, - }); - if (content === undefined) { - this.agent - .getClient() - ?.publish( - this.definition.topic, - Buffer.from([]), - this.publishOptions - ); - } else if (content instanceof Uint8Array) { - this.agent - .getClient() - ?.publish( - this.definition.topic, - Buffer.from(content), - this.publishOptions - ); - } else { - this.agent - .getClient() - ?.publish(this.definition.topic, content, this.publishOptions); - } - } catch (e) { - logger.error("Error publishing message:", e); - } - }; - - /** Automatically encode the content as MsgPack, and publish using the ChannelSender topic */ - send = async (content?: T) => { - if (!this.agent.getIsConnected()) { - throw Error( - "trying to send without connection; not possible until connected" - ); - } - try { - const buffer = encode(content); - this.sendRaw(buffer); - } catch (e) { - throw Error(`Error encoding content: ${e}`); - } - }; -} +}; export const topicMatchesChannel = ( channelGeneratedTopic: string, @@ -270,36 +187,3 @@ export const topicMatchesChannel = ( // ); // } }; - -const containsWildcards = (topicOrPart: string) => - topicOrPart.includes("+") || topicOrPart.includes("#"); - -const buildInputTopic = ( - channelName: string, - specifyRole?: string, - specifyID?: string -): string => { - const role = specifyRole || "+"; - if (specifyID) { - return `${role}/${channelName}/${specifyID}`; - } else { - return `${role}/${channelName}/#`; - } -}; - -const buildOutputTopic = ( - channelName: string, - specifyRole: string, - specifyID?: string -): string => { - const role = specifyRole; - if (specifyID) { - return `${role}/${channelName}/${specifyID}`; - } else { - return `${role}/${channelName}`; - } -}; - -export const parseChannelName = (topic: string) => topic.split(`/`)[1]; -export const parseAgentIdOrGroup = (topic: string) => topic.split(`/`)[2]; -export const parseAgentRole = (topic: string) => topic.split(`/`)[0]; diff --git a/lib/js/src/Channel/ChannelSender.ts b/lib/js/src/Channel/ChannelSender.ts new file mode 100644 index 0000000..1bccb38 --- /dev/null +++ b/lib/js/src/Channel/ChannelSender.ts @@ -0,0 +1,110 @@ +import { IClientPublishOptions } from "async-mqtt"; +import { Channel, containsWildcards } from "."; +import { + encode, + logger, + parseAgentIdOrGroup, + parseAgentRole, + parseChannelName, + TetherAgent, +} from ".."; + +export class ChannelSender extends Channel { + private publishOptions: IClientPublishOptions; + + constructor( + agent: TetherAgent, + channelName: string, + options?: { + overrideTopic?: string; + id?: string; + publishOptions?: IClientPublishOptions; + } + ) { + super(agent, { + name: channelName, + topic: + options?.overrideTopic || + buildOutputTopic( + channelName, + agent.getConfig().role, + options?.id || agent.getConfig().id + ), + }); + this.publishOptions = options?.publishOptions || { + retain: false, + qos: 1, + }; + if (channelName === undefined) { + throw Error("No name provided for Output"); + } + if (agent.getConfig().autoConnect === true && !agent.getIsConnected()) { + throw Error("trying to create an Output before client is connected"); + } + } + + /** Do NOT encode the content (assume it's already encoded), and then publish */ + sendRaw = async (content?: Uint8Array) => { + if (!this.agent.getIsConnected()) { + throw Error( + "trying to send without connection; not possible until connected" + ); + } + try { + logger.debug("Sending on topic", this.definition.topic, "with options", { + ...this.publishOptions, + }); + if (content === undefined) { + this.agent + .getClient() + ?.publish( + this.definition.topic, + Buffer.from([]), + this.publishOptions + ); + } else if (content instanceof Uint8Array) { + this.agent + .getClient() + ?.publish( + this.definition.topic, + Buffer.from(content), + this.publishOptions + ); + } else { + this.agent + .getClient() + ?.publish(this.definition.topic, content, this.publishOptions); + } + } catch (e) { + logger.error("Error publishing message:", e); + } + }; + + /** Automatically encode the content as MsgPack, and publish using the ChannelSender topic */ + send = async (content?: T) => { + if (!this.agent.getIsConnected()) { + throw Error( + "trying to send without connection; not possible until connected" + ); + } + try { + const buffer = encode(content); + this.sendRaw(buffer); + } catch (e) { + throw Error(`Error encoding content: ${e}`); + } + }; +} + +const buildOutputTopic = ( + channelName: string, + specifyRole: string, + specifyID?: string +): string => { + const role = specifyRole; + if (specifyID) { + return `${role}/${channelName}/${specifyID}`; + } else { + return `${role}/${channelName}`; + } +}; diff --git a/lib/js/src/Channel/index.ts b/lib/js/src/Channel/index.ts new file mode 100644 index 0000000..7ba48da --- /dev/null +++ b/lib/js/src/Channel/index.ts @@ -0,0 +1,25 @@ +import { TetherAgent, logger } from "../"; +import { ChannelDefinition } from "../types"; +import { ChannelReceiver } from "./ChannelReceiver"; +import { ChannelSender } from "./ChannelSender"; + +export class Channel { + protected definition: ChannelDefinition; + protected agent: TetherAgent; + + constructor(agent: TetherAgent, definition: ChannelDefinition) { + this.agent = agent; + + logger.debug("Channel super definition:", JSON.stringify(definition)); + this.definition = definition; + } + + public getDefinition = () => this.definition; +} + +export const containsWildcards = (topicOrPart: string) => + topicOrPart.includes("+") || topicOrPart.includes("#"); + +export const parseChannelName = (topic: string) => topic.split(`/`)[1]; +export const parseAgentIdOrGroup = (topic: string) => topic.split(`/`)[2]; +export const parseAgentRole = (topic: string) => topic.split(`/`)[0]; diff --git a/lib/js/src/index.test.ts b/lib/js/src/index.test.ts index 16eb722..ee3792a 100644 --- a/lib/js/src/index.test.ts +++ b/lib/js/src/index.test.ts @@ -1,5 +1,5 @@ import { ChannelReceiver, ChannelSender, TetherAgent } from "."; -import { topicMatchesChannel } from "./Channel"; +import { topicMatchesChannel } from "./Channel/ChannelReceiver"; import { describe, test, expect } from "@jest/globals"; describe("building topic strings", () => { @@ -25,7 +25,7 @@ describe("building topic strings", () => { const output = new ChannelSender(agent, "someChannelName"); expect(output.getDefinition().name).toEqual("someChannelName"); expect(output.getDefinition().topic).toEqual( - "tester/someChannelName/specialGroup", + "tester/someChannelName/specialGroup" ); }); @@ -39,7 +39,7 @@ describe("building topic strings", () => { }); expect(output.getDefinition().name).toEqual("someChannelName"); expect(output.getDefinition().topic).toEqual( - "tester/someChannelName/overrideOnChannelCreation", + "tester/someChannelName/overrideOnChannelCreation" ); }); @@ -51,7 +51,7 @@ describe("building topic strings", () => { const input = await ChannelReceiver.create(agent, "someChannelName"); expect(input.getDefinition().name).toEqual("someChannelName"); expect(input.getDefinition().topic).toEqual( - "+/someChannelName/specialGroup", + "+/someChannelName/specialGroup" ); }); @@ -65,11 +65,11 @@ describe("building topic strings", () => { "someChannelName", { id: "specialID", - }, + } ); expect(inputCustomID.getDefinition().name).toEqual("someChannelName"); expect(inputCustomID.getDefinition().topic).toEqual( - "+/someChannelName/specialID", + "+/someChannelName/specialID" ); const inputCustomRole = await ChannelReceiver.create( @@ -77,11 +77,11 @@ describe("building topic strings", () => { "someChannelName", { role: "specialRole", - }, + } ); expect(inputCustomRole.getDefinition().name).toEqual("someChannelName"); expect(inputCustomRole.getDefinition().topic).toEqual( - "specialRole/someChannelName/#", + "specialRole/someChannelName/#" ); const inputCustomBoth = await ChannelReceiver.create( @@ -90,11 +90,11 @@ describe("building topic strings", () => { { id: "id2", role: "role2", - }, + } ); expect(inputCustomBoth.getDefinition().name).toEqual("someChannelName"); expect(inputCustomBoth.getDefinition().topic).toEqual( - "role2/someChannelName/id2", + "role2/someChannelName/id2" ); }); }); @@ -106,28 +106,28 @@ describe("matching topics to Channels", () => { expect( topicMatchesChannel( channelDefinedTopic, - "someType/someChannelName/someID", - ), + "someType/someChannelName/someID" + ) ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "other/someChannelName/otherGroup", - ), + "other/someChannelName/otherGroup" + ) ).toBeFalsy(); }); test("if ONLY Channel Name specified, match any with same ChanelName", () => { const channelDefinedTopic = "+/someChannelName/#"; expect( - topicMatchesChannel(channelDefinedTopic, "something/someChannelName"), + topicMatchesChannel(channelDefinedTopic, "something/someChannelName") ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "something/someChannelName/something", - ), + "something/someChannelName/something" + ) ).toBeTruthy(); }); @@ -136,14 +136,14 @@ describe("matching topics to Channels", () => { expect( topicMatchesChannel( channelDefinedTopic, - "something/someChannelName/something", - ), + "something/someChannelName/something" + ) ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "something/someOtherChannelName.something", - ), + "something/someOtherChannelName.something" + ) ).toBeFalsy(); }); @@ -151,35 +151,35 @@ describe("matching topics to Channels", () => { const channelDefinedTopic = "specificAgent/channelName/#"; expect( - topicMatchesChannel(channelDefinedTopic, "specificAgent/channelName"), + topicMatchesChannel(channelDefinedTopic, "specificAgent/channelName") ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "specificAgent/channelName/anything", - ), + "specificAgent/channelName/anything" + ) ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "specificAgent/channelName/somethingElse", - ), + "specificAgent/channelName/somethingElse" + ) ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "differentAgent/channelName/anything", - ), + "differentAgent/channelName/anything" + ) ).toBeFalsy(); }); test("# wildcard should match any topic", () => { const channelDefinedTopic = "#"; expect( - topicMatchesChannel(channelDefinedTopic, "something/something/something"), + topicMatchesChannel(channelDefinedTopic, "something/something/something") ).toBeTruthy(); expect( - topicMatchesChannel(channelDefinedTopic, "not/event/tether/standard"), + topicMatchesChannel(channelDefinedTopic, "not/event/tether/standard") ).toBeTruthy(); }); @@ -189,20 +189,20 @@ describe("matching topics to Channels", () => { expect( topicMatchesChannel( channelDefinedTopic, - "LidarConsolidation/clusters/e933b82f-cb0d-4f91-a4a7-5625ce3ed20b", - ), + "LidarConsolidation/clusters/e933b82f-cb0d-4f91-a4a7-5625ce3ed20b" + ) ).toBeFalsy(); expect( topicMatchesChannel( channelDefinedTopic, - "LidarConsolidation/trackedPoints/e933b82f-cb0d-4f91-a4a7-5625ce3ed20b", - ), + "LidarConsolidation/trackedPoints/e933b82f-cb0d-4f91-a4a7-5625ce3ed20b" + ) ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "SomethingElse/trackedPoints/e933b82f-cb0d-4f91-a4a7-5625ce3ed20b", - ), + "SomethingElse/trackedPoints/e933b82f-cb0d-4f91-a4a7-5625ce3ed20b" + ) ).toBeFalsy(); }); @@ -210,25 +210,25 @@ describe("matching topics to Channels", () => { const channelDefinedTopic = "+/channelName/specificGroupOrId"; expect( - topicMatchesChannel(channelDefinedTopic, "someAgentRole/channelName"), + topicMatchesChannel(channelDefinedTopic, "someAgentRole/channelName") ).toBeFalsy(); expect( topicMatchesChannel( channelDefinedTopic, - "someAgentRole/channelName/specificGroupOrId", - ), + "someAgentRole/channelName/specificGroupOrId" + ) ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "anotherAgent/channelName/specificGroupOrId", - ), + "anotherAgent/channelName/specificGroupOrId" + ) ).toBeTruthy(); expect( topicMatchesChannel( channelDefinedTopic, - "someAgent/channelName/wrongGroup", - ), + "someAgent/channelName/wrongGroup" + ) ).toBeFalsy(); }); @@ -236,7 +236,7 @@ describe("matching topics to Channels", () => { const channelDefinedTopic = "something/something/+"; try { expect( - topicMatchesChannel(channelDefinedTopic, "anything/anything/anything"), + topicMatchesChannel(channelDefinedTopic, "anything/anything/anything") ).toThrow(); } catch (e) { // diff --git a/lib/js/src/index.ts b/lib/js/src/index.ts index 36f1eae..bca7548 100644 --- a/lib/js/src/index.ts +++ b/lib/js/src/index.ts @@ -1,9 +1,10 @@ import { IClientOptions } from "async-mqtt"; import { BROWSER, NODEJS } from "./defaults"; -import { ChannelReceiver, ChannelSender } from "./Channel"; import logger from "loglevel"; import { encode, decode } from "@msgpack/msgpack"; import { TetherAgent } from "./Agent"; +import { ChannelReceiver } from "./Channel/ChannelReceiver"; +import { ChannelSender } from "./Channel/ChannelSender"; export { parseChannelName, parseAgentIdOrGroup, From df6d498124374cbfcd4a610e7d67299ac7fe2200 Mon Sep 17 00:00:00 2001 From: Stephen Buchanan Date: Thu, 17 Apr 2025 13:38:48 +0200 Subject: [PATCH 20/21] Examples updated for shorter Channel creation functions, also supporting options --- examples/nodejs-ts/src/index.ts | 11 +++-------- examples/react-ts/src/Tether/Receiver.tsx | 10 ++++++---- examples/react-ts/src/Tether/Sender.tsx | 16 +++++++++------- examples/svelte-ts/src/lib/TetherTester.svelte | 7 +++---- lib/js/src/Agent.ts | 13 ++++++++++--- lib/js/src/Channel/ChannelReceiver.ts | 14 ++++++++------ lib/js/src/Channel/ChannelSender.ts | 12 +++++++----- 7 files changed, 46 insertions(+), 37 deletions(-) diff --git a/examples/nodejs-ts/src/index.ts b/examples/nodejs-ts/src/index.ts index e720e2c..df57021 100644 --- a/examples/nodejs-ts/src/index.ts +++ b/examples/nodejs-ts/src/index.ts @@ -28,8 +28,7 @@ const main = async () => { something: "one", }); - const genericReceiver = await ChannelReceiver.create( - agent, + const genericReceiver = await agent.createReceiver( "randomValuesStrictlyTyped" ); genericReceiver.on("message", (payload, topic) => { @@ -42,18 +41,14 @@ const main = async () => { ); }); - const typedReceiver = await ChannelReceiver.create( - agent, + const typedReceiver = await agent.createReceiver( "randomValuesStrictlyTyped" ); typedReceiver.on("message", (payload) => { logger.info("Our typed receiver got", payload, typeof payload); }); - const typedSender = new ChannelSender( - agent, - "randomValuesStrictlyTyped" - ); + const typedSender = agent.createSender("randomValuesStrictlyTyped"); // This will be rejected by TypeScript compiler: // typedSender.send({ // value: Math.random(), diff --git a/examples/react-ts/src/Tether/Receiver.tsx b/examples/react-ts/src/Tether/Receiver.tsx index 3694702..67f0d86 100644 --- a/examples/react-ts/src/Tether/Receiver.tsx +++ b/examples/react-ts/src/Tether/Receiver.tsx @@ -6,13 +6,15 @@ interface Props { } export const Receiver = (props: Props) => { + const { agent } = props; const [channel, setChannel] = useState | null>(null); const [lastMessage, setLastMessage] = useState(""); useEffect(() => { - ChannelReceiver.create(props.agent, "everything", { - overrideTopic: "#", - }) + agent + .createReceiver("everything", { + overrideTopic: "#", + }) .then((channel) => { setChannel(channel); channel.on("message", (payload, topic) => { @@ -24,7 +26,7 @@ export const Receiver = (props: Props) => { .catch((e) => { console.error("Error creating Channel Receiver:", e); }); - }, [props.agent]); + }, [agent]); return (
diff --git a/examples/react-ts/src/Tether/Sender.tsx b/examples/react-ts/src/Tether/Sender.tsx index 2f21e96..c3370c8 100644 --- a/examples/react-ts/src/Tether/Sender.tsx +++ b/examples/react-ts/src/Tether/Sender.tsx @@ -6,10 +6,12 @@ interface Props { } export const Sender = (props: Props) => { + const { agent } = props; + useEffect(() => { console.log("Sender useEffect"); - setChannel(new ChannelSender(props.agent, "sender")); - }, [props.agent]); + setChannel(new ChannelSender(agent, "sender")); + }, [agent]); const [useCustomTopic, setUseCustomTopic] = useState(false); const [customTopic, setTCustomTopic] = useState(""); @@ -32,7 +34,7 @@ export const Sender = (props: Props) => {