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(
35 "Topic pattern structure mismatch.\nOriginal: '{original}'\nCustom: \
36 '{custom}'\nHint: Both patterns must have the same parameter \
37 structure (same wildcards in same positions)"
38 )]
39 PatternStructureMismatch {
40 original: String,
42 custom: String,
44 },
45}
46
47impl TopicPatternError {
48 pub fn hash_position(pattern: impl Into<String>) -> Self {
50 Self::HashPosition {
51 pattern: pattern.into(),
52 }
53 }
54
55 pub fn wildcard_usage(usage: impl Into<String>) -> Self {
57 Self::WildcardUsage {
58 usage: usage.into(),
59 }
60 }
61
62 pub fn pattern_mismatch(
64 original: impl Into<String>,
65 custom: impl Into<String>,
66 ) -> Self {
67 Self::PatternStructureMismatch {
68 original: original.into(),
69 custom: custom.into(),
70 }
71 }
72}
73
74impl From<std::convert::Infallible> for TopicPatternError {
75 fn from(_: std::convert::Infallible) -> Self {
76 unreachable!("Infallible can never be constructed")
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash)]
82pub enum TopicPatternItem {
83 Str(Substr),
85 Plus(Option<Substr>),
87 Hash(Option<Substr>),
89}
90
91impl TopicPatternItem {
92 pub fn as_str(&self) -> &str {
94 match self {
95 | TopicPatternItem::Str(s) => s,
96 | TopicPatternItem::Plus(_) => "+",
97 | TopicPatternItem::Hash(_) => "#",
98 }
99 }
100
101 pub fn as_wildcard(&self) -> Cow<'_, str> {
103 match self {
104 | TopicPatternItem::Plus(None) => Cow::Borrowed("+"),
105 | TopicPatternItem::Hash(None) => Cow::Borrowed("#"),
106 | TopicPatternItem::Plus(Some(name)) => {
107 Cow::Owned(format!("{{{name}}}"))
108 }
109 | TopicPatternItem::Hash(Some(name)) => {
110 Cow::Owned(format!("{{{name}:#}}"))
111 }
112 | TopicPatternItem::Str(s) => Cow::Borrowed(s),
113 }
114 }
115
116 pub fn param_name(&self) -> Option<Substr> {
118 match self {
119 | TopicPatternItem::Plus(Some(name))
120 | TopicPatternItem::Hash(Some(name)) => Some(name.clone()),
121 | _ => None,
122 }
123 }
124
125 pub fn is_wildcard(&self) -> bool {
127 matches!(self, TopicPatternItem::Plus(_) | TopicPatternItem::Hash(_))
128 }
129}
130
131impl From<&TopicPatternItem> for String {
132 fn from(item: &TopicPatternItem) -> Self {
133 item.as_str().to_string()
134 }
135}
136
137impl std::fmt::Display for TopicPatternItem {
138 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139 write!(f, "{}", self.as_str())
140 }
141}
142
143impl TryFrom<Substr> for TopicPatternItem {
144 type Error = TopicPatternError;
145 fn try_from(item: Substr) -> Result<Self, Self::Error> {
146 let res = match item.as_str() {
147 | "+" => TopicPatternItem::Plus(None),
148 | "#" => TopicPatternItem::Hash(None),
149 | _ if item.starts_with("{") && item.ends_with(":#}") => {
150 let inner =
151 item.trim_start_matches('{').trim_end_matches(":#}");
152 if inner.is_empty() {
153 return Err(TopicPatternError::wildcard_usage(
154 item.as_str(),
155 ));
156 }
157 TopicPatternItem::Hash(Some(item.substr_from(inner)))
158 }
159 | _ if item.starts_with("{") && item.ends_with("}") => {
160 let inner = item.trim_start_matches('{').trim_end_matches("}");
161 if inner.is_empty() {
162 return Err(TopicPatternError::wildcard_usage(
163 item.as_str(),
164 ));
165 }
166 TopicPatternItem::Plus(Some(item.substr_from(inner)))
167 }
168 | _ if item.contains(['+', '#']) => {
169 return Err(TopicPatternError::wildcard_usage(item.as_str()));
170 }
171 | _ => TopicPatternItem::Str(item),
172 };
173 Ok(res)
174 }
175}