ironflow_engine/notify/webhook.rs
1//! [`WebhookSubscriber`] -- POSTs events as JSON to a URL.
2
3use reqwest::Client;
4
5use super::retry::{RetryConfig, deliver_with_retry, is_success_2xx};
6use super::{Event, EventSubscriber, SubscriberFuture};
7
8/// Subscriber that POSTs the event as JSON to a webhook URL.
9///
10/// Retries failed deliveries with exponential backoff (up to 3 attempts,
11/// 5 s timeout per attempt). The HTTP client is created once and reused.
12///
13/// Event type filtering is handled by the
14/// [`EventPublisher`](super::EventPublisher) at subscription time -- this
15/// subscriber receives only events that already passed the filter.
16///
17/// # Examples
18///
19/// ```no_run
20/// use ironflow_engine::notify::{Event, EventPublisher, WebhookSubscriber};
21///
22/// let mut publisher = EventPublisher::new();
23/// publisher.subscribe(
24/// WebhookSubscriber::new("https://hooks.example.com/events"),
25/// &[Event::RUN_STATUS_CHANGED, Event::STEP_FAILED],
26/// );
27/// ```
28pub struct WebhookSubscriber {
29 url: String,
30 client: Client,
31 retry_config: RetryConfig,
32}
33
34impl WebhookSubscriber {
35 /// Create a new webhook subscriber targeting the given URL.
36 ///
37 /// Uses the default [`RetryConfig`] (3 retries, 5 s timeout, 500 ms
38 /// base backoff).
39 ///
40 /// # Panics
41 ///
42 /// Panics if the HTTP client cannot be built (TLS backend unavailable).
43 ///
44 /// # Examples
45 ///
46 /// ```
47 /// use ironflow_engine::notify::WebhookSubscriber;
48 ///
49 /// let subscriber = WebhookSubscriber::new("https://example.com/hook");
50 /// assert_eq!(subscriber.url(), "https://example.com/hook");
51 /// ```
52 pub fn new(url: &str) -> Self {
53 Self::with_retry_config(url, RetryConfig::default())
54 }
55
56 /// Create a webhook subscriber with a custom retry configuration.
57 ///
58 /// # Panics
59 ///
60 /// Panics if the HTTP client cannot be built (TLS backend unavailable).
61 ///
62 /// # Examples
63 ///
64 /// ```
65 /// use ironflow_engine::notify::{RetryConfig, WebhookSubscriber};
66 ///
67 /// let config = RetryConfig::new(
68 /// 5,
69 /// std::time::Duration::from_secs(10),
70 /// std::time::Duration::from_secs(1),
71 /// );
72 /// let subscriber = WebhookSubscriber::with_retry_config("https://example.com/hook", config);
73 /// ```
74 pub fn with_retry_config(url: &str, retry_config: RetryConfig) -> Self {
75 let client = retry_config.build_client();
76 Self {
77 url: url.to_string(),
78 client,
79 retry_config,
80 }
81 }
82
83 /// Returns the target URL.
84 pub fn url(&self) -> &str {
85 &self.url
86 }
87}
88
89impl EventSubscriber for WebhookSubscriber {
90 fn name(&self) -> &str {
91 "webhook"
92 }
93
94 fn handle<'a>(&'a self, event: &'a Event) -> SubscriberFuture<'a> {
95 Box::pin(async move {
96 deliver_with_retry(
97 &self.retry_config,
98 || self.client.post(&self.url).json(event),
99 is_success_2xx,
100 "webhook",
101 &self.url,
102 )
103 .await;
104 })
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn url_accessor() {
114 let sub = WebhookSubscriber::new("https://example.com/hook");
115 assert_eq!(sub.url(), "https://example.com/hook");
116 }
117
118 #[test]
119 fn name_is_webhook() {
120 let sub = WebhookSubscriber::new("https://example.com");
121 assert_eq!(sub.name(), "webhook");
122 }
123}