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#[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 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 "AccessDeniedException" | "UnrecognizedClientException" | "ExpiredTokenException" => {
176 ErrorData::RemoteAccessDenied {
177 resource_type: "Rule".into(),
178 resource_name: resource.into(),
179 }
180 }
181 "ThrottlingException" | "LimitExceededException" => {
183 ErrorData::RateLimitExceeded { message }
184 }
185 "InternalException" | "ServiceUnavailableException" => {
187 ErrorData::RemoteServiceUnavailable { message }
188 }
189 "ResourceNotFoundException" => ErrorData::RemoteResourceNotFound {
191 resource_type: "Rule".into(),
192 resource_name: resource.into(),
193 },
194 "ResourceAlreadyExistsException" => ErrorData::RemoteResourceConflict {
196 message,
197 resource_type: "Rule".into(),
198 resource_name: resource.into(),
199 },
200 "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#[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#[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#[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}