use std::{fmt, time::SystemTime};
macro_rules! string_identifier {
($name:ident, $description:literal) => {
#[doc = $description]
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> Self {
Self(value.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(&self.0)
}
}
impl From<String> for $name {
fn from(value: String) -> Self {
Self::new(value)
}
}
impl From<&str> for $name {
fn from(value: &str) -> Self {
Self::new(value)
}
}
};
}
string_identifier!(
MessageId,
"Stable identity of a command, query, or event message."
);
string_identifier!(
CorrelationId,
"Identity shared by messages that belong to one application flow."
);
string_identifier!(
CausationId,
"Identity of the message that directly caused another message."
);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageMetadata {
id: MessageId,
created_at: SystemTime,
correlation_id: Option<CorrelationId>,
causation_id: Option<CausationId>,
initiated_by: Option<String>,
source: Option<String>,
}
impl MessageMetadata {
pub fn new(id: impl Into<MessageId>, created_at: SystemTime) -> Self {
Self {
id: id.into(),
created_at,
correlation_id: None,
causation_id: None,
initiated_by: None,
source: None,
}
}
#[must_use]
pub fn with_correlation_id(mut self, correlation_id: impl Into<CorrelationId>) -> Self {
self.correlation_id = Some(correlation_id.into());
self
}
#[must_use]
pub fn with_causation_id(mut self, causation_id: impl Into<CausationId>) -> Self {
self.causation_id = Some(causation_id.into());
self
}
#[must_use]
pub fn with_initiated_by(mut self, initiated_by: impl Into<String>) -> Self {
self.initiated_by = Some(initiated_by.into());
self
}
#[must_use]
pub fn with_source(mut self, source: impl Into<String>) -> Self {
self.source = Some(source.into());
self
}
pub const fn id(&self) -> &MessageId {
&self.id
}
pub const fn created_at(&self) -> SystemTime {
self.created_at
}
pub const fn correlation_id(&self) -> Option<&CorrelationId> {
self.correlation_id.as_ref()
}
pub const fn causation_id(&self) -> Option<&CausationId> {
self.causation_id.as_ref()
}
pub fn initiated_by(&self) -> Option<&str> {
self.initiated_by.as_deref()
}
pub fn source(&self) -> Option<&str> {
self.source.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MessageEnvelope<M> {
pub message: M,
pub metadata: MessageMetadata,
}
impl<M> MessageEnvelope<M> {
pub const fn new(message: M, metadata: MessageMetadata) -> Self {
Self { message, metadata }
}
pub fn map<N>(self, transform: impl FnOnce(M) -> N) -> MessageEnvelope<N> {
MessageEnvelope {
message: transform(self.message),
metadata: self.metadata,
}
}
}
#[cfg(test)]
mod tests {
use std::time::UNIX_EPOCH;
use super::MessageMetadata;
#[test]
fn message_metadata_keeps_external_identity_and_trace_context() {
let metadata = MessageMetadata::new("message-1", UNIX_EPOCH)
.with_correlation_id("correlation-1")
.with_causation_id("cause-1")
.with_initiated_by("user-1")
.with_source("orders");
assert_eq!(metadata.id().as_str(), "message-1");
assert_eq!(
metadata.correlation_id().map(|value| value.as_str()),
Some("correlation-1")
);
assert_eq!(metadata.initiated_by(), Some("user-1"));
}
}