Skip to main content

alien_aws_clients/aws/
lambda.rs

1use crate::aws::aws_request_utils::{AwsRequestBuilderExt, AwsRequestSigner, AwsSignConfig};
2use crate::aws::credential_provider::AwsCredentialProvider;
3use alien_client_core::{ErrorData, Result};
4
5use alien_error::{Context, ContextError, IntoAlienError};
6use bon::Builder;
7use form_urlencoded;
8use reqwest::{Client, Method, StatusCode};
9use serde::de::DeserializeOwned;
10use serde::{Deserialize, Serialize};
11
12#[cfg(feature = "test-utils")]
13use mockall::automock;
14
15#[cfg_attr(feature = "test-utils", automock)]
16#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
17#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
18pub trait LambdaApi: Send + Sync + std::fmt::Debug {
19    async fn create_function(
20        &self,
21        request: CreateFunctionRequest,
22    ) -> Result<FunctionConfiguration>;
23    async fn create_function_url_config(
24        &self,
25        function_name: &str,
26        request: CreateFunctionUrlConfigRequest,
27    ) -> Result<CreateFunctionUrlConfigResponse>;
28    async fn add_permission(
29        &self,
30        function_name: &str,
31        request: AddPermissionRequest,
32    ) -> Result<AddPermissionResponse>;
33    async fn update_function_code(
34        &self,
35        function_name: &str,
36        request: UpdateFunctionCodeRequest,
37    ) -> Result<FunctionConfiguration>;
38    async fn update_function_configuration(
39        &self,
40        function_name: &str,
41        request: UpdateFunctionConfigurationRequest,
42    ) -> Result<FunctionConfiguration>;
43    async fn get_function_configuration(
44        &self,
45        function_name: &str,
46        qualifier: Option<String>,
47    ) -> Result<FunctionConfiguration>;
48    async fn delete_function_url_config(
49        &self,
50        function_name: &str,
51        qualifier: Option<String>,
52    ) -> Result<()>;
53    async fn get_function_url_config(
54        &self,
55        function_name: &str,
56        qualifier: Option<String>,
57    ) -> Result<FunctionUrlConfig>;
58    async fn delete_function(&self, function_name: &str, qualifier: Option<String>) -> Result<()>;
59    async fn get_policy(
60        &self,
61        function_name: &str,
62        qualifier: Option<String>,
63    ) -> Result<GetPolicyResponse>;
64
65    // Function invocation operations
66    async fn invoke(&self, request: InvokeRequest) -> Result<InvokeResponse>;
67
68    // Event Source Mapping operations
69    async fn create_event_source_mapping(
70        &self,
71        request: CreateEventSourceMappingRequest,
72    ) -> Result<EventSourceMapping>;
73    async fn get_event_source_mapping(&self, uuid: &str) -> Result<EventSourceMapping>;
74    async fn update_event_source_mapping(
75        &self,
76        uuid: &str,
77        request: UpdateEventSourceMappingRequest,
78    ) -> Result<EventSourceMapping>;
79    async fn delete_event_source_mapping(&self, uuid: &str) -> Result<EventSourceMapping>;
80    async fn list_event_source_mappings(
81        &self,
82        request: ListEventSourceMappingsRequest,
83    ) -> Result<ListEventSourceMappingsResponse>;
84
85    // Concurrency operations
86    async fn put_function_concurrency(
87        &self,
88        function_name: &str,
89        reserved_concurrent_executions: u32,
90    ) -> Result<()>;
91    async fn delete_function_concurrency(&self, function_name: &str) -> Result<()>;
92}
93
94// ---------------------------------------------------------------------------
95// Lambda client using new request helpers.
96// ---------------------------------------------------------------------------
97#[derive(Debug, Clone)]
98pub struct LambdaClient {
99    client: Client,
100    credentials: AwsCredentialProvider,
101}
102
103impl LambdaClient {
104    pub fn new(client: Client, credentials: AwsCredentialProvider) -> Self {
105        Self {
106            client,
107            credentials,
108        }
109    }
110
111    fn sign_config(&self) -> AwsSignConfig {
112        AwsSignConfig {
113            service_name: "lambda".into(),
114            region: self.credentials.region().to_string(),
115            credentials: self.credentials.get_credentials(),
116            signing_region: None,
117        }
118    }
119
120    fn get_base_url(&self) -> String {
121        if let Some(override_url) = self.credentials.get_service_endpoint_option("lambda") {
122            override_url.to_string()
123        } else {
124            format!("https://lambda.{}.amazonaws.com", self.credentials.region())
125        }
126    }
127
128    // ------------------------- internal helpers -------------------------
129
130    async fn send_json<T: DeserializeOwned + Send + 'static>(
131        &self,
132        method: Method,
133        path: &str,
134        query_params: Option<Vec<(&str, String)>>,
135        body: Option<String>,
136        operation: &str,
137        resource: &str,
138    ) -> Result<T> {
139        self.credentials.ensure_fresh().await?;
140        let base_url = self.get_base_url();
141        let mut url = format!("{}{}", base_url.trim_end_matches('/'), path);
142        if let Some(qs) = query_params {
143            if !qs.is_empty() {
144                url.push('?');
145                url.push_str(
146                    &qs.iter()
147                        .map(|(k, v)| {
148                            format!(
149                                "{}={}",
150                                k,
151                                form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
152                            )
153                        })
154                        .collect::<Vec<_>>()
155                        .join("&"),
156                );
157            }
158        }
159
160        let builder = self
161            .client
162            .request(method.clone(), &url)
163            .host(&format!(
164                "lambda.{}.amazonaws.com",
165                self.credentials.region()
166            ))
167            .content_type_json();
168
169        let builder = if let Some(ref b) = body {
170            builder.content_sha256(b).body(b.clone())
171        } else {
172            builder.content_sha256("")
173        };
174
175        let result =
176            crate::aws::aws_request_utils::sign_send_json(builder, &self.sign_config()).await;
177
178        Self::map_result(result, operation, resource, body.as_deref())
179    }
180
181    async fn send_no_body(
182        &self,
183        method: Method,
184        path: &str,
185        query_params: Option<Vec<(&str, String)>>,
186        operation: &str,
187        resource: &str,
188    ) -> Result<()> {
189        self.credentials.ensure_fresh().await?;
190        let base_url = self.get_base_url();
191        let mut url = format!("{}{}", base_url.trim_end_matches('/'), path);
192        if let Some(qs) = query_params {
193            if !qs.is_empty() {
194                url.push('?');
195                url.push_str(
196                    &qs.iter()
197                        .map(|(k, v)| {
198                            format!(
199                                "{}={}",
200                                k,
201                                form_urlencoded::byte_serialize(v.as_bytes()).collect::<String>()
202                            )
203                        })
204                        .collect::<Vec<_>>()
205                        .join("&"),
206                );
207            }
208        }
209
210        let builder = self
211            .client
212            .request(method, &url)
213            .host(&format!(
214                "lambda.{}.amazonaws.com",
215                self.credentials.region()
216            ))
217            .content_sha256("");
218
219        let result =
220            crate::aws::aws_request_utils::sign_send_no_response(builder, &self.sign_config())
221                .await;
222
223        Self::map_result(result, operation, resource, None)
224    }
225
226    fn map_result<T>(
227        result: Result<T>,
228        operation: &str,
229        resource: &str,
230        request_body: Option<&str>,
231    ) -> Result<T> {
232        match result {
233            Ok(v) => Ok(v),
234            Err(e) => {
235                if let Some(ErrorData::HttpResponseError {
236                    http_status,
237                    http_response_text: Some(ref text),
238                    ..
239                }) = &e.error
240                {
241                    let status = StatusCode::from_u16(*http_status)
242                        .unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
243                    if let Some(mapped) =
244                        Self::map_lambda_error(status, text, operation, resource, request_body)
245                    {
246                        Err(e.context(mapped))
247                    } else {
248                        // Couldn't parse Lambda error, use original error
249                        Err(e)
250                    }
251                } else {
252                    Err(e)
253                }
254            }
255        }
256    }
257
258    fn map_lambda_error(
259        status: StatusCode,
260        body: &str,
261        operation: &str,
262        resource: &str,
263        request_body: Option<&str>,
264    ) -> Option<ErrorData> {
265        let parsed: std::result::Result<LambdaErrorResponse, _> = serde_json::from_str(body);
266        let (code, message) = match parsed {
267            Ok(e) => {
268                let c = e
269                    .type_field_underscore
270                    .or(e.type_field)
271                    .or_else(|| e.error.as_ref().and_then(|d| d.code.clone()))
272                    .unwrap_or_else(|| "UnknownErrorCode".into());
273                let m = e
274                    .message
275                    .or(e.message_capital)
276                    .or_else(|| e.error.as_ref().and_then(|d| d.message.clone()))
277                    .unwrap_or_else(|| "Unknown error".into());
278                (c, m)
279            }
280            Err(_) => {
281                // If we can't parse the response, return None to use original error
282                return None;
283            }
284        };
285
286        Some(match code.as_str() {
287            // Access / auth
288            "AccessDeniedException"
289            | "NotAuthorized"
290            | "UnrecognizedClientException"
291            | "ExpiredTokenException" => ErrorData::RemoteAccessDenied {
292                resource_type: "Function".into(),
293                resource_name: resource.into(),
294            },
295            // Throttling
296            "ThrottlingException" | "TooManyRequestsException" => {
297                ErrorData::RateLimitExceeded { message }
298            }
299            // Service unavailable
300            "ServiceUnavailable" | "InternalFailure" | "ServiceException" => {
301                ErrorData::RemoteServiceUnavailable { message }
302            }
303            // Timeout
304            "RequestTimeoutException" => ErrorData::Timeout { message },
305            // Resource not found
306            "ResourceNotFoundException" => ErrorData::RemoteResourceNotFound {
307                resource_type: "Function".into(),
308                resource_name: resource.into(),
309            },
310            // Conflict / already exists
311            "ResourceConflictException" => ErrorData::RemoteResourceConflict {
312                message,
313                resource_type: "Function".into(),
314                resource_name: resource.into(),
315            },
316            // Lambda invoke-specific errors
317            "EC2AccessDeniedException" | "EC2ThrottledException" | "EC2UnexpectedException" => {
318                ErrorData::RemoteServiceUnavailable { message }
319            }
320            "EFSIOException"
321            | "EFSMountConnectivityException"
322            | "EFSMountFailureException"
323            | "EFSMountTimeoutException" => ErrorData::RemoteServiceUnavailable { message },
324            "ENILimitReachedException"
325            | "InvalidSubnetIDException"
326            | "InvalidSecurityGroupIDException"
327            | "SubnetIPAddressLimitReachedException" => {
328                ErrorData::RemoteServiceUnavailable { message }
329            }
330            "InvalidParameterValueException"
331            | "InvalidRequestContentException"
332            | "UnsupportedMediaTypeException" => {
333                // IAM eventual consistency: Lambda may return InvalidParameterValueException
334                // when it cannot yet assume a just-created execution role, or when ECR
335                // cross-account permissions haven't propagated yet.
336                if message.contains("cannot be assumed")
337                    || message.contains("not authorized")
338                    || message.contains("does not have permission to access the ECR image")
339                {
340                    ErrorData::RemoteServiceUnavailable { message }
341                } else {
342                    ErrorData::InvalidInput {
343                        message,
344                        field_name: None,
345                    }
346                }
347            }
348            "InvalidRuntimeException" | "InvalidZipFileException" => {
349                ErrorData::RemoteServiceUnavailable { message }
350            }
351            "KMSAccessDeniedException"
352            | "KMSDisabledException"
353            | "KMSInvalidStateException"
354            | "KMSNotFoundException" => ErrorData::RemoteAccessDenied {
355                resource_type: "KMS Key".into(),
356                resource_name: resource.into(),
357            },
358            "RecursiveInvocationException" => ErrorData::InvalidInput {
359                message,
360                field_name: None,
361            },
362            "RequestTooLargeException" => ErrorData::InvalidInput {
363                message,
364                field_name: None,
365            },
366            "ResourceNotReadyException" => ErrorData::RemoteServiceUnavailable { message },
367            "SnapStartException" | "SnapStartNotReadyException" | "SnapStartTimeoutException" => {
368                ErrorData::RemoteServiceUnavailable { message }
369            }
370            _ => match status {
371                StatusCode::NOT_FOUND => ErrorData::RemoteResourceNotFound {
372                    resource_type: "Function".into(),
373                    resource_name: resource.into(),
374                },
375                StatusCode::CONFLICT => ErrorData::RemoteResourceConflict {
376                    message,
377                    resource_type: "Function".into(),
378                    resource_name: resource.into(),
379                },
380                StatusCode::FORBIDDEN | StatusCode::UNAUTHORIZED => ErrorData::RemoteAccessDenied {
381                    resource_type: "Function".into(),
382                    resource_name: resource.into(),
383                },
384                StatusCode::TOO_MANY_REQUESTS => ErrorData::RateLimitExceeded { message },
385                StatusCode::SERVICE_UNAVAILABLE
386                | StatusCode::BAD_GATEWAY
387                | StatusCode::GATEWAY_TIMEOUT => ErrorData::RemoteServiceUnavailable { message },
388                _ => ErrorData::HttpResponseError {
389                    message: format!("Lambda {operation} failed: {message}"),
390                    url: format!("lambda.amazonaws.com"),
391                    http_status: status.as_u16(),
392                    http_response_text: Some(body.into()),
393                    http_request_text: request_body.map(|s| s.to_string()),
394                },
395            },
396        })
397    }
398}
399
400#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
401#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
402impl LambdaApi for LambdaClient {
403    async fn create_function(
404        &self,
405        request: CreateFunctionRequest,
406    ) -> Result<FunctionConfiguration> {
407        let body = serde_json::to_string(&request).into_alien_error().context(
408            ErrorData::SerializationError {
409                message: format!(
410                    "Failed to serialize CreateFunctionRequest for function '{}'",
411                    request.function_name
412                ),
413            },
414        )?;
415        self.send_json(
416            Method::POST,
417            "/2015-03-31/functions",
418            None,
419            Some(body),
420            "CreateFunction",
421            &request.function_name,
422        )
423        .await
424    }
425
426    async fn create_function_url_config(
427        &self,
428        function_name: &str,
429        request: CreateFunctionUrlConfigRequest,
430    ) -> Result<CreateFunctionUrlConfigResponse> {
431        let body = serde_json::to_string(&request).into_alien_error().context(
432            ErrorData::SerializationError {
433                message: format!(
434                    "Failed to serialize CreateFunctionUrlConfigRequest for function '{}'",
435                    function_name
436                ),
437            },
438        )?;
439        let path = format!("/2021-10-31/functions/{}/url", function_name);
440        self.send_json(
441            Method::POST,
442            &path,
443            None,
444            Some(body),
445            "CreateFunctionUrlConfig",
446            function_name,
447        )
448        .await
449    }
450
451    async fn add_permission(
452        &self,
453        function_name: &str,
454        request: AddPermissionRequest,
455    ) -> Result<AddPermissionResponse> {
456        let body = serde_json::to_string(&request).into_alien_error().context(
457            ErrorData::SerializationError {
458                message: format!(
459                    "Failed to serialize AddPermissionRequest for function '{}'",
460                    function_name
461                ),
462            },
463        )?;
464        let path = format!("/2015-03-31/functions/{}/policy", function_name);
465        self.send_json(
466            Method::POST,
467            &path,
468            None,
469            Some(body),
470            "AddPermission",
471            function_name,
472        )
473        .await
474    }
475
476    async fn update_function_code(
477        &self,
478        function_name: &str,
479        request: UpdateFunctionCodeRequest,
480    ) -> Result<FunctionConfiguration> {
481        let body = serde_json::to_string(&request).into_alien_error().context(
482            ErrorData::SerializationError {
483                message: format!(
484                    "Failed to serialize UpdateFunctionCodeRequest for function '{}'",
485                    function_name
486                ),
487            },
488        )?;
489        let path = format!("/2015-03-31/functions/{}/code", function_name);
490        self.send_json(
491            Method::PUT,
492            &path,
493            None,
494            Some(body),
495            "UpdateFunctionCode",
496            function_name,
497        )
498        .await
499    }
500
501    async fn update_function_configuration(
502        &self,
503        function_name: &str,
504        request: UpdateFunctionConfigurationRequest,
505    ) -> Result<FunctionConfiguration> {
506        let body = serde_json::to_string(&request).into_alien_error().context(
507            ErrorData::SerializationError {
508                message: format!(
509                    "Failed to serialize UpdateFunctionConfigurationRequest for function '{}'",
510                    function_name
511                ),
512            },
513        )?;
514        let path = format!("/2015-03-31/functions/{}/configuration", function_name);
515        self.send_json(
516            Method::PUT,
517            &path,
518            None,
519            Some(body),
520            "UpdateFunctionConfiguration",
521            function_name,
522        )
523        .await
524    }
525
526    async fn get_function_configuration(
527        &self,
528        function_name: &str,
529        qualifier: Option<String>,
530    ) -> Result<FunctionConfiguration> {
531        let path = format!("/2015-03-31/functions/{}", function_name);
532        let mut qp = Vec::new();
533        if let Some(q) = qualifier {
534            qp.push(("Qualifier", q));
535        }
536        let resp: GetFunctionResponse = self
537            .send_json(
538                Method::GET,
539                &path,
540                Some(qp),
541                None,
542                "GetFunctionConfiguration",
543                function_name,
544            )
545            .await?;
546        Ok(resp.configuration)
547    }
548
549    async fn delete_function_url_config(
550        &self,
551        function_name: &str,
552        qualifier: Option<String>,
553    ) -> Result<()> {
554        let path = format!("/2021-10-31/functions/{}/url", function_name);
555        let mut qp = Vec::new();
556        if let Some(q) = qualifier {
557            qp.push(("Qualifier", q));
558        }
559        self.send_no_body(
560            Method::DELETE,
561            &path,
562            if qp.is_empty() { None } else { Some(qp) },
563            "DeleteFunctionUrlConfig",
564            function_name,
565        )
566        .await
567    }
568
569    async fn get_function_url_config(
570        &self,
571        function_name: &str,
572        qualifier: Option<String>,
573    ) -> Result<FunctionUrlConfig> {
574        let path = format!("/2021-10-31/functions/{}/url", function_name);
575        let mut qp = Vec::new();
576        if let Some(q) = qualifier {
577            qp.push(("Qualifier", q));
578        }
579        self.send_json(
580            Method::GET,
581            &path,
582            if qp.is_empty() { None } else { Some(qp) },
583            None,
584            "GetFunctionUrlConfig",
585            function_name,
586        )
587        .await
588    }
589
590    async fn delete_function(&self, function_name: &str, qualifier: Option<String>) -> Result<()> {
591        let path = format!("/2015-03-31/functions/{}", function_name);
592        let mut qp = Vec::new();
593        if let Some(q) = qualifier {
594            qp.push(("Qualifier", q));
595        }
596        self.send_no_body(
597            Method::DELETE,
598            &path,
599            if qp.is_empty() { None } else { Some(qp) },
600            "DeleteFunction",
601            function_name,
602        )
603        .await
604    }
605
606    async fn get_policy(
607        &self,
608        function_name: &str,
609        qualifier: Option<String>,
610    ) -> Result<GetPolicyResponse> {
611        let path = format!("/2015-03-31/functions/{}/policy", function_name);
612        let mut qp = Vec::new();
613        if let Some(q) = qualifier {
614            qp.push(("Qualifier", q));
615        }
616        self.send_json(
617            Method::GET,
618            &path,
619            if qp.is_empty() { None } else { Some(qp) },
620            None,
621            "GetPolicy",
622            function_name,
623        )
624        .await
625    }
626
627    async fn invoke(&self, request: InvokeRequest) -> Result<InvokeResponse> {
628        self.credentials.ensure_fresh().await?;
629        let function_name = &request.function_name;
630        let path = format!("/2015-03-31/functions/{}/invocations", function_name);
631
632        // Build URL with query parameters
633        let base_url = self.get_base_url();
634        let mut url = format!("{}{}", base_url.trim_end_matches('/'), path);
635
636        if let Some(ref qualifier) = request.qualifier {
637            url.push('?');
638            url.push_str(&format!(
639                "Qualifier={}",
640                form_urlencoded::byte_serialize(qualifier.as_bytes()).collect::<String>()
641            ));
642        }
643
644        let mut builder = self.client.request(Method::POST, &url).host(&format!(
645            "lambda.{}.amazonaws.com",
646            self.credentials.region()
647        ));
648
649        // Set invocation type header
650        match request.invocation_type {
651            InvocationType::RequestResponse => {
652                builder = builder.header("X-Amz-Invocation-Type", "RequestResponse");
653            }
654            InvocationType::Event => {
655                builder = builder.header("X-Amz-Invocation-Type", "Event");
656            }
657            InvocationType::DryRun => {
658                builder = builder.header("X-Amz-Invocation-Type", "DryRun");
659            }
660        }
661
662        // Set log type header if specified
663        if let Some(ref log_type) = request.log_type {
664            builder = builder.header("X-Amz-Log-Type", log_type);
665        }
666
667        // Set client context if specified
668        if let Some(ref client_context) = request.client_context {
669            builder = builder.header("X-Amz-Client-Context", client_context);
670        }
671
672        // Set payload as binary data (not JSON)
673        builder = builder.content_sha256_bytes(&request.payload);
674        if !request.payload.is_empty() {
675            builder = builder.body(reqwest::Body::from(request.payload.clone()));
676        }
677
678        let signed_builder = builder.sign_aws_request(&self.sign_config())?;
679        let response = signed_builder.send().await.into_alien_error().context(
680            ErrorData::HttpRequestFailed {
681                message: format!("Failed to invoke Lambda function '{}'", function_name),
682            },
683        )?;
684
685        let status = response.status().as_u16();
686        let headers = response.headers().clone();
687        let payload =
688            response
689                .bytes()
690                .await
691                .into_alien_error()
692                .context(ErrorData::HttpRequestFailed {
693                    message: format!(
694                        "Failed to read invoke response body for function '{}'",
695                        function_name
696                    ),
697                })?;
698
699        // Parse function error from headers
700        let function_error = headers
701            .get("X-Amz-Function-Error")
702            .and_then(|v| v.to_str().ok())
703            .map(|s| s.to_string());
704
705        let log_result = headers
706            .get("X-Amz-Log-Result")
707            .and_then(|v| v.to_str().ok())
708            .map(|s| s.to_string());
709
710        let executed_version = headers
711            .get("X-Amz-Executed-Version")
712            .and_then(|v| v.to_str().ok())
713            .map(|s| s.to_string());
714
715        Ok(InvokeResponse {
716            status_code: status,
717            function_error,
718            log_result,
719            payload: payload.to_vec(),
720            executed_version,
721        })
722    }
723
724    async fn create_event_source_mapping(
725        &self,
726        request: CreateEventSourceMappingRequest,
727    ) -> Result<EventSourceMapping> {
728        let body = serde_json::to_string(&request).into_alien_error().context(
729            ErrorData::SerializationError {
730                message: format!(
731                    "Failed to serialize CreateEventSourceMappingRequest for function '{}'",
732                    request.function_name
733                ),
734            },
735        )?;
736        self.send_json(
737            Method::POST,
738            "/2015-03-31/event-source-mappings",
739            None,
740            Some(body),
741            "CreateEventSourceMapping",
742            &request.function_name,
743        )
744        .await
745    }
746
747    async fn get_event_source_mapping(&self, uuid: &str) -> Result<EventSourceMapping> {
748        let path = format!("/2015-03-31/event-source-mappings/{}", uuid);
749        self.send_json(
750            Method::GET,
751            &path,
752            None,
753            None,
754            "GetEventSourceMapping",
755            uuid,
756        )
757        .await
758    }
759
760    async fn update_event_source_mapping(
761        &self,
762        uuid: &str,
763        request: UpdateEventSourceMappingRequest,
764    ) -> Result<EventSourceMapping> {
765        let body = serde_json::to_string(&request).into_alien_error().context(
766            ErrorData::SerializationError {
767                message: format!(
768                    "Failed to serialize UpdateEventSourceMappingRequest for UUID '{}'",
769                    uuid
770                ),
771            },
772        )?;
773        let path = format!("/2015-03-31/event-source-mappings/{}", uuid);
774        self.send_json(
775            Method::PUT,
776            &path,
777            None,
778            Some(body),
779            "UpdateEventSourceMapping",
780            uuid,
781        )
782        .await
783    }
784
785    async fn delete_event_source_mapping(&self, uuid: &str) -> Result<EventSourceMapping> {
786        let path = format!("/2015-03-31/event-source-mappings/{}", uuid);
787        self.send_json(
788            Method::DELETE,
789            &path,
790            None,
791            None,
792            "DeleteEventSourceMapping",
793            uuid,
794        )
795        .await
796    }
797
798    async fn list_event_source_mappings(
799        &self,
800        request: ListEventSourceMappingsRequest,
801    ) -> Result<ListEventSourceMappingsResponse> {
802        let mut qp = Vec::new();
803        if let Some(ref arn) = request.event_source_arn {
804            qp.push(("EventSourceArn", arn.clone()));
805        }
806        if let Some(ref func) = request.function_name {
807            qp.push(("FunctionName", func.clone()));
808        }
809        if let Some(ref marker) = request.marker {
810            qp.push(("Marker", marker.clone()));
811        }
812        if let Some(max_items) = request.max_items {
813            qp.push(("MaxItems", max_items.to_string()));
814        }
815
816        let resource_name = request.function_name.as_deref().unwrap_or("unknown");
817        self.send_json(
818            Method::GET,
819            "/2015-03-31/event-source-mappings",
820            if qp.is_empty() { None } else { Some(qp) },
821            None,
822            "ListEventSourceMappings",
823            resource_name,
824        )
825        .await
826    }
827
828    async fn put_function_concurrency(
829        &self,
830        function_name: &str,
831        reserved_concurrent_executions: u32,
832    ) -> Result<()> {
833        let body =
834            serde_json::json!({ "ReservedConcurrentExecutions": reserved_concurrent_executions })
835                .to_string();
836        let path = format!("/2017-10-31/functions/{}/concurrency", function_name);
837        let _: serde_json::Value = self
838            .send_json(
839                Method::PUT,
840                &path,
841                None,
842                Some(body),
843                "PutFunctionConcurrency",
844                function_name,
845            )
846            .await?;
847        Ok(())
848    }
849
850    async fn delete_function_concurrency(&self, function_name: &str) -> Result<()> {
851        let path = format!("/2017-10-31/functions/{}/concurrency", function_name);
852        self.send_no_body(
853            Method::DELETE,
854            &path,
855            None,
856            "DeleteFunctionConcurrency",
857            function_name,
858        )
859        .await
860    }
861}
862
863// ---------------------------------------------------------------------------
864// Error JSON mapping structs
865// ---------------------------------------------------------------------------
866#[derive(Debug, Deserialize)]
867struct LambdaErrorResponse {
868    #[serde(rename = "Type")]
869    type_field: Option<String>,
870    #[serde(rename = "__type")]
871    type_field_underscore: Option<String>,
872    #[serde(rename = "message")]
873    message: Option<String>,
874    #[serde(rename = "Message")]
875    message_capital: Option<String>,
876    #[serde(rename = "Error")]
877    error: Option<LambdaErrorDetails>,
878}
879
880#[derive(Debug, Deserialize)]
881struct LambdaErrorDetails {
882    #[serde(rename = "Code")]
883    code: Option<String>,
884    #[serde(rename = "Message")]
885    message: Option<String>,
886}
887
888// ---------------------------------------------------------------------------
889// Request / response payloads (subset required by infra/tests)
890// ---------------------------------------------------------------------------
891
892#[derive(Debug, Clone, Serialize, Builder)]
893#[serde(rename_all = "PascalCase")]
894pub struct CreateFunctionRequest {
895    pub function_name: String,
896    pub role: String,
897    pub code: FunctionCode,
898    #[builder(default = "Image".to_string())]
899    pub package_type: String,
900    #[serde(skip_serializing_if = "Option::is_none")]
901    pub description: Option<String>,
902    #[serde(skip_serializing_if = "Option::is_none")]
903    pub timeout: Option<i32>,
904    #[serde(skip_serializing_if = "Option::is_none")]
905    pub memory_size: Option<i32>,
906    #[serde(skip_serializing_if = "Option::is_none")]
907    pub publish: Option<bool>,
908    #[serde(skip_serializing_if = "Option::is_none")]
909    pub environment: Option<Environment>,
910    #[serde(skip_serializing_if = "Option::is_none")]
911    pub architectures: Option<Vec<String>>,
912    #[serde(skip_serializing_if = "Option::is_none")]
913    pub tracing_config: Option<TracingConfig>,
914    #[serde(skip_serializing_if = "Option::is_none")]
915    pub tags: Option<std::collections::HashMap<String, String>>,
916    #[serde(skip_serializing_if = "Option::is_none")]
917    pub ephemeral_storage: Option<EphemeralStorage>,
918    #[serde(skip_serializing_if = "Option::is_none", rename = "KMSKeyArn")]
919    pub kms_key_arn: Option<String>,
920    /// VPC configuration for running the function inside a VPC
921    #[serde(skip_serializing_if = "Option::is_none")]
922    pub vpc_config: Option<VpcConfig>,
923}
924
925#[derive(Debug, Clone, Serialize, Builder)]
926#[serde(rename_all = "PascalCase")]
927pub struct FunctionCode {
928    pub image_uri: Option<String>,
929}
930
931#[derive(Debug, Clone, Serialize, Builder)]
932#[serde(rename_all = "PascalCase")]
933pub struct Environment {
934    pub variables: Option<std::collections::HashMap<String, String>>,
935}
936
937#[derive(Debug, Clone, Deserialize)]
938#[serde(rename_all = "PascalCase")]
939pub struct FunctionConfiguration {
940    pub function_name: Option<String>,
941    pub function_arn: Option<String>,
942    pub state: Option<String>,
943    pub last_update_status: Option<String>,
944    #[serde(rename = "KMSKeyArn")]
945    pub kms_key_arn: Option<String>,
946}
947
948#[derive(Debug, Deserialize)]
949#[serde(rename_all = "PascalCase")]
950pub struct GetFunctionResponse {
951    pub configuration: FunctionConfiguration,
952}
953
954#[derive(Debug, Clone, Serialize, Builder)]
955#[serde(rename_all = "PascalCase")]
956pub struct CreateFunctionUrlConfigRequest {
957    pub auth_type: String,
958    pub cors: Option<Cors>,
959    pub invoke_mode: Option<String>,
960}
961
962#[derive(Debug, Deserialize)]
963#[serde(rename_all = "PascalCase")]
964pub struct CreateFunctionUrlConfigResponse {
965    pub function_url: String,
966    pub function_arn: String,
967    pub auth_type: String,
968}
969
970#[derive(Debug, Clone, Deserialize)]
971#[serde(rename_all = "PascalCase")]
972pub struct FunctionUrlConfig {
973    pub function_url: String,
974    pub auth_type: String,
975    pub cors: Option<Cors>,
976}
977
978#[derive(Debug, Serialize, Builder)]
979#[serde(rename_all = "PascalCase")]
980pub struct AddPermissionRequest {
981    pub statement_id: String,
982    pub action: String,
983    pub principal: String,
984    #[serde(skip_serializing_if = "Option::is_none")]
985    pub function_url_auth_type: Option<String>,
986    #[serde(skip_serializing_if = "Option::is_none")]
987    pub source_arn: Option<String>,
988    #[serde(skip_serializing_if = "Option::is_none")]
989    pub source_account: Option<String>,
990}
991
992#[derive(Debug, Deserialize)]
993#[serde(rename_all = "PascalCase")]
994pub struct AddPermissionResponse {
995    pub statement: Option<String>,
996}
997
998#[derive(Debug, Serialize, Builder)]
999#[serde(rename_all = "PascalCase")]
1000pub struct UpdateFunctionCodeRequest {
1001    pub image_uri: String,
1002    pub publish: Option<bool>,
1003}
1004
1005#[derive(Debug, Serialize, Builder)]
1006#[serde(rename_all = "PascalCase")]
1007pub struct UpdateFunctionConfigurationRequest {
1008    #[serde(skip_serializing_if = "Option::is_none")]
1009    pub role: Option<String>,
1010    #[serde(skip_serializing_if = "Option::is_none")]
1011    pub timeout: Option<i32>,
1012    #[serde(skip_serializing_if = "Option::is_none")]
1013    pub memory_size: Option<i32>,
1014    #[serde(skip_serializing_if = "Option::is_none")]
1015    pub environment: Option<Environment>,
1016    /// VPC configuration for running the function inside a VPC
1017    #[serde(skip_serializing_if = "Option::is_none")]
1018    pub vpc_config: Option<VpcConfig>,
1019}
1020
1021#[derive(Debug, Deserialize)]
1022#[serde(rename_all = "PascalCase")]
1023pub struct GetPolicyResponse {
1024    pub policy: Option<String>,
1025}
1026
1027// ---------------------------------------------------------------------------
1028// Supporting structs newly added for test compilation
1029// ---------------------------------------------------------------------------
1030
1031#[derive(Debug, Clone, Serialize, Builder)]
1032#[serde(rename_all = "PascalCase")]
1033pub struct TracingConfig {
1034    pub mode: Option<String>,
1035}
1036
1037#[derive(Debug, Clone, Serialize, Builder)]
1038#[serde(rename_all = "PascalCase")]
1039pub struct EphemeralStorage {
1040    pub size: i32,
1041}
1042
1043/// VPC configuration for Lambda function.
1044///
1045/// When configured, the Lambda function runs inside the specified VPC with access
1046/// to the specified subnets and security groups.
1047#[derive(Debug, Clone, Serialize, Builder)]
1048#[serde(rename_all = "PascalCase")]
1049pub struct VpcConfig {
1050    /// A list of VPC subnet IDs where the function will run.
1051    /// Lambda creates an elastic network interface for each subnet.
1052    #[serde(skip_serializing_if = "Option::is_none")]
1053    pub subnet_ids: Option<Vec<String>>,
1054
1055    /// A list of VPC security group IDs for the function's network interfaces.
1056    #[serde(skip_serializing_if = "Option::is_none")]
1057    pub security_group_ids: Option<Vec<String>>,
1058}
1059
1060#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
1061#[serde(rename_all = "PascalCase")]
1062pub struct Cors {
1063    #[serde(skip_serializing_if = "Option::is_none")]
1064    pub allow_credentials: Option<bool>,
1065    #[serde(skip_serializing_if = "Option::is_none")]
1066    pub allow_headers: Option<Vec<String>>,
1067    #[serde(skip_serializing_if = "Option::is_none")]
1068    pub allow_methods: Option<Vec<String>>,
1069    #[serde(skip_serializing_if = "Option::is_none")]
1070    pub allow_origins: Option<Vec<String>>,
1071    #[serde(skip_serializing_if = "Option::is_none")]
1072    pub max_age: Option<i32>,
1073}
1074
1075// ---------------------------------------------------------------------------
1076// Event Source Mapping structs
1077// ---------------------------------------------------------------------------
1078
1079/// Request to create an event source mapping
1080#[derive(Debug, Clone, Serialize, Builder)]
1081#[serde(rename_all = "PascalCase")]
1082pub struct CreateEventSourceMappingRequest {
1083    /// The Amazon Resource Name (ARN) of the event source (e.g., SQS queue)
1084    pub event_source_arn: String,
1085
1086    /// The name or ARN of the Lambda function
1087    pub function_name: String,
1088
1089    /// The maximum number of records in each batch that Lambda pulls from your queue and sends to your function
1090    #[serde(skip_serializing_if = "Option::is_none")]
1091    pub batch_size: Option<i32>,
1092
1093    /// When true, the event source mapping is active. When false, Lambda pauses polling and invocation
1094    #[serde(skip_serializing_if = "Option::is_none")]
1095    pub enabled: Option<bool>,
1096
1097    /// The maximum amount of time, in seconds, that Lambda spends gathering records before invoking the function
1098    #[serde(skip_serializing_if = "Option::is_none")]
1099    pub maximum_batching_window_in_seconds: Option<i32>,
1100
1101    /// A list of current response type enums applied to the event source mapping
1102    #[serde(skip_serializing_if = "Option::is_none")]
1103    pub function_response_types: Option<Vec<String>>,
1104
1105    /// An object that defines the filter criteria that determine whether Lambda should process an event
1106    #[serde(skip_serializing_if = "Option::is_none")]
1107    pub filter_criteria: Option<FilterCriteria>,
1108
1109    /// The maximum concurrency setting limits the number of concurrent instances of the function that an Amazon SQS event source can invoke
1110    #[serde(skip_serializing_if = "Option::is_none")]
1111    pub scaling_config: Option<ScalingConfig>,
1112}
1113
1114/// Request to update an event source mapping
1115#[derive(Debug, Clone, Serialize, Builder)]
1116#[serde(rename_all = "PascalCase")]
1117pub struct UpdateEventSourceMappingRequest {
1118    /// The maximum number of records in each batch that Lambda pulls from your queue and sends to your function
1119    #[serde(skip_serializing_if = "Option::is_none")]
1120    pub batch_size: Option<i32>,
1121
1122    /// When true, the event source mapping is active. When false, Lambda pauses polling and invocation
1123    #[serde(skip_serializing_if = "Option::is_none")]
1124    pub enabled: Option<bool>,
1125
1126    /// The name or ARN of the Lambda function
1127    #[serde(skip_serializing_if = "Option::is_none")]
1128    pub function_name: Option<String>,
1129
1130    /// The maximum amount of time, in seconds, that Lambda spends gathering records before invoking the function
1131    #[serde(skip_serializing_if = "Option::is_none")]
1132    pub maximum_batching_window_in_seconds: Option<i32>,
1133
1134    /// A list of current response type enums applied to the event source mapping
1135    #[serde(skip_serializing_if = "Option::is_none")]
1136    pub function_response_types: Option<Vec<String>>,
1137
1138    /// An object that defines the filter criteria that determine whether Lambda should process an event
1139    #[serde(skip_serializing_if = "Option::is_none")]
1140    pub filter_criteria: Option<FilterCriteria>,
1141
1142    /// The maximum concurrency setting limits the number of concurrent instances of the function that an Amazon SQS event source can invoke
1143    #[serde(skip_serializing_if = "Option::is_none")]
1144    pub scaling_config: Option<ScalingConfig>,
1145}
1146
1147/// Request to list event source mappings
1148#[derive(Debug, Clone, Serialize, Builder)]
1149#[serde(rename_all = "PascalCase")]
1150pub struct ListEventSourceMappingsRequest {
1151    /// The Amazon Resource Name (ARN) of the event source
1152    #[serde(skip_serializing_if = "Option::is_none")]
1153    pub event_source_arn: Option<String>,
1154
1155    /// The name or ARN of the Lambda function
1156    #[serde(skip_serializing_if = "Option::is_none")]
1157    pub function_name: Option<String>,
1158
1159    /// A pagination token returned by a previous call
1160    #[serde(skip_serializing_if = "Option::is_none")]
1161    pub marker: Option<String>,
1162
1163    /// The maximum number of event source mappings to return
1164    #[serde(skip_serializing_if = "Option::is_none")]
1165    pub max_items: Option<i32>,
1166}
1167
1168/// Response from listing event source mappings
1169#[derive(Debug, Deserialize)]
1170#[serde(rename_all = "PascalCase")]
1171pub struct ListEventSourceMappingsResponse {
1172    /// A list of event source mappings
1173    pub event_source_mappings: Option<Vec<EventSourceMapping>>,
1174
1175    /// A pagination token that's returned when the response doesn't contain all event source mappings
1176    pub next_marker: Option<String>,
1177}
1178
1179/// Represents an event source mapping
1180#[derive(Debug, Clone, Deserialize)]
1181#[serde(rename_all = "PascalCase")]
1182pub struct EventSourceMapping {
1183    /// The identifier of the event source mapping
1184    #[serde(rename = "UUID")]
1185    pub uuid: Option<String>,
1186
1187    /// The Amazon Resource Name (ARN) of the event source
1188    pub event_source_arn: Option<String>,
1189
1190    /// The ARN of the Lambda function
1191    pub function_arn: Option<String>,
1192
1193    /// The maximum number of records in each batch that Lambda pulls from your queue and sends to your function
1194    pub batch_size: Option<i32>,
1195
1196    /// The date that the event source mapping was last updated or that its state changed
1197    pub last_modified: Option<f64>,
1198
1199    /// The result of the last AWS Lambda invocation of your Lambda function
1200    pub last_processing_result: Option<String>,
1201
1202    /// The state of the event source mapping
1203    pub state: Option<String>,
1204
1205    /// Indicates whether a user or Lambda made the last change to the event source mapping
1206    pub state_transition_reason: Option<String>,
1207
1208    /// The maximum amount of time, in seconds, that Lambda spends gathering records before invoking the function
1209    pub maximum_batching_window_in_seconds: Option<i32>,
1210
1211    /// A list of current response type enums applied to the event source mapping
1212    pub function_response_types: Option<Vec<String>>,
1213
1214    /// An object that defines the filter criteria that determine whether Lambda should process an event
1215    pub filter_criteria: Option<FilterCriteria>,
1216
1217    /// The maximum concurrency setting limits the number of concurrent instances of the function that an Amazon SQS event source can invoke
1218    pub scaling_config: Option<ScalingConfig>,
1219}
1220
1221/// Filter criteria for event source mapping
1222#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
1223#[serde(rename_all = "PascalCase")]
1224pub struct FilterCriteria {
1225    /// A list of filters to apply to the event
1226    #[serde(skip_serializing_if = "Option::is_none")]
1227    pub filters: Option<Vec<Filter>>,
1228}
1229
1230/// Individual filter in filter criteria
1231#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
1232#[serde(rename_all = "PascalCase")]
1233pub struct Filter {
1234    /// A filter pattern
1235    #[serde(skip_serializing_if = "Option::is_none")]
1236    pub pattern: Option<String>,
1237}
1238
1239/// Scaling configuration for SQS event source mapping
1240#[derive(Debug, Clone, Serialize, Deserialize, Builder)]
1241#[serde(rename_all = "PascalCase")]
1242pub struct ScalingConfig {
1243    /// The maximum number of concurrent instances of the function that an Amazon SQS event source can invoke
1244    #[serde(skip_serializing_if = "Option::is_none")]
1245    pub maximum_concurrency: Option<i32>,
1246}
1247
1248// ---------------------------------------------------------------------------
1249// Lambda invocation types and structs
1250// ---------------------------------------------------------------------------
1251
1252/// Lambda invocation type
1253#[derive(Debug, Clone)]
1254pub enum InvocationType {
1255    /// Synchronous invocation
1256    RequestResponse,
1257    /// Asynchronous invocation
1258    Event,
1259    /// Dry run validation
1260    DryRun,
1261}
1262
1263impl Default for InvocationType {
1264    fn default() -> Self {
1265        InvocationType::RequestResponse
1266    }
1267}
1268
1269/// Request for invoking a Lambda function
1270#[derive(Debug, Clone, Builder)]
1271pub struct InvokeRequest {
1272    /// The name of the Lambda function
1273    pub function_name: String,
1274    /// The invocation type
1275    #[builder(default)]
1276    pub invocation_type: InvocationType,
1277    /// For versioned functions, the version to invoke
1278    pub qualifier: Option<String>,
1279    /// Up to 3,583 bytes of base64-encoded data about the invoking client to pass to the function
1280    pub client_context: Option<String>,
1281    /// Set to Tail to include the execution log in the response
1282    pub log_type: Option<String>,
1283    /// The JSON that you want to provide to your Lambda function as input
1284    #[builder(default)]
1285    pub payload: Vec<u8>,
1286}
1287
1288/// Response from invoking a Lambda function
1289#[derive(Debug, Clone)]
1290pub struct InvokeResponse {
1291    /// The HTTP status code for the invoke response
1292    pub status_code: u16,
1293    /// If present, indicates that an error occurred during function execution
1294    pub function_error: Option<String>,
1295    /// The last 4 KB of the execution log (only if you set log_type to Tail)
1296    pub log_result: Option<String>,
1297    /// The response from the function
1298    pub payload: Vec<u8>,
1299    /// The version of the function that was executed
1300    pub executed_version: Option<String>,
1301}