Skip to main content

mqtt_topic_engine/
topic_match.rs

1//! Matched-topic types.
2//!
3//! [`TopicPath`] is a concrete topic split into segments; [`TopicMatch`] is the
4//! result of matching such a topic against a pattern, exposing the captured
5//! positional and named parameters.
6
7#![allow(clippy::missing_docs_in_private_items)]
8
9use std::fmt;
10use std::ops::Range;
11use std::sync::Arc;
12
13use arcstr::{ArcStr, Substr};
14use smallvec::SmallVec;
15use thiserror::Error;
16
17/// A concrete MQTT topic, split into its `/`-delimited segments.
18///
19/// The original topic string and the segment slices share the same backing
20/// [`ArcStr`] allocation, so cloning and slicing are cheap.
21#[derive(Debug, Clone)]
22pub struct TopicPath {
23	/// The full topic string.
24	pub path: ArcStr,
25	/// The topic split on `/`; each segment is a slice into [`path`](Self::path).
26	pub segments: Vec<Substr>,
27}
28
29impl TopicPath {
30	/// Builds a [`TopicPath`] by splitting `path` on `/` into segments.
31	pub fn new(path: impl Into<ArcStr>) -> Self {
32		let path = path.into();
33		let segments: Vec<Substr> =
34			path.split('/').map(|s| path.substr_from(s)).collect();
35		Self { path, segments }
36	}
37
38	/// Returns a cheap (refcounted) clone of the full topic string.
39	pub fn path(&self) -> ArcStr {
40		self.path.clone()
41	}
42}
43
44impl fmt::Display for TopicPath {
45	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46		write!(f, "{}", self.path)
47	}
48}
49
50/// Errors returned when matching a topic against a pattern.
51#[derive(Error, Debug, Clone, PartialEq, Eq)]
52pub enum TopicMatchError {
53	/// Pattern ended unexpectedly while matching topic
54	#[error("Pattern ended unexpectedly while matching topic")]
55	UnexpectedEndOfPattern,
56
57	/// Topic ended unexpectedly while matching pattern
58	#[error("Topic ended unexpectedly while matching pattern")]
59	UnexpectedEndOfTopic,
60
61	/// Hash wildcard (#) found in unexpected position
62	#[error("Hash wildcard (#) found in unexpected position")]
63	UnexpectedHashSegment,
64
65	/// Segment mismatch during topic matching
66	#[error(
67		"Segment mismatch at position {position}: expected '{expected}', \
68		 found '{found}'"
69	)]
70	SegmentMismatch {
71		/// Expected segment value
72		expected: String,
73		/// Actually found segment value
74		found: String,
75		/// Position where mismatch occurred
76		position: usize,
77	},
78
79	/// Duplicate parameter name found in pattern
80	#[error("Duplicate parameter name found in pattern")]
81	DuplicateParameterName,
82}
83
84/// The result of matching a [`TopicPath`] against a pattern.
85///
86/// Holds the matched topic plus the ranges of segments captured by the
87/// pattern's wildcards, accessible by position ([`get_param`](Self::get_param))
88/// or by name ([`get_named_param`](Self::get_named_param)).
89pub struct TopicMatch {
90	topic: Arc<TopicPath>,
91	params: SmallVec<[Range<usize>; 3]>,
92	named_params: SmallVec<[(Substr, Range<usize>); 3]>,
93}
94
95impl TopicMatch {
96	pub(crate) fn from_match_result(
97		topic: Arc<TopicPath>,
98		params: SmallVec<[Range<usize>; 3]>,
99		named_params: SmallVec<[(Substr, Range<usize>); 3]>,
100	) -> Self {
101		Self {
102			topic,
103			params,
104			named_params,
105		}
106	}
107
108	/// Returns the matched topic's segments.
109	pub fn path_segments(&self) -> &Vec<Substr> {
110		&self.topic.segments
111	}
112
113	fn get_param_range(&self, range: &Range<usize>) -> Substr {
114		if range.is_empty() {
115			self.topic.path.substr(0 .. 0)
116		} else if range.len() == 1 {
117			self.topic.segments[range.start].clone()
118		} else {
119			let start_segment = &self.topic.segments[range.start];
120			let end_segment = &self.topic.segments[range.end - 1];
121
122			let start_pos = start_segment.as_ptr() as usize
123				- self.topic.path.as_ptr() as usize;
124			let end_pos = end_segment.as_ptr() as usize
125				- self.topic.path.as_ptr() as usize
126				+ end_segment.len();
127
128			self.topic.path.substr(start_pos .. end_pos)
129		}
130	}
131
132	/// Returns the positional parameter captured at `index`, if any.
133	///
134	/// Parameters are numbered in pattern order; a `#` wildcard yields the
135	/// joined remainder of the topic.
136	pub fn get_param(&self, index: usize) -> Option<Substr> {
137		self.params
138			.get(index)
139			.map(|range| self.get_param_range(range))
140	}
141
142	/// Returns the value of the named parameter `name`, if the pattern bound one.
143	pub fn get_named_param(&self, name: &str) -> Option<Substr> {
144		self.named_params
145			.iter()
146			.find(|(n, _)| n.as_str() == name)
147			.map(|(_, range)| self.get_param_range(range))
148
149		//self.named_params.get(name).map(|range| self.get_param_range(range))
150	}
151
152	/// Returns a cheap (refcounted) clone of the matched topic string.
153	pub fn topic_path(&self) -> ArcStr {
154		self.topic.path.clone()
155	}
156}
157
158//Implement Debug for TopicMatch, using get_param and get_named_param
159impl fmt::Debug for TopicMatch {
160	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
161		write!(f, "TopicMatch {{ topic: {}, params: [", self.topic.path)?;
162		for (i, param) in self.params.iter().enumerate() {
163			if i > 0 {
164				write!(f, ", ")?;
165			}
166			write!(f, "{}", self.get_param_range(param))?;
167		}
168		write!(f, "]")?;
169
170		if !self.named_params.is_empty() {
171			write!(f, ", named_params: {{")?;
172			for (name, range) in &self.named_params {
173				write!(f, "{}: {}, ", name, self.get_param_range(range))?;
174			}
175			write!(f, "}}")?;
176		}
177
178		write!(f, " }}")
179	}
180}
181
182impl fmt::Display for TopicMatch {
183	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184		write!(f, "Match({})", self.topic.path)?;
185
186		if !self.params.is_empty() {
187			write!(f, " with {} params", self.params.len())?;
188		}
189
190		Ok(())
191	}
192}