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 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub parent_execution_id: Option<ExecutionId>,
136 #[serde(default)]
138 pub segment_number: u32,
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 #[test]
146 fn valid_transitions() {
147 let s = WorkflowStatus::Pending;
148 assert!(s.validate_transition(&WorkflowStatus::Running).is_ok());
149 }
150
151 #[test]
152 fn invalid_transitions() {
153 let s = WorkflowStatus::Completed;
154 assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
155 }
156
157 #[test]
158 fn terminal_states() {
159 assert!(WorkflowStatus::Completed.is_terminal());
160 assert!(WorkflowStatus::Failed.is_terminal());
161 assert!(WorkflowStatus::Cancelled.is_terminal());
162 assert!(WorkflowStatus::LimitExceeded.is_terminal());
163 assert!(!WorkflowStatus::Running.is_terminal());
164 assert!(!WorkflowStatus::Paused.is_terminal());
165 }
166
167 #[test]
168 fn limit_exceeded_transition() {
169 let s = WorkflowStatus::Running;
170 assert!(s
171 .validate_transition(&WorkflowStatus::LimitExceeded)
172 .is_ok());
173 let s = WorkflowStatus::LimitExceeded;
174 assert!(s.validate_transition(&WorkflowStatus::Running).is_err());
175 }
176
177 #[test]
178 fn execution_id_display() {
179 let id = ExecutionId::new();
180 assert!(id.to_string().starts_with("exec_"));
181 }
182}