Skip to main content

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}