Skip to main content

arminius_lib_message_bus/
event.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Fire-and-forget event messaging: traits for publishing events and
6//! subscribing to them as acknowledgeable streams.
7
8use std::{future::Future, ops::Deref};
9
10pub use futures::Stream;
11use serde::{Serialize, de::DeserializeOwned};
12
13/// Errors that can occur during message bus operations.
14#[derive(Debug, thiserror::Error)]
15pub enum Error {
16	/// JSON serialization or deserialization failure.
17	#[error("Failed to serialize/deserialize payload: {0}")]
18	Payload(#[from] serde_json::Error),
19
20	/// Failed to publish a message to the specified subject.
21	#[error("Failed to send message: {error}")]
22	SendFailed {
23		/// Underlying error description.
24		error: String,
25	},
26
27	/// Failed to subscribe to requested subjects.
28	#[error("Failed to subscribe to subjects '{subject}': {error}")]
29	SubscriptionFailed {
30		/// The subject that could not be subscribed to.
31		subject: String,
32		/// Underlying error description.
33		error: String,
34	},
35
36	/// Failed while receiving a message from a subscribed subject.
37	#[error("Failure while receiving message from subject '{subject}': {error}")]
38	ReceptionFailed {
39		/// The subject on which the failure occurred.
40		subject: String,
41		/// Underlying error description.
42		error: String,
43	},
44
45	/// Failed to acknowledge handling of a message.
46	/// The message may be reprocessed by this or another service instance.
47	#[error("Failed to acknowledge message: {error}")]
48	AckFailed {
49		/// Underlying error description.
50		error: String,
51	},
52}
53
54/// Generic event type that can be sent between services.
55///
56/// Events must be serializable and define a subject (topic) where they will be
57/// published. The subject typically includes version information for
58/// compatibility handling.
59///
60/// # Example
61///
62/// ```rust
63/// use arminius_lib_message_bus::Event;
64/// use serde::{Deserialize, Serialize};
65///
66/// #[derive(Serialize, Deserialize)]
67/// struct RoomCreated {
68/// 	room_id: String,
69/// }
70///
71/// impl Event for RoomCreated {
72/// 	const SUBJECT: &str = "RoomCreated.v1";
73/// }
74/// ```
75///
76/// # Derive macro
77///
78/// Use `#[derive(Event)]` with a `#[subject(...)]` attribute to avoid the
79/// boilerplate:
80///
81/// ```rust
82/// use arminius_lib_message_bus::Event;
83/// use serde::{Deserialize, Serialize};
84///
85/// #[derive(Serialize, Deserialize, Event)]
86/// #[subject(version = "1")]
87/// struct RoomCreated {
88/// 	room_id: String,
89/// }
90/// ```
91///
92/// Supported `#[subject(...)]` forms:
93///
94/// 1. `#[subject("MyEvent.v1")]`
95/// 2. `#[subject(name = "MyEvent", version = 1)]`
96/// 2. `#[subject(name = "MyEvent", version = "1")]`
97/// 3. `#[subject(name = "MyEvent", version("1"))]`
98/// 4. `#[subject(name = "MyEvent", version = "2", compatible = [v0::MyEvent])]`
99///
100/// When `name` is omitted the struct name is used. The `compatible` list names
101/// older event types whose subjects the subscriber will also consume,
102/// converting each to `Self` via the required `From<OldType> for Self`
103/// implementation.
104///
105/// # Per-instance (dynamic) subjects
106///
107/// A subject template may contain `{field}` placeholders that name fields of
108/// the struct. Each placeholder becomes a NATS `*` wildcard in
109/// [`Event::SUBJECT`] (the subscribe/stream pattern) and is interpolated via
110/// [`Display`] in [`Event::subject`] (the concrete publish subject):
111///
112/// ```rust
113/// use arminius_lib_message_bus::Event;
114/// use serde::{Deserialize, Serialize};
115///
116/// #[derive(Serialize, Deserialize, Event)]
117/// #[subject(name = "rooms.{room_id}.events", version = "1")]
118/// struct RoomCreated {
119/// 	room_id: u64,
120/// }
121/// // RoomCreated::SUBJECT             == "rooms.*.events.v1"   (subscribe to all)
122/// // RoomCreated { room_id: 7 }.subject() == "rooms.7.events.v1"   (publish target)
123/// ```
124///
125/// [`Display`]: std::fmt::Display
126pub trait Event: Serialize + DeserializeOwned + Send + Sync {
127	/// The NATS subject *pattern* this event type lives under.
128	///
129	/// Used for stream creation and subscription, so it may contain NATS
130	/// wildcards (`*`) when the event uses per-instance subjects. For static
131	/// events it equals the concrete publish subject. Should include version
132	/// information for backward compatibility, e.g., `"RoomCreated.v1"`,
133	/// `"RoomCreated.v2"`, `"rooms.*.events.v1"`.
134	const SUBJECT: &str;
135
136	/// Subjects of older compatible versions of this event type.
137	///
138	/// When non-empty, a subscriber will also consume messages on these
139	/// subjects and convert them to `Self` via the generated `From` impls.
140	/// Populated automatically by `#[subject(version = ..., compatible =
141	/// [OldType, ...])]`.
142	const COMPATIBLE_SUBJECTS: &'static [&'static str] = &[];
143
144	/// The concrete subject *this instance* is published to.
145	///
146	/// Defaults to [`Event::SUBJECT`]. Events that use `{field}` placeholders
147	/// in their `#[subject(...)]` template get a generated override that
148	/// interpolates the named fields, e.g. `rooms.7.events.v1`.
149	fn subject(&self) -> std::borrow::Cow<'static, str> {
150		std::borrow::Cow::Borrowed(Self::SUBJECT)
151	}
152
153	/// Serialize this event into the transport payload.
154	///
155	/// The wire format is owned here, not by the transport, so the encoding can
156	/// change (e.g. JSON to protobuf) without the publisher knowing.
157	fn into_payload(self) -> Result<Vec<u8>, Error> {
158		Ok(serde_json::to_vec(&self)?)
159	}
160
161	/// Deserialize a raw payload into `Self`, using `subject` to select the
162	/// right schema when compatible older versions are present.
163	///
164	/// The default implementation ignores `subject` and deserializes directly
165	/// as `Self`.
166	fn from_payload(subject: &str, payload: &[u8]) -> Result<Self, Error> {
167		let _ = subject;
168		Ok(serde_json::from_slice(payload)?)
169	}
170}
171
172/// An event received from the message bus that can be acknowledged.
173///
174/// Implements [`Deref`] to give direct access to the inner event data.
175pub trait ReceivedEvent<E: Event>: Deref<Target = E> {
176	/// Returns a reference to the received event data.
177	fn event(&self) -> &E {
178		self
179	}
180
181	/// Acknowledge processing of this event.
182	///
183	/// Tells the message broker that the event was processed successfully and
184	/// should not be redelivered. If acknowledgment fails, the event may be
185	/// reprocessed by this or another service instance.
186	///
187	/// # Errors
188	///
189	/// Returns [`Error::AckFailed`] if the acknowledgment could not be sent.
190	fn ack(&self) -> impl Future<Output = Result<(), Error>> + Send;
191
192	/// Returns the OpenTelemetry context propagated with this event.
193	///
194	/// Use this to continue a distributed trace across the service boundary:
195	///
196	/// ```rust,no_run
197	/// # use arminius_lib_message_bus::ReceivedEvent;
198	/// # use opentelemetry::trace::FutureExt;
199	/// # async fn process<E: arminius_lib_message_bus::Event + std::fmt::Debug>(
200	/// #     event: impl ReceivedEvent<E>
201	/// # ) -> Result<(), arminius_lib_message_bus::Error> {
202	/// let ctx = event.context().clone();
203	/// async move {
204	/// 	println!("{:?}", event.event());
205	/// 	event.ack().await
206	/// }
207	/// .with_context(ctx)
208	/// .await
209	/// # }
210	/// ```
211	fn context(&self) -> &opentelemetry::Context;
212}
213
214/// Publishes events to the message bus.
215pub trait Publisher {
216	/// Publish an event to its subject.
217	///
218	/// # Errors
219	///
220	/// Returns [`Error::SendFailed`] if the message could not be published.
221	fn publish<E>(&self, message: E) -> impl Future<Output = Result<(), Error>> + Send
222	where
223		E: Event + Clone + 'static;
224}
225
226/// Subscribes to events from the message bus.
227///
228/// Multiple service instances with the same consumer name will automatically
229/// load-balance message processing.
230pub trait Subscriber {
231	/// Subscribe to events of type `E`.
232	///
233	/// Returns a stream that yields acknowledged-capable event wrappers.
234	///
235	/// # Errors
236	///
237	/// Returns [`Error::SubscriptionFailed`] if the subscription could not be
238	/// created.
239	fn subscribe<E: Event + 'static>(
240		&self,
241	) -> impl Future<
242		Output = Result<
243			impl Stream<Item = Result<impl ReceivedEvent<E> + Send + Sync + 'static, Error>>
244			+ Send
245			+ 'static,
246			Error,
247		>,
248	> + Send;
249}