libits/transport/mqtt/
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::topic::Topic;
13use std::fmt::{Display, Formatter};
14use std::str::FromStr;
15use thiserror::Error;
16
17/// Error during topic manipulation
18#[derive(Error, Debug, PartialEq)]
19pub enum StrTopicError {
20    #[error("Cannot update topic at level 0")]
21    LevelZero,
22    #[error("Cannot update topic at level {0}: level is too high")]
23    LevelTooHigh(u8),
24}
25
26/// Represents a topic as a string.
27#[derive(Clone, Default, Debug, Hash, PartialEq, Eq)]
28pub struct StrTopic {
29    /// Topic as a string.
30    topic: String,
31}
32
33impl StrTopic {
34    /// Returns the topic parts
35    pub fn parts(&self) -> Vec<&str> {
36        self.topic.split('/').collect()
37    }
38
39    /// Updates the topic with a new value at the specified level
40    pub fn replace_at(&mut self, level: u8, value: &str) -> Result<(), StrTopicError> {
41        if level == 0 {
42            return Err(StrTopicError::LevelZero);
43        }
44        let mut parts = self.parts();
45        if level as usize <= parts.len() {
46            parts[level as usize - 1] = value;
47            self.topic = parts.join("/");
48            Ok(())
49        } else {
50            Err(StrTopicError::LevelTooHigh(level))
51        }
52    }
53}
54
55impl Display for StrTopic {
56    /// Formats the `StrTopic` for display.
57    ///
58    /// # Arguments
59    ///
60    /// * `f` - Formatter.
61    ///
62    /// # Returns
63    ///
64    /// A result indicating success or failure.
65    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
66        write!(f, "{}", self.topic)
67    }
68}
69
70impl FromStr for StrTopic {
71    type Err = std::str::Utf8Error;
72
73    /// Creates a `StrTopic` from a string slice.
74    ///
75    /// # Arguments
76    ///
77    /// * `s` - String slice.
78    ///
79    /// # Returns
80    ///
81    /// A result containing the `StrTopic` or an error.
82    fn from_str(s: &str) -> Result<Self, Self::Err> {
83        Ok(StrTopic {
84            topic: String::from(s),
85        })
86    }
87}
88
89impl Topic for StrTopic {
90    /// Returns the topic as a route.
91    ///
92    /// # Returns
93    ///
94    /// A string representing the route.
95    fn as_route(&self) -> String {
96        // Assume the topic is the route
97        // Assumed clone is inexpensive
98        self.topic.clone()
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105    use std::str::FromStr;
106
107    /// Helper function to create a `StrTopic`.
108    ///
109    /// # Arguments
110    ///
111    /// * `topic` - Topic string.
112    ///
113    /// # Returns
114    ///
115    /// A `StrTopic` instance.
116    fn create_str_topic(topic: &str) -> StrTopic {
117        StrTopic {
118            topic: topic.to_string(),
119        }
120    }
121
122    #[test]
123    fn str_topic_display() {
124        let topic = create_str_topic("test/topic");
125        assert_eq!(format!("{topic}"), "test/topic");
126    }
127
128    #[test]
129    fn str_topic_from_str_valid() {
130        let topic = StrTopic::from_str("test/topic").unwrap();
131        assert_eq!(topic, create_str_topic("test/topic"));
132    }
133
134    #[test]
135    fn str_topic_from_str_empty() {
136        let topic = StrTopic::from_str("").unwrap();
137        assert_eq!(topic, create_str_topic(""));
138    }
139
140    #[test]
141    fn str_topic_as_route() {
142        let topic = create_str_topic("test/route");
143        assert_eq!(topic.as_route(), "test/route");
144    }
145
146    #[test]
147    fn str_topic_not_replace_at_at_level_0() {
148        let mut topic = create_str_topic("a/b/c/d");
149        let result = topic.replace_at(0, "x");
150        assert!(result.is_err());
151        assert_eq!(result.unwrap_err(), StrTopicError::LevelZero);
152        assert_eq!(topic.to_string(), "a/b/c/d");
153    }
154
155    #[test]
156    fn str_topic_replace_at_at_first_level() {
157        let mut topic = create_str_topic("a/b/c/d");
158        let result = topic.replace_at(1, "x");
159        assert!(result.is_ok());
160        assert_eq!(topic.to_string(), "x/b/c/d");
161    }
162
163    #[test]
164    fn str_topic_replace_at_at_level_2() {
165        let mut topic = create_str_topic("a/b/c/d");
166        let result = topic.replace_at(2, "x");
167        assert!(result.is_ok());
168        assert_eq!(topic.to_string(), "a/x/c/d");
169    }
170
171    #[test]
172    fn str_topic_replace_at_at_level_3() {
173        let mut topic = create_str_topic("a/b/c/d");
174        let result = topic.replace_at(3, "x");
175        assert!(result.is_ok());
176        assert_eq!(topic.to_string(), "a/b/x/d");
177    }
178
179    #[test]
180    fn str_topic_replace_at_at_last_level() {
181        let mut topic = create_str_topic("a/b/c/d");
182        let result = topic.replace_at(4, "x");
183        assert!(result.is_ok());
184        assert_eq!(topic.to_string(), "a/b/c/x");
185    }
186
187    #[test]
188    fn str_topic_not_replace_at_at_level_too_high() {
189        let mut topic = create_str_topic("a/b/c/d");
190        let result = topic.replace_at(5, "x");
191        assert!(result.is_err());
192        assert_eq!(result.unwrap_err(), StrTopicError::LevelTooHigh(5));
193        assert_eq!(topic.to_string(), "a/b/c/d");
194    }
195}