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}