ironflow_engine/notify/event.rs
1//! Domain events emitted throughout the ironflow lifecycle.
2
3use chrono::{DateTime, Utc};
4use rust_decimal::Decimal;
5use serde::{Deserialize, Serialize};
6use uuid::Uuid;
7
8use ironflow_store::models::{RunStatus, StepKind};
9
10/// A domain event emitted by the ironflow system.
11///
12/// Covers the full lifecycle: runs, steps, approvals, and authentication.
13/// Subscribers receive these via [`EventPublisher`](super::EventPublisher)
14/// and pattern-match on the variants they care about.
15///
16/// # Examples
17///
18/// ```
19/// use ironflow_engine::notify::Event;
20/// use ironflow_store::models::RunStatus;
21/// use uuid::Uuid;
22///
23/// let event = Event::RunStatusChanged {
24/// run_id: Uuid::now_v7(),
25/// workflow_name: "deploy".to_string(),
26/// from: RunStatus::Running,
27/// to: RunStatus::Completed,
28/// error: None,
29/// cost_usd: rust_decimal::Decimal::ZERO,
30/// duration_ms: 5000,
31/// at: chrono::Utc::now(),
32/// };
33/// ```
34#[derive(Debug, Clone, Serialize, Deserialize)]
35#[serde(tag = "type", rename_all = "snake_case")]
36pub enum Event {
37 // -- Run lifecycle --
38 /// A new run was created (status: Pending).
39 RunCreated {
40 /// Run identifier.
41 run_id: Uuid,
42 /// Workflow name.
43 workflow_name: String,
44 /// When the run was created.
45 at: DateTime<Utc>,
46 },
47
48 /// A run changed status.
49 RunStatusChanged {
50 /// Run identifier.
51 run_id: Uuid,
52 /// Workflow name.
53 workflow_name: String,
54 /// Previous status.
55 from: RunStatus,
56 /// New status.
57 to: RunStatus,
58 /// Error message (when transitioning to Failed).
59 error: Option<String>,
60 /// Aggregated cost in USD at the time of transition.
61 cost_usd: Decimal,
62 /// Aggregated duration in milliseconds at the time of transition.
63 duration_ms: u64,
64 /// When the transition occurred.
65 at: DateTime<Utc>,
66 },
67
68 // -- Step lifecycle --
69 /// A step completed successfully.
70 StepCompleted {
71 /// Run identifier.
72 run_id: Uuid,
73 /// Step identifier.
74 step_id: Uuid,
75 /// Human-readable step name.
76 step_name: String,
77 /// Step operation kind.
78 kind: StepKind,
79 /// Step duration in milliseconds.
80 duration_ms: u64,
81 /// Step cost in USD.
82 cost_usd: Decimal,
83 /// When the step completed.
84 at: DateTime<Utc>,
85 },
86
87 /// A step failed.
88 StepFailed {
89 /// Run identifier.
90 run_id: Uuid,
91 /// Step identifier.
92 step_id: Uuid,
93 /// Human-readable step name.
94 step_name: String,
95 /// Step operation kind.
96 kind: StepKind,
97 /// Error message.
98 error: String,
99 /// When the step failed.
100 at: DateTime<Utc>,
101 },
102
103 // -- Approval --
104 /// A run is waiting for human approval.
105 ApprovalRequested {
106 /// Run identifier.
107 run_id: Uuid,
108 /// Approval step identifier.
109 step_id: Uuid,
110 /// Message displayed to reviewers.
111 message: String,
112 /// When the approval was requested.
113 at: DateTime<Utc>,
114 },
115
116 /// A run was approved by a human.
117 ApprovalGranted {
118 /// Run identifier.
119 run_id: Uuid,
120 /// User who approved (ID or username).
121 approved_by: String,
122 /// When the approval was granted.
123 at: DateTime<Utc>,
124 },
125
126 /// A run was rejected by a human.
127 ApprovalRejected {
128 /// Run identifier.
129 run_id: Uuid,
130 /// User who rejected (ID or username).
131 rejected_by: String,
132 /// When the rejection occurred.
133 at: DateTime<Utc>,
134 },
135
136 // -- Authentication --
137 /// A user signed in.
138 UserSignedIn {
139 /// User identifier.
140 user_id: Uuid,
141 /// Username.
142 username: String,
143 /// When the sign-in occurred.
144 at: DateTime<Utc>,
145 },
146
147 /// A new user signed up.
148 UserSignedUp {
149 /// User identifier.
150 user_id: Uuid,
151 /// Username.
152 username: String,
153 /// When the sign-up occurred.
154 at: DateTime<Utc>,
155 },
156
157 /// A user signed out.
158 UserSignedOut {
159 /// User identifier.
160 user_id: Uuid,
161 /// When the sign-out occurred.
162 at: DateTime<Utc>,
163 },
164}
165
166impl Event {
167 /// Event type constant for [`RunCreated`](Event::RunCreated).
168 pub const RUN_CREATED: &'static str = "run_created";
169 /// Event type constant for [`RunStatusChanged`](Event::RunStatusChanged).
170 pub const RUN_STATUS_CHANGED: &'static str = "run_status_changed";
171 /// Event type constant for [`StepCompleted`](Event::StepCompleted).
172 pub const STEP_COMPLETED: &'static str = "step_completed";
173 /// Event type constant for [`StepFailed`](Event::StepFailed).
174 pub const STEP_FAILED: &'static str = "step_failed";
175 /// Event type constant for [`ApprovalRequested`](Event::ApprovalRequested).
176 pub const APPROVAL_REQUESTED: &'static str = "approval_requested";
177 /// Event type constant for [`ApprovalGranted`](Event::ApprovalGranted).
178 pub const APPROVAL_GRANTED: &'static str = "approval_granted";
179 /// Event type constant for [`ApprovalRejected`](Event::ApprovalRejected).
180 pub const APPROVAL_REJECTED: &'static str = "approval_rejected";
181 /// Event type constant for [`UserSignedIn`](Event::UserSignedIn).
182 pub const USER_SIGNED_IN: &'static str = "user_signed_in";
183 /// Event type constant for [`UserSignedUp`](Event::UserSignedUp).
184 pub const USER_SIGNED_UP: &'static str = "user_signed_up";
185 /// Event type constant for [`UserSignedOut`](Event::UserSignedOut).
186 pub const USER_SIGNED_OUT: &'static str = "user_signed_out";
187
188 /// All event types. Pass this to
189 /// [`EventPublisher::subscribe`](super::EventPublisher::subscribe) to
190 /// receive every event.
191 ///
192 /// # Examples
193 ///
194 /// ```no_run
195 /// use ironflow_engine::notify::{Event, EventPublisher, WebhookSubscriber};
196 ///
197 /// let mut publisher = EventPublisher::new();
198 /// publisher.subscribe(
199 /// WebhookSubscriber::new("https://example.com/all"),
200 /// Event::ALL,
201 /// );
202 /// ```
203 pub const ALL: &'static [&'static str] = &[
204 Self::RUN_CREATED,
205 Self::RUN_STATUS_CHANGED,
206 Self::STEP_COMPLETED,
207 Self::STEP_FAILED,
208 Self::APPROVAL_REQUESTED,
209 Self::APPROVAL_GRANTED,
210 Self::APPROVAL_REJECTED,
211 Self::USER_SIGNED_IN,
212 Self::USER_SIGNED_UP,
213 Self::USER_SIGNED_OUT,
214 ];
215
216 /// Returns the event type as a static string (e.g. `"run_status_changed"`).
217 ///
218 /// Useful for filtering and logging without deserializing.
219 ///
220 /// # Examples
221 ///
222 /// ```
223 /// use ironflow_engine::notify::Event;
224 /// use uuid::Uuid;
225 /// use chrono::Utc;
226 ///
227 /// let event = Event::UserSignedIn {
228 /// user_id: Uuid::now_v7(),
229 /// username: "alice".to_string(),
230 /// at: Utc::now(),
231 /// };
232 /// assert_eq!(event.event_type(), "user_signed_in");
233 /// ```
234 pub fn event_type(&self) -> &'static str {
235 match self {
236 Event::RunCreated { .. } => Self::RUN_CREATED,
237 Event::RunStatusChanged { .. } => Self::RUN_STATUS_CHANGED,
238 Event::StepCompleted { .. } => Self::STEP_COMPLETED,
239 Event::StepFailed { .. } => Self::STEP_FAILED,
240 Event::ApprovalRequested { .. } => Self::APPROVAL_REQUESTED,
241 Event::ApprovalGranted { .. } => Self::APPROVAL_GRANTED,
242 Event::ApprovalRejected { .. } => Self::APPROVAL_REJECTED,
243 Event::UserSignedIn { .. } => Self::USER_SIGNED_IN,
244 Event::UserSignedUp { .. } => Self::USER_SIGNED_UP,
245 Event::UserSignedOut { .. } => Self::USER_SIGNED_OUT,
246 }
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn run_status_changed_serde_roundtrip() {
256 let event = Event::RunStatusChanged {
257 run_id: Uuid::now_v7(),
258 workflow_name: "deploy".to_string(),
259 from: RunStatus::Running,
260 to: RunStatus::Completed,
261 error: None,
262 cost_usd: Decimal::new(42, 2),
263 duration_ms: 5000,
264 at: Utc::now(),
265 };
266
267 let json = serde_json::to_string(&event).expect("serialize");
268 let back: Event = serde_json::from_str(&json).expect("deserialize");
269
270 assert_eq!(back.event_type(), "run_status_changed");
271 assert!(json.contains("\"type\":\"run_status_changed\""));
272 }
273
274 #[test]
275 fn user_signed_in_serde_roundtrip() {
276 let event = Event::UserSignedIn {
277 user_id: Uuid::now_v7(),
278 username: "alice".to_string(),
279 at: Utc::now(),
280 };
281
282 let json = serde_json::to_string(&event).expect("serialize");
283 let back: Event = serde_json::from_str(&json).expect("deserialize");
284
285 assert_eq!(back.event_type(), "user_signed_in");
286 assert!(json.contains("alice"));
287 }
288
289 #[test]
290 fn step_failed_serde_roundtrip() {
291 let event = Event::StepFailed {
292 run_id: Uuid::now_v7(),
293 step_id: Uuid::now_v7(),
294 step_name: "build".to_string(),
295 kind: StepKind::Shell,
296 error: "exit code 1".to_string(),
297 at: Utc::now(),
298 };
299
300 let json = serde_json::to_string(&event).expect("serialize");
301 let back: Event = serde_json::from_str(&json).expect("deserialize");
302
303 assert_eq!(back.event_type(), "step_failed");
304 }
305
306 #[test]
307 fn approval_requested_serde_roundtrip() {
308 let event = Event::ApprovalRequested {
309 run_id: Uuid::now_v7(),
310 step_id: Uuid::now_v7(),
311 message: "Deploy to prod?".to_string(),
312 at: Utc::now(),
313 };
314
315 let json = serde_json::to_string(&event).expect("serialize");
316 assert!(json.contains("approval_requested"));
317 }
318
319 #[test]
320 fn event_type_all_variants() {
321 let id = Uuid::now_v7();
322 let now = Utc::now();
323
324 let cases: Vec<(Event, &str)> = vec![
325 (
326 Event::RunCreated {
327 run_id: id,
328 workflow_name: "w".to_string(),
329 at: now,
330 },
331 "run_created",
332 ),
333 (
334 Event::RunStatusChanged {
335 run_id: id,
336 workflow_name: "w".to_string(),
337 from: RunStatus::Pending,
338 to: RunStatus::Running,
339 error: None,
340 cost_usd: Decimal::ZERO,
341 duration_ms: 0,
342 at: now,
343 },
344 "run_status_changed",
345 ),
346 (
347 Event::StepCompleted {
348 run_id: id,
349 step_id: id,
350 step_name: "s".to_string(),
351 kind: StepKind::Shell,
352 duration_ms: 0,
353 cost_usd: Decimal::ZERO,
354 at: now,
355 },
356 "step_completed",
357 ),
358 (
359 Event::StepFailed {
360 run_id: id,
361 step_id: id,
362 step_name: "s".to_string(),
363 kind: StepKind::Shell,
364 error: "err".to_string(),
365 at: now,
366 },
367 "step_failed",
368 ),
369 (
370 Event::ApprovalRequested {
371 run_id: id,
372 step_id: id,
373 message: "ok?".to_string(),
374 at: now,
375 },
376 "approval_requested",
377 ),
378 (
379 Event::ApprovalGranted {
380 run_id: id,
381 approved_by: "alice".to_string(),
382 at: now,
383 },
384 "approval_granted",
385 ),
386 (
387 Event::ApprovalRejected {
388 run_id: id,
389 rejected_by: "bob".to_string(),
390 at: now,
391 },
392 "approval_rejected",
393 ),
394 (
395 Event::UserSignedIn {
396 user_id: id,
397 username: "u".to_string(),
398 at: now,
399 },
400 "user_signed_in",
401 ),
402 (
403 Event::UserSignedUp {
404 user_id: id,
405 username: "u".to_string(),
406 at: now,
407 },
408 "user_signed_up",
409 ),
410 (
411 Event::UserSignedOut {
412 user_id: id,
413 at: now,
414 },
415 "user_signed_out",
416 ),
417 ];
418
419 for (event, expected_type) in cases {
420 assert_eq!(event.event_type(), expected_type);
421 }
422 }
423}