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}