Skip to main content

ralph_proto/
event.rs

1//! Event types for pub/sub messaging.
2
3use crate::{HatId, Topic};
4use serde::{Deserialize, Serialize};
5
6/// An event in the pub/sub system.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct Event {
9    /// The routing topic for this event.
10    pub topic: Topic,
11
12    /// The content/payload of the event.
13    pub payload: String,
14
15    /// The hat that published this event (if any).
16    pub source: Option<HatId>,
17
18    /// Optional target hat for direct handoff.
19    pub target: Option<HatId>,
20
21    /// Wave correlation ID (e.g., "w-1a2b3c4d").
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub wave_id: Option<String>,
24
25    /// Index of this event within the wave (0-based).
26    #[serde(skip_serializing_if = "Option::is_none")]
27    pub wave_index: Option<u32>,
28
29    /// Total number of events in the wave.
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub wave_total: Option<u32>,
32}
33
34impl Event {
35    /// Creates a new event with the given topic and payload.
36    pub fn new(topic: impl Into<Topic>, payload: impl Into<String>) -> Self {
37        Self {
38            topic: topic.into(),
39            payload: payload.into(),
40            source: None,
41            target: None,
42            wave_id: None,
43            wave_index: None,
44            wave_total: None,
45        }
46    }
47
48    /// Sets the source hat for this event.
49    #[must_use]
50    pub fn with_source(mut self, source: impl Into<HatId>) -> Self {
51        self.source = Some(source.into());
52        self
53    }
54
55    /// Sets the target hat for direct handoff.
56    #[must_use]
57    pub fn with_target(mut self, target: impl Into<HatId>) -> Self {
58        self.target = Some(target.into());
59        self
60    }
61
62    /// Sets wave correlation metadata on this event.
63    #[must_use]
64    pub fn with_wave(mut self, wave_id: impl Into<String>, index: u32, total: u32) -> Self {
65        self.wave_id = Some(wave_id.into());
66        self.wave_index = Some(index);
67        self.wave_total = Some(total);
68        self
69    }
70
71    /// Returns true if this event has wave correlation metadata.
72    pub fn is_wave_event(&self) -> bool {
73        self.wave_id.is_some()
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_new_event_has_no_wave_metadata() {
83        let event = Event::new("test.topic", "payload");
84        assert!(!event.is_wave_event());
85        assert!(event.wave_id.is_none());
86        assert!(event.wave_index.is_none());
87        assert!(event.wave_total.is_none());
88    }
89
90    #[test]
91    fn test_with_wave_sets_metadata() {
92        let event = Event::new("review.file", "src/main.rs").with_wave("w-1a2b3c4d", 0, 3);
93        assert!(event.is_wave_event());
94        assert_eq!(event.wave_id.as_deref(), Some("w-1a2b3c4d"));
95        assert_eq!(event.wave_index, Some(0));
96        assert_eq!(event.wave_total, Some(3));
97    }
98
99    #[test]
100    fn test_wave_metadata_roundtrips_through_serde() {
101        let event = Event::new("review.file", "src/main.rs")
102            .with_source("dispatcher")
103            .with_wave("w-abcd1234", 2, 5);
104
105        let json = serde_json::to_string(&event).unwrap();
106        let deserialized: Event = serde_json::from_str(&json).unwrap();
107
108        assert_eq!(deserialized.wave_id.as_deref(), Some("w-abcd1234"));
109        assert_eq!(deserialized.wave_index, Some(2));
110        assert_eq!(deserialized.wave_total, Some(5));
111        assert_eq!(deserialized.topic.as_str(), "review.file");
112        assert_eq!(deserialized.payload, "src/main.rs");
113    }
114
115    #[test]
116    fn test_event_without_wave_serializes_without_wave_fields() {
117        let event = Event::new("test.topic", "payload");
118        let json = serde_json::to_string(&event).unwrap();
119        assert!(!json.contains("wave_id"));
120        assert!(!json.contains("wave_index"));
121        assert!(!json.contains("wave_total"));
122    }
123
124    #[test]
125    fn test_event_without_wave_fields_deserializes() {
126        let json = r#"{"topic":"test.topic","payload":"hello"}"#;
127        let event: Event = serde_json::from_str(json).unwrap();
128        assert!(!event.is_wave_event());
129        assert_eq!(event.topic.as_str(), "test.topic");
130        assert_eq!(event.payload, "hello");
131    }
132}