libits_client/mqtt/topic/
mod.rs

1// Software Name: its-client
2// SPDX-FileCopyrightText: Copyright (c) 2016-2022 Orange
3// SPDX-License-Identifier: MIT License
4//
5// This software is distributed under the MIT license, see LICENSE.txt file for more details.
6//
7// Author: Frédéric GARDES <frederic.gardes@orange.com> et al.
8// Software description: This Intelligent Transportation Systems (ITS) [MQTT](https://mqtt.org/) client based on the [JSon](https://www.json.org) [ETSI](https://www.etsi.org/committee/its) specification transcription provides a ready to connect project for the mobility (connected and autonomous vehicles, road side units, vulnerable road users,...).
9pub mod geo_extension;
10mod message_type;
11mod parse_error;
12mod queue;
13
14use std::{cmp, convert, fmt, hash, str, str::FromStr};
15
16use log::error;
17
18use crate::analyse::configuration::Configuration;
19use crate::mqtt::topic::geo_extension::{GeoExtension, Tile};
20use crate::mqtt::topic::message_type::MessageType;
21use crate::mqtt::topic::parse_error::ParseError;
22use crate::mqtt::topic::queue::Queue;
23
24#[derive(Default, Debug, Clone)]
25// TODO implement a generic to manage a subscription with wild cards differently of a publish
26pub struct Topic {
27    // Project name at the topic root
28    project: String,
29
30    // Base topic
31    queue: Queue,
32    server: String,
33    message_type: MessageType,
34
35    // Source / destination extension: userUUID, roadUUID, oemUUID, appUUID, or +
36    uuid: String,
37
38    // GeoExtension
39    pub geo_extension: GeoExtension,
40}
41
42impl Topic {
43    fn empty() -> Topic {
44        Topic {
45            ..Default::default()
46        }
47    }
48
49    pub(crate) fn new<Q, T>(
50        queue: Option<Q>,
51        message_type: Option<T>,
52        uuid: Option<String>,
53        geo_extension: Option<GeoExtension>,
54    ) -> Topic
55    where
56        Q: Into<Queue> + Default,
57        T: Into<MessageType> + Default,
58    {
59        Topic {
60            project: "default".to_string(),
61            queue: match queue {
62                Some(into_queue) => into_queue.into(),
63                None => Queue::default(),
64            },
65            server: "v2x".to_string(),
66            message_type: match message_type {
67                Some(into_queue) => into_queue.into(),
68                None => MessageType::default(),
69            },
70            uuid: uuid.unwrap_or("+".to_string()),
71            geo_extension: geo_extension.unwrap_or_default(),
72
73            ..Default::default()
74        }
75    }
76
77    pub fn new_denm(component_name: String, geo_extension: &GeoExtension) -> Topic {
78        Topic::new(
79            Some("inQueue".to_string()),
80            Some("denm".to_string()),
81            Some(component_name),
82            // assumed clone, we build a new topic
83            Some(geo_extension.clone()),
84        )
85    }
86
87    pub fn project_base(&self) -> String {
88        format!(
89            "{}/{}/{}/{}",
90            self.project, self.queue, self.server, self.message_type
91        )
92    }
93
94    // TODO find a better way to appropriate
95    pub fn appropriate(&mut self, configuration: &Configuration) {
96        self.uuid = configuration.component_name(None);
97        self.queue = Queue::In;
98    }
99}
100
101impl hash::Hash for Topic {
102    fn hash<H: hash::Hasher>(&self, state: &mut H) {
103        self.project.hash(state);
104        self.queue.hash(state);
105        self.server.hash(state);
106        self.message_type.hash(state);
107        self.uuid.hash(state);
108        self.geo_extension.hash(state);
109    }
110}
111
112impl cmp::PartialEq for Topic {
113    fn eq(&self, other: &Self) -> bool {
114        self.project == other.project
115            && self.queue == other.queue
116            && self.server == other.server
117            && self.message_type == other.message_type
118            && self.uuid == other.uuid
119            && self.geo_extension == other.geo_extension
120    }
121}
122
123impl cmp::Eq for Topic {}
124
125impl cmp::PartialEq<String> for Topic {
126    fn eq(&self, other: &String) -> bool {
127        match Topic::from_str(other) {
128            Ok(topic) => self == &topic,
129            Err(error) => {
130                error!("We can't compare the topic with a bad string: {}", error);
131                false
132            }
133        }
134    }
135}
136
137impl convert::From<String> for Topic {
138    fn from(topic: String) -> Self {
139        Topic::from(topic.as_str())
140    }
141}
142
143impl convert::From<&str> for Topic {
144    fn from(topic: &str) -> Self {
145        match Topic::from_str(topic) {
146            Ok(topic) => topic,
147            Err(error) => panic!(
148                "Unable to convert the String {} as a Topic: {}, use from_str instead",
149                topic, error
150            ),
151        }
152    }
153}
154
155impl str::FromStr for Topic {
156    type Err = ParseError;
157
158    fn from_str(s: &str) -> Result<Self, Self::Err> {
159        s.trim_matches('/').split('/').enumerate().try_fold(
160            Topic::empty(),
161            |mut topic_struct, (i, element)| {
162                match i {
163                    // project
164                    0 => topic_struct.project = element.to_string(),
165                    // queue
166                    1 => topic_struct.queue = Queue::from_str(element)?,
167                    // server
168                    2 => topic_struct.server = element.to_string(),
169                    // message type
170                    3 => topic_struct.message_type = MessageType::from_str(element)?,
171                    // uuid
172                    4 => topic_struct.uuid = element.to_string(),
173                    // TODO use geo_extension FromStr trait instead
174                    // geo extension
175                    _n => {
176                        let result = Tile::from_str(element)?;
177                        topic_struct.geo_extension.tiles.push(result)
178                    }
179                }
180                Ok(topic_struct)
181            },
182        )
183    }
184}
185
186impl fmt::Display for Topic {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        write!(
189            f,
190            "{}/{}{}",
191            self.project_base(),
192            self.uuid,
193            self.geo_extension
194        )
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use crate::mqtt::topic::geo_extension::Tile;
201    use crate::mqtt::topic::message_type::MessageType;
202    use crate::mqtt::topic::queue::Queue;
203    use crate::mqtt::topic::Topic;
204    use std::str::FromStr;
205
206    #[test]
207    fn test_cam_topic_from_str() {
208        let topic_string = "default/outQueue/v2x/cam/car_1/0/1/2/3";
209        let topic_result = Topic::from_str(topic_string);
210        assert!(topic_result.is_ok());
211        let topic = topic_result.unwrap();
212        assert_eq!(topic.project, "default".to_string());
213        assert_eq!(topic.queue, Queue::Out);
214        assert_eq!(topic.server, "v2x".to_string());
215        assert_eq!(topic.message_type, MessageType::CAM);
216        assert_eq!(topic.uuid, "car_1".to_string());
217        assert_eq!(topic.geo_extension.tiles.len(), 4);
218        for i in 0..4 {
219            assert_eq!(topic.geo_extension.tiles[i], Tile::from(i as u8));
220        }
221    }
222
223    #[test]
224    fn test_denm_topic_from_str() {
225        let topic_string =
226            "default/outQueue/v2x/denm/wse_app_bcn1/1/2/0/2/2/2/2/3/3/0/0/3/2/0/2/0/1/0/1/0/3/1/";
227        let topic_result = Topic::from_str(topic_string);
228        assert!(topic_result.is_ok());
229        let topic = topic_result.unwrap();
230        assert_eq!(topic.project, "default".to_string());
231        assert_eq!(topic.queue, Queue::Out);
232        assert_eq!(topic.server, "v2x".to_string());
233        assert_eq!(topic.message_type, MessageType::DENM);
234        assert_eq!(topic.uuid, "wse_app_bcn1".to_string());
235        assert_eq!(topic.geo_extension.tiles.len(), 22);
236    }
237
238    #[test]
239    fn test_info_topic_from_str() {
240        let topic_string = "default/outQueue/v2x/info/broker";
241        let topic_result = Topic::from_str(topic_string);
242        assert!(topic_result.is_ok());
243        let topic = topic_result.unwrap();
244        assert_eq!(topic.project, "default".to_string());
245        assert_eq!(topic.queue, Queue::Out);
246        assert_eq!(topic.server, "v2x".to_string());
247        assert_eq!(topic.message_type, MessageType::INFO);
248        assert_eq!(topic.uuid, "broker".to_string());
249        assert_eq!(topic.geo_extension.tiles.len(), 0);
250    }
251
252    #[test]
253    fn test_in_queue_cam_topic_from_str() {
254        let topic_string = "default/inQueue/v2x/cam/car_1/0/1/2/3";
255        let topic_result = Topic::from_str(topic_string);
256        assert!(topic_result.is_ok());
257        let topic = topic_result.unwrap();
258        assert_eq!(topic.project, "default".to_string());
259        assert_eq!(topic.queue, Queue::In);
260        assert_eq!(topic.server, "v2x".to_string());
261        assert_eq!(topic.message_type, MessageType::CAM);
262        assert_eq!(topic.uuid, "car_1".to_string());
263        assert_eq!(topic.geo_extension.tiles.len(), 4);
264        for i in 0..4 {
265            assert_eq!(topic.geo_extension.tiles[i], Tile::from(i as u8));
266        }
267    }
268}