Skip to main content

ironflow_store/entities/
event_kind.rs

1//! Strongly-typed event kind enum for domain events.
2
3use serde::{Deserialize, Serialize};
4use strum::{Display, EnumString, IntoStaticStr};
5
6/// Strongly-typed event kind matching domain event variants.
7///
8/// Serializes to/from `snake_case` strings (e.g. `"run_status_changed"`).
9///
10/// # Examples
11///
12/// ```
13/// use ironflow_store::entities::EventKind;
14///
15/// let kind: EventKind = "run_status_changed".parse().unwrap();
16/// assert_eq!(kind, EventKind::RunStatusChanged);
17/// assert_eq!(kind.as_str(), "run_status_changed");
18/// ```
19#[derive(
20    Debug,
21    Clone,
22    Copy,
23    PartialEq,
24    Eq,
25    Hash,
26    Serialize,
27    Deserialize,
28    Display,
29    EnumString,
30    IntoStaticStr,
31)]
32#[serde(rename_all = "snake_case")]
33#[strum(serialize_all = "snake_case")]
34#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
35pub enum EventKind {
36    /// A new run was created.
37    RunCreated,
38    /// A run changed status.
39    RunStatusChanged,
40    /// A run failed.
41    RunFailed,
42    /// A run was stopped because it reached its cumulative cost cap.
43    RunBudgetExceeded,
44    /// A step completed successfully.
45    StepCompleted,
46    /// A step failed.
47    StepFailed,
48    /// A run is waiting for human approval.
49    ApprovalRequested,
50    /// A run was approved.
51    ApprovalGranted,
52    /// A run was rejected.
53    ApprovalRejected,
54    /// A log line was emitted by a running step.
55    LogLine,
56    /// A user signed in.
57    UserSignedIn,
58    /// A new user signed up.
59    UserSignedUp,
60    /// A user signed out.
61    UserSignedOut,
62    /// A batch of secrets was re-encrypted with a different key version.
63    SecretsRotated,
64    /// A retry was forced despite a handler version mismatch.
65    RetryForced,
66}
67
68impl EventKind {
69    /// All known event kinds.
70    pub const ALL: &'static [EventKind] = &[
71        Self::RunCreated,
72        Self::RunStatusChanged,
73        Self::RunFailed,
74        Self::RunBudgetExceeded,
75        Self::StepCompleted,
76        Self::StepFailed,
77        Self::ApprovalRequested,
78        Self::ApprovalGranted,
79        Self::ApprovalRejected,
80        Self::LogLine,
81        Self::UserSignedIn,
82        Self::UserSignedUp,
83        Self::UserSignedOut,
84        Self::SecretsRotated,
85        Self::RetryForced,
86    ];
87
88    /// Returns the wire-format string for this kind.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use ironflow_store::entities::EventKind;
94    ///
95    /// assert_eq!(EventKind::RunCreated.as_str(), "run_created");
96    /// assert_eq!(EventKind::StepFailed.as_str(), "step_failed");
97    /// ```
98    pub fn as_str(self) -> &'static str {
99        self.into()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn all_variants_roundtrip_via_str() {
109        for kind in EventKind::ALL {
110            let s = kind.as_str();
111            let parsed: EventKind = s.parse().unwrap();
112            assert_eq!(*kind, parsed);
113        }
114    }
115
116    #[test]
117    fn all_has_correct_count() {
118        assert_eq!(EventKind::ALL.len(), 15);
119    }
120
121    #[test]
122    fn secrets_rotated_uses_snake_case_wire_format() {
123        assert_eq!(EventKind::SecretsRotated.as_str(), "secrets_rotated");
124        assert_eq!(
125            "secrets_rotated".parse::<EventKind>().unwrap(),
126            EventKind::SecretsRotated
127        );
128    }
129
130    #[test]
131    fn run_budget_exceeded_uses_snake_case_wire_format() {
132        assert_eq!(EventKind::RunBudgetExceeded.as_str(), "run_budget_exceeded");
133        assert_eq!(
134            "run_budget_exceeded".parse::<EventKind>().unwrap(),
135            EventKind::RunBudgetExceeded
136        );
137    }
138
139    #[test]
140    fn display_matches_as_str() {
141        for kind in EventKind::ALL {
142            assert_eq!(kind.to_string(), kind.as_str());
143        }
144    }
145
146    #[test]
147    fn parse_unknown_returns_error() {
148        assert!("bogus".parse::<EventKind>().is_err());
149    }
150
151    #[test]
152    fn serde_roundtrip() {
153        let kind = EventKind::RunStatusChanged;
154        let json = serde_json::to_string(&kind).unwrap();
155        assert_eq!(json, "\"run_status_changed\"");
156        let back: EventKind = serde_json::from_str(&json).unwrap();
157        assert_eq!(back, kind);
158    }
159
160    #[test]
161    fn serde_all_variants() {
162        for kind in EventKind::ALL {
163            let json = serde_json::to_string(kind).unwrap();
164            let back: EventKind = serde_json::from_str(&json).unwrap();
165            assert_eq!(*kind, back);
166        }
167    }
168}