1use std::fmt;
2
3use serde::{Deserialize, Serialize};
4
5const RESERVED_CHARS: &[char] = &['.', '*', '>'];
13
14#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
20pub enum Segment {
21 Str(String),
23 Int(i64),
25 Any,
27 Rest,
29}
30
31impl Segment {
32 pub fn s(s: impl Into<String>) -> Self {
34 Self::Str(s.into())
35 }
36
37 pub fn i(v: i64) -> Self {
39 Self::Int(v)
40 }
41
42 pub fn any() -> Self {
44 Self::Any
45 }
46
47 pub fn rest() -> Self {
49 Self::Rest
50 }
51
52 pub fn is_wildcard(&self) -> bool {
54 matches!(self, Self::Any | Self::Rest)
55 }
56}
57
58impl From<&str> for Segment {
59 fn from(s: &str) -> Self {
60 Self::Str(s.to_owned())
61 }
62}
63
64impl From<String> for Segment {
65 fn from(s: String) -> Self {
66 Self::Str(s)
67 }
68}
69
70impl From<i64> for Segment {
71 fn from(v: i64) -> Self {
72 Self::Int(v)
73 }
74}
75
76impl fmt::Display for Segment {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 Self::Str(s) => f.write_str(s),
80 Self::Int(n) => write!(f, "{n}"),
81 Self::Any => f.write_str("*"),
82 Self::Rest => f.write_str(">"),
83 }
84 }
85}
86
87#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
112#[serde(transparent)]
113pub struct Subject {
114 segments: Vec<Segment>,
115}
116
117impl Subject {
118 pub fn new() -> Self {
120 Self {
121 segments: Vec::new(),
122 }
123 }
124
125 fn assert_not_terminated(&self) {
127 assert!(
128 !matches!(self.segments.last(), Some(Segment::Rest)),
129 "cannot append a segment after Rest (>)"
130 );
131 }
132
133 pub fn str(mut self, s: impl Into<String>) -> Self {
140 self.assert_not_terminated();
141 let s = s.into();
142 assert!(
143 !s.is_empty() && !s.contains(RESERVED_CHARS),
144 "segment string must not be empty or contain reserved characters (., *, >): {s:?}"
145 );
146 self.segments.push(Segment::Str(s));
147 self
148 }
149
150 pub fn int(mut self, v: i64) -> Self {
156 self.assert_not_terminated();
157 self.segments.push(Segment::Int(v));
158 self
159 }
160
161 pub fn any(mut self) -> Self {
167 self.assert_not_terminated();
168 self.segments.push(Segment::Any);
169 self
170 }
171
172 pub fn rest(mut self) -> Self {
178 assert!(
179 !self.segments.iter().any(|s| matches!(s, Segment::Rest)),
180 "Rest (>) must be the last segment and cannot appear more than once"
181 );
182 self.segments.push(Segment::Rest);
183 self
184 }
185
186 pub fn segment(mut self, seg: Segment) -> Self {
192 self.assert_not_terminated();
193 self.segments.push(seg);
194 self
195 }
196
197 pub fn segments(&self) -> &[Segment] {
199 &self.segments
200 }
201
202 pub fn len(&self) -> usize {
204 self.segments.len()
205 }
206
207 pub fn is_empty(&self) -> bool {
209 self.segments.is_empty()
210 }
211
212 pub fn is_concrete(&self) -> bool {
216 self.segments
217 .iter()
218 .all(|s| matches!(s, Segment::Str(_) | Segment::Int(_)))
219 }
220
221 pub fn is_pattern(&self) -> bool {
223 !self.is_concrete()
224 }
225
226 pub fn matches(&self, concrete: &Subject) -> bool {
230 let pat = &self.segments;
231 let subj = &concrete.segments;
232
233 if let Some(Segment::Rest) = pat.last() {
235 let prefix = &pat[..pat.len() - 1];
236 if subj.len() < prefix.len() {
237 return false;
238 }
239 return prefix
240 .iter()
241 .zip(subj.iter())
242 .all(|(p, s)| Self::segment_matches(p, s));
243 }
244
245 if pat.len() != subj.len() {
247 return false;
248 }
249
250 pat.iter()
251 .zip(subj.iter())
252 .all(|(p, s)| Self::segment_matches(p, s))
253 }
254
255 fn segment_matches(pattern: &Segment, concrete: &Segment) -> bool {
257 match pattern {
258 Segment::Any | Segment::Rest => true,
259 other => other == concrete,
260 }
261 }
262
263 pub fn to_bytes(&self) -> Vec<u8> {
265 bincode::serde::encode_to_vec(self, bincode::config::standard())
266 .expect("Subject serialization cannot fail")
267 }
268
269 pub fn from_bytes(bytes: &[u8]) -> Result<Self, crate::DecodeError> {
271 let (val, _) = bincode::serde::decode_from_slice(bytes, bincode::config::standard())?;
272 Ok(val)
273 }
274
275 pub fn from_module_path(module_path: &str, type_name: &str) -> Self {
280 let mut subject = Self::new();
281 for part in module_path.split("::") {
282 subject.segments.push(Segment::Str(part.to_owned()));
283 }
284 subject.segments.push(Segment::Str(type_name.to_owned()));
285 subject
286 }
287}
288
289impl Default for Subject {
290 fn default() -> Self {
291 Self::new()
292 }
293}
294
295impl fmt::Display for Subject {
296 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
298 for (i, seg) in self.segments.iter().enumerate() {
299 if i > 0 {
300 f.write_str(".")?;
301 }
302 write!(f, "{seg}")?;
303 }
304 Ok(())
305 }
306}
307
308impl From<&str> for Subject {
309 fn from(s: &str) -> Self {
313 let segments = s
314 .split('.')
315 .filter(|p| !p.is_empty())
316 .map(|part| match part {
317 "*" => Segment::Any,
318 ">" => Segment::Rest,
319 _ => part
320 .parse::<i64>()
321 .map(Segment::Int)
322 .unwrap_or_else(|_| Segment::Str(part.to_owned())),
323 })
324 .collect();
325 Self { segments }
326 }
327}
328
329#[cfg(test)]
334mod tests {
335 use super::*;
336
337 #[test]
340 fn subject_builder() {
341 let s = Subject::new().str("job").int(42).str("logs");
342 assert_eq!(s.segments().len(), 3);
343 assert_eq!(s.segments()[0], Segment::Str("job".into()));
344 assert_eq!(s.segments()[1], Segment::Int(42));
345 assert_eq!(s.segments()[2], Segment::Str("logs".into()));
346 assert!(s.is_concrete());
347 }
348
349 #[test]
350 fn subject_display() {
351 let s = Subject::new().str("job").int(42).str("logs");
352 assert_eq!(s.to_string(), "job.42.logs");
353 }
354
355 #[test]
356 fn subject_bytes_roundtrip() {
357 let s = Subject::new().str("job").int(42).str("logs");
358 let bytes = s.to_bytes();
359 let s2 = Subject::from_bytes(&bytes).unwrap();
360 assert_eq!(s, s2);
361 }
362
363 #[test]
364 fn subject_from_str() {
365 let s = Subject::from("job.42.logs");
366 assert_eq!(s.segments()[0], Segment::Str("job".into()));
367 assert_eq!(s.segments()[1], Segment::Int(42));
368 assert_eq!(s.segments()[2], Segment::Str("logs".into()));
369 }
370
371 #[test]
372 fn subject_from_module_path() {
373 let s = Subject::from_module_path("app::events", "ChatMessage");
374 assert_eq!(s.to_string(), "app.events.ChatMessage");
375 let roundtripped = Subject::from_bytes(&s.to_bytes()).unwrap();
376 assert_eq!(s, roundtripped);
377 }
378
379 #[test]
382 fn pattern_exact_match() {
383 let p = Subject::new().str("job").int(42).str("logs");
384 let s = Subject::new().str("job").int(42).str("logs");
385 assert!(p.matches(&s));
386 assert!(p.is_concrete());
387 }
388
389 #[test]
390 fn pattern_exact_mismatch() {
391 let p = Subject::new().str("job").int(42).str("logs");
392 let s = Subject::new().str("job").int(99).str("logs");
393 assert!(!p.matches(&s));
394 }
395
396 #[test]
397 fn pattern_any_single() {
398 let p = Subject::new().str("job").any().str("logs");
399 assert!(p.matches(&Subject::new().str("job").int(42).str("logs")));
400 assert!(p.matches(&Subject::new().str("job").str("foo").str("logs")));
401 assert!(!p.matches(&Subject::new().str("job").str("logs")));
402 assert!(p.is_pattern());
403 }
404
405 #[test]
406 fn pattern_rest() {
407 let p = Subject::new().str("job").rest();
408 assert!(p.matches(&Subject::new().str("job")));
409 assert!(p.matches(&Subject::new().str("job").int(42)));
410 assert!(p.matches(&Subject::new().str("job").int(42).str("logs")));
411 assert!(!p.matches(&Subject::new().str("other")));
412 }
413
414 #[test]
415 fn pattern_rest_with_any() {
416 let p = Subject::new().str("a").any().rest();
417 assert!(p.matches(&Subject::new().str("a").str("b")));
418 assert!(p.matches(&Subject::new().str("a").int(1).str("c").str("d")));
419 assert!(!p.matches(&Subject::new().str("a"))); }
421
422 #[test]
423 fn pattern_bytes_roundtrip() {
424 let p = Subject::new().str("job").any().rest();
425 let bytes = p.to_bytes();
426 let p2 = Subject::from_bytes(&bytes).unwrap();
427 assert_eq!(p, p2);
428 }
429
430 #[test]
431 fn pattern_mixed_roundtrip() {
432 let p = Subject::new().str("a").int(1).any().rest();
433 let bytes = p.to_bytes();
434 let p2 = Subject::from_bytes(&bytes).unwrap();
435 assert_eq!(p, p2);
436 }
437
438 #[test]
439 fn pattern_display() {
440 let p = Subject::new().str("job").any().rest();
441 assert_eq!(p.to_string(), "job.*.>");
442 }
443
444 #[test]
445 fn from_str_wildcards() {
446 let s = Subject::from("job.*.>");
447 assert_eq!(s.segments()[0], Segment::Str("job".into()));
448 assert_eq!(s.segments()[1], Segment::Any);
449 assert_eq!(s.segments()[2], Segment::Rest);
450 assert!(s.is_pattern());
451 }
452
453 #[test]
454 fn concrete_is_not_pattern() {
455 let s = Subject::new().str("a").int(1);
456 assert!(s.is_concrete());
457 assert!(!s.is_pattern());
458 }
459
460 #[test]
461 #[should_panic(expected = "cannot append")]
462 fn rest_must_be_last() {
463 Subject::new().str("a").rest().str("b");
464 }
465}