Skip to main content

arc_core/
event.rs

1//! # Event Module
2//!
3//! Core event type for event sourcing. Events represent immutable facts about
4//! things that have happened in the system.
5//!
6//! ## Audit metadata
7//!
8//! Every event carries an [`AuditMetadata`](crate::audit::AuditMetadata) value.
9//! Aggregates produce events with `AuditMetadata::pending()`; the
10//! `CommandBus::dispatch` implementation overwrites that placeholder with a
11//! validated audit struct sourced from the request `CommandContext` before
12//! calling `EventStore::append`. Stores reject events whose audit fails
13//! validation.
14//!
15//! ## Design Principles
16//!
17//! - **Immutable**: Once persisted, events cannot be changed.
18//! - **Serializable**: All events can be stored as JSON.
19//! - **Self-describing**: Events contain all metadata needed to understand them.
20//! - **Ordered**: Events have a sequence number within their aggregate.
21//! - **Audited**: Every persisted event carries `who/when/where/why` audit data.
22
23use crate::audit::AuditMetadata;
24use serde::{Deserialize, Serialize};
25use std::time::{SystemTime, UNIX_EPOCH};
26use uuid::Uuid;
27
28/// Core event type representing an immutable domain event.
29///
30/// # Fields
31///
32/// - `event_id`: Unique identifier for this specific event occurrence
33/// - `aggregate_type`: Type of aggregate this event belongs to (e.g., "User")
34/// - `aggregate_id`: ID of the specific aggregate instance
35/// - `sequence`: Sequential number within the aggregate stream (starts at 1)
36/// - `event_type`: Type of event (e.g., "UserRegistered")
37/// - `payload`: Event data as JSON (flexible, evolvable schema)
38/// - `audit`: HIPAA audit metadata. `pending()` until the bus stamps it.
39/// - `timestamp`: When the event occurred (milliseconds since UNIX epoch)
40#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
41pub struct Event {
42    /// Unique identifier for this event
43    pub event_id: Uuid,
44
45    /// Aggregate type (e.g., "User", "Order")
46    pub aggregate_type: String,
47
48    /// Aggregate instance identifier
49    pub aggregate_id: String,
50
51    /// Sequence number within the aggregate (starts at 1)
52    pub sequence: i64,
53
54    /// Event type (e.g., "UserCreated", "ProfileUpdated")
55    /// Convention: Past tense, PascalCase
56    pub event_type: String,
57
58    /// Event payload as JSON
59    pub payload: serde_json::Value,
60
61    /// HIPAA audit metadata. `AuditMetadata::pending()` until the
62    /// `CommandBus` overwrites it before `append`. `EventStore::append`
63    /// implementations call `audit.validate()` and reject pending values.
64    pub audit: AuditMetadata,
65
66    /// Wall-clock timestamp (milliseconds since UNIX epoch). For HIPAA
67    /// audit-quality time, use `audit.timestamp_utc_us` (microsecond precision).
68    pub timestamp: u64,
69}
70
71impl Event {
72    /// Create a new event with `audit = AuditMetadata::pending()`.
73    ///
74    /// Aggregates call this from `handle()` and the `CommandBus` overwrites the
75    /// audit field with a request-scoped value before persisting. The
76    /// pending placeholder fails store-side validation, so a forgotten stamp
77    /// is impossible to commit.
78    ///
79    /// # Example
80    ///
81    /// ```rust
82    /// use arc_core::event::Event;
83    /// use serde_json::json;
84    ///
85    /// let event = Event::new(
86    ///     "User",
87    ///     "user-456",
88    ///     1,
89    ///     "UserCreated",
90    ///     json!({ "name": "Bob", "email": "bob@example.com" }),
91    /// );
92    /// assert!(event.audit.is_pending());
93    /// ```
94    pub fn new(
95        aggregate_type: impl Into<String>,
96        aggregate_id: impl Into<String>,
97        sequence: i64,
98        event_type: impl Into<String>,
99        payload: serde_json::Value,
100    ) -> Self {
101        Self {
102            event_id: Uuid::new_v4(),
103            aggregate_type: aggregate_type.into(),
104            aggregate_id: aggregate_id.into(),
105            sequence,
106            event_type: event_type.into(),
107            payload,
108            audit: AuditMetadata::pending(),
109            timestamp: SystemTime::now()
110                .duration_since(UNIX_EPOCH)
111                .expect("Time went backwards")
112                .as_millis() as u64,
113        }
114    }
115
116    /// Replace `audit` with a fully-stamped value. Used by `CommandBus`
117    /// before calling `EventStore::append`.
118    pub fn with_audit(mut self, audit: AuditMetadata) -> Self {
119        self.audit = audit;
120        self
121    }
122
123    /// Serialize event to JSON string.
124    pub fn to_json(&self) -> Result<String, serde_json::Error> {
125        serde_json::to_string(self)
126    }
127
128    /// Deserialize event from JSON string.
129    pub fn from_json(json_str: &str) -> Result<Self, serde_json::Error> {
130        serde_json::from_str(json_str)
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use crate::audit::AuditMetadata;
138    use serde_json::json;
139
140    #[test]
141    fn test_event_creation() {
142        let event = Event::new(
143            "User",
144            "user-123",
145            1,
146            "UserCreated",
147            json!({
148                "name": "Test User",
149                "email": "test@example.com"
150            }),
151        );
152
153        assert_eq!(event.aggregate_type, "User");
154        assert_eq!(event.aggregate_id, "user-123");
155        assert_eq!(event.sequence, 1);
156        assert_eq!(event.event_type, "UserCreated");
157        assert_eq!(event.payload["name"], "Test User");
158        assert!(event.timestamp > 0);
159        assert!(event.audit.is_pending());
160    }
161
162    #[test]
163    fn test_with_audit_overwrites_pending() {
164        let stamp = AuditMetadata::test_default();
165        let event =
166            Event::new("User", "user-1", 1, "UserCreated", json!({})).with_audit(stamp.clone());
167        assert!(!event.audit.is_pending());
168        assert_eq!(event.audit, stamp);
169    }
170
171    #[test]
172    fn test_event_serialization_roundtrips_audit() {
173        let event = Event::new(
174            "User",
175            "user-789",
176            3,
177            "ProfileUpdated",
178            json!({"name": "X"}),
179        )
180        .with_audit(AuditMetadata::test_default());
181
182        let json_str = event.to_json().unwrap();
183        let deserialized = Event::from_json(&json_str).unwrap();
184
185        assert_eq!(event.event_id, deserialized.event_id);
186        assert_eq!(event.audit, deserialized.audit);
187    }
188
189    #[test]
190    fn test_event_ordering() {
191        let event1 = Event::new("User", "user-1", 1, "UserCreated", json!({}));
192        let event2 = Event::new("User", "user-1", 2, "ProfileUpdated", json!({}));
193
194        assert!(event1.sequence < event2.sequence);
195    }
196
197    #[test]
198    fn test_event_uniqueness() {
199        let event1 = Event::new("User", "user-1", 1, "UserCreated", json!({}));
200        let event2 = Event::new("User", "user-1", 1, "UserCreated", json!({}));
201        assert_ne!(event1.event_id, event2.event_id);
202    }
203}