Skip to main content

mqtt_topic_engine/
topic_pattern_item.rs

1//! MQTT topic pattern item types and functionality
2
3use std::borrow::Cow;
4use std::convert::TryFrom;
5
6use arcstr::Substr;
7use thiserror::Error;
8
9/// Error types for topic pattern parsing
10#[derive(Error, Debug, Clone, PartialEq, Eq)]
11pub enum TopicPatternError {
12	/// Hash wildcard (#) used not at the end of the pattern
13	#[error(
14		"Invalid topic pattern '{pattern}': # wildcard can only be the last \
15		 segment"
16	)]
17	HashPosition {
18		/// The invalid pattern
19		pattern: String,
20	},
21
22	/// Wildcard characters (+ or #) used incorrectly
23	#[error("Invalid wildcard usage: {usage}")]
24	WildcardUsage {
25		/// Description of invalid usage
26		usage: String,
27	},
28
29	/// Empty topic is not valid
30	#[error("Topic pattern cannot be empty")]
31	EmptyTopic,
32
33	/// Topic pattern contained the null character (U+0000), which MQTT ยง4.7.3
34	/// forbids.
35	#[error("Topic pattern must not contain the null character (U+0000)")]
36	NullChar,
37
38	/// Topic pattern structure mismatch when trying to use compatible pattern
39	#[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 pattern from the struct
46		original: String,
47		/// Custom pattern that doesn't match
48		custom: String,
49	},
50}
51
52impl TopicPatternError {
53	/// Creates a new HashPosition error
54	pub fn hash_position(pattern: impl Into<String>) -> Self {
55		Self::HashPosition {
56			pattern: pattern.into(),
57		}
58	}
59
60	/// Creates a new WildcardUsage error
61	pub fn wildcard_usage(usage: impl Into<String>) -> Self {
62		Self::WildcardUsage {
63			usage: usage.into(),
64		}
65	}
66
67	/// Creates a new PatternStructureMismatch error
68	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/// MQTT topic pattern segment: literal string or wildcard
96#[derive(Debug, Clone, PartialEq, Eq, Hash)]
97pub enum TopicPatternItem {
98	/// Literal string segment
99	Str(Substr),
100	/// Single-level wildcard `+` or named `{param}`
101	Plus(Option<Substr>),
102	/// Multi-level wildcard `#` or named `{param:#}`
103	Hash(Option<Substr>),
104}
105
106impl TopicPatternItem {
107	/// Returns string representation of the pattern item.
108	pub fn as_str(&self) -> &str {
109		match self {
110			| TopicPatternItem::Str(s) => s,
111			| TopicPatternItem::Plus(_) => "+",
112			| TopicPatternItem::Hash(_) => "#",
113		}
114	}
115
116	/// Returns pattern representation with named parameters in braces.
117	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	/// Returns parameter name for named wildcards.
132	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	/// Returns true if this item is a wildcard (+ or #).
141	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}