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