sparkplug_rs/
topic_name.rs

1use crate::{DeviceMessageType, NodeMessageType, TopicNamespace};
2use std::error::Error;
3use std::fmt::{Debug, Display, Formatter};
4use std::str::FromStr;
5
6/// Rust representation of a sparkplug™ MQTT topic-name.
7///
8/// The [TopicName] can be one of three possible types:
9/// - [TopicName::NodeMessage] for Edge Nodes
10/// - [TopicName::DeviceMessage] for devices
11/// - [TopicName::StateMessage] for SCADA applications
12///
13/// # Examples
14/// ```
15/// # use std::str::FromStr;
16/// # use sparkplug_rs::{NodeMessageType, TopicName, TopicNamespace};
17/// let node = TopicName::new_node_message(TopicNamespace::SPBV1_0,
18///                                        "my_group".to_string(),
19///                                        NodeMessageType::NBIRTH,
20///                                        "nodeId".to_string());
21/// assert_eq!(node.to_string(), "spBv1.0/my_group/NBIRTH/nodeId");
22///
23/// let topic: TopicName = TopicName::from_str("spBv1.0/my_group/NBIRTH/nodeId").unwrap();
24/// assert_eq!(topic, node);
25/// ```
26///
27/// ```
28/// # use std::str::FromStr;
29/// # use sparkplug_rs::{DeviceMessageType, NodeMessageType, TopicName, TopicNamespace};
30/// let device = TopicName::new_device_message(TopicNamespace::SPBV1_0,
31///                                            "my_group".to_string(),
32///                                            DeviceMessageType::DBIRTH,
33///                                            "nodeId".to_string(),
34///                                            "deviceId".to_string());
35/// assert_eq!(device.to_string(), "spBv1.0/my_group/DBIRTH/nodeId/deviceId");
36///
37/// let topic: TopicName = TopicName::from_str("spBv1.0/my_group/DBIRTH/nodeId/deviceId").unwrap();
38/// assert_eq!(topic, device);
39/// ```
40///
41/// ```
42/// # use std::str::FromStr;
43/// # use sparkplug_rs::{DeviceMessageType, NodeMessageType, TopicName, TopicNamespace};
44/// let state = TopicName::new_state_message("scada_host_id".to_string());
45/// assert_eq!(state.to_string(), "STATE/scada_host_id");
46///
47/// let topic: TopicName = TopicName::from_str("STATE/scada_host_id").unwrap();
48/// assert_eq!(state, topic);
49/// ```
50#[derive(Debug, Clone, PartialEq)]
51pub enum TopicName {
52    /// A message for edge-nodes
53    NodeMessage {
54        /// The namespace element of the Topic Namespace is the root element that will define
55        /// both the structure of the remaining namespace elements as well as the encoding used
56        /// for the associated payload data.
57        namespace: TopicNamespace,
58
59        /// The group_id element of the Topic Namespace provides for a logical grouping
60        /// of MQTT EoN nodes into the MQTT Server and back out to the consuming MQTT Clients.
61        group_id: String,
62
63        /// The message_type element of the Topic Namespace provides an indication as to
64        /// how to handle the MQTT payload of the message.
65        node_message_type: NodeMessageType,
66
67        /// The edge_node_id element of the Sparkplug (TM) Topic Namespace uniquely identifies the MQTT EoN node within the infrastructure.
68        edge_node_id: String,
69    },
70
71    /// A message for devices
72    DeviceMessage {
73        /// The namespace element of the Topic Namespace is the root element that will define
74        /// both the structure of the remaining namespace elements as well as the encoding used
75        /// for the associated payload data.
76        namespace: TopicNamespace,
77
78        /// The group_id element of the Topic Namespace provides for a logical grouping
79        /// of MQTT EoN nodes into the MQTT Server and back out to the consuming MQTT Clients.
80        group_id: String,
81
82        /// The message_type element of the Topic Namespace provides an indication as to
83        /// how to handle the MQTT payload of the message.
84        device_message_type: DeviceMessageType,
85
86        /// The edge_node_id element of the Sparkplug (TM) Topic Namespace uniquely identifies the MQTT EoN node within the infrastructure.
87        edge_node_id: String,
88
89        /// The device_id element of the Sparkplug (TM) Topic Namespace identifies a device attached (physically or logically) to the MQTT EoN node.
90        device_id: String,
91    },
92
93    /// A state message for scada systems
94    StateMessage {
95        /// The id of the SCADA application
96        scada_host_id: String,
97    },
98}
99
100impl TopicName {
101    /// Constructs a new [TopicName] of type [TopicName::NodeMessage]
102    pub const fn new_node_message(
103        namespace: TopicNamespace,
104        group_id: String,
105        node_message_type: NodeMessageType,
106        edge_node_id: String,
107    ) -> Self {
108        TopicName::NodeMessage {
109            namespace,
110            group_id,
111            node_message_type,
112            edge_node_id,
113        }
114    }
115
116    /// Constructs a new [TopicName] of type [TopicName::DeviceMessage]
117    pub const fn new_device_message(
118        namespace: TopicNamespace,
119        group_id: String,
120        device_message_type: DeviceMessageType,
121        edge_node_id: String,
122        device_id: String,
123    ) -> Self {
124        TopicName::DeviceMessage {
125            namespace,
126            group_id,
127            device_message_type,
128            edge_node_id,
129            device_id,
130        }
131    }
132
133    /// Constructs a new [TopicName] of type [TopicName::StateMessage]
134    pub const fn new_state_message(scada_host_id: String) -> Self {
135        TopicName::StateMessage { scada_host_id }
136    }
137}
138
139impl FromStr for TopicName {
140    type Err = Box<dyn Error + Sync + Send + 'static>;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        let parts: Vec<&str> = s.split('/').collect();
144        if parts.len() == 2 {
145            let mut iter = parts.iter();
146
147            if Some(&"STATE") == iter.next() {
148                if let Some(scada_host_id) = iter.next() {
149                    return Ok(TopicName::StateMessage {
150                        scada_host_id: scada_host_id.to_string(),
151                    });
152                }
153            }
154        } else if parts.len() == 4 {
155            let mut iter = parts.iter();
156            let namespace;
157            let group_id;
158            let node_message_type;
159            let edge_node_id;
160
161            if let Some(s) = iter.next() {
162                namespace = TopicNamespace::from_str(s)?;
163
164                if let Some(s) = iter.next() {
165                    group_id = s.to_string();
166
167                    if let Some(s) = iter.next() {
168                        node_message_type = NodeMessageType::from_str(s)?;
169
170                        if let Some(s) = iter.next() {
171                            edge_node_id = s.to_string();
172
173                            return Ok(TopicName::NodeMessage {
174                                namespace,
175                                group_id,
176                                node_message_type,
177                                edge_node_id,
178                            });
179                        }
180                    }
181                }
182            }
183        } else if parts.len() == 5 {
184            let mut iter = parts.iter();
185            let namespace;
186            let group_id;
187            let device_message_type;
188            let edge_node_id;
189
190            if let Some(s) = iter.next() {
191                namespace = TopicNamespace::from_str(s)?;
192
193                if let Some(s) = iter.next() {
194                    group_id = s.to_string();
195
196                    if let Some(s) = iter.next() {
197                        device_message_type = DeviceMessageType::from_str(s)?;
198
199                        if let Some(s) = iter.next() {
200                            edge_node_id = s.to_string();
201
202                            if let Some(s) = iter.next() {
203                                return Ok(TopicName::DeviceMessage {
204                                    namespace,
205                                    group_id,
206                                    device_message_type,
207                                    edge_node_id,
208                                    device_id: s.to_string(),
209                                });
210                            }
211                        }
212                    }
213                }
214            }
215        }
216
217        Err(Box::from(TopicNameParseError))
218    }
219}
220
221impl ToString for TopicName {
222    fn to_string(&self) -> String {
223        match self {
224            TopicName::NodeMessage {
225                namespace,
226                group_id,
227                node_message_type,
228                edge_node_id,
229            } => format!(
230                "{}/{}/{}/{}",
231                namespace.to_string(),
232                group_id,
233                node_message_type.to_string(),
234                edge_node_id
235            ),
236            TopicName::DeviceMessage {
237                namespace,
238                group_id,
239                device_message_type,
240                edge_node_id,
241                device_id,
242            } => format!(
243                "{}/{}/{}/{}/{}",
244                namespace.to_string(),
245                group_id,
246                device_message_type.to_string(),
247                edge_node_id,
248                device_id
249            ),
250            TopicName::StateMessage { scada_host_id } => format!("STATE/{}", scada_host_id),
251        }
252    }
253}
254
255#[derive(Debug, PartialEq)]
256pub struct TopicNameParseError;
257
258impl Display for TopicNameParseError {
259    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
260        Debug::fmt(self, f)
261    }
262}
263
264impl Error for TopicNameParseError {}
265
266#[cfg(test)]
267mod tests {
268    use crate::{DeviceMessageType, NodeMessageType, TopicName, TopicNamespace};
269    use std::str::FromStr;
270
271    #[test]
272    fn parse_state() {
273        TopicName::from_str("STATE/scada_id").unwrap();
274    }
275
276    #[test]
277    fn test_errors() {
278        assert_eq!(
279            TopicName::from_str("").unwrap_err().to_string(),
280            "TopicNameParseError"
281        );
282        assert_eq!(
283            TopicName::from_str("STATE/to_many/slashes")
284                .unwrap_err()
285                .to_string(),
286            "TopicNameParseError"
287        );
288        assert_eq!(
289            TopicName::from_str("STATE/to_many/slashes/x/x/x")
290                .unwrap_err()
291                .to_string(),
292            "TopicNameParseError"
293        );
294        assert_eq!(
295            TopicName::from_str("wrong_namespace/to_many/slashes/x/x")
296                .unwrap_err()
297                .to_string(),
298            "Error parsing topic's namespace"
299        );
300        assert_eq!(
301            TopicName::from_str("wrong_namespace/to_many/slashes/x")
302                .unwrap_err()
303                .to_string(),
304            "Error parsing topic's namespace"
305        );
306        assert_eq!(
307            TopicName::from_str("wrong_namespace/to_many/slashes/x")
308                .unwrap_err()
309                .to_string(),
310            "Error parsing topic's namespace"
311        );
312        assert_eq!(
313            TopicName::from_str("spBv1.0/my_group/WRONG/nodeId")
314                .unwrap_err()
315                .to_string(),
316            "Unknown NodeMessageType"
317        );
318        assert_eq!(
319            TopicName::from_str("spBv1.0/my_group/WRONG/nodeId/deviceId")
320                .unwrap_err()
321                .to_string(),
322            "Unknown DeviceMessageType"
323        );
324    }
325
326    #[test]
327    fn test_parse_node_cmd() {
328        if let TopicName::NodeMessage {
329            namespace,
330            group_id,
331            node_message_type,
332            edge_node_id,
333        } = TopicName::from_str("spBv1.0/my_group/NBIRTH/nodeId").unwrap()
334        {
335            assert_eq!(namespace, TopicNamespace::SPBV1_0);
336            assert_eq!(group_id, "my_group");
337            assert_eq!(node_message_type, NodeMessageType::NBIRTH);
338            assert_eq!(edge_node_id, "nodeId");
339        } else {
340            panic!();
341        }
342    }
343
344    #[test]
345    fn test_parse_device_cmd() {
346        if let TopicName::DeviceMessage {
347            namespace,
348            group_id,
349            device_message_type,
350            edge_node_id,
351            device_id,
352        } = TopicName::from_str("spBv1.0/my_group/DBIRTH/nodeId/deviceId").unwrap()
353        {
354            assert_eq!(namespace, TopicNamespace::SPBV1_0);
355            assert_eq!(group_id, "my_group");
356            assert_eq!(device_message_type, DeviceMessageType::DBIRTH);
357            assert_eq!(edge_node_id, "nodeId");
358            assert_eq!(device_id, "deviceId");
359        } else {
360            panic!();
361        }
362    }
363}