Skip to main content

alien_bindings/providers/function/
aws_lambda.rs

1use crate::error::{ErrorData, Result};
2use crate::traits::{Binding, Function, FunctionInvokeRequest, FunctionInvokeResponse};
3use alien_aws_clients::lambda::{InvocationType, InvokeRequest, LambdaApi, LambdaClient};
4use alien_aws_clients::AwsCredentialProvider;
5use alien_core::bindings::LambdaFunctionBinding;
6use alien_error::{AlienError, Context, IntoAlienError};
7use async_trait::async_trait;
8use base64::Engine;
9use reqwest::Client;
10use std::collections::BTreeMap;
11
12/// AWS Lambda function binding implementation
13#[derive(Debug)]
14pub struct LambdaFunction {
15    client: LambdaClient,
16    binding: LambdaFunctionBinding,
17}
18
19impl LambdaFunction {
20    pub fn new(
21        client: Client,
22        credentials: AwsCredentialProvider,
23        binding: LambdaFunctionBinding,
24    ) -> Self {
25        let lambda_client = LambdaClient::new(client, credentials);
26        Self {
27            client: lambda_client,
28            binding,
29        }
30    }
31
32    /// Get the function name from the binding, resolving template expressions if needed
33    fn get_function_name(&self) -> Result<String> {
34        self.binding
35            .function_name
36            .clone()
37            .into_value("function", "function_name")
38            .context(ErrorData::BindingConfigInvalid {
39                binding_name: "function".to_string(),
40                reason: "Failed to resolve function_name from binding".to_string(),
41            })
42    }
43}
44
45impl Binding for LambdaFunction {}
46
47#[async_trait]
48impl Function for LambdaFunction {
49    async fn invoke(&self, request: FunctionInvokeRequest) -> Result<FunctionInvokeResponse> {
50        let function_name = self.get_function_name()?;
51
52        // Create the invoke request payload
53        // For Lambda, we need to construct an HTTP-like payload that the runtime can understand
54        let payload = serde_json::json!({
55            "httpMethod": request.method.to_uppercase(),
56            "path": request.path,
57            "headers": request.headers,
58            "body": base64::engine::general_purpose::STANDARD.encode(&request.body),
59            "isBase64Encoded": true
60        });
61
62        let payload_bytes =
63            serde_json::to_vec(&payload)
64                .into_alien_error()
65                .context(ErrorData::Other {
66                    message: "Failed to serialize Lambda invoke payload".to_string(),
67                })?;
68
69        // Use the target_function if provided, otherwise use the bound function
70        let target_function = if !request.target_function.is_empty() {
71            request.target_function.clone()
72        } else {
73            function_name
74        };
75
76        let invoke_request = InvokeRequest::builder()
77            .function_name(target_function.clone())
78            .invocation_type(InvocationType::RequestResponse)
79            .payload(payload_bytes)
80            .build();
81
82        let response = self
83            .client
84            .invoke(invoke_request)
85            .await
86            .context(ErrorData::Other {
87                message: format!("Failed to invoke Lambda function '{}'", target_function),
88            })?;
89
90        // Check for function error
91        if let Some(function_error) = response.function_error {
92            return Err(AlienError::new(ErrorData::Other {
93                message: format!(
94                    "Lambda function '{}' returned error: {}",
95                    target_function, function_error
96                ),
97            }));
98        }
99
100        // Parse the response payload
101        let lambda_response: serde_json::Value = serde_json::from_slice(&response.payload)
102            .into_alien_error()
103            .context(ErrorData::Other {
104                message: "Failed to parse Lambda response payload".to_string(),
105            })?;
106
107        // Extract HTTP response components
108        let status = lambda_response
109            .get("statusCode")
110            .and_then(|s| s.as_u64())
111            .unwrap_or(200) as u16;
112
113        let headers = lambda_response
114            .get("headers")
115            .and_then(|h| h.as_object())
116            .map(|obj| {
117                obj.iter()
118                    .map(|(k, v)| (k.clone(), v.as_str().unwrap_or("").to_string()))
119                    .collect::<BTreeMap<String, String>>()
120            })
121            .unwrap_or_default();
122
123        let body = if let Some(body_str) = lambda_response.get("body").and_then(|b| b.as_str()) {
124            // Check if body is base64 encoded
125            let is_base64 = lambda_response
126                .get("isBase64Encoded")
127                .and_then(|b| b.as_bool())
128                .unwrap_or(false);
129
130            if is_base64 {
131                base64::engine::general_purpose::STANDARD
132                    .decode(body_str)
133                    .into_alien_error()
134                    .context(ErrorData::Other {
135                        message: "Failed to decode base64 response body".to_string(),
136                    })?
137            } else {
138                body_str.as_bytes().to_vec()
139            }
140        } else {
141            Vec::new()
142        };
143
144        Ok(FunctionInvokeResponse {
145            status,
146            headers,
147            body,
148        })
149    }
150
151    async fn get_function_url(&self) -> Result<Option<String>> {
152        // First check if we have it in the binding
153        if let Some(url_binding) = &self.binding.url {
154            let url = url_binding.clone().into_value("function", "url").context(
155                ErrorData::BindingConfigInvalid {
156                    binding_name: "function".to_string(),
157                    reason: "Failed to resolve url from binding".to_string(),
158                },
159            )?;
160            return Ok(Some(url));
161        }
162
163        // If not in binding, try to fetch it from AWS
164        let function_name = self.get_function_name()?;
165        match self
166            .client
167            .get_function_url_config(&function_name, None)
168            .await
169        {
170            Ok(url_config) => Ok(Some(url_config.function_url)),
171            Err(_) => Ok(None), // Function URL doesn't exist
172        }
173    }
174
175    fn as_any(&self) -> &dyn std::any::Any {
176        self
177    }
178}