forge_core/workflow/
suspend.rs1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7#[non_exhaustive]
8pub enum SuspendReason {
9 Sleep { wake_at: DateTime<Utc> },
11 WaitingEvent {
13 event_name: String,
14 timeout: Option<DateTime<Utc>>,
15 },
16}
17
18impl SuspendReason {
19 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 pub fn is_sleep(&self) -> bool {
29 matches!(self, Self::Sleep { .. })
30 }
31
32 pub fn is_event_wait(&self) -> bool {
34 matches!(self, Self::WaitingEvent { .. })
35 }
36
37 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#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct WorkflowEvent {
49 pub id: Uuid,
51 pub event_name: String,
53 pub correlation_id: String,
55 pub payload: Option<serde_json::Value>,
57 pub created_at: DateTime<Utc>,
59}
60
61impl WorkflowEvent {
62 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 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}