Skip to main content

hexeract_core/
envelope.rs

1use crate::command::Command;
2use crate::ids::{CorrelationId, MessageId};
3use crate::notification::Notification;
4use crate::query::Query;
5
6/// Metadata carried alongside every dispatch.
7///
8/// The envelope exposes the message type name and identifiers without
9/// requiring the middleware to know the concrete `Command` or `Query` type.
10#[derive(Debug, Clone)]
11pub struct MessageEnvelope {
12    type_name: &'static str,
13    message_id: MessageId,
14    correlation_id: CorrelationId,
15}
16
17impl MessageEnvelope {
18    /// Builds an envelope for a [`Command`] dispatch.
19    #[must_use]
20    pub fn for_command<C: Command>(message_id: MessageId, correlation_id: CorrelationId) -> Self {
21        Self {
22            type_name: std::any::type_name::<C>(),
23            message_id,
24            correlation_id,
25        }
26    }
27
28    /// Builds an envelope for a [`Query`] dispatch.
29    #[must_use]
30    pub fn for_query<Q: Query>(message_id: MessageId, correlation_id: CorrelationId) -> Self {
31        Self {
32            type_name: std::any::type_name::<Q>(),
33            message_id,
34            correlation_id,
35        }
36    }
37
38    /// Builds an envelope for a [`Notification`] dispatch. Each handler in a
39    /// fan-out receives its own envelope; callers typically share the
40    /// [`CorrelationId`] across the whole fan-out to preserve the causal link
41    /// in traces.
42    #[must_use]
43    pub fn for_notification<N: Notification>(
44        message_id: MessageId,
45        correlation_id: CorrelationId,
46    ) -> Self {
47        Self {
48            type_name: std::any::type_name::<N>(),
49            message_id,
50            correlation_id,
51        }
52    }
53
54    /// The fully-qualified type name of the dispatched message.
55    #[must_use]
56    pub fn type_name(&self) -> &'static str {
57        self.type_name
58    }
59
60    /// The unique identifier of this dispatch.
61    #[must_use]
62    pub fn message_id(&self) -> MessageId {
63        self.message_id
64    }
65
66    /// The correlation identifier linking this dispatch to its causal chain.
67    #[must_use]
68    pub fn correlation_id(&self) -> CorrelationId {
69        self.correlation_id
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76
77    struct Ping;
78    impl Command for Ping {
79        type Output = ();
80    }
81
82    struct Pong;
83    impl Query for Pong {
84        type Output = ();
85    }
86
87    #[derive(Clone)]
88    struct Bell;
89    impl Notification for Bell {}
90
91    #[test]
92    fn for_notification_records_notification_type_name() {
93        let env = MessageEnvelope::for_notification::<Bell>(MessageId::new(), CorrelationId::new());
94        assert!(env.type_name().ends_with("::Bell"));
95    }
96
97    #[test]
98    fn for_command_records_command_type_name() {
99        let env = MessageEnvelope::for_command::<Ping>(MessageId::new(), CorrelationId::new());
100        assert!(env.type_name().ends_with("::Ping"));
101    }
102
103    #[test]
104    fn for_query_records_query_type_name() {
105        let env = MessageEnvelope::for_query::<Pong>(MessageId::new(), CorrelationId::new());
106        assert!(env.type_name().ends_with("::Pong"));
107    }
108
109    #[test]
110    fn envelope_preserves_identifiers() {
111        let msg = MessageId::new();
112        let corr = CorrelationId::new();
113        let env = MessageEnvelope::for_command::<Ping>(msg, corr);
114        assert_eq!(env.message_id(), msg);
115        assert_eq!(env.correlation_id(), corr);
116    }
117
118    #[test]
119    fn envelope_is_clone() {
120        let env = MessageEnvelope::for_command::<Ping>(MessageId::new(), CorrelationId::new());
121        let cloned = env.clone();
122        assert_eq!(env.message_id(), cloned.message_id());
123        assert_eq!(env.type_name(), cloned.type_name());
124    }
125}