Skip to main content

arminius_lib_message_bus/
command.rs

1// SPDX-FileCopyrightText: 2025 BWI GmbH
2//
3// SPDX-License-Identifier: Apache-2.0
4
5//! Request-reply command messaging: traits for sending commands and awaiting
6//! typed replies, and for responding to received commands.
7
8use std::{borrow::Cow, future::Future, ops::Deref};
9
10pub use futures::Stream;
11use serde::{Serialize, de::DeserializeOwned};
12
13use crate::event::Error;
14
15/// Errors during command payload serialization or subject parsing.
16#[derive(Debug, thiserror::Error)]
17pub enum CommandError {
18	/// JSON serialization or deserialization failure.
19	#[error("Failed to serialize/deserialize payload: {0}")]
20	Payload(#[from] serde_json::Error),
21	/// Base64url decoding of a subject segment failed.
22	#[error("Failed to decode subject: {0}")]
23	SubjectDecode(String),
24	/// Parsed string could not be converted to the expected type.
25	#[error("Failed to parse subject: {0}")]
26	SubjectParse(String),
27}
28
29/// How often a command may reach its handler.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum Delivery {
32	/// Redelivered until it is acknowledged, so handling it twice must be
33	/// harmless.
34	AtLeastOnce,
35	/// Acknowledged before it is handled and never redelivered, so a handler
36	/// that does not finish loses the command.
37	AtMostOnce,
38}
39
40/// Typed command endpoint for request-reply over a message bus.
41///
42/// Each struct represents one command endpoint.
43pub trait Command: Serialize + DeserializeOwned + Send + Sync {
44	/// NATS subject pattern, e.g. `"rooms.*.create"`.
45	const SUBJECT: &'static str;
46	/// JetStream stream filter (auto-inferred by derive), e.g. `"rooms.>"`.
47	const SUBJECT_FILTER: &'static str;
48	/// How often this command may reach its handler.
49	///
50	/// Defaults to [`Delivery::AtLeastOnce`], which suits any handler that can
51	/// run twice without harm.
52	const DELIVERY: Delivery = Delivery::AtLeastOnce;
53	/// Wire response type — `Requester::request` returns `Self::Reply`.
54	type Reply: Serialize + DeserializeOwned + Send;
55
56	/// The concrete subject *this instance* is sent to.
57	///
58	/// Encodes any wildcard fields of [`Command::SUBJECT`]. Defaults to the
59	/// static `SUBJECT` for commands without wildcard fields.
60	fn subject(&self) -> Cow<'static, str> {
61		Cow::Borrowed(Self::SUBJECT)
62	}
63
64	/// Serialize this command into the transport payload.
65	///
66	/// The wire format is owned here, not by the transport, so the encoding can
67	/// change (e.g. JSON to protobuf) without the bus implementation knowing.
68	/// The full struct is encoded, including any fields also carried in the
69	/// subject (so the two copies cannot drift); the reply address travels
70	/// out-of-band (e.g. a NATS header), not in these bytes.
71	fn into_payload(self) -> Result<Vec<u8>, CommandError> {
72		Ok(serde_json::to_vec(&self)?)
73	}
74
75	/// Reconstruct `Self` from a received command's `payload`.
76	///
77	/// The full struct is serialized on the wire (including any fields also
78	/// encoded in the subject, so the two copies cannot drift), and the reply
79	/// address is carried out-of-band by the transport (e.g. a NATS header).
80	/// The payload therefore suffices to recover every field; `subject` is
81	/// retained for transports that wish to validate routing.
82	fn from_payload(subject: &str, payload: &[u8]) -> Result<Self, CommandError> {
83		let _ = subject;
84		Ok(serde_json::from_slice(payload)?)
85	}
86}
87
88/// A received command that must be acknowledged with a typed reply.
89pub trait ReceivedCommand<C: Command>: Deref<Target = C> {
90	/// Returns a reference to the received command data.
91	fn command(&self) -> &C {
92		self
93	}
94
95	/// Take responsibility for the command without answering it yet.
96	///
97	/// A handler that acknowledges up front trades the certainty of
98	/// eventually receiving an answer for the certainty that nothing can hand
99	/// the command out a second time: a crash or a stall past this point costs
100	/// the reply rather than repeating whatever the handler had already done.
101	/// This is what a [`Delivery::AtMostOnce`] command asks for:
102	///
103	/// ```ignore
104	/// received.ack().await?;
105	/// let reply = claim_keys(received.command()).await;
106	/// received.reply(reply).await?;
107	/// ```
108	///
109	/// [`reply`](Self::reply) acknowledges as well, so a handler that is happy
110	/// to be run again should never call this.
111	fn ack(&self) -> impl Future<Output = Result<(), Error>> + Send;
112
113	/// Send `response` back to the requester and acknowledge the command.
114	fn reply(&self, response: C::Reply) -> impl Future<Output = Result<(), Error>> + Send;
115
116	/// Returns the OpenTelemetry context propagated with this command.
117	fn context(&self) -> &opentelemetry::Context;
118}
119
120/// Sends commands and awaits typed replies. Mirrors
121/// [`Publisher`](crate::Publisher).
122pub trait Requester {
123	/// Send `cmd` and wait for a typed reply.
124	fn request<C: Command>(&self, cmd: C) -> impl Future<Output = Result<C::Reply, Error>> + Send;
125}
126
127impl<T: Requester + Sync> Requester for std::sync::Arc<T> {
128	fn request<C: Command>(&self, cmd: C) -> impl Future<Output = Result<C::Reply, Error>> + Send {
129		(**self).request(cmd)
130	}
131}
132
133/// Subscribes to incoming commands. Mirrors [`Subscriber`](crate::Subscriber).
134///
135/// `respond` takes `self` by value so the returned future is `'static` with no
136/// borrow of the implementor. Callers that need to keep a handle should clone
137/// before calling (e.g. `conn.clone().respond::<C>()`).
138pub trait Responder {
139	/// Subscribe to commands of type `C` and return a stream of received
140	/// commands.
141	fn respond<C: Command + 'static>(
142		self,
143	) -> impl Future<
144		Output = Result<
145			impl Stream<Item = Result<impl ReceivedCommand<C> + Send + Sync + 'static, Error>>
146			+ Send
147			+ 'static,
148			Error,
149		>,
150	> + Send
151	+ 'static;
152}
153
154impl<T: Responder + Clone + Send + Sync + 'static> Responder for std::sync::Arc<T> {
155	fn respond<C: Command + 'static>(
156		self,
157	) -> impl Future<
158		Output = Result<
159			impl Stream<Item = Result<impl ReceivedCommand<C> + Send + Sync + 'static, Error>>
160			+ Send
161			+ 'static,
162			Error,
163		>,
164	> + Send
165	+ 'static {
166		let inner = (*self).clone();
167		async move { inner.respond::<C>().await }
168	}
169}