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}