tether_agent/plugs/
definitions.rs

1use log::{debug, error, warn};
2use serde::{Deserialize, Serialize};
3
4use super::three_part_topic::TetherOrCustomTopic;
5
6pub trait PlugDefinitionCommon<'a> {
7    fn name(&'a self) -> &'a str;
8    fn topic_str(&'a self) -> &'a str;
9    fn topic(&'a self) -> &'a TetherOrCustomTopic;
10    fn qos(&'a self) -> i32;
11}
12
13#[derive(Serialize, Deserialize, Debug)]
14pub struct InputPlugDefinition {
15    name: String,
16    topic: TetherOrCustomTopic,
17    qos: i32,
18}
19
20impl PlugDefinitionCommon<'_> for InputPlugDefinition {
21    fn name(&self) -> &str {
22        &self.name
23    }
24
25    fn topic_str(&self) -> &str {
26        match &self.topic {
27            TetherOrCustomTopic::Custom(s) => {
28                debug!("Plug named \"{}\" has custom topic \"{}\"", &self.name, &s);
29                s
30            }
31            TetherOrCustomTopic::Tether(t) => {
32                debug!(
33                    "Plug named \"{}\" has Three Part topic \"{:?}\"",
34                    &self.name, t
35                );
36                t.topic()
37            }
38        }
39    }
40
41    fn topic(&'_ self) -> &'_ TetherOrCustomTopic {
42        &self.topic
43    }
44
45    fn qos(&self) -> i32 {
46        self.qos
47    }
48}
49
50impl InputPlugDefinition {
51    pub fn new(name: &str, topic: TetherOrCustomTopic, qos: Option<i32>) -> InputPlugDefinition {
52        InputPlugDefinition {
53            name: String::from(name),
54            topic,
55            qos: qos.unwrap_or(1),
56        }
57    }
58
59    /// Use the topic of an incoming message to check against the definition of an Input Plug.
60    ///
61    /// Due to the use of wildcard subscriptions, multiple topic strings might match a given
62    /// Input Plug definition. e.g. `someRole/any/plugMessages` and `anotherRole/any/plugMessages`
63    /// should both match on an Input Plug named `plugMessages` unless more specific Role and/or ID
64    /// parts were specified in the Input Plug Definition.
65    ///
66    /// In the case where an Input Plug was defined with a completely manually-specified topic string,
67    /// this function returns a warning and marks ANY incoming message as a valid match; the end-user
68    /// developer is expected to match against topic strings themselves.
69    pub fn matches(&self, incoming_topic: &TetherOrCustomTopic) -> bool {
70        match incoming_topic {
71            TetherOrCustomTopic::Tether(incoming_three_parts) => match &self.topic {
72                TetherOrCustomTopic::Tether(my_tpt) => {
73                    let matches_role =
74                        my_tpt.role() == "+" || my_tpt.role().eq(incoming_three_parts.role());
75                    let matches_id =
76                        my_tpt.id() == "+" || my_tpt.id().eq(incoming_three_parts.id());
77                    let matches_plug_name = my_tpt.plug_name() == "+"
78                        || my_tpt.plug_name().eq(incoming_three_parts.plug_name());
79                    debug!("Test match for plug named \"{}\" with def {:?} against {:?} => matches_role? {}, matches_id? {}, matches_plug_name? {}", &self.name, &self.topic, &incoming_three_parts, matches_role, matches_id, matches_plug_name);
80                    matches_role && matches_id && matches_plug_name
81                }
82                TetherOrCustomTopic::Custom(my_custom_topic) => {
83                    debug!(
84                    "Custom/manual topic \"{}\" on Plug \"{}\" cannot be matched automatically; please filter manually for this",
85                    &my_custom_topic,
86                    self.name()
87                );
88                    my_custom_topic.as_str() == "#"
89                        || my_custom_topic.as_str() == incoming_three_parts.topic()
90                }
91            },
92            TetherOrCustomTopic::Custom(incoming_custom) => match &self.topic {
93                TetherOrCustomTopic::Custom(my_custom_topic) => {
94                    if my_custom_topic.as_str() == "#"
95                        || my_custom_topic.as_str() == incoming_custom.as_str()
96                    {
97                        true
98                    } else {
99                        warn!(
100                            "Incoming topic \"{}\" is not a three-part topic",
101                            &incoming_custom
102                        );
103                        false
104                    }
105                }
106                TetherOrCustomTopic::Tether(_) => {
107                    error!("Incoming is NOT Three Part Topic but this plug DOES have Three Part Topic; cannot decide match");
108                    false
109                }
110            },
111        }
112    }
113}
114
115#[derive(Serialize, Deserialize, Debug)]
116pub struct OutputPlugDefinition {
117    name: String,
118    topic: TetherOrCustomTopic,
119    qos: i32,
120    retain: bool,
121}
122
123impl PlugDefinitionCommon<'_> for OutputPlugDefinition {
124    fn name(&'_ self) -> &'_ str {
125        &self.name
126    }
127
128    fn topic_str(&self) -> &str {
129        match &self.topic {
130            TetherOrCustomTopic::Custom(s) => s,
131            TetherOrCustomTopic::Tether(t) => t.topic(),
132        }
133    }
134
135    fn topic(&'_ self) -> &'_ TetherOrCustomTopic {
136        &self.topic
137    }
138
139    fn qos(&'_ self) -> i32 {
140        self.qos
141    }
142}
143
144impl OutputPlugDefinition {
145    pub fn new(
146        name: &str,
147        topic: TetherOrCustomTopic,
148        qos: Option<i32>,
149        retain: Option<bool>,
150    ) -> OutputPlugDefinition {
151        OutputPlugDefinition {
152            name: String::from(name),
153            topic,
154            qos: qos.unwrap_or(1),
155            retain: retain.unwrap_or(false),
156        }
157    }
158
159    pub fn retain(&self) -> bool {
160        self.retain
161    }
162}
163
164#[derive(Serialize, Deserialize, Debug)]
165pub enum PlugDefinition {
166    InputPlug(InputPlugDefinition),
167    OutputPlug(OutputPlugDefinition),
168}
169
170impl PlugDefinition {
171    pub fn name(&self) -> &str {
172        match self {
173            PlugDefinition::InputPlug(p) => p.name(),
174            PlugDefinition::OutputPlug(p) => p.name(),
175        }
176    }
177
178    pub fn topic(&self) -> &str {
179        match self {
180            PlugDefinition::InputPlug(p) => p.topic_str(),
181            PlugDefinition::OutputPlug(p) => p.topic_str(),
182        }
183    }
184
185    pub fn matches(&self, topic: &TetherOrCustomTopic) -> bool {
186        match self {
187            PlugDefinition::InputPlug(p) => p.matches(topic),
188            PlugDefinition::OutputPlug(_) => {
189                error!("We don't check matches for Output Plugs");
190                false
191            }
192        }
193    }
194}
195
196#[cfg(test)]
197mod tests {
198
199    use crate::{
200        three_part_topic::{parse_plug_name, TetherOrCustomTopic, ThreePartTopic},
201        InputPlugDefinition, PlugDefinitionCommon,
202    };
203
204    #[test]
205    fn input_match_tpt() {
206        let plug_def = InputPlugDefinition::new(
207            "testPlug",
208            TetherOrCustomTopic::Tether(ThreePartTopic::new_for_subscribe(
209                "testPlug", None, None, None,
210            )),
211            None,
212        );
213
214        assert_eq!(&plug_def.name, "testPlug");
215        assert_eq!(plug_def.topic_str(), "+/+/testPlug");
216        assert_eq!(parse_plug_name("+/+/testPlug"), Some("testPlug"));
217        assert!(
218            plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
219                "dummy", "any", "testPlug"
220            )))
221        );
222        assert!(
223            !plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
224                "dummy",
225                "any",
226                "anotherPlug"
227            )))
228        );
229        // assert!(!plug_def.matches(&TetherOrCustomTopic::Custom("dummy/any/anotherPlug".into())));
230    }
231
232    #[test]
233    fn input_match_tpt_custom_role() {
234        let plug_def = InputPlugDefinition::new(
235            "customPlug",
236            TetherOrCustomTopic::Tether(ThreePartTopic::new_for_subscribe(
237                "customPlug",
238                Some("customRole"),
239                None,
240                None,
241            )),
242            None,
243        );
244
245        assert_eq!(&plug_def.name, "customPlug");
246        assert_eq!(plug_def.topic_str(), "customRole/+/customPlug");
247        assert!(
248            plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
249                "customRole",
250                "any",
251                "customPlug"
252            )))
253        );
254        assert!(
255            plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
256                "customRole",
257                "andAnythingElse",
258                "customPlug"
259            )))
260        );
261        assert!(
262            !plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
263                "customRole",
264                "any",
265                "notMyPlug"
266            )))
267        ); // wrong incoming Plug N.into())ame
268        assert!(
269            !plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
270                "someOtherRole",
271                "any",
272                "customPlug"
273            )))
274        ); // wrong incoming R.into())ole
275    }
276
277    #[test]
278    fn input_match_custom_id() {
279        let plug_def = InputPlugDefinition::new(
280            "customPlug",
281            TetherOrCustomTopic::Tether(ThreePartTopic::new_for_subscribe(
282                "customPlug",
283                None,
284                Some("specificID"),
285                None,
286            )),
287            None,
288        );
289
290        assert_eq!(&plug_def.name, "customPlug");
291        assert_eq!(plug_def.topic_str(), "+/specificID/customPlug");
292        assert!(
293            plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
294                "anyRole",
295                "specificID",
296                "customPlug"
297            )))
298        );
299        assert!(
300            plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
301                "anotherRole",
302                "specificID",
303                "customPlug"
304            )))
305        ); // wrong incoming Role
306        assert!(
307            !plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
308                "anyRole",
309                "specificID",
310                "notMyPlug"
311            )))
312        ); // wrong incoming Plug Name
313        assert!(
314            !plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
315                "anyRole",
316                "anotherID",
317                "customPlug"
318            )))
319        ); // wrong incoming ID
320    }
321
322    #[test]
323    fn input_match_both() {
324        let plug_def = InputPlugDefinition::new(
325            "customPlug",
326            TetherOrCustomTopic::Tether(ThreePartTopic::new_for_subscribe(
327                "customPlug",
328                Some("specificRole"),
329                Some("specificID"),
330                None,
331            )),
332            None,
333        );
334
335        assert_eq!(&plug_def.name, "customPlug");
336        assert_eq!(plug_def.topic_str(), "specificRole/specificID/customPlug");
337        assert!(
338            plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
339                "specificRole",
340                "specificID",
341                "customPlug"
342            )))
343        );
344        assert!(!plug_def.matches(&TetherOrCustomTopic::Custom(
345            "specificRole/specificID/notMyPlug".into()
346        ))); // wrong incoming Plug N.into())ame
347        assert!(!plug_def.matches(&TetherOrCustomTopic::Custom(
348            "specificRole/anotherID/customPlug".into()
349        ))); // wrong incoming.into()) ID
350        assert!(!plug_def.matches(&TetherOrCustomTopic::Custom(
351            "anotherRole/anotherID/customPlug".into()
352        ))); // wrong incoming R.into())ole
353    }
354
355    #[test]
356    fn input_match_custom_topic() {
357        let plug_def = InputPlugDefinition::new(
358            "customPlug",
359            TetherOrCustomTopic::Custom("one/two/three/four/five".into()), // not a standard Tether Three Part Topic
360            None,
361        );
362
363        assert_eq!(plug_def.name(), "customPlug");
364        // it will match on exactly the same topic:
365        assert!(plug_def.matches(&TetherOrCustomTopic::Custom(
366            "one/two/three/four/five".into()
367        )));
368
369        // it will NOT match on anything else:
370        assert!(!plug_def.matches(&TetherOrCustomTopic::Custom("one/one/one/one/one".into())));
371    }
372
373    #[test]
374    fn input_match_wildcard() {
375        let plug_def = InputPlugDefinition::new(
376            "everything",
377            TetherOrCustomTopic::Custom("#".into()), // fully legal, but not a standard Three Part Topic
378            None,
379        );
380
381        assert_eq!(plug_def.name(), "everything");
382
383        // Standard TPT will match
384        assert!(
385            plug_def.matches(&TetherOrCustomTopic::Tether(ThreePartTopic::new(
386                "any", "any", "plugName"
387            )))
388        );
389
390        // Anything will match, even custom incoming
391        assert!(plug_def.matches(&TetherOrCustomTopic::Custom(
392            "one/two/three/four/five".into()
393        )));
394    }
395}