Skip to main content

ironflow_engine/config/
http.rs

1//! [`HttpConfig`] — serializable configuration for an HTTP step.
2
3use ironflow_core::retry::RetryPolicy;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6
7/// Serializable configuration for an HTTP step.
8///
9/// # Examples
10///
11/// ```
12/// use ironflow_engine::config::HttpConfig;
13///
14/// let config = HttpConfig::get("https://api.example.com/health");
15/// ```
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct HttpConfig {
18    /// HTTP method (GET, POST, PUT, PATCH, DELETE).
19    pub method: String,
20    /// Request URL.
21    pub url: String,
22    /// Request headers.
23    pub headers: Vec<(String, String)>,
24    /// Request body as JSON.
25    pub body: Option<Value>,
26    /// Timeout in seconds (default: 30).
27    pub timeout_secs: Option<u64>,
28    /// When `true`, a failure of this step does not fail the run.
29    #[serde(default)]
30    pub allow_failure: bool,
31    /// Optional step-level retry policy.
32    #[serde(default, skip_serializing_if = "Option::is_none")]
33    pub retry: Option<RetryPolicy>,
34}
35
36impl HttpConfig {
37    /// Create a GET request config.
38    ///
39    /// # Examples
40    ///
41    /// ```
42    /// use ironflow_engine::config::HttpConfig;
43    ///
44    /// let config = HttpConfig::get("https://example.com");
45    /// assert_eq!(config.method, "GET");
46    /// ```
47    pub fn get(url: &str) -> Self {
48        Self::new("GET", url)
49    }
50
51    /// Create a POST request config.
52    pub fn post(url: &str) -> Self {
53        Self::new("POST", url)
54    }
55
56    /// Create a PUT request config.
57    pub fn put(url: &str) -> Self {
58        Self::new("PUT", url)
59    }
60
61    /// Create a PATCH request config.
62    pub fn patch(url: &str) -> Self {
63        Self::new("PATCH", url)
64    }
65
66    /// Create a DELETE request config.
67    pub fn delete(url: &str) -> Self {
68        Self::new("DELETE", url)
69    }
70
71    fn new(method: &str, url: &str) -> Self {
72        Self {
73            method: method.to_string(),
74            url: url.to_string(),
75            headers: Vec::new(),
76            body: None,
77            timeout_secs: None,
78            allow_failure: false,
79            retry: None,
80        }
81    }
82
83    /// Add a request header.
84    pub fn header(mut self, name: &str, value: &str) -> Self {
85        self.headers.push((name.to_string(), value.to_string()));
86        self
87    }
88
89    /// Set the request body as JSON.
90    pub fn json(mut self, body: Value) -> Self {
91        self.body = Some(body);
92        self
93    }
94
95    /// Set the timeout in seconds.
96    pub fn timeout_secs(mut self, secs: u64) -> Self {
97        self.timeout_secs = Some(secs);
98        self
99    }
100
101    /// Mark this step as allowed to fail without stopping the run.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use ironflow_engine::config::HttpConfig;
107    ///
108    /// let config = HttpConfig::get("https://example.com").allow_failure();
109    /// assert!(config.allow_failure);
110    /// ```
111    pub fn allow_failure(mut self) -> Self {
112        self.allow_failure = true;
113        self
114    }
115
116    /// Set a step-level retry policy.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// use ironflow_core::retry::RetryPolicy;
122    /// use ironflow_engine::config::HttpConfig;
123    ///
124    /// let config = HttpConfig::get("https://api.example.com")
125    ///     .retry_policy(RetryPolicy::new(5));
126    /// assert!(config.retry.is_some());
127    /// ```
128    pub fn retry_policy(mut self, policy: RetryPolicy) -> Self {
129        self.retry = Some(policy);
130        self
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137    use serde_json::json;
138
139    #[test]
140    fn methods() {
141        assert_eq!(HttpConfig::get("http://x").method, "GET");
142        assert_eq!(HttpConfig::post("http://x").method, "POST");
143        assert_eq!(HttpConfig::put("http://x").method, "PUT");
144        assert_eq!(HttpConfig::patch("http://x").method, "PATCH");
145        assert_eq!(HttpConfig::delete("http://x").method, "DELETE");
146    }
147
148    #[test]
149    fn builder() {
150        let config = HttpConfig::post("http://api.example.com")
151            .header("Authorization", "Bearer token")
152            .json(json!({"key": "value"}))
153            .timeout_secs(10);
154
155        assert_eq!(config.headers.len(), 1);
156        assert!(config.body.is_some());
157        assert_eq!(config.timeout_secs, Some(10));
158    }
159
160    #[test]
161    fn a_config_predating_retry_still_deserializes() {
162        let config: HttpConfig = serde_json::from_str(
163            r#"{"url":"http://x","method":"GET","headers":[],"body":null,"timeout_secs":null}"#,
164        )
165        .expect("deserialize");
166        assert!(config.retry.is_none());
167    }
168
169    #[test]
170    fn retry_policy_roundtrip() {
171        use ironflow_core::retry::RetryPolicy;
172
173        let config = HttpConfig::get("http://api").retry_policy(RetryPolicy::new(3));
174        let json = serde_json::to_string(&config).expect("serialize");
175        let back: HttpConfig = serde_json::from_str(&json).expect("deserialize");
176        assert_eq!(back.retry.as_ref().unwrap().max_retries(), 3);
177    }
178}