Skip to main content

ironflow_engine/config/
mod.rs

1//! Serializable step configurations — one per operation type.
2//!
3//! These types mirror the builder options in [`ironflow_core`] operations but
4//! are fully serializable, allowing them to be stored as JSON in the database
5//! and reconstructed by the executor at runtime.
6
7mod agent;
8mod approval;
9mod artifact;
10mod decision;
11pub mod delay;
12mod http;
13mod shell;
14mod workflow;
15
16pub use agent::AgentStepConfig;
17pub use approval::ApprovalConfig;
18pub use artifact::{ArtifactInput, ArtifactOutput};
19pub use decision::{DEFAULT_DECISION_MODEL, DecisionConfig};
20pub use delay::DelayConfig;
21pub use http::HttpConfig;
22pub use shell::ShellConfig;
23pub use workflow::WorkflowStepConfig;
24
25use ironflow_core::retry::RetryPolicy;
26use ironflow_store::entities::StepKind;
27use serde::{Deserialize, Serialize};
28
29/// A serializable step configuration, wrapping one of the operation-specific configs.
30///
31/// Stored as JSON in the `steps.input` column and reconstructed by the
32/// executor at runtime.
33///
34/// # Examples
35///
36/// ```
37/// use ironflow_engine::config::{StepConfig, ShellConfig};
38///
39/// let config = StepConfig::Shell(ShellConfig::new("echo hello"));
40/// let json = serde_json::to_string(&config).unwrap();
41/// assert!(json.contains("echo hello"));
42/// ```
43#[derive(Debug, Clone, Serialize, Deserialize)]
44#[serde(tag = "type", rename_all = "snake_case")]
45pub enum StepConfig {
46    /// A shell command step.
47    Shell(ShellConfig),
48    /// An HTTP request step.
49    Http(HttpConfig),
50    /// An AI agent step.
51    Agent(AgentStepConfig),
52    /// A sub-workflow invocation step.
53    Workflow(WorkflowStepConfig),
54    /// A human approval gate step.
55    Approval(ApprovalConfig),
56    /// A typed machine-decision step (System One / Jev).
57    Decision(DecisionConfig),
58    /// A timed delay/sleep step.
59    Delay(DelayConfig),
60}
61
62impl StepConfig {
63    /// Whether this step is allowed to fail without stopping the run.
64    ///
65    /// # Examples
66    ///
67    /// ```
68    /// use ironflow_engine::config::{StepConfig, ShellConfig};
69    ///
70    /// let config = StepConfig::Shell(ShellConfig::new("cargo clippy").allow_failure());
71    /// assert!(config.allow_failure());
72    ///
73    /// let config = StepConfig::Shell(ShellConfig::new("cargo build"));
74    /// assert!(!config.allow_failure());
75    /// ```
76    pub fn allow_failure(&self) -> bool {
77        match self {
78            StepConfig::Shell(c) => c.allow_failure,
79            StepConfig::Http(c) => c.allow_failure,
80            StepConfig::Agent(c) => c.allow_failure,
81            StepConfig::Workflow(_)
82            | StepConfig::Approval(_)
83            | StepConfig::Decision(_)
84            | StepConfig::Delay(_) => false,
85        }
86    }
87
88    /// Get the step-level retry policy, if any.
89    ///
90    /// # Examples
91    ///
92    /// ```
93    /// use ironflow_core::retry::RetryPolicy;
94    /// use ironflow_engine::config::{StepConfig, ShellConfig};
95    ///
96    /// let config = StepConfig::Shell(ShellConfig::new("echo test").retry_policy(RetryPolicy::new(3)));
97    /// assert!(config.retry().is_some());
98    ///
99    /// let config = StepConfig::Shell(ShellConfig::new("echo test"));
100    /// assert!(config.retry().is_none());
101    /// ```
102    pub fn retry(&self) -> Option<&RetryPolicy> {
103        match self {
104            StepConfig::Shell(c) => c.retry.as_ref(),
105            StepConfig::Http(c) => c.retry.as_ref(),
106            StepConfig::Agent(c) => c.retry.as_ref(),
107            StepConfig::Workflow(c) => c.retry.as_ref(),
108            StepConfig::Approval(_) | StepConfig::Decision(_) | StepConfig::Delay(_) => None,
109        }
110    }
111
112    /// Get the kind of step this configuration represents.
113    ///
114    /// # Examples
115    ///
116    /// ```
117    /// use ironflow_engine::config::{StepConfig, ShellConfig};
118    /// use ironflow_store::entities::StepKind;
119    ///
120    /// let config = StepConfig::Shell(ShellConfig::new("echo test"));
121    /// assert_eq!(config.kind(), StepKind::Shell);
122    /// ```
123    pub fn kind(&self) -> StepKind {
124        match self {
125            StepConfig::Shell(_) => StepKind::Shell,
126            StepConfig::Http(_) => StepKind::Http,
127            StepConfig::Agent(_) => StepKind::Agent,
128            StepConfig::Workflow(_) => StepKind::Workflow,
129            StepConfig::Approval(_) => StepKind::Approval,
130            StepConfig::Decision(_) => StepKind::Decision,
131            StepConfig::Delay(_) => StepKind::Custom("delay".to_string()),
132        }
133    }
134}
135
136impl From<ShellConfig> for StepConfig {
137    fn from(c: ShellConfig) -> Self {
138        StepConfig::Shell(c)
139    }
140}
141
142impl From<HttpConfig> for StepConfig {
143    fn from(c: HttpConfig) -> Self {
144        StepConfig::Http(c)
145    }
146}
147
148impl From<AgentStepConfig> for StepConfig {
149    fn from(c: AgentStepConfig) -> Self {
150        StepConfig::Agent(c)
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use ironflow_core::retry::RetryPolicy;
157
158    use super::*;
159
160    #[test]
161    fn retry_accessor_returns_policy_for_each_variant() {
162        let shell = StepConfig::Shell(ShellConfig::new("echo").retry_policy(RetryPolicy::new(2)));
163        assert_eq!(shell.retry().unwrap().max_retries(), 2);
164
165        let http = StepConfig::Http(HttpConfig::get("http://x").retry_policy(RetryPolicy::new(3)));
166        assert_eq!(http.retry().unwrap().max_retries(), 3);
167
168        let workflow = StepConfig::Workflow(
169            WorkflowStepConfig::new("build", serde_json::json!({}))
170                .retry_policy(RetryPolicy::new(4)),
171        );
172        assert_eq!(workflow.retry().unwrap().max_retries(), 4);
173
174        let agent = StepConfig::Agent(AgentStepConfig::new("test"));
175        assert!(agent.retry().is_none());
176
177        let approval = StepConfig::Approval(ApprovalConfig::new("ok?"));
178        assert!(approval.retry().is_none());
179    }
180
181    #[test]
182    fn serde_roundtrip() {
183        let configs = vec![
184            StepConfig::Shell(ShellConfig::new("echo test")),
185            StepConfig::Http(HttpConfig::get("http://example.com")),
186            StepConfig::Agent(AgentStepConfig::new("summarize")),
187            StepConfig::Workflow(WorkflowStepConfig::new("build", serde_json::json!({}))),
188            StepConfig::Approval(ApprovalConfig::new("Deploy to production?")),
189            StepConfig::Delay(DelayConfig::from_secs(60)),
190        ];
191
192        for config in configs {
193            let json = serde_json::to_string(&config).expect("serialize");
194            let back: StepConfig = serde_json::from_str(&json).expect("deserialize");
195            let json2 = serde_json::to_string(&back).expect("serialize2");
196            assert_eq!(json, json2);
197        }
198    }
199}