1use std::{fmt, time::SystemTime};
4
5macro_rules! string_identifier {
6 ($name:ident, $description:literal) => {
7 #[doc = $description]
8 #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
9 pub struct $name(String);
10
11 impl $name {
12 pub fn new(value: impl Into<String>) -> Self {
14 Self(value.into())
15 }
16
17 pub fn as_str(&self) -> &str {
19 &self.0
20 }
21 }
22
23 impl fmt::Display for $name {
24 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
25 formatter.write_str(&self.0)
26 }
27 }
28
29 impl From<String> for $name {
30 fn from(value: String) -> Self {
31 Self::new(value)
32 }
33 }
34
35 impl From<&str> for $name {
36 fn from(value: &str) -> Self {
37 Self::new(value)
38 }
39 }
40 };
41}
42
43string_identifier!(
44 MessageId,
45 "Stable identity of a command, query, or event message."
46);
47string_identifier!(
48 CorrelationId,
49 "Identity shared by messages that belong to one application flow."
50);
51string_identifier!(
52 CausationId,
53 "Identity of the message that directly caused another message."
54);
55
56#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct MessageMetadata {
62 id: MessageId,
63 created_at: SystemTime,
64 correlation_id: Option<CorrelationId>,
65 causation_id: Option<CausationId>,
66 initiated_by: Option<String>,
67 source: Option<String>,
68}
69
70impl MessageMetadata {
71 pub fn new(id: impl Into<MessageId>, created_at: SystemTime) -> Self {
74 Self {
75 id: id.into(),
76 created_at,
77 correlation_id: None,
78 causation_id: None,
79 initiated_by: None,
80 source: None,
81 }
82 }
83
84 #[must_use]
86 pub fn with_correlation_id(mut self, correlation_id: impl Into<CorrelationId>) -> Self {
87 self.correlation_id = Some(correlation_id.into());
88 self
89 }
90
91 #[must_use]
93 pub fn with_causation_id(mut self, causation_id: impl Into<CausationId>) -> Self {
94 self.causation_id = Some(causation_id.into());
95 self
96 }
97
98 #[must_use]
100 pub fn with_initiated_by(mut self, initiated_by: impl Into<String>) -> Self {
101 self.initiated_by = Some(initiated_by.into());
102 self
103 }
104
105 #[must_use]
107 pub fn with_source(mut self, source: impl Into<String>) -> Self {
108 self.source = Some(source.into());
109 self
110 }
111
112 pub const fn id(&self) -> &MessageId {
114 &self.id
115 }
116
117 pub const fn created_at(&self) -> SystemTime {
119 self.created_at
120 }
121
122 pub const fn correlation_id(&self) -> Option<&CorrelationId> {
124 self.correlation_id.as_ref()
125 }
126
127 pub const fn causation_id(&self) -> Option<&CausationId> {
129 self.causation_id.as_ref()
130 }
131
132 pub fn initiated_by(&self) -> Option<&str> {
134 self.initiated_by.as_deref()
135 }
136
137 pub fn source(&self) -> Option<&str> {
139 self.source.as_deref()
140 }
141}
142
143#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct MessageEnvelope<M> {
146 pub message: M,
148 pub metadata: MessageMetadata,
150}
151
152impl<M> MessageEnvelope<M> {
153 pub const fn new(message: M, metadata: MessageMetadata) -> Self {
155 Self { message, metadata }
156 }
157
158 pub fn map<N>(self, transform: impl FnOnce(M) -> N) -> MessageEnvelope<N> {
160 MessageEnvelope {
161 message: transform(self.message),
162 metadata: self.metadata,
163 }
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use std::time::UNIX_EPOCH;
170
171 use super::MessageMetadata;
172
173 #[test]
174 fn message_metadata_keeps_external_identity_and_trace_context() {
175 let metadata = MessageMetadata::new("message-1", UNIX_EPOCH)
176 .with_correlation_id("correlation-1")
177 .with_causation_id("cause-1")
178 .with_initiated_by("user-1")
179 .with_source("orders");
180
181 assert_eq!(metadata.id().as_str(), "message-1");
182 assert_eq!(
183 metadata.correlation_id().map(|value| value.as_str()),
184 Some("correlation-1")
185 );
186 assert_eq!(metadata.initiated_by(), Some("user-1"));
187 }
188}