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}