Skip to main content

alien_aws_clients/aws/
eventbridge.rs

1use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsSignConfig};
2use crate::aws::credential_provider::AwsCredentialProvider;
3use alien_client_core::{ErrorData, Result};
4
5use alien_error::{Context, ContextError, IntoAlienError};
6use bon::Builder;
7use reqwest::{Client, StatusCode};
8use serde::de::DeserializeOwned;
9use serde::{Deserialize, Serialize};
10
11#[cfg(feature = "test-utils")]
12use mockall::automock;
13
14#[cfg_attr(feature = "test-utils", automock)]
15#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
16#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
17pub trait EventBridgeApi: Send + Sync + std::fmt::Debug {
18    async fn put_rule(&self, request: PutRuleRequest) -> Result<PutRuleResponse>;
19    async fn put_targets(&self, request: PutTargetsRequest) -> Result<()>;
20    async fn remove_targets(&self, rule_name: &str, target_ids: Vec<String>) -> Result<()>;
21    async fn delete_rule(&self, rule_name: &str) -> Result<()>;
22}
23
24// ---------------------------------------------------------------------------
25// EventBridge client using AWS JSON 1.1 protocol.
26// ---------------------------------------------------------------------------
27#[derive(Debug, Clone)]
28pub struct EventBridgeClient {
29    client: Client,
30    credentials: AwsCredentialProvider,
31}
32
33impl EventBridgeClient {
34    pub fn new(client: Client, credentials: AwsCredentialProvider) -> Self {
35        Self {
36            client,
37            credentials,
38        }
39    }
40
41    fn sign_config(&self) -> AwsSignConfig {
42        AwsSignConfig {
43            service_name: "events".into(),
44            region: self.credentials.region().to_string(),
45            credentials: self.credentials.get_credentials(),
46            signing_region: None,
47        }
48    }
49
50    fn get_base_url(&self) -> String {
51        if let Some(override_url) = self.credentials.get_service_endpoint_option("events") {
52            override_url.to_string()
53        } else {
54            format!("https://events.{}.amazonaws.com", self.credentials.region())
55        }
56    }
57
58    // ------------------------- internal helpers -------------------------
59
60    async fn send_json<T: DeserializeOwned + Send + 'static>(
61        &self,
62        target: &str,
63        body: String,
64        operation: &str,
65        resource: &str,
66    ) -> Result<T> {
67        self.credentials.ensure_fresh().await?;
68        let base_url = self.get_base_url();
69        let url = format!("{}/", base_url.trim_end_matches('/'));
70
71        let builder = self
72            .client
73            .post(&url)
74            .host(&format!(
75                "events.{}.amazonaws.com",
76                self.credentials.region()
77            ))
78            .header("X-Amz-Target", target)
79            .content_type_amz_json()
80            .content_sha256(&body)
81            .body(body.clone());
82
83        let result =
84            crate::aws::aws_request_utils::sign_send_json(builder, &self.sign_config()).await;
85
86        Self::map_result(result, operation, resource, Some(&body))
87    }
88
89    async fn send_json_no_response(
90        &self,
91        target: &str,
92        body: String,
93        operation: &str,
94        resource: &str,
95    ) -> Result<()> {
96        self.credentials.ensure_fresh().await?;
97        let base_url = self.get_base_url();
98        let url = format!("{}/", base_url.trim_end_matches('/'));
99
100        let builder = self
101            .client
102            .post(&url)
103            .host(&format!(
104                "events.{}.amazonaws.com",
105                self.credentials.region()
106            ))
107            .header("X-Amz-Target", target)
108            .content_type_amz_json()
109            .content_sha256(&body)
110            .body(body.clone());
111
112        let result =
113            crate::aws::aws_request_utils::sign_send_no_response(builder, &self.sign_config())
114                .await;
115
116        Self::map_result(result, operation, resource, Some(&body))
117    }
118
119    fn map_result<T>(
120        result: Result<T>,
121        operation: &str,
122        resource: &str,
123        request_body: Option<&str>,
124    ) -> Result<T> {
125        match result {
126            Ok(v) => Ok(v),
127            Err(e) => {
128                if let Some(ErrorData::HttpResponseError {
129                    http_status,
130                    http_response_text: Some(ref text),
131                    ..
132                }) = &e.error
133                {
134                    let status = StatusCode::from_u16(*http_status)
135                        .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
136                    if let Some(mapped) =
137                        Self::map_eventbridge_error(status, text, operation, resource, request_body)
138                    {
139                        Err(e.context(mapped))
140                    } else {
141                        Err(e)
142                    }
143                } else {
144                    Err(e)
145                }
146            }
147        }
148    }
149
150    fn map_eventbridge_error(
151        status: StatusCode,
152        body: &str,
153        _operation: &str,
154        resource: &str,
155        request_body: Option<&str>,
156    ) -> Option<ErrorData> {
157        let parsed: std::result::Result<EventBridgeErrorResponse, _> = serde_json::from_str(body);
158        let (code, message) = match parsed {
159            Ok(e) => {
160                let c = e
161                    .type_field_underscore
162                    .or(e.type_field)
163                    .unwrap_or_else(|| "UnknownErrorCode".into());
164                let m = e
165                    .message
166                    .or(e.message_capital)
167                    .unwrap_or_else(|| "Unknown error".into());
168                (c, m)
169            }
170            Err(_) => return None,
171        };
172
173        Some(match code.as_str() {
174            // Access / auth
175            "AccessDeniedException" | "UnrecognizedClientException" | "ExpiredTokenException" => {
176                ErrorData::RemoteAccessDenied {
177                    resource_type: "Rule".into(),
178                    resource_name: resource.into(),
179                }
180            }
181            // Throttling
182            "ThrottlingException" | "LimitExceededException" => {
183                ErrorData::RateLimitExceeded { message }
184            }
185            // Service unavailable
186            "InternalException" | "ServiceUnavailableException" => {
187                ErrorData::RemoteServiceUnavailable { message }
188            }
189            // Resource not found
190            "ResourceNotFoundException" => ErrorData::RemoteResourceNotFound {
191                resource_type: "Rule".into(),
192                resource_name: resource.into(),
193            },
194            // Resource already exists
195            "ResourceAlreadyExistsException" => ErrorData::RemoteResourceConflict {
196                message,
197                resource_type: "Rule".into(),
198                resource_name: resource.into(),
199            },
200            // Invalid input
201            "ValidationException" | "InvalidEventPatternException" => ErrorData::InvalidInput {
202                message,
203                field_name: None,
204            },
205            _ => match status {
206                StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound {
207                    resource_type: "Rule".into(),
208                    resource_name: resource.into(),
209                },
210                StatusCode::CONFLICT => ErrorData::RemoteResourceConflict {
211                    message,
212                    resource_type: "Rule".into(),
213                    resource_name: resource.into(),
214                },
215                StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied {
216                    resource_type: "Rule".into(),
217                    resource_name: resource.into(),
218                },
219                StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded { message },
220                StatusCode::SERVICE_UNAVAILABLE
221                | StatusCode::BAD_GATEWAY
222                | StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable { message },
223                _ => ErrorData::HttpResponseError {
224                    message: format!("EventBridge operation failed: {}", message),
225                    url: "events.amazonaws.com".into(),
226                    http_status: status.as_u16(),
227                    http_response_text: Some(body.into()),
228                    http_request_text: request_body.map(|s| s.to_string()),
229                },
230            },
231        })
232    }
233}
234
235#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
236#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
237impl EventBridgeApi for EventBridgeClient {
238    async fn put_rule(&self, request: PutRuleRequest) -> Result<PutRuleResponse> {
239        let body = serde_json::to_string(&request).into_alien_error().context(
240            ErrorData::InvalidInput {
241                message: "Failed to serialize PutRule request".into(),
242                field_name: None,
243            },
244        )?;
245
246        self.send_json("AWSEvents.PutRule", body, "PutRule", &request.name)
247            .await
248    }
249
250    async fn put_targets(&self, request: PutTargetsRequest) -> Result<()> {
251        let body = serde_json::to_string(&request).into_alien_error().context(
252            ErrorData::InvalidInput {
253                message: "Failed to serialize PutTargets request".into(),
254                field_name: None,
255            },
256        )?;
257
258        self.send_json_no_response("AWSEvents.PutTargets", body, "PutTargets", &request.rule)
259            .await
260    }
261
262    async fn remove_targets(&self, rule_name: &str, target_ids: Vec<String>) -> Result<()> {
263        let body = serde_json::to_string(&RemoveTargetsRequest {
264            rule: rule_name.to_string(),
265            ids: target_ids,
266        })
267        .into_alien_error()
268        .context(ErrorData::InvalidInput {
269            message: "Failed to serialize RemoveTargets request".into(),
270            field_name: None,
271        })?;
272
273        self.send_json_no_response("AWSEvents.RemoveTargets", body, "RemoveTargets", rule_name)
274            .await
275    }
276
277    async fn delete_rule(&self, rule_name: &str) -> Result<()> {
278        let body = serde_json::to_string(&DeleteRuleRequest {
279            name: rule_name.to_string(),
280        })
281        .into_alien_error()
282        .context(ErrorData::InvalidInput {
283            message: "Failed to serialize DeleteRule request".into(),
284            field_name: None,
285        })?;
286
287        self.send_json_no_response("AWSEvents.DeleteRule", body, "DeleteRule", rule_name)
288            .await
289    }
290}
291
292// ---------------------------------------------------------------------------
293// Request / response payloads
294// ---------------------------------------------------------------------------
295
296#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
297#[serde(rename_all = "PascalCase")]
298pub struct PutRuleRequest {
299    pub name: String,
300    pub schedule_expression: String,
301    pub state: Option<String>,
302    pub description: Option<String>,
303    #[serde(skip_serializing_if = "Option::is_none")]
304    pub tags: Option<Vec<EventBridgeTag>>,
305}
306
307#[derive(Debug, Clone, Serialize, Deserialize)]
308#[serde(rename_all = "PascalCase")]
309pub struct PutRuleResponse {
310    pub rule_arn: Option<String>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
314#[serde(rename_all = "PascalCase")]
315pub struct PutTargetsRequest {
316    pub rule: String,
317    pub targets: Vec<EventBridgeTarget>,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize)]
321#[serde(rename_all = "PascalCase")]
322pub struct EventBridgeTarget {
323    pub id: String,
324    pub arn: String,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
328#[serde(rename_all = "PascalCase")]
329pub struct EventBridgeTag {
330    pub key: String,
331    pub value: String,
332}
333
334// Internal request types (not exposed publicly)
335
336#[derive(Debug, Clone, Serialize)]
337#[serde(rename_all = "PascalCase")]
338struct RemoveTargetsRequest {
339    pub rule: String,
340    pub ids: Vec<String>,
341}
342
343#[derive(Debug, Clone, Serialize)]
344#[serde(rename_all = "PascalCase")]
345struct DeleteRuleRequest {
346    pub name: String,
347}
348
349// ---------------------------------------------------------------------------
350// Error JSON mapping structs
351// ---------------------------------------------------------------------------
352
353#[derive(Debug, Deserialize)]
354struct EventBridgeErrorResponse {
355    #[serde(rename = "Type")]
356    type_field: Option<String>,
357    #[serde(rename = "__type")]
358    type_field_underscore: Option<String>,
359    #[serde(rename = "message")]
360    message: Option<String>,
361    #[serde(rename = "Message")]
362    message_capital: Option<String>,
363}