armature_lambda/
request.rs

1//! Lambda request conversion.
2
3use bytes::Bytes;
4use lambda_http::Request;
5use std::collections::HashMap;
6
7/// Wrapper for Lambda HTTP requests.
8pub struct LambdaRequest {
9    /// HTTP method.
10    pub method: http::Method,
11    /// Request path.
12    pub path: String,
13    /// Query string.
14    pub query_string: Option<String>,
15    /// Headers.
16    pub headers: HashMap<String, String>,
17    /// Request body.
18    pub body: Bytes,
19    /// Path parameters (from API Gateway).
20    pub path_parameters: HashMap<String, String>,
21    /// Stage variables (from API Gateway).
22    pub stage_variables: HashMap<String, String>,
23    /// Request context.
24    pub request_context: RequestContext,
25}
26
27/// Request context from API Gateway.
28#[derive(Debug, Clone, Default)]
29pub struct RequestContext {
30    /// Request ID.
31    pub request_id: Option<String>,
32    /// Stage name.
33    pub stage: Option<String>,
34    /// Domain name.
35    pub domain_name: Option<String>,
36    /// HTTP method.
37    pub http_method: Option<String>,
38    /// Source IP.
39    pub source_ip: Option<String>,
40    /// User agent.
41    pub user_agent: Option<String>,
42    /// Authorizer claims (for Cognito).
43    pub authorizer_claims: HashMap<String, String>,
44}
45
46impl LambdaRequest {
47    /// Create from a lambda_http::Request.
48    pub fn from_lambda_request(request: Request) -> Self {
49        let (parts, body) = request.into_parts();
50
51        // Extract headers
52        let mut headers = HashMap::new();
53        for (name, value) in parts.headers.iter() {
54            if let Ok(v) = value.to_str() {
55                headers.insert(name.to_string(), v.to_string());
56            }
57        }
58
59        // Extract query string
60        let query_string = parts.uri.query().map(String::from);
61
62        // Extract path parameters and request context from extensions
63        let (path_parameters, request_context) = parts
64            .extensions
65            .get::<lambda_http::request::RequestContext>()
66            .map(|ctx| {
67                match ctx {
68                    lambda_http::request::RequestContext::ApiGatewayV2(v2) => {
69                        // V2 doesn't expose path parameters directly in the same way
70                        let params = HashMap::new();
71
72                        let ctx = RequestContext {
73                            request_id: v2.request_id.clone(),
74                            stage: v2.stage.clone(),
75                            domain_name: v2.domain_name.clone(),
76                            http_method: Some(v2.http.method.to_string()),
77                            source_ip: v2.http.source_ip.clone(),
78                            user_agent: v2.http.user_agent.clone(),
79                            authorizer_claims: HashMap::new(),
80                        };
81                        (params, ctx)
82                    }
83                    lambda_http::request::RequestContext::ApiGatewayV1(v1) => {
84                        let params = HashMap::new();
85                        let ctx = RequestContext {
86                            request_id: v1.request_id.clone(),
87                            stage: v1.stage.clone(),
88                            domain_name: v1.domain_name.clone(),
89                            http_method: Some(v1.http_method.to_string()),
90                            source_ip: v1.identity.source_ip.clone(),
91                            user_agent: v1.identity.user_agent.clone(),
92                            authorizer_claims: extract_claims_v1(v1),
93                        };
94                        (params, ctx)
95                    }
96                    lambda_http::request::RequestContext::Alb(_) => {
97                        (HashMap::new(), RequestContext::default())
98                    }
99                    _ => (HashMap::new(), RequestContext::default()),
100                }
101            })
102            .unwrap_or_default();
103
104        // Convert body
105        let body_bytes = match body {
106            lambda_http::Body::Empty => Bytes::new(),
107            lambda_http::Body::Text(s) => Bytes::from(s),
108            lambda_http::Body::Binary(b) => Bytes::from(b),
109        };
110
111        Self {
112            method: parts.method,
113            path: parts.uri.path().to_string(),
114            query_string,
115            headers,
116            body: body_bytes,
117            path_parameters,
118            stage_variables: HashMap::new(),
119            request_context,
120        }
121    }
122
123    /// Get a header value.
124    pub fn header(&self, name: &str) -> Option<&str> {
125        self.headers
126            .get(&name.to_lowercase())
127            .or_else(|| self.headers.get(name))
128            .map(|s| s.as_str())
129    }
130
131    /// Get the content type.
132    pub fn content_type(&self) -> Option<&str> {
133        self.header("content-type")
134    }
135
136    /// Check if the request is JSON.
137    pub fn is_json(&self) -> bool {
138        self.content_type()
139            .map(|ct| ct.contains("application/json"))
140            .unwrap_or(false)
141    }
142
143    /// Get the source IP.
144    pub fn source_ip(&self) -> Option<&str> {
145        self.request_context.source_ip.as_deref()
146    }
147
148    /// Get authorizer claims (for Cognito).
149    pub fn claims(&self) -> &HashMap<String, String> {
150        &self.request_context.authorizer_claims
151    }
152
153    /// Get a specific claim.
154    pub fn claim(&self, key: &str) -> Option<&str> {
155        self.request_context
156            .authorizer_claims
157            .get(key)
158            .map(|s| s.as_str())
159    }
160}
161
162/// Extract claims from API Gateway V1 authorizer.
163fn extract_claims_v1(
164    _v1: &lambda_http::aws_lambda_events::apigw::ApiGatewayProxyRequestContext,
165) -> HashMap<String, String> {
166    // The authorizer field structure has changed - just return empty for now
167    // Users can access raw context if needed
168    HashMap::new()
169}