1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use uuid::Uuid;
4
5pub type WorkflowId = String;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct ExecutionId(pub Uuid);
11
12impl ExecutionId {
13 pub fn new() -> Self {
14 Self(Uuid::new_v4())
15 }
16}
17
18impl Default for ExecutionId {
19 fn default() -> Self {
20 Self::new()
21 }
22}
23
24impl std::fmt::Display for ExecutionId {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "exec_{}", self.0.simple())
27 }
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum WorkflowStatus {
34 Pending,
36 Running,
38 Paused,
40 Completed,
42 Failed,
44 Cancelled,
46 LimitExceeded,
49}
50
51impl WorkflowStatus {
52 pub fn is_terminal(&self) -> bool {
54 matches!(
55 self,
56 Self::Completed | Self::Failed | Self::Cancelled | Self::LimitExceeded
57 )
58 }
59
60 pub fn is_active(&self) -> bool {
62 matches!(self, Self::Pending | Self::Running | Self::Paused)
63 }
64
65 pub fn validate_transition(&self, next: &WorkflowStatus) -> crate::error::Result<()> {
67 let valid = matches!(
68 (self, next),
69 (Self::Pending, Self::Running)
70 | (Self::Running, Self::Paused)
71 | (Self::Running, Self::Completed)
72 | (Self::Running, Self::Failed)
73 | (Self::Running, Self::Cancelled)
74 | (Self::Running, Self::LimitExceeded)
75 | (Self::Paused, Self::Running)
76 | (Self::Paused, Self::Cancelled)
77 );
78 if valid {
79 Ok(())
80 } else {
81 Err(crate::Error::InvalidTransition {
82 current: self.clone(),
83 requested: next.clone(),
84 })
85 }
86 }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum SessionType {
94 Stateless,
96 Resumable,
98 PersistentGoverned,
100 Ephemeral,
102 ApprovalGated,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct WorkflowMetadata {
109 pub id: WorkflowId,
110 pub version: String,
111 pub name: Option<String>,
112 pub description: Option<String>,
113 pub state_schema: String,
114 pub created_at: DateTime<Utc>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct WorkflowExecution {
120 pub execution_id: ExecutionId,
121 pub workflow_id: WorkflowId,
122 pub workflow_version: String,
123 pub status: WorkflowStatus,
124 pub initial_input: serde_json::Value,
125 pub current_state: serde_json::Value,
126 pub started_at: DateTime<Utc>,
127 pub updated_at: DateTime<Utc>,
128 pub completed_at: Option<DateTime<Utc>>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub session_type: Option<SessionType>,
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn valid_transitions() {
140 let s = WorkflowStatus::Pending;
141 assert!(s.validate_transition(&WorkflowStatus::Running).is_ok());
142 }
143
144 #[test]
145 fn invalid_transitions() {
146 let s = WorkflowStatus::Completed;
147 assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
148 }
149
150 #[test]
151 fn terminal_states() {
152 assert!(WorkflowStatus::Completed.is_terminal());
153 assert!(WorkflowStatus::Failed.is_terminal());
154 assert!(WorkflowStatus::Cancelled.is_terminal());
155 assert!(WorkflowStatus::LimitExceeded.is_terminal());
156 assert!(!WorkflowStatus::Running.is_terminal());
157 assert!(!WorkflowStatus::Paused.is_terminal());
158 }
159
160 #[test]
161 fn limit_exceeded_transition() {
162 let s = WorkflowStatus::Running;
163 assert!(s
164 .validate_transition(&WorkflowStatus::LimitExceeded)
165 .is_ok());
166 let s = WorkflowStatus::LimitExceeded;
167 assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
168 }
169
170 #[test]
171 fn execution_id_display() {
172 let id = ExecutionId::new();
173 assert!(id.to_string().starts_with("exec_"));
174 }
175}