Skip to main content

forge_core/workflow/
traits.rs

1use std::future::Future;
2use std::pin::Pin;
3use std::str::FromStr;
4use std::time::Duration;
5
6use serde::{Serialize, de::DeserializeOwned};
7
8use super::context::WorkflowContext;
9use crate::Result;
10
11/// Trait for workflow handlers.
12pub trait ForgeWorkflow: Send + Sync + 'static {
13    /// Input type for the workflow.
14    type Input: DeserializeOwned + Serialize + Send + Sync;
15    /// Output type for the workflow.
16    type Output: Serialize + Send;
17
18    /// Get workflow metadata.
19    fn info() -> WorkflowInfo;
20
21    /// Execute the workflow.
22    fn execute(
23        ctx: &WorkflowContext,
24        input: Self::Input,
25    ) -> Pin<Box<dyn Future<Output = Result<Self::Output>> + Send + '_>>;
26}
27
28/// Workflow metadata.
29#[derive(Debug, Clone)]
30pub struct WorkflowInfo {
31    /// Workflow name.
32    pub name: &'static str,
33    /// Workflow version.
34    pub version: u32,
35    /// Default timeout for the entire workflow.
36    pub timeout: Duration,
37    /// Default timeout for outbound HTTP requests made by the workflow.
38    pub http_timeout: Option<Duration>,
39    /// Whether the workflow is deprecated.
40    pub deprecated: bool,
41    /// Whether the workflow is public (no auth required).
42    pub is_public: bool,
43    /// Required role for authorization (implies auth required).
44    pub required_role: Option<&'static str>,
45}
46
47impl Default for WorkflowInfo {
48    fn default() -> Self {
49        Self {
50            name: "",
51            version: 1,
52            timeout: Duration::from_secs(86400), // 24 hours
53            http_timeout: None,
54            deprecated: false,
55            is_public: false,
56            required_role: None,
57        }
58    }
59}
60
61/// Workflow execution status.
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum WorkflowStatus {
64    /// Workflow is created but not started.
65    Created,
66    /// Workflow is actively running.
67    Running,
68    /// Workflow is waiting for an external event.
69    Waiting,
70    /// Workflow completed successfully.
71    Completed,
72    /// Workflow failed and is running compensation.
73    Compensating,
74    /// Workflow compensation completed.
75    Compensated,
76    /// Workflow failed (compensation also failed or not available).
77    Failed,
78}
79
80impl WorkflowStatus {
81    /// Convert to string for database storage.
82    pub fn as_str(&self) -> &'static str {
83        match self {
84            Self::Created => "created",
85            Self::Running => "running",
86            Self::Waiting => "waiting",
87            Self::Completed => "completed",
88            Self::Compensating => "compensating",
89            Self::Compensated => "compensated",
90            Self::Failed => "failed",
91        }
92    }
93
94    /// Check if the workflow is terminal (no longer running).
95    pub fn is_terminal(&self) -> bool {
96        matches!(self, Self::Completed | Self::Compensated | Self::Failed)
97    }
98}
99
100#[derive(Debug, Clone, PartialEq, Eq)]
101pub struct ParseWorkflowStatusError(pub String);
102
103impl std::fmt::Display for ParseWorkflowStatusError {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(f, "invalid workflow status: '{}'", self.0)
106    }
107}
108
109impl std::error::Error for ParseWorkflowStatusError {}
110
111impl FromStr for WorkflowStatus {
112    type Err = ParseWorkflowStatusError;
113
114    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
115        match s {
116            "created" => Ok(Self::Created),
117            "running" => Ok(Self::Running),
118            "waiting" => Ok(Self::Waiting),
119            "completed" => Ok(Self::Completed),
120            "compensating" => Ok(Self::Compensating),
121            "compensated" => Ok(Self::Compensated),
122            "failed" => Ok(Self::Failed),
123            _ => Err(ParseWorkflowStatusError(s.to_string())),
124        }
125    }
126}
127
128#[cfg(test)]
129#[allow(clippy::unwrap_used, clippy::indexing_slicing)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_workflow_info_default() {
135        let info = WorkflowInfo::default();
136        assert_eq!(info.name, "");
137        assert_eq!(info.version, 1);
138        assert!(!info.deprecated);
139    }
140
141    #[test]
142    fn test_workflow_status_conversion() {
143        assert_eq!(WorkflowStatus::Running.as_str(), "running");
144        assert_eq!(WorkflowStatus::Completed.as_str(), "completed");
145        assert_eq!(WorkflowStatus::Compensating.as_str(), "compensating");
146
147        assert_eq!(
148            "running".parse::<WorkflowStatus>(),
149            Ok(WorkflowStatus::Running)
150        );
151        assert_eq!(
152            "completed".parse::<WorkflowStatus>(),
153            Ok(WorkflowStatus::Completed)
154        );
155    }
156
157    #[test]
158    fn test_workflow_status_is_terminal() {
159        assert!(!WorkflowStatus::Running.is_terminal());
160        assert!(!WorkflowStatus::Waiting.is_terminal());
161        assert!(WorkflowStatus::Completed.is_terminal());
162        assert!(WorkflowStatus::Failed.is_terminal());
163        assert!(WorkflowStatus::Compensated.is_terminal());
164    }
165}