forge_core/workflow/
suspend.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub enum SuspendReason {
8 Sleep { wake_at: DateTime<Utc> },
10 WaitingEvent {
12 event_name: String,
13 timeout: Option<DateTime<Utc>>,
14 },
15}
16
17impl SuspendReason {
18 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 pub fn is_sleep(&self) -> bool {
28 matches!(self, Self::Sleep { .. })
29 }
30
31 pub fn is_event_wait(&self) -> bool {
33 matches!(self, Self::WaitingEvent { .. })
34 }
35
36 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#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct WorkflowEvent {
48 pub id: Uuid,
50 pub event_name: String,
52 pub correlation_id: String,
54 pub payload: Option<serde_json::Value>,
56 pub created_at: DateTime<Utc>,
58}
59
60impl WorkflowEvent {
61 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 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}