mqtt_topic_engine/
topic_pattern_item.rs1use std::borrow::Cow;
4use std::convert::TryFrom;
5
6use arcstr::Substr;
7use thiserror::Error;
8
9#[derive(Error, Debug, Clone, PartialEq, Eq)]
11pub enum TopicPatternError {
12 #[error(
14 "Invalid topic pattern '{pattern}': # wildcard can only be the last \
15 segment"
16 )]
17 HashPosition {
18 pattern: String,
20 },
21
22 #[error("Invalid wildcard usage: {usage}")]
24 WildcardUsage {
25 usage: String,
27 },
28
29 #[error("Topic pattern cannot be empty")]
31 EmptyTopic,
32
33 #[error("Topic pattern must not contain the null character (U+0000)")]
36 NullChar,
37
38 #[error(
40 "Topic pattern structure mismatch.\nOriginal: '{original}'\nCustom: \
41 '{custom}'\nHint: Both patterns must have the same parameter \
42 structure (same wildcards in same positions)"
43 )]
44 PatternStructureMismatch {
45 original: String,
47 custom: String,
49 },
50}
51
52impl TopicPatternError {
53 pub fn hash_position(pattern: impl Into<String>) -> Self {
55 Self::HashPosition {
56 pattern: pattern.into(),
57 }
58 }
59
60 pub fn wildcard_usage(usage: impl Into<String>) -> Self {
62 Self::WildcardUsage {
63 usage: usage.into(),
64 }
65 }
66
67 pub fn pattern_mismatch(
69 original: impl Into<String>,
70 custom: impl Into<String>,
71 ) -> Self {
72 Self::PatternStructureMismatch {
73 original: original.into(),
74 custom: custom.into(),
75 }
76 }
77}
78
79impl From<std::convert::Infallible> for TopicPatternError {
80 fn from(_: std::convert::Infallible) -> Self {
81 unreachable!("Infallible can never be constructed")
82 }
83}
84
85impl From<crate::topic_match::Malformed> for TopicPatternError {
86 fn from(malformed: crate::topic_match::Malformed) -> Self {
87 use crate::topic_match::Malformed;
88 match malformed {
89 | Malformed::Empty => TopicPatternError::EmptyTopic,
90 | Malformed::NullChar => TopicPatternError::NullChar,
91 }
92 }
93}
94
95#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub enum TopicPatternItem {
98 Str(Substr),
100 Plus(Option<Substr>),
102 Hash(Option<Substr>),
104}
105
106impl TopicPatternItem {
107 pub fn as_str(&self) -> &str {
109 match self {
110 | TopicPatternItem::Str(s) => s,
111 | TopicPatternItem::Plus(_) => "+",
112 | TopicPatternItem::Hash(_) => "#",
113 }
114 }
115
116 pub fn as_wildcard(&self) -> Cow<'_, str> {
118 match self {
119 | TopicPatternItem::Plus(None) => Cow::Borrowed("+"),
120 | TopicPatternItem::Hash(None) => Cow::Borrowed("#"),
121 | TopicPatternItem::Plus(Some(name)) => {
122 Cow::Owned(format!("{{{name}}}"))
123 }
124 | TopicPatternItem::Hash(Some(name)) => {
125 Cow::Owned(format!("{{{name}:#}}"))
126 }
127 | TopicPatternItem::Str(s) => Cow::Borrowed(s),
128 }
129 }
130
131 pub fn param_name(&self) -> Option<Substr> {
133 match self {
134 | TopicPatternItem::Plus(Some(name))
135 | TopicPatternItem::Hash(Some(name)) => Some(name.clone()),
136 | _ => None,
137 }
138 }
139
140 pub fn is_wildcard(&self) -> bool {
142 matches!(self, TopicPatternItem::Plus(_) | TopicPatternItem::Hash(_))
143 }
144}
145
146impl From<&TopicPatternItem> for String {
147 fn from(item: &TopicPatternItem) -> Self {
148 item.as_str().to_string()
149 }
150}
151
152impl std::fmt::Display for TopicPatternItem {
153 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154 write!(f, "{}", self.as_str())
155 }
156}
157
158impl TryFrom<Substr> for TopicPatternItem {
159 type Error = TopicPatternError;
160 fn try_from(item: Substr) -> Result<Self, Self::Error> {
161 let res = match item.as_str() {
162 | "+" => TopicPatternItem::Plus(None),
163 | "#" => TopicPatternItem::Hash(None),
164 | _ if item.starts_with("{") && item.ends_with(":#}") => {
165 let inner =
166 item.trim_start_matches('{').trim_end_matches(":#}");
167 if inner.is_empty() {
168 return Err(TopicPatternError::wildcard_usage(
169 item.as_str(),
170 ));
171 }
172 TopicPatternItem::Hash(Some(item.substr_from(inner)))
173 }
174 | _ if item.starts_with("{") && item.ends_with("}") => {
175 let inner = item.trim_start_matches('{').trim_end_matches("}");
176 if inner.is_empty() {
177 return Err(TopicPatternError::wildcard_usage(
178 item.as_str(),
179 ));
180 }
181 TopicPatternItem::Plus(Some(item.substr_from(inner)))
182 }
183 | _ if item.contains(['+', '#']) => {
184 return Err(TopicPatternError::wildcard_usage(item.as_str()));
185 }
186 | _ => TopicPatternItem::Str(item),
187 };
188 Ok(res)
189 }
190}