Skip to main content

reliar_core/
message.rs

1//! Stable message contract identity (ADR 0010).
2
3use core::fmt;
4use std::borrow::Cow;
5
6/// A type that can be built into an [`Envelope`](crate::Envelope) and persisted or published.
7///
8/// `TYPE`/`VERSION` are stable **application contracts**: they identify a message across
9/// serialization, storage and the wire, and are never derived from
10/// `std::any::type_name::<T>()` or a module path — renaming or moving the Rust type must not
11/// orphan a pending row or a message already in flight (ADR 0010).
12///
13/// ```
14/// use reliar_core::Message;
15///
16/// #[derive(serde::Serialize, serde::Deserialize)]
17/// struct OrderCancelled {
18///     order_id: u64,
19/// }
20///
21/// impl Message for OrderCancelled {
22///     const TYPE: &'static str = "orders.cancelled";
23///     const VERSION: u16 = 1;
24/// }
25///
26/// assert_eq!(OrderCancelled::TYPE, "orders.cancelled");
27/// assert_eq!(OrderCancelled::VERSION, 1);
28/// ```
29pub trait Message: serde::Serialize + serde::de::DeserializeOwned {
30    /// The message's name, e.g. `"orders.created"`. Stable once anything has published it.
31    const TYPE: &'static str;
32    /// The message's version. Bump when the wire shape changes incompatibly.
33    const VERSION: u16;
34}
35
36/// A message's name and version, carried separately so a query can filter a name across every
37/// version. Renders as `"{name}.v{version}"` via its [`Display`](fmt::Display) impl.
38///
39/// ```
40/// use reliar_core::{Message, MessageType};
41///
42/// #[derive(serde::Serialize, serde::Deserialize)]
43/// struct OrderCreated;
44/// impl Message for OrderCreated {
45///     const TYPE: &'static str = "orders.created";
46///     const VERSION: u16 = 1;
47/// }
48///
49/// let message_type = MessageType::of::<OrderCreated>();
50/// assert_eq!(message_type.name(), "orders.created");
51/// assert_eq!(message_type.version(), 1);
52/// assert_eq!(message_type.to_string(), "orders.created.v1");
53/// ```
54#[derive(Clone, Debug, PartialEq, Eq, Hash)]
55pub struct MessageType {
56    name: Cow<'static, str>,
57
58    version: u16,
59}
60
61impl MessageType {
62    /// Builds a `MessageType` from a `'static` name and a version.
63    ///
64    /// ```
65    /// use reliar_core::MessageType;
66    ///
67    /// let message_type = MessageType::new("orders.created", 1);
68    /// assert_eq!(message_type.to_string(), "orders.created.v1");
69    /// ```
70    #[must_use]
71    pub const fn new(name: &'static str, version: u16) -> Self {
72        Self {
73            name: Cow::Borrowed(name),
74            version,
75        }
76    }
77
78    /// Rehydration path: a provider reads `message_type`/`message_version` columns back into a
79    /// `MessageType` for which it has no Rust type.
80    ///
81    /// ```
82    /// use reliar_core::MessageType;
83    ///
84    /// let message_type = MessageType::from_parts("orders.created".to_string(), 1);
85    /// assert_eq!(message_type.name(), "orders.created");
86    /// ```
87    pub fn from_parts(name: impl Into<Cow<'static, str>>, version: u16) -> Self {
88        Self {
89            name: name.into(),
90            version,
91        }
92    }
93
94    /// Builds the `MessageType` a `T: Message` declares: `T::TYPE` + `T::VERSION`. Never derived
95    /// from `std::any::type_name::<T>()`.
96    ///
97    /// ```
98    /// use reliar_core::{Message, MessageType};
99    ///
100    /// #[derive(serde::Serialize, serde::Deserialize)]
101    /// struct Ping;
102    /// impl Message for Ping {
103    ///     const TYPE: &'static str = "ping";
104    ///     const VERSION: u16 = 1;
105    /// }
106    ///
107    /// assert_eq!(MessageType::of::<Ping>(), MessageType::new("ping", 1));
108    /// ```
109    #[must_use]
110    pub fn of<T: Message>() -> Self {
111        Self::new(T::TYPE, T::VERSION)
112    }
113
114    /// The message name, e.g. `"orders.created"`.
115    ///
116    /// ```
117    /// use reliar_core::MessageType;
118    ///
119    /// assert_eq!(MessageType::new("orders.created", 1).name(), "orders.created");
120    /// ```
121    #[must_use]
122    pub fn name(&self) -> &str {
123        &self.name
124    }
125
126    /// The message version.
127    ///
128    /// ```
129    /// use reliar_core::MessageType;
130    ///
131    /// assert_eq!(MessageType::new("orders.created", 3).version(), 3);
132    /// ```
133    #[must_use]
134    pub const fn version(&self) -> u16 {
135        self.version
136    }
137}
138
139/// Renders `"{name}.v{version}"`, e.g. `orders.created.v1`. **A stable public contract**:
140/// clients parse this string. Two distinct Rust types sharing `TYPE`/`VERSION` render
141/// identically — that is intended, not a bug (ADR 0010).
142impl fmt::Display for MessageType {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "{}.v{}", self.name, self.version)
145    }
146}