1use std::collections::HashSet;
8use std::convert::TryFrom;
9use std::fmt::{self, Display, Write};
10use std::slice::Iter;
11use std::sync::Arc;
12#[cfg(feature = "lru-cache")]
13use std::sync::Mutex;
14
15use arcstr::ArcStr;
16#[cfg(feature = "lru-cache")]
17use lru::LruCache;
18use smallvec::SmallVec;
19use thiserror::Error;
20
21use crate::cache_strategy::CacheStrategy;
22use crate::topic_match::{TopicMatch, TopicMatchError, TopicPath};
23use crate::topic_pattern_item::{TopicPatternError, TopicPatternItem};
24
25#[derive(Error, Debug, Clone, PartialEq, Eq)]
27pub enum TopicFormatError {
28 #[error("Cannot format topic with # wildcard for publishing")]
30 HashWildcardNotSupported,
31
32 #[error(
34 "Parameter count mismatch: expected {expected}, provided {provided}"
35 )]
36 ParameterCountMismatch {
37 expected: usize,
39 provided: usize,
41 },
42 #[error("Error formatting topic")]
44 FormatError {
45 #[source]
46 source: fmt::Error,
48 },
49}
50
51impl From<fmt::Error> for TopicFormatError {
52 fn from(source: fmt::Error) -> Self {
53 TopicFormatError::FormatError { source }
54 }
55}
56
57#[derive(Debug)]
59pub struct TopicPatternPath {
60 template_pattern: ArcStr, mqtt_topic_subscription: ArcStr, segments: Vec<TopicPatternItem>,
63 #[cfg(feature = "lru-cache")]
73 match_cache: Option<Mutex<LruCache<ArcStr, Arc<TopicMatch>>>>,
74
75 parameter_bindings: Option<SmallVec<[(ArcStr, ArcStr); 4]>>,
76}
77
78impl Clone for TopicPatternPath {
79 fn clone(&self) -> Self {
80 Self {
81 template_pattern: self.template_pattern.clone(),
82 mqtt_topic_subscription: self.mqtt_topic_subscription.clone(),
83 segments: self.segments.clone(),
84 #[cfg(feature = "lru-cache")]
85 match_cache: self.match_cache.as_ref().map(|cache| {
86 let cache_guard = cache.lock().unwrap();
87 let capacity = cache_guard.cap();
88 drop(cache_guard);
89 Mutex::new(LruCache::new(capacity))
90 }),
91 parameter_bindings: self.parameter_bindings.clone(),
92 }
93 }
94}
95
96impl TopicPatternPath {
97 pub fn new_from_string(
99 topic_pattern: impl Into<ArcStr>,
100 cache_strategy: CacheStrategy,
101 ) -> Result<Self, TopicPatternError> {
102 let topic_pattern = topic_pattern.into();
103 if topic_pattern.is_empty() || topic_pattern.trim().is_empty() {
104 return Err(TopicPatternError::EmptyTopic);
105 }
106
107 let segments: Result<Vec<_>, _> = topic_pattern
108 .split('/')
109 .map(|s| topic_pattern.substr_from(s))
110 .map(TopicPatternItem::try_from)
111 .collect();
112
113 let segments = segments?;
114
115 let mut seen_names = HashSet::new();
117 for segment in &segments {
118 if let Some(name) = segment.param_name() {
119 if !seen_names.insert(name.to_string()) {
120 return Err(TopicPatternError::wildcard_usage(
121 segment.as_str(),
122 ));
123 }
124 }
125 }
126
127 if let Some(hash_pos) = segments
128 .iter()
129 .position(|s| matches!(*s, TopicPatternItem::Hash(_)))
130 {
131 if hash_pos != segments.len() - 1 {
132 return Err(TopicPatternError::hash_position(
133 topic_pattern.as_str(),
134 ));
135 }
136 }
137
138 #[cfg(feature = "lru-cache")]
139 let match_cache = match cache_strategy {
140 | CacheStrategy::Lru(cache_size) => {
141 Some(Mutex::new(LruCache::new(cache_size)))
142 }
143 | CacheStrategy::NoCache => None,
144 };
145
146 #[cfg(not(feature = "lru-cache"))]
147 {
148 if let Some(capacity) = cache_strategy.capacity() {
149 tracing::warn!(
150 capacity = capacity.get(),
151 pattern = %topic_pattern,
152 "LRU cache strategy provided for topic pattern '{}' with capacity {}, \
153 but 'lru-cache' feature is disabled. Caching will not be used. \
154 Enable 'lru-cache' feature in Cargo.toml to use caching.",
155 topic_pattern,
156 capacity.get()
157 );
158 }
159 }
160
161 Ok(Self {
162 template_pattern: topic_pattern,
163 mqtt_topic_subscription: ArcStr::from(
164 Self::to_mqtt_subscription_pattern(&segments),
165 ),
166 segments,
167 #[cfg(feature = "lru-cache")]
168 match_cache,
169 parameter_bindings: None,
170 })
171 }
172
173 pub fn cache_strategy(&self) -> CacheStrategy {
175 #[cfg(feature = "lru-cache")]
176 {
177 match &self.match_cache {
178 | Some(cache_mutex) => {
179 let cache_guard = cache_mutex.lock().unwrap();
180 CacheStrategy::Lru(cache_guard.cap())
181 }
182 | None => CacheStrategy::NoCache,
183 }
184 }
185 #[cfg(not(feature = "lru-cache"))]
186 {
187 CacheStrategy::NoCache
188 }
189 }
190
191 pub fn parameter_bindings(
193 &self,
194 ) -> Option<&SmallVec<[(ArcStr, ArcStr); 4]>> {
195 self.parameter_bindings.as_ref()
196 }
197
198 pub fn get_bound_value(&self, param_name: Option<&str>) -> Option<&ArcStr> {
200 let name = param_name?; self.parameter_bindings
202 .as_ref()?
203 .iter()
204 .find(|(binding_name, _)| binding_name == name)
205 .map(|(_, value)| value)
206 }
207
208 #[cfg(all(test, feature = "router"))]
209 pub(crate) fn new_from_segments(
211 segments: &[TopicPatternItem],
212 ) -> Result<Self, TopicPatternError> {
213 let topic_pattern = ArcStr::from(Self::to_template_pattern(segments));
214 let pattern = Self {
215 mqtt_topic_subscription: ArcStr::from(
216 Self::to_mqtt_subscription_pattern(segments),
217 ),
218 template_pattern: topic_pattern.clone(),
219 segments: segments.to_vec(),
220 #[cfg(feature = "lru-cache")]
221 match_cache: None,
222 parameter_bindings: None,
223 };
224 if let Some(hash_pos) = segments
225 .iter()
226 .position(|s| matches!(*s, TopicPatternItem::Hash(_)))
227 {
228 if hash_pos != segments.len() - 1 {
229 return Err(TopicPatternError::hash_position(
230 topic_pattern.as_str(),
231 ));
232 }
233 }
234 Ok(pattern)
235 }
236
237 pub fn mqtt_pattern(&self) -> ArcStr {
239 match &self.parameter_bindings {
240 | Some(bindings) => {
241 let new_segments = self.apply_bindings_to_segments(bindings);
242 ArcStr::from(Self::to_mqtt_subscription_pattern(&new_segments))
243 }
244 | None => self.mqtt_topic_subscription.clone(),
245 }
246 }
247
248 pub fn resolve_bound_segments(&self) -> Vec<TopicPatternItem> {
253 if let Some(ref bindings) = self.parameter_bindings {
254 self.apply_bindings_to_segments(bindings)
255 } else {
256 self.segments.clone()
257 }
258 }
259
260 fn apply_bindings_to_segments(
262 &self,
263 bindings: &[(ArcStr, ArcStr)],
264 ) -> Vec<TopicPatternItem> {
265 let mut new_segments = self.segments.clone();
266
267 for (param_name, value) in bindings {
268 if let Some(segment_pos) = new_segments.iter().position(|segment| {
269 matches!(segment, TopicPatternItem::Plus(Some(name)) if name == param_name)
270 }) {
271 new_segments[segment_pos] = TopicPatternItem::Str(value.into());
272 } else {
273 tracing::debug!(
274 pattern = %self.topic_pattern(),
275 "Parameter '{param_name}' not found in pattern"
276 );
277 }
278 }
279
280 new_segments
281 }
282
283 pub fn topic_pattern(&self) -> ArcStr {
285 self.template_pattern.clone()
286 }
287
288 pub fn is_empty(&self) -> bool {
290 self.segments.is_empty()
291 }
292
293 pub fn contains_hash(&self) -> bool {
295 self.segments
296 .last()
297 .is_some_and(|s| matches!(s, TopicPatternItem::Hash(_)))
298 }
299
300 pub fn iter(&self) -> Iter<'_, TopicPatternItem> {
302 self.segments.iter()
303 }
304
305 pub fn len(&self) -> usize {
307 self.segments.len()
308 }
309
310 fn str_len(segments: &[TopicPatternItem]) -> usize {
311 if segments.is_empty() {
312 return 0;
313 }
314 (segments.len() - 1) + segments.iter().map(|s| s.as_str().len()).sum::<usize>()
316 }
317
318 pub fn slice(&self) -> &[TopicPatternItem] {
320 &self.segments
321 }
322
323 #[cfg(test)]
325 pub fn segments(&self) -> &Vec<TopicPatternItem> {
326 &self.segments
327 }
328
329 pub fn format_topic(
331 &self,
332 params: &[&dyn Display],
333 ) -> Result<String, TopicFormatError> {
334 let wildcard_count =
335 self.segments.iter().filter(|s| s.is_wildcard()).count();
336
337 if params.len() != wildcard_count {
338 return Err(TopicFormatError::ParameterCountMismatch {
339 expected: wildcard_count,
340 provided: params.len(),
341 });
342 }
343
344 let mut result = String::with_capacity(self.topic_pattern().len() + 10); let mut param_index = 0;
346
347 for (i, segment) in self.segments.iter().enumerate() {
348 if i > 0 {
349 result.push('/');
350 }
351
352 match segment {
353 | TopicPatternItem::Str(s) => result.push_str(s),
354 | TopicPatternItem::Plus(_) => {
355 write!(result, "{}", params[param_index])?;
356 param_index += 1;
357 }
358 | TopicPatternItem::Hash(_) => {
359 return Err(TopicFormatError::HashWildcardNotSupported);
360 }
361 }
362 }
363
364 Ok(result)
365 }
366
367 fn to_mqtt_subscription_pattern(segments: &[TopicPatternItem]) -> String {
368 if segments.is_empty() {
370 return String::new();
371 }
372 let mut mqtt_topic = String::with_capacity(Self::str_len(segments));
373 segments.iter().enumerate().for_each(|(i, segment)| {
374 if i > 0 {
375 mqtt_topic.push('/');
376 }
377 mqtt_topic.push_str(segment.as_str());
378 });
379 mqtt_topic
380 }
381
382 #[cfg(all(test, feature = "router"))]
383 fn to_template_pattern(segments: &[TopicPatternItem]) -> String {
384 if segments.is_empty() {
386 return String::new();
387 }
388 let mut mqtt_topic = String::new();
389 segments.iter().enumerate().for_each(|(i, segment)| {
390 if i > 0 {
391 mqtt_topic.push('/');
392 }
393 mqtt_topic.push_str(segment.as_wildcard().as_ref());
394 });
395 mqtt_topic
396 }
397
398 pub fn check_pattern_compatibility(
403 &self,
404 custom_topic: impl TryInto<TopicPatternPath, Error: Into<TopicPatternError>>,
405 ) -> Result<Self, TopicPatternError> {
406 let candidate = custom_topic.try_into().map_err(Into::into)?;
407 let self_wildcards =
409 self.segments.iter().filter(|item| item.is_wildcard());
410 let candidate_wildcards =
411 candidate.segments.iter().filter(|item| item.is_wildcard());
412
413 if !self_wildcards.eq(candidate_wildcards) {
414 return Err(TopicPatternError::pattern_mismatch(
415 self.template_pattern.as_str(),
416 candidate.template_pattern.as_str(),
417 ));
418 }
419
420 Ok(candidate)
421 }
422
423 pub fn with_cache_strategy(&self, new_cache: CacheStrategy) -> Self {
425 let mut new_pattern =
426 Self::new_from_string(self.template_pattern.clone(), new_cache)
427 .expect("Pattern already validated");
428 new_pattern.parameter_bindings = self.parameter_bindings.clone();
429 new_pattern
430 }
431
432 pub fn bind_parameter(
434 mut self,
435 param_name: impl Into<ArcStr>,
436 value: impl Into<ArcStr>,
437 ) -> Result<Self, TopicPatternError> {
438 let param_name_arc = param_name.into();
439
440 let param_exists = self.segments.iter().any(|segment| {
441 matches!(segment, TopicPatternItem::Plus(Some(name)) if name.as_str() == param_name_arc.as_str())
442 });
443 if !param_exists {
444 return Err(TopicPatternError::wildcard_usage(format!(
445 "Parameter '{param_name_arc}' not found in pattern '{}'",
446 self.topic_pattern()
447 )));
448 }
449
450 let value_arc = value.into();
451
452 let bindings =
453 self.parameter_bindings.get_or_insert_with(SmallVec::new);
454
455 if let Some(pos) =
456 bindings.iter().position(|(k, _)| k == ¶m_name_arc)
457 {
458 bindings[pos].1 = value_arc;
459 } else {
460 bindings.push((param_name_arc, value_arc));
461 }
462
463 Ok(self)
464 }
465
466 pub fn try_match(
474 &self,
475 topic: Arc<TopicPath>,
476 ) -> Result<Arc<TopicMatch>, TopicMatchError> {
477 #[cfg(feature = "lru-cache")]
478 {
479 match &self.match_cache {
480 | Some(cache_mutex) => {
481 {
482 let mut match_cache = cache_mutex.lock().unwrap();
483 if let Some(cached_match) = match_cache.get(&topic.path)
484 {
485 return Ok(cached_match.clone());
486 }
487 }
488
489 let topic_match = self.try_match_internal(topic.clone())?;
490 let topic_match_arc = Arc::new(topic_match);
491 {
492 let mut match_cache = cache_mutex.lock().unwrap();
493 match_cache.put(
494 topic.path.clone(),
495 Arc::clone(&topic_match_arc),
496 );
497 }
498 Ok(topic_match_arc)
499 }
500 | None => {
501 let topic_match = self.try_match_internal(topic)?;
502 Ok(Arc::new(topic_match))
503 }
504 }
505 }
506 #[cfg(not(feature = "lru-cache"))]
507 {
508 let topic_match = self.try_match_internal(topic)?;
509 Ok(Arc::new(topic_match))
510 }
511 }
512
513 pub fn try_match_str(
520 &self,
521 topic: impl Into<ArcStr>,
522 ) -> Result<Arc<TopicMatch>, TopicMatchError> {
523 self.try_match(Arc::new(TopicPath::new(topic)))
524 }
525
526 #[allow(clippy::missing_docs_in_private_items)]
527 fn try_match_internal(
528 &self,
529 topic: Arc<TopicPath>,
530 ) -> Result<TopicMatch, TopicMatchError> {
531 let mut topic_index = 0;
532 let mut params = SmallVec::new();
533 let mut named_params = SmallVec::new();
534 for (i, pattern_segment) in self.iter().enumerate() {
535 match pattern_segment {
536 | TopicPatternItem::Str(expected) => {
537 if topic_index >= topic.segments.len() {
538 return Err(TopicMatchError::UnexpectedEndOfTopic);
539 }
540 if topic.segments[topic_index] != *expected {
541 return Err(TopicMatchError::SegmentMismatch {
542 expected: expected.to_string(),
543 found: topic.segments[topic_index].to_string(),
544 position: topic_index,
545 });
546 }
547 topic_index += 1;
548 }
549 | TopicPatternItem::Plus(opt_name) => {
550 if topic_index >= topic.segments.len() {
551 return Err(TopicMatchError::UnexpectedEndOfTopic);
552 }
553 let param_range = topic_index .. topic_index + 1;
554 params.push(param_range.clone());
555 topic_index += 1;
556 if let Some(name) = opt_name {
557 named_params.push((name.clone(), param_range));
558 }
559 }
560 | TopicPatternItem::Hash(opt_name) => {
561 let param_range = topic_index .. topic.segments.len();
562 params.push(param_range.clone());
563 if let Some(name) = opt_name {
564 named_params.push((name.clone(), param_range));
565 }
566 if i < self.len() - 1 {
567 return Err(TopicMatchError::UnexpectedHashSegment);
568 }
569 return Ok(TopicMatch::from_match_result(
570 topic,
571 params,
572 named_params,
573 ));
574 }
575 }
576 }
577 if topic_index < topic.segments.len() {
578 return Err(TopicMatchError::UnexpectedEndOfPattern);
579 }
580 Ok(TopicMatch::from_match_result(topic, params, named_params))
581 }
582}
583
584impl std::fmt::Display for TopicPatternPath {
585 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
586 let path = self.topic_pattern();
588 write!(f, "{path}")
589 }
590}
591
592impl TryFrom<String> for TopicPatternPath {
593 type Error = TopicPatternError;
594
595 fn try_from(value: String) -> Result<Self, Self::Error> {
596 Self::new_from_string(value, CacheStrategy::NoCache)
597 }
598}
599
600impl TryFrom<&str> for TopicPatternPath {
601 type Error = TopicPatternError;
602
603 fn try_from(value: &str) -> Result<Self, Self::Error> {
604 Self::new_from_string(value, CacheStrategy::NoCache)
605 }
606}
607
608impl TryFrom<ArcStr> for TopicPatternPath {
609 type Error = TopicPatternError;
610
611 fn try_from(value: ArcStr) -> Result<Self, Self::Error> {
612 Self::new_from_string(value, CacheStrategy::NoCache)
613 }
614}