arminius_lib_message_bus/lib.rs
1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Message bus abstractions: [`Publisher`], [`Subscriber`], [`Event`], and
6//! [`ReceivedEvent`].
7#![doc = include_str!("../README.md")]
8
9#[cfg(feature = "mock")]
10pub mod mock;
11
12mod command;
13mod event;
14mod subject;
15pub use arminius_lib_message_derive::{Command, Event};
16pub use command::{Command, CommandError, Delivery, ReceivedCommand, Requester, Responder};
17pub use event::{Error, Event, Publisher, ReceivedEvent, Subscriber};
18pub use subject::{EscapingSubject, TokenEscapeError};
19
20/// Returns `true` if the concrete `subject` matches the NATS `pattern`.
21///
22/// `*` matches exactly one token; `>` matches one or more trailing tokens.
23/// Used by generated [`command::Command::from_payload`] dispatch so that an
24/// event published to a per-instance subject (e.g. `rooms.1.events.v1`) is
25/// matched against a wildcard pattern (e.g. `rooms.*.events.v1`).
26#[must_use]
27pub fn subject_matches(pattern: &str, subject: &str) -> bool {
28 let mut pattern_tokens = pattern.split('.');
29 let mut subject_tokens = subject.split('.');
30 loop {
31 match pattern_tokens.next() {
32 Some(">") => return subject_tokens.next().is_some(),
33 Some(token) => match subject_tokens.next() {
34 Some(segment) if token == "*" || token == segment => continue,
35 _ => return false,
36 },
37 None => return subject_tokens.next().is_none(),
38 }
39 }
40}
41
42/// Encodes a string for use as a single NATS subject segment by escaping the
43/// characters NATS reserves (see [`EscapingSubject`]).
44///
45/// The result contains no `.`, `*` or `>`, so it cannot split the segment or
46/// act as a wildcard. Use this on the client side when constructing command
47/// subjects. The matching decode is in [`__private::decode_subject_segment`].
48#[must_use]
49pub fn encode_subject_segment(value: &str) -> String {
50 subject::escape_token(value)
51}
52
53/// Compile-time helpers used by the `#[derive(Event)]` generated code.
54#[doc(hidden)]
55pub mod __private {
56 use crate::{TokenEscapeError, subject};
57
58 /// Decodes an escaped NATS subject segment back to its original string.
59 ///
60 /// Inverse of [`super::encode_subject_segment`]. Returns `Err` when the
61 /// segment contains an unknown escape sequence or ends in a lone escape
62 /// character.
63 ///
64 /// Nothing in the generated code calls this today: both the `Event` and
65 /// `Command` receive paths rebuild a message from its payload and use the
66 /// subject only to pick a version.
67 pub fn decode_subject_segment(value: &str) -> Result<String, TokenEscapeError> {
68 subject::unescape_token(value)
69 }
70
71 /// Returns `true` if `haystack` contains `needle`.
72 ///
73 /// This is a manual loop instead of the standard `slice::contains` because
74 /// it must run at compile time. The `Event` derive calls it inside
75 /// `const _: () = assert!(...)` to reject redundant `compatible(...)`
76 /// entries during the build, and standard slice/iterator methods cannot be
77 /// used in a `const` context.
78 #[must_use]
79 pub const fn contains(haystack: &[&str], needle: &str) -> bool {
80 let mut index = 0;
81 #[expect(
82 clippy::indexing_slicing,
83 reason = "FIXME(#2259): This was present before we enabled the lint."
84 )]
85 while index < haystack.len() {
86 if str_eq(haystack[index], needle) {
87 return true;
88 }
89 index += 1;
90 }
91 false
92 }
93
94 /// Compares two `&str` for equality in a `const` context.
95 ///
96 /// The `==` operator cannot be used here because it relies on the
97 /// `PartialEq` trait, and trait methods are not callable in `const`. So we
98 /// compare the raw bytes directly instead.
99 const fn str_eq(lhs: &str, rhs: &str) -> bool {
100 if lhs.len() != rhs.len() {
101 return false;
102 }
103 let (lhs, rhs) = (lhs.as_bytes(), rhs.as_bytes());
104 let mut index = 0;
105 #[expect(
106 clippy::indexing_slicing,
107 reason = "FIXME(#2259): This was present before we enabled the lint."
108 )]
109 while index < lhs.len() {
110 if lhs[index] != rhs[index] {
111 return false;
112 }
113 index += 1;
114 }
115 true
116 }
117}