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