monocoque_core/
subscription.rs1use bytes::{Bytes, BytesMut};
7use std::collections::BTreeSet;
8
9#[derive(Debug, Clone)]
11pub struct Subscription {
12 pub prefix: Bytes,
14}
15
16impl Subscription {
17 #[must_use]
19 pub const fn new(prefix: Bytes) -> Self {
20 Self { prefix }
21 }
22
23 #[must_use]
25 pub fn matches(&self, topic: &[u8]) -> bool {
26 if self.prefix.is_empty() {
28 return true;
29 }
30
31 topic.len() >= self.prefix.len() && topic[..self.prefix.len()] == self.prefix[..]
33 }
34}
35
36#[derive(Debug, Default)]
42pub struct SubscriptionTrie {
43 prefixes: BTreeSet<Vec<u8>>,
44}
45
46impl SubscriptionTrie {
47 #[must_use]
49 pub fn new() -> Self {
50 Self {
51 prefixes: BTreeSet::new(),
52 }
53 }
54
55 pub fn subscribe(&mut self, prefix: Bytes) {
57 self.prefixes.insert(prefix.to_vec());
58 }
59
60 pub fn unsubscribe(&mut self, prefix: &Bytes) {
62 self.prefixes.remove(prefix.as_ref());
63 }
64
65 #[must_use]
70 pub fn matches(&self, topic: &[u8]) -> bool {
71 if self.prefixes.is_empty() {
72 return false;
73 }
74
75 (0..=topic.len()).any(|len| self.prefixes.contains(&topic[..len]))
76 }
77
78 #[must_use]
80 pub fn subscriptions(&self) -> Vec<Subscription> {
81 self.prefixes
82 .iter()
83 .map(|p| Subscription::new(Bytes::copy_from_slice(p)))
84 .collect()
85 }
86
87 #[must_use]
89 pub fn is_empty(&self) -> bool {
90 self.prefixes.is_empty()
91 }
92
93 #[must_use]
95 pub fn len(&self) -> usize {
96 self.prefixes.len()
97 }
98
99 pub fn clear(&mut self) {
101 self.prefixes.clear();
102 }
103}
104
105#[must_use]
109pub fn topic_matches_prefixes(topic: &[u8], prefixes: &[Bytes]) -> bool {
110 prefixes
111 .iter()
112 .any(|prefix| prefix.is_empty() || topic.starts_with(prefix))
113}
114
115#[derive(Debug, Clone, PartialEq, Eq)]
117pub enum SubscriptionEvent {
118 Subscribe(Bytes),
120 Unsubscribe(Bytes),
122}
123
124impl SubscriptionEvent {
125 #[must_use]
129 pub fn from_message(msg: &[u8]) -> Option<Self> {
130 if msg.is_empty() {
131 return None;
132 }
133
134 let prefix = Bytes::copy_from_slice(&msg[1..]);
135 match msg[0] {
136 0x01 => Some(Self::Subscribe(prefix)),
137 0x00 => Some(Self::Unsubscribe(prefix)),
138 _ => None,
139 }
140 }
141
142 #[must_use]
144 pub fn from_bytes(msg: Bytes) -> Option<Self> {
145 if msg.is_empty() {
146 return None;
147 }
148
149 let prefix = msg.slice(1..);
150 match msg[0] {
151 0x01 => Some(Self::Subscribe(prefix)),
152 0x00 => Some(Self::Unsubscribe(prefix)),
153 _ => None,
154 }
155 }
156
157 #[must_use]
159 pub fn to_message(&self) -> Bytes {
160 let (cmd, prefix) = match self {
161 Self::Subscribe(p) => (0x01u8, p),
162 Self::Unsubscribe(p) => (0x00u8, p),
163 };
164
165 let mut msg = BytesMut::with_capacity(1 + prefix.len());
166 msg.extend_from_slice(&[cmd]);
167 msg.extend_from_slice(prefix);
168 msg.freeze()
169 }
170
171 #[must_use]
173 pub const fn prefix(&self) -> &Bytes {
174 match self {
175 Self::Subscribe(p) | Self::Unsubscribe(p) => p,
176 }
177 }
178
179 #[must_use]
181 pub const fn is_subscribe(&self) -> bool {
182 matches!(self, Self::Subscribe(_))
183 }
184
185 #[must_use]
187 pub const fn is_unsubscribe(&self) -> bool {
188 matches!(self, Self::Unsubscribe(_))
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn test_subscription_matches() {
198 let sub = Subscription::new(Bytes::from_static(b"topic."));
199
200 assert!(sub.matches(b"topic.foo"));
201 assert!(sub.matches(b"topic.bar"));
202 assert!(!sub.matches(b"other.foo"));
203 assert!(!sub.matches(b"topi"));
204 }
205
206 #[test]
207 fn test_empty_subscription_matches_all() {
208 let sub = Subscription::new(Bytes::new());
209
210 assert!(sub.matches(b"anything"));
211 assert!(sub.matches(b""));
212 }
213
214 #[test]
215 fn test_trie_basic() {
216 let mut trie = SubscriptionTrie::new();
217
218 assert!(!trie.matches(b"topic.foo"));
219
220 trie.subscribe(Bytes::from_static(b"topic."));
221 assert!(trie.matches(b"topic.foo"));
222 assert!(!trie.matches(b"other.foo"));
223
224 trie.unsubscribe(&Bytes::from_static(b"topic."));
225 assert!(!trie.matches(b"topic.foo"));
226 }
227
228 #[test]
229 fn test_trie_multiple_subscriptions() {
230 let mut trie = SubscriptionTrie::new();
231
232 trie.subscribe(Bytes::from_static(b"topic."));
233 trie.subscribe(Bytes::from_static(b"events."));
234
235 assert!(trie.matches(b"topic.foo"));
236 assert!(trie.matches(b"events.bar"));
237 assert!(!trie.matches(b"other.baz"));
238 }
239
240 #[test]
241 fn test_trie_empty_prefix_matches_all() {
242 let mut trie = SubscriptionTrie::new();
243 trie.subscribe(Bytes::new());
244
245 assert!(trie.matches(b"anything"));
246 assert!(trie.matches(b""));
247 }
248
249 #[test]
250 fn test_trie_no_false_prefix_match() {
251 let mut trie = SubscriptionTrie::new();
252 trie.subscribe(Bytes::from_static(b"topic."));
253
254 assert!(!trie.matches(b"topic"));
256 assert!(trie.matches(b"topic."));
257 assert!(trie.matches(b"topic.sub"));
258 }
259
260 #[test]
261 fn test_trie_checks_broader_prefix_after_non_matching_candidate() {
262 let mut trie = SubscriptionTrie::new();
263 trie.subscribe(Bytes::from_static(b"a"));
264 trie.subscribe(Bytes::from_static(b"aa~"));
265
266 assert!(trie.matches(b"ab"));
267 }
268
269 #[test]
270 fn test_subscription_event() {
271 let sub = SubscriptionEvent::Subscribe(Bytes::from_static(b"topic"));
272 let msg = sub.to_message();
273
274 assert_eq!(msg[0], 0x01);
275 assert_eq!(&msg[1..], b"topic");
276
277 let parsed = SubscriptionEvent::from_message(&msg).unwrap();
278 assert_eq!(parsed, sub);
279 }
280
281 #[test]
282 fn test_unsubscription_event() {
283 let unsub = SubscriptionEvent::Unsubscribe(Bytes::from_static(b"topic"));
284 let msg = unsub.to_message();
285
286 assert_eq!(msg[0], 0x00);
287 assert_eq!(&msg[1..], b"topic");
288
289 let parsed = SubscriptionEvent::from_message(&msg).unwrap();
290 assert_eq!(parsed, unsub);
291 }
292}