Skip to main content

ironflow_engine/config/
approval.rs

1//! [`ApprovalConfig`] -- configuration for human approval gates.
2
3use serde::{Deserialize, Serialize};
4
5/// Configuration for a human approval step.
6///
7/// When the workflow reaches an approval step, the run transitions to
8/// `AwaitingApproval` and waits for a human to approve or reject via
9/// the API.
10///
11/// # Examples
12///
13/// ```
14/// use ironflow_engine::config::ApprovalConfig;
15///
16/// let config = ApprovalConfig::new("Deploy to production?");
17/// assert_eq!(config.message(), "Deploy to production?");
18/// assert!(config.timeout_seconds().is_none());
19/// ```
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct ApprovalConfig {
22    message: String,
23    #[serde(default, skip_serializing_if = "Option::is_none")]
24    timeout_seconds: Option<u64>,
25}
26
27impl ApprovalConfig {
28    /// Create a new approval config with the given message.
29    ///
30    /// # Examples
31    ///
32    /// ```
33    /// use ironflow_engine::config::ApprovalConfig;
34    ///
35    /// let config = ApprovalConfig::new("Approve this deployment?");
36    /// assert_eq!(config.message(), "Approve this deployment?");
37    /// ```
38    pub fn new(message: &str) -> Self {
39        Self {
40            message: message.to_string(),
41            timeout_seconds: None,
42        }
43    }
44
45    /// Set an auto-reject timeout in seconds.
46    ///
47    /// If no approval or rejection is received within this duration,
48    /// the run is automatically rejected (marked as Failed).
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use ironflow_engine::config::ApprovalConfig;
54    ///
55    /// let config = ApprovalConfig::new("Approve?")
56    ///     .with_timeout_seconds(3600);
57    /// assert_eq!(config.timeout_seconds(), Some(3600));
58    /// ```
59    pub fn with_timeout_seconds(mut self, seconds: u64) -> Self {
60        self.timeout_seconds = Some(seconds);
61        self
62    }
63
64    /// The approval message displayed to reviewers.
65    pub fn message(&self) -> &str {
66        &self.message
67    }
68
69    /// Optional auto-reject timeout in seconds.
70    pub fn timeout_seconds(&self) -> Option<u64> {
71        self.timeout_seconds
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn new_sets_message() {
81        let config = ApprovalConfig::new("Deploy?");
82        assert_eq!(config.message(), "Deploy?");
83        assert!(config.timeout_seconds().is_none());
84    }
85
86    #[test]
87    fn with_timeout() {
88        let config = ApprovalConfig::new("Approve?").with_timeout_seconds(7200);
89        assert_eq!(config.timeout_seconds(), Some(7200));
90    }
91
92    #[test]
93    fn serde_roundtrip() {
94        let config = ApprovalConfig::new("Deploy to prod?").with_timeout_seconds(3600);
95
96        let json = serde_json::to_string(&config).expect("serialize");
97        let back: ApprovalConfig = serde_json::from_str(&json).expect("deserialize");
98
99        assert_eq!(back.message(), config.message());
100        assert_eq!(back.timeout_seconds(), config.timeout_seconds());
101    }
102
103    #[test]
104    fn serde_minimal() {
105        let config = ApprovalConfig::new("Approve?");
106        let json = serde_json::to_string(&config).expect("serialize");
107        assert!(!json.contains("timeout_seconds"));
108    }
109}