tether_agent/plugs/
three_part_topic.rs

1use anyhow::anyhow;
2use log::{debug, error};
3use serde::{Deserialize, Serialize};
4
5use crate::TetherAgent;
6
7#[derive(Serialize, Deserialize, Debug)]
8pub struct ThreePartTopic {
9    role: String,
10    id: String,
11    plug_name: String,
12    full_topic: String,
13}
14
15#[derive(Serialize, Deserialize, Debug)]
16pub enum TetherOrCustomTopic {
17    Tether(ThreePartTopic),
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 ThreePartTopic {
31    /// Publish topics fall back to the ID and/or role associated with the agent, if not explicitly provided
32    pub fn new_for_publish(
33        role: Option<&str>,
34        id: Option<&str>,
35        plug_name: &str,
36        agent: &TetherAgent,
37    ) -> ThreePartTopic {
38        let role = role.unwrap_or(agent.role());
39        let id = id.unwrap_or(agent.id());
40        let full_topic = build_topic(role, id, plug_name);
41        ThreePartTopic {
42            role: role.into(),
43            id: id.into(),
44            plug_name: plug_name.into(),
45            full_topic,
46        }
47    }
48
49    /// Subscribe topics fall back to wildcard `+` for role and/or id if not explicitly provided.
50    /// If `plug_name_part` is specified as `Some(String)` then the plug name part of the generated
51    /// topic is changed but the plug name itself is left alone.
52    pub fn new_for_subscribe(
53        plug_name: &str,
54        role_part_override: Option<&str>,
55        id_part_override: Option<&str>,
56        plug_name_part_override: Option<&str>,
57    ) -> ThreePartTopic {
58        let role = role_part_override.unwrap_or("+");
59        let id = id_part_override.unwrap_or("+");
60        let plug_name_part = match plug_name_part_override {
61            Some(s) => {
62                if !&s.eq("+") {
63                    error!("The only valid override for the Plug Name part is a wildcard (+)");
64                }
65                s
66            }
67            None => plug_name,
68        };
69        let full_topic = build_topic(role, id, plug_name_part);
70
71        ThreePartTopic {
72            role: role.into(),
73            id: id.into(),
74            plug_name: plug_name_part.into(),
75            full_topic,
76        }
77    }
78
79    pub fn new(role: &str, id: &str, plug_name: &str) -> ThreePartTopic {
80        ThreePartTopic {
81            role: role.into(),
82            id: id.into(),
83            plug_name: plug_name.into(),
84            full_topic: build_topic(role, id, plug_name),
85        }
86    }
87
88    pub fn topic(&self) -> &str {
89        &self.full_topic
90    }
91
92    pub fn role(&self) -> &str {
93        &self.role
94    }
95
96    pub fn id(&self) -> &str {
97        &self.id
98    }
99
100    pub fn plug_name(&self) -> &str {
101        &self.plug_name
102    }
103
104    pub fn set_role(&mut self, role: &str) {
105        self.role = role.into();
106        self.update_full_topic();
107    }
108
109    pub fn set_id(&mut self, id: &str) {
110        self.id = id.into();
111        self.update_full_topic();
112    }
113
114    pub fn set_plug_name(&mut self, plug_name: &str) {
115        self.plug_name = plug_name.into();
116        self.update_full_topic();
117    }
118
119    fn update_full_topic(&mut self) {
120        self.full_topic = build_topic(&self.role, &self.id, &self.plug_name);
121    }
122}
123
124impl TryFrom<&str> for ThreePartTopic {
125    type Error = anyhow::Error;
126
127    /// Try to convert a topic string into a valid Tether Three Part Topic
128    fn try_from(value: &str) -> Result<Self, Self::Error> {
129        let parts = value.split('/').collect::<Vec<&str>>();
130
131        if parts.len() != 3 {
132            return Err(anyhow!(
133                "Did not find exactly three parts in the topic {}",
134                value
135            ));
136        } else {
137            debug!("parts: {:?}", parts);
138        }
139
140        let role = parts.first().expect("the role part should exist");
141        let id = parts.get(1).expect("the id part should exist");
142        let plug_name = parts.get(2).expect("the plug_name part should exist");
143
144        Ok(ThreePartTopic::new(role, id, plug_name))
145    }
146}
147
148pub fn build_topic(role: &str, id: &str, plug_name: &str) -> String {
149    format!("{role}/{id}/{plug_name}")
150}
151
152pub fn parse_plug_name(topic: &str) -> Option<&str> {
153    let parts: Vec<&str> = topic.split('/').collect();
154    match parts.get(2) {
155        Some(s) => Some(*s),
156        None => None,
157    }
158}
159
160pub fn parse_agent_id(topic: &str) -> Option<&str> {
161    let parts: Vec<&str> = topic.split('/').collect();
162    match parts.get(1) {
163        Some(s) => Some(*s),
164        None => None,
165    }
166}
167
168pub fn parse_agent_role(topic: &str) -> Option<&str> {
169    let parts: Vec<&str> = topic.split('/').collect();
170    match parts.first() {
171        Some(s) => Some(*s),
172        None => None,
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use crate::three_part_topic::{parse_agent_id, parse_agent_role, parse_plug_name};
179
180    #[test]
181    fn util_parsers() {
182        assert_eq!(parse_agent_role("one/two/three"), Some("one"));
183        assert_eq!(parse_agent_id("one/two/three"), Some("two"));
184        assert_eq!(parse_plug_name("one/two/three"), Some("three"));
185        assert_eq!(parse_plug_name("just/two"), None);
186    }
187}