Skip to main content

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}