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}