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)]
85mod tests {
86    use super::*;
87
88    #[test]
89    fn test_suspend_reason_sleep() {
90        let wake_at = Utc::now() + chrono::Duration::hours(1);
91        let reason = SuspendReason::Sleep { wake_at };
92
93        assert!(reason.is_sleep());
94        assert!(!reason.is_event_wait());
95        assert_eq!(reason.wake_at(), Some(wake_at));
96        assert!(reason.event_name().is_none());
97    }
98
99    #[test]
100    fn test_suspend_reason_event() {
101        let timeout = Utc::now() + chrono::Duration::days(7);
102        let reason = SuspendReason::WaitingEvent {
103            event_name: "payment_confirmed".to_string(),
104            timeout: Some(timeout),
105        };
106
107        assert!(!reason.is_sleep());
108        assert!(reason.is_event_wait());
109        assert_eq!(reason.wake_at(), Some(timeout));
110        assert_eq!(reason.event_name(), Some("payment_confirmed"));
111    }
112
113    #[test]
114    fn test_workflow_event_creation() {
115        let event = WorkflowEvent::new(
116            "order_completed",
117            "workflow-123",
118            Some(serde_json::json!({"order_id": "ABC123"})),
119        );
120
121        assert_eq!(event.event_name, "order_completed");
122        assert_eq!(event.correlation_id, "workflow-123");
123        assert!(event.payload.is_some());
124    }
125
126    #[test]
127    fn test_workflow_event_payload_typed() {
128        #[derive(Debug, Deserialize, PartialEq)]
129        struct OrderData {
130            order_id: String,
131        }
132
133        let event = WorkflowEvent::new(
134            "order_completed",
135            "workflow-123",
136            Some(serde_json::json!({"order_id": "ABC123"})),
137        );
138
139        let data: Option<OrderData> = event.payload_as();
140        assert!(data.is_some());
141        assert_eq!(
142            data.unwrap(),
143            OrderData {
144                order_id: "ABC123".to_string()
145            }
146        );
147    }
148}