Skip to main content

forge_core/workflow/
suspend.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5/// Reason for workflow suspension.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7#[non_exhaustive]
8pub enum SuspendReason {
9    /// Workflow is sleeping until a specific time.
10    Sleep { wake_at: DateTime<Utc> },
11    /// Workflow is waiting for an external event.
12    WaitingEvent {
13        event_name: String,
14        timeout: Option<DateTime<Utc>>,
15    },
16}
17
18impl SuspendReason {
19    /// Get the wake time for this suspension.
20    pub fn wake_at(&self) -> Option<DateTime<Utc>> {
21        match self {
22            Self::Sleep { wake_at } => Some(*wake_at),
23            Self::WaitingEvent { timeout, .. } => *timeout,
24        }
25    }
26
27    /// Check if this is a sleep suspension.
28    pub fn is_sleep(&self) -> bool {
29        matches!(self, Self::Sleep { .. })
30    }
31
32    /// Check if this is an event wait.
33    pub fn is_event_wait(&self) -> bool {
34        matches!(self, Self::WaitingEvent { .. })
35    }
36
37    /// Get the event name if waiting for an event.
38    pub fn event_name(&self) -> Option<&str> {
39        match self {
40            Self::WaitingEvent { event_name, .. } => Some(event_name),
41            _ => None,
42        }
43    }
44}
45
46/// A workflow event that can wake suspended workflows.
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct WorkflowEvent {
49    /// Event ID.
50    pub id: Uuid,
51    /// Event name/type.
52    pub event_name: String,
53    /// Correlation ID (typically workflow run ID).
54    pub correlation_id: String,
55    /// Event payload.
56    pub payload: Option<serde_json::Value>,
57    /// When the event was created.
58    pub created_at: DateTime<Utc>,
59}
60
61impl WorkflowEvent {
62    /// Create a new workflow event.
63    pub fn new(
64        event_name: impl Into<String>,
65        correlation_id: impl Into<String>,
66        payload: Option<serde_json::Value>,
67    ) -> Self {
68        Self {
69            id: Uuid::new_v4(),
70            event_name: event_name.into(),
71            correlation_id: correlation_id.into(),
72            payload,
73            created_at: Utc::now(),
74        }
75    }
76
77    /// Get the payload as a typed value.
78    pub fn payload_as<T: serde::de::DeserializeOwned>(&self) -> Option<T> {
79        self.payload
80            .as_ref()
81            .and_then(|p| serde_json::from_value(p.clone()).ok())
82    }
83}
84
85#[cfg(test)]
86#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn test_suspend_reason_sleep() {
92        let wake_at = Utc::now() + chrono::Duration::hours(1);
93        let reason = SuspendReason::Sleep { wake_at };
94
95        assert!(reason.is_sleep());
96        assert!(!reason.is_event_wait());
97        assert_eq!(reason.wake_at(), Some(wake_at));
98        assert!(reason.event_name().is_none());
99    }
100
101    #[test]
102    fn test_suspend_reason_event() {
103        let timeout = Utc::now() + chrono::Duration::days(7);
104        let reason = SuspendReason::WaitingEvent {
105            event_name: "payment_confirmed".to_string(),
106            timeout: Some(timeout),
107        };
108
109        assert!(!reason.is_sleep());
110        assert!(reason.is_event_wait());
111        assert_eq!(reason.wake_at(), Some(timeout));
112        assert_eq!(reason.event_name(), Some("payment_confirmed"));
113    }
114
115    #[test]
116    fn test_workflow_event_creation() {
117        let event = WorkflowEvent::new(
118            "order_completed",
119            "workflow-123",
120            Some(serde_json::json!({"order_id": "ABC123"})),
121        );
122
123        assert_eq!(event.event_name, "order_completed");
124        assert_eq!(event.correlation_id, "workflow-123");
125        assert!(event.payload.is_some());
126    }
127
128    #[test]
129    fn test_workflow_event_payload_typed() {
130        #[derive(Debug, Deserialize, PartialEq)]
131        struct OrderData {
132            order_id: String,
133        }
134
135        let event = WorkflowEvent::new(
136            "order_completed",
137            "workflow-123",
138            Some(serde_json::json!({"order_id": "ABC123"})),
139        );
140
141        let data: Option<OrderData> = event.payload_as();
142        assert!(data.is_some());
143        assert_eq!(
144            data.unwrap(),
145            OrderData {
146                order_id: "ABC123".to_string()
147            }
148        );
149    }
150}