tether_agent/channels/
tether_compliant_topic.rs1use anyhow::anyhow;
2use log::*;
3use serde::{Deserialize, Serialize};
4
5use crate::TetherAgent;
6
7#[derive(Clone, Serialize, Deserialize, Debug)]
8pub struct TetherCompliantTopic {
9 role: String,
10 id: Option<String>,
11 channel_name: String,
12 full_topic: String,
13}
14
15#[derive(Clone, Serialize, Deserialize, Debug)]
16pub enum TetherOrCustomTopic {
17 Tether(TetherCompliantTopic),
18 Custom(String),
19}
20
21impl TetherOrCustomTopic {
22 pub fn full_topic_string(&self) -> String {
23 match self {
24 TetherOrCustomTopic::Tether(three_part_topic) => String::from(three_part_topic.topic()),
25 TetherOrCustomTopic::Custom(t) => String::from(t),
26 }
27 }
28}
29
30impl TetherCompliantTopic {
31 pub fn new_for_publish(
33 agent: &TetherAgent,
34 channel_name: &str,
35 role_part_override: Option<&str>,
36 id_part_override: Option<&str>,
37 ) -> TetherCompliantTopic {
38 let role = role_part_override.unwrap_or(agent.role());
39 let full_topic = build_publish_topic(role, channel_name, id_part_override);
40 TetherCompliantTopic {
41 role: role.into(),
42 id: id_part_override.map(String::from),
43 channel_name: channel_name.into(),
44 full_topic,
45 }
46 }
47
48 pub fn new_for_subscribe(
50 channel_name: &str,
51 role_part_override: Option<&str>,
52 id_part_override: Option<&str>,
53 ) -> TetherCompliantTopic {
54 let role = role_part_override.unwrap_or("+");
55 let full_topic = build_subscribe_topic(role, channel_name, id_part_override);
56
57 TetherCompliantTopic {
58 role: role.into(),
59 id: id_part_override.map(String::from),
60 channel_name: channel_name.into(),
61 full_topic,
62 }
63 }
64
65 pub fn new_three(role: &str, channel_name: &str, id: &str) -> TetherCompliantTopic {
67 TetherCompliantTopic {
68 role: role.into(),
69 id: Some(id.into()),
70 channel_name: channel_name.into(),
71 full_topic: build_subscribe_topic(role, channel_name, Some(id)),
72 }
73 }
74
75 pub fn new_two(role: &str, channel_name: &str) -> TetherCompliantTopic {
77 TetherCompliantTopic {
78 role: role.into(),
79 id: None,
80 channel_name: channel_name.into(),
81 full_topic: format!("{role}/{channel_name}"),
82 }
83 }
84
85 pub fn topic(&self) -> &str {
86 &self.full_topic
87 }
88
89 pub fn role(&self) -> &str {
90 &self.role
91 }
92
93 pub fn id(&self) -> Option<&str> {
94 self.id.as_deref()
95 }
96
97 pub fn channel_name(&self) -> &str {
98 &self.channel_name
99 }
100
101 pub fn set_role(&mut self, role: &str) {
102 self.role = role.into();
103 self.update_full_topic();
104 }
105
106 pub fn set_id(&mut self, id: Option<&str>) {
107 self.id = id.map(|id| id.into());
108 self.update_full_topic();
109 }
110
111 pub fn set_channel_name(&mut self, channel_name: &str) {
112 self.channel_name = channel_name.into();
113 self.update_full_topic();
114 }
115
116 fn update_full_topic(&mut self) {
117 self.full_topic = build_subscribe_topic(&self.role, &self.channel_name, self.id.as_deref());
118 }
119}
120
121impl TryFrom<&str> for TetherCompliantTopic {
122 type Error = anyhow::Error;
123
124 fn try_from(value: &str) -> Result<Self, Self::Error> {
126 let parts = value.split('/').collect::<Vec<&str>>();
127
128 if parts.len() < 2 || parts.len() > 3 {
129 return Err(anyhow!(
130 "Did not find exactly 2 or 3 parts in the topic {}",
131 value
132 ));
133 } else {
134 debug!("parts: {:?}", parts);
135 }
136
137 let role = parts.first().expect("the role part should exist");
138
139 let channel_name = parts.get(1).expect("the channel_name part should exist");
140
141 match parts.get(2) {
142 Some(id_part) => {
143 if *id_part == "#" {
144 debug!("This must be a topic used for subscribing");
145 Ok(TetherCompliantTopic::new_for_subscribe(
146 channel_name,
147 Some(role),
148 Some("#"),
149 ))
150 } else {
151 Ok(TetherCompliantTopic::new_three(role, channel_name, id_part))
152 }
153 }
154 None => Ok(TetherCompliantTopic::new_two(role, channel_name)),
155 }
156 }
157}
158
159pub fn build_subscribe_topic(role: &str, channel_name: &str, id: Option<&str>) -> String {
160 match id {
161 Some(id) => format!("{role}/{channel_name}/{id}"),
162 None => format!("{role}/{channel_name}/#"),
163 }
164}
165
166pub fn build_publish_topic(role: &str, channel_name: &str, id: Option<&str>) -> String {
167 match id {
168 Some(id) => format!("{role}/{channel_name}/{id}"),
169 None => format!("{role}/{channel_name}"),
170 }
171}
172
173pub fn parse_channel_name(topic: &str) -> Option<&str> {
174 let parts: Vec<&str> = topic.split('/').collect();
175 parts.get(1).copied()
176}
177
178pub fn parse_agent_id(topic: &str) -> Option<&str> {
179 let parts: Vec<&str> = topic.split('/').collect();
180 parts.get(2).copied()
181}
182
183pub fn parse_agent_role(topic: &str) -> Option<&str> {
184 let parts: Vec<&str> = topic.split('/').collect();
185 parts.first().copied()
186}
187
188#[cfg(test)]
189mod tests {
190 use crate::{
191 agent::builder::TetherAgentBuilder,
192 tether_compliant_topic::{parse_agent_id, parse_agent_role, parse_channel_name},
193 };
194
195 use super::TetherCompliantTopic;
196
197 #[test]
198 fn util_parsers() {
199 assert_eq!(parse_agent_role("one/two/three"), Some("one"));
200 assert_eq!(parse_agent_id("one/two/three"), Some("three"));
201 assert_eq!(parse_channel_name("one/two/three"), Some("two"));
202 assert_eq!(parse_channel_name("just/two"), Some("two"));
203 }
204
205 #[test]
206 fn build_full_topic() {
207 let agent = TetherAgentBuilder::new("testingRole")
208 .auto_connect(false)
209 .build()
210 .expect("failed to construct agent");
211 let publishing_chanel_topic =
212 TetherCompliantTopic::new_for_publish(&agent, "testChannel", None, None);
213 assert_eq!(
214 &publishing_chanel_topic.full_topic,
215 "testingRole/testChannel"
216 );
217 }
218}