libits/transport/mqtt/
routed_str_topic.rs

1/*
2 * Software Name : libits-client
3 * SPDX-FileCopyrightText: Copyright (c) Orange SA
4 * SPDX-License-Identifier: MIT
5 *
6 * This software is distributed under the MIT license,
7 * see the "LICENSE.txt" file for more details or https://opensource.org/license/MIT/
8 *
9 * Authors: see CONTRIBUTORS.md
10 */
11
12use crate::transport::mqtt::str_topic::{StrTopic, StrTopicError};
13use crate::transport::mqtt::topic::Topic;
14use std::fmt::{Display, Formatter};
15use std::str::FromStr;
16
17/// Represents a routed topic as a string.
18///
19/// The ROUTE_LEVEL const generic parameter represents the route level:
20/// - 255 means no specific route level (use the full topic as route)
21/// - Any other value (0-254) represents the number of levels to use for routing
22#[derive(Clone, Debug, Hash, PartialEq, Eq)]
23pub struct RoutedStrTopic<const ROUTE_LEVEL: u8 = 255> {
24    /// Topic as a StrTopic.
25    topic: StrTopic,
26}
27
28impl<const ROUTE_LEVEL: u8> RoutedStrTopic<ROUTE_LEVEL> {
29    /// Returns the route level
30    pub fn route_level(self) -> u8 {
31        ROUTE_LEVEL
32    }
33
34    /// Updates the topic with a new value
35    pub fn replace_at(&mut self, level: u8, value: &str) -> Result<(), StrTopicError> {
36        self.topic.replace_at(level, value)
37    }
38}
39
40impl<const ROUTE_LEVEL: u8> Default for RoutedStrTopic<ROUTE_LEVEL> {
41    fn default() -> Self {
42        Self {
43            topic: StrTopic::default(),
44        }
45    }
46}
47
48impl<const ROUTE_LEVEL: u8> Display for RoutedStrTopic<ROUTE_LEVEL> {
49    /// Formats the `RoutedStrTopic` for display.
50    ///
51    /// # Arguments
52    ///
53    /// * `f` - Formatter.
54    ///
55    /// # Returns
56    ///
57    /// A result indicating success or failure.
58    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
59        write!(f, "{}", self.topic)
60    }
61}
62
63impl<const ROUTE_LEVEL: u8> FromStr for RoutedStrTopic<ROUTE_LEVEL> {
64    type Err = std::str::Utf8Error;
65
66    /// Creates a `RoutedStrTopic` from a string slice.
67    ///
68    /// # Arguments
69    ///
70    /// * `s` - String slice.
71    ///
72    /// # Returns
73    ///
74    /// A result containing the `RoutedStrTopic` or an error.
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        match StrTopic::from_str(s) {
77            Ok(topic) => Ok(RoutedStrTopic { topic }),
78            Err(e) => Err(e),
79        }
80    }
81}
82
83impl<const ROUTE_LEVEL: u8> Topic for RoutedStrTopic<ROUTE_LEVEL> {
84    /// Returns the topic as a route.
85    ///
86    /// # Returns
87    ///
88    /// A string representing the route.
89    fn as_route(&self) -> String {
90        if ROUTE_LEVEL == 0 {
91            // Route level 0 means an empty route
92            String::new()
93        } else {
94            let string_topic = self.topic.to_string();
95            // Use the specified number of levels
96            let parts = self.topic.parts();
97            if ROUTE_LEVEL as usize >= parts.len() {
98                string_topic
99            } else {
100                parts[..ROUTE_LEVEL as usize].join("/")
101            }
102        }
103    }
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109    use std::str::FromStr;
110
111    /// Helper function to create a `RoutedStrTopic`.
112    ///
113    /// # Arguments
114    ///
115    /// * `topic` - Topic string.
116    ///
117    /// # Returns
118    ///
119    /// A `RoutedStrTopic` instance.
120    fn create_routed_str_topic(topic: &str) -> RoutedStrTopic {
121        RoutedStrTopic {
122            topic: StrTopic::from_str(topic).unwrap(),
123        }
124    }
125
126    fn create_routed_str_topic_with_levels<const LEVEL: u8>(topic: &str) -> RoutedStrTopic<LEVEL> {
127        RoutedStrTopic {
128            topic: StrTopic::from_str(topic).unwrap(),
129        }
130    }
131
132    #[test]
133    fn routed_str_topic_display() {
134        let topic = create_routed_str_topic("test/topic");
135        assert_eq!(format!("{topic}"), "test/topic");
136    }
137
138    #[test]
139    fn routed_str_topic_from_str_valid() {
140        let topic = RoutedStrTopic::from_str("test/topic").unwrap();
141        assert_eq!(topic, create_routed_str_topic("test/topic"));
142    }
143
144    #[test]
145    fn routed_str_topic_from_str_empty() {
146        let topic = RoutedStrTopic::from_str("").unwrap();
147        assert_eq!(topic, create_routed_str_topic(""));
148    }
149
150    #[test]
151    fn routed_str_topic_from_str_with_route_level_3() {
152        let topic = RoutedStrTopic::<3>::from_str("a/b/c/d/e").unwrap();
153        assert_eq!(topic.as_route(), "a/b/c");
154        assert_eq!(topic.route_level(), 3);
155    }
156
157    #[test]
158    fn routed_str_topic_const_generic_default() {
159        let topic = RoutedStrTopic::<255>::default();
160        assert_eq!(topic.route_level(), 255);
161    }
162
163    #[test]
164    fn routed_str_topic_as_route() {
165        let topic = create_routed_str_topic("test/route");
166        assert_eq!(topic.as_route(), "test/route");
167    }
168
169    #[test]
170    fn routed_str_topic_as_route_with_none_levels() {
171        let topic = create_routed_str_topic_with_levels::<255>("a/b/c/d/e");
172        assert_eq!(topic.as_route(), "a/b/c/d/e");
173    }
174
175    #[test]
176    fn routed_str_topic_as_route_with_zero_levels() {
177        let topic = create_routed_str_topic_with_levels::<0>("a/b/c/d/e");
178        assert_eq!(topic.as_route(), "");
179    }
180
181    #[test]
182    fn routed_str_topic_as_route_with_one_level() {
183        let topic = create_routed_str_topic_with_levels::<1>("a/b/c/d/e");
184        assert_eq!(topic.as_route(), "a");
185    }
186
187    #[test]
188    fn routed_str_topic_as_route_with_three_levels() {
189        let topic = create_routed_str_topic_with_levels::<3>("a/b/c/d/e");
190        assert_eq!(topic.as_route(), "a/b/c");
191    }
192
193    #[test]
194    fn routed_str_topic_as_route_with_more_levels_than_available() {
195        let topic = create_routed_str_topic_with_levels::<10>("a/b/c");
196        assert_eq!(topic.as_route(), "a/b/c");
197    }
198
199    #[test]
200    fn routed_str_topic_as_route_single_level_topic() {
201        let topic = create_routed_str_topic_with_levels::<1>("single");
202        assert_eq!(topic.as_route(), "single");
203    }
204
205    #[test]
206    fn routed_str_topic_as_route_single_level_topic_zero_levels() {
207        let topic = create_routed_str_topic_with_levels::<0>("single");
208        assert_eq!(topic.as_route(), "");
209    }
210
211    #[test]
212    fn routed_str_topic_with_route_levels_constructor() {
213        let topic = create_routed_str_topic_with_levels::<2>("a/b/c/d");
214        assert_eq!(topic.to_string(), "a/b/c/d");
215        assert_eq!(topic.as_route(), "a/b");
216    }
217
218    #[test]
219    fn routed_str_topic_not_replace_at_at_level_0() {
220        let mut topic = create_routed_str_topic("a/b/c/d");
221        let result = topic.replace_at(0, "x");
222        assert!(result.is_err());
223        assert_eq!(result.unwrap_err(), StrTopicError::LevelZero);
224        assert_eq!(topic.to_string(), "a/b/c/d");
225    }
226
227    #[test]
228    fn routed_str_topic_replace_at_at_first_level() {
229        let mut topic = create_routed_str_topic("a/b/c/d");
230        let result = topic.replace_at(1, "x");
231        assert!(result.is_ok());
232        assert_eq!(topic.to_string(), "x/b/c/d");
233    }
234
235    #[test]
236    fn routed_str_topic_replace_at_at_level_2() {
237        let mut topic = create_routed_str_topic("a/b/c/d");
238        let result = topic.replace_at(2, "x");
239        assert!(result.is_ok());
240        assert_eq!(topic.to_string(), "a/x/c/d");
241    }
242
243    #[test]
244    fn routed_str_topic_replace_at_at_level_3() {
245        let mut topic = create_routed_str_topic("a/b/c/d");
246        let result = topic.replace_at(3, "x");
247        assert!(result.is_ok());
248        assert_eq!(topic.to_string(), "a/b/x/d");
249    }
250
251    #[test]
252    fn routed_str_topic_replace_at_at_last_level() {
253        let mut topic = create_routed_str_topic("a/b/c/d");
254        let result = topic.replace_at(4, "x");
255        assert!(result.is_ok());
256        assert_eq!(topic.to_string(), "a/b/c/x");
257    }
258
259    #[test]
260    fn routed_str_topic_not_replace_at_at_level_too_high() {
261        let mut topic = create_routed_str_topic("a/b/c/d");
262        let result = topic.replace_at(5, "x");
263        assert!(result.is_err());
264        assert_eq!(result.unwrap_err(), StrTopicError::LevelTooHigh(5));
265        assert_eq!(topic.to_string(), "a/b/c/d");
266    }
267}