Skip to main content

rskit_messaging/
router.rs

1//! Topic-based message routing with wildcard pattern support.
2
3use std::sync::Arc;
4
5use async_trait::async_trait;
6use rskit_errors::{AppError, AppResult, ErrorCode};
7
8use crate::handler::MessageHandler;
9use crate::message::Message;
10
11/// Routes incoming messages to handlers based on topic patterns.
12///
13/// Supports exact-match and simple wildcard patterns where `*` matches
14/// any sequence of characters.
15///
16/// # Example
17///
18/// ```rust,ignore
19/// use rskit_messaging::router::MessageRouter;
20///
21/// let router = MessageRouter::<String>::new()
22///     .handle("orders.created", order_handler)
23///     .handle("orders.*", fallback_handler)
24///     .default_handler(catch_all)
25///     .build();
26/// ```
27pub struct MessageRouter<T: Send + Sync + 'static> {
28    routes: Vec<(String, Arc<dyn MessageHandler<T>>)>,
29    default: Option<Arc<dyn MessageHandler<T>>>,
30}
31
32impl<T: Send + Sync + 'static> MessageRouter<T> {
33    /// Create a new empty router.
34    pub fn new() -> Self {
35        Self {
36            routes: Vec::new(),
37            default: None,
38        }
39    }
40
41    /// Register a handler for the given topic pattern.
42    ///
43    /// Patterns support `*` as a wildcard that matches any sequence of
44    /// characters. For example, `"content.*"` matches `"content.discovered"`.
45    /// Patterns are evaluated in registration order; the first match wins.
46    #[must_use]
47    pub fn handle(mut self, pattern: &str, handler: Arc<dyn MessageHandler<T>>) -> Self {
48        self.routes.push((pattern.to_string(), handler));
49        self
50    }
51
52    /// Register a default handler for messages that match no pattern.
53    #[must_use]
54    pub fn default_handler(mut self, handler: Arc<dyn MessageHandler<T>>) -> Self {
55        self.default = Some(handler);
56        self
57    }
58
59    /// Build an [`Arc<dyn MessageHandler<T>>`] that dispatches messages
60    /// according to the registered patterns.
61    pub fn build(self) -> Arc<dyn MessageHandler<T>> {
62        Arc::new(RouterHandler {
63            routes: self.routes,
64            default: self.default,
65        })
66    }
67}
68
69impl<T: Send + Sync + 'static> Default for MessageRouter<T> {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75/// Internal handler produced by [`MessageRouter::build`].
76struct RouterHandler<T: Send + Sync + 'static> {
77    routes: Vec<(String, Arc<dyn MessageHandler<T>>)>,
78    default: Option<Arc<dyn MessageHandler<T>>>,
79}
80
81#[async_trait]
82impl<T: Send + Sync + 'static> MessageHandler<T> for RouterHandler<T> {
83    async fn handle(&self, msg: Message<T>) -> AppResult<()> {
84        for (pattern, handler) in &self.routes {
85            if matches_pattern(pattern, &msg.topic) {
86                return handler.handle(msg).await;
87            }
88        }
89        if let Some(default) = &self.default {
90            return default.handle(msg).await;
91        }
92        Err(AppError::new(
93            ErrorCode::NotFound,
94            format!("no handler for topic '{}'", msg.topic),
95        ))
96    }
97}
98
99/// Check whether `topic` matches the given `pattern`.
100///
101/// `*` in the pattern matches any sequence of characters (including none).
102fn matches_pattern(pattern: &str, topic: &str) -> bool {
103    if !pattern.contains('*') {
104        return pattern == topic;
105    }
106    wildcard_match(pattern.as_bytes(), topic.as_bytes())
107}
108
109/// Recursive wildcard matching for patterns with `*`.
110fn wildcard_match(pattern: &[u8], text: &[u8]) -> bool {
111    match (pattern.first(), text.first()) {
112        (None, None) => true,
113        (Some(b'*'), _) => {
114            wildcard_match(&pattern[1..], text)
115                || (!text.is_empty() && wildcard_match(pattern, &text[1..]))
116        }
117        (Some(p), Some(t)) if p == t => wildcard_match(&pattern[1..], &text[1..]),
118        (Some(_), None) => pattern.iter().all(|&b| b == b'*'),
119        (_, Some(_)) => false,
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use std::sync::atomic::{AtomicU32, Ordering};
126
127    use super::*;
128    use crate::handler::FnHandler;
129
130    fn counting_handler(counter: &Arc<AtomicU32>) -> Arc<dyn MessageHandler<String>> {
131        let c = counter.clone();
132        Arc::new(FnHandler::new(move |_msg: Message<String>| {
133            let c = c.clone();
134            async move {
135                c.fetch_add(1, Ordering::SeqCst);
136                Ok(())
137            }
138        }))
139    }
140
141    #[test]
142    fn pattern_exact_match() {
143        assert!(matches_pattern("orders.created", "orders.created"));
144        assert!(!matches_pattern("orders.created", "orders.updated"));
145    }
146
147    #[test]
148    fn pattern_wildcard_suffix() {
149        assert!(matches_pattern("content.*", "content.discovered"));
150        assert!(matches_pattern("content.*", "content.updated"));
151        assert!(matches_pattern("content.*", "content."));
152        assert!(!matches_pattern("content.*", "other.discovered"));
153    }
154
155    #[test]
156    fn pattern_wildcard_prefix() {
157        assert!(matches_pattern("*.events", "user.events"));
158        assert!(matches_pattern("*.events", "system.events"));
159        assert!(!matches_pattern("*.events", "user.logs"));
160    }
161
162    #[test]
163    fn pattern_wildcard_only() {
164        assert!(matches_pattern("*", "anything"));
165        assert!(matches_pattern("*", ""));
166    }
167
168    #[tokio::test]
169    async fn router_exact_match() {
170        let counter = Arc::new(AtomicU32::new(0));
171        let router = MessageRouter::<String>::new()
172            .handle("orders.created", counting_handler(&counter))
173            .build();
174
175        let msg = Message::new("orders.created", "data".to_string());
176        router.handle(msg).await.unwrap();
177        assert_eq!(counter.load(Ordering::SeqCst), 1);
178    }
179
180    #[tokio::test]
181    async fn router_wildcard_match() {
182        let counter = Arc::new(AtomicU32::new(0));
183        let router = MessageRouter::<String>::new()
184            .handle("content.*", counting_handler(&counter))
185            .build();
186
187        let msg = Message::new("content.discovered", "data".to_string());
188        router.handle(msg).await.unwrap();
189        assert_eq!(counter.load(Ordering::SeqCst), 1);
190    }
191
192    #[tokio::test]
193    async fn router_default_handler() {
194        let specific = Arc::new(AtomicU32::new(0));
195        let default = Arc::new(AtomicU32::new(0));
196        let router = MessageRouter::<String>::new()
197            .handle("orders.*", counting_handler(&specific))
198            .default_handler(counting_handler(&default))
199            .build();
200
201        let msg = Message::new("unknown.topic", "data".to_string());
202        router.handle(msg).await.unwrap();
203        assert_eq!(specific.load(Ordering::SeqCst), 0);
204        assert_eq!(default.load(Ordering::SeqCst), 1);
205    }
206
207    #[tokio::test]
208    async fn router_no_match_returns_error() {
209        let router = MessageRouter::<String>::new().build();
210
211        let msg = Message::new("unknown", "data".to_string());
212        let result = router.handle(msg).await;
213        assert!(result.is_err());
214    }
215
216    #[tokio::test]
217    async fn router_first_match_wins() {
218        let first = Arc::new(AtomicU32::new(0));
219        let second = Arc::new(AtomicU32::new(0));
220
221        let router = MessageRouter::<String>::new()
222            .handle("orders.*", counting_handler(&first))
223            .handle("orders.created", counting_handler(&second))
224            .build();
225
226        let msg = Message::new("orders.created", "data".to_string());
227        router.handle(msg).await.unwrap();
228        assert_eq!(first.load(Ordering::SeqCst), 1);
229        assert_eq!(second.load(Ordering::SeqCst), 0);
230    }
231}