1use std::collections::HashMap;
2use std::fmt;
3
4use serde::de::DeserializeOwned;
5use serde_json::Value;
6
7use crate::error::FunctionsError;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum HttpMethod {
12 Get,
13 Post,
14 Put,
15 Patch,
16 Delete,
17 Options,
18 Head,
19}
20
21impl fmt::Display for HttpMethod {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self {
24 Self::Get => write!(f, "GET"),
25 Self::Post => write!(f, "POST"),
26 Self::Put => write!(f, "PUT"),
27 Self::Patch => write!(f, "PATCH"),
28 Self::Delete => write!(f, "DELETE"),
29 Self::Options => write!(f, "OPTIONS"),
30 Self::Head => write!(f, "HEAD"),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
37pub enum FunctionRegion {
38 UsEast1,
39 UsWest1,
40 UsCentral1,
41 EuWest1,
42 EuWest2,
43 EuWest3,
44 EuCentral1,
45 EuCentral2,
46 ApSoutheast1,
47 ApSoutheast2,
48 ApNortheast1,
49 ApNortheast2,
50 ApSouth1,
51 SaEast1,
52 CaCentral1,
53 MeSouth1,
54 AfSouth1,
55 Any,
56 Custom(String),
57}
58
59impl fmt::Display for FunctionRegion {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 match self {
62 Self::UsEast1 => write!(f, "us-east-1"),
63 Self::UsWest1 => write!(f, "us-west-1"),
64 Self::UsCentral1 => write!(f, "us-central-1"),
65 Self::EuWest1 => write!(f, "eu-west-1"),
66 Self::EuWest2 => write!(f, "eu-west-2"),
67 Self::EuWest3 => write!(f, "eu-west-3"),
68 Self::EuCentral1 => write!(f, "eu-central-1"),
69 Self::EuCentral2 => write!(f, "eu-central-2"),
70 Self::ApSoutheast1 => write!(f, "ap-southeast-1"),
71 Self::ApSoutheast2 => write!(f, "ap-southeast-2"),
72 Self::ApNortheast1 => write!(f, "ap-northeast-1"),
73 Self::ApNortheast2 => write!(f, "ap-northeast-2"),
74 Self::ApSouth1 => write!(f, "ap-south-1"),
75 Self::SaEast1 => write!(f, "sa-east-1"),
76 Self::CaCentral1 => write!(f, "ca-central-1"),
77 Self::MeSouth1 => write!(f, "me-south-1"),
78 Self::AfSouth1 => write!(f, "af-south-1"),
79 Self::Any => write!(f, "any"),
80 Self::Custom(s) => write!(f, "{}", s),
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
87pub enum InvokeBody {
88 Json(Value),
89 Bytes(Vec<u8>),
90 Text(String),
91 None,
92}
93
94#[derive(Debug, Clone)]
106pub struct InvokeOptions {
107 pub(crate) body: InvokeBody,
108 pub(crate) method: HttpMethod,
109 pub(crate) headers: HashMap<String, String>,
110 pub(crate) region: Option<FunctionRegion>,
111 pub(crate) content_type: Option<String>,
112 pub(crate) authorization: Option<String>,
113}
114
115impl Default for InvokeOptions {
116 fn default() -> Self {
117 Self::new()
118 }
119}
120
121impl InvokeOptions {
122 pub fn new() -> Self {
124 Self {
125 body: InvokeBody::None,
126 method: HttpMethod::Post,
127 headers: HashMap::new(),
128 region: None,
129 content_type: None,
130 authorization: None,
131 }
132 }
133
134 pub fn body(mut self, value: Value) -> Self {
136 self.body = InvokeBody::Json(value);
137 self
138 }
139
140 pub fn body_bytes(mut self, bytes: Vec<u8>) -> Self {
142 self.body = InvokeBody::Bytes(bytes);
143 self
144 }
145
146 pub fn body_text(mut self, text: impl Into<String>) -> Self {
148 self.body = InvokeBody::Text(text.into());
149 self
150 }
151
152 pub fn method(mut self, method: HttpMethod) -> Self {
154 self.method = method;
155 self
156 }
157
158 pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
160 self.headers.insert(key.into(), value.into());
161 self
162 }
163
164 pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
166 self.headers.extend(headers);
167 self
168 }
169
170 pub fn region(mut self, region: FunctionRegion) -> Self {
172 self.region = Some(region);
173 self
174 }
175
176 pub fn content_type(mut self, ct: impl Into<String>) -> Self {
178 self.content_type = Some(ct.into());
179 self
180 }
181
182 pub fn authorization(mut self, auth: impl Into<String>) -> Self {
184 self.authorization = Some(auth.into());
185 self
186 }
187}
188
189#[derive(Debug, Clone)]
191pub struct FunctionResponse {
192 status: u16,
193 headers: HashMap<String, String>,
194 body: Vec<u8>,
195}
196
197impl FunctionResponse {
198 pub(crate) fn new(status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Self {
199 Self {
200 status,
201 headers,
202 body,
203 }
204 }
205
206 pub fn status(&self) -> u16 {
208 self.status
209 }
210
211 pub fn headers(&self) -> &HashMap<String, String> {
213 &self.headers
214 }
215
216 pub fn header(&self, name: &str) -> Option<&str> {
218 let lower = name.to_lowercase();
219 self.headers.get(&lower).map(|s| s.as_str())
220 }
221
222 pub fn json<T: DeserializeOwned>(&self) -> Result<T, FunctionsError> {
224 serde_json::from_slice(&self.body).map_err(FunctionsError::from)
225 }
226
227 pub fn text(&self) -> Result<String, FunctionsError> {
229 String::from_utf8(self.body.clone()).map_err(|e| {
230 FunctionsError::InvalidConfig(format!("Response body is not valid UTF-8: {}", e))
231 })
232 }
233
234 pub fn bytes(&self) -> &[u8] {
236 &self.body
237 }
238
239 pub fn into_bytes(self) -> Vec<u8> {
241 self.body
242 }
243
244 pub fn content_type(&self) -> Option<&str> {
246 self.header("content-type")
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 use super::*;
253
254 #[test]
255 fn http_method_display() {
256 assert_eq!(HttpMethod::Get.to_string(), "GET");
257 assert_eq!(HttpMethod::Post.to_string(), "POST");
258 assert_eq!(HttpMethod::Put.to_string(), "PUT");
259 assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
260 assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
261 assert_eq!(HttpMethod::Options.to_string(), "OPTIONS");
262 assert_eq!(HttpMethod::Head.to_string(), "HEAD");
263 }
264
265 #[test]
266 fn function_region_display() {
267 assert_eq!(FunctionRegion::UsEast1.to_string(), "us-east-1");
268 assert_eq!(FunctionRegion::EuWest1.to_string(), "eu-west-1");
269 assert_eq!(FunctionRegion::ApNortheast1.to_string(), "ap-northeast-1");
270 assert_eq!(FunctionRegion::Any.to_string(), "any");
271 assert_eq!(
272 FunctionRegion::Custom("my-region".into()).to_string(),
273 "my-region"
274 );
275 }
276
277 #[test]
278 fn invoke_options_defaults() {
279 let opts = InvokeOptions::new();
280 assert!(matches!(opts.body, InvokeBody::None));
281 assert_eq!(opts.method, HttpMethod::Post);
282 assert!(opts.headers.is_empty());
283 assert!(opts.region.is_none());
284 assert!(opts.content_type.is_none());
285 assert!(opts.authorization.is_none());
286 }
287
288 #[test]
289 fn invoke_options_builder() {
290 let opts = InvokeOptions::new()
291 .body(serde_json::json!({"key": "value"}))
292 .method(HttpMethod::Put)
293 .header("x-custom", "test")
294 .region(FunctionRegion::UsEast1)
295 .content_type("text/plain")
296 .authorization("Bearer token123");
297
298 assert!(matches!(opts.body, InvokeBody::Json(_)));
299 assert_eq!(opts.method, HttpMethod::Put);
300 assert_eq!(opts.headers.get("x-custom"), Some(&"test".to_string()));
301 assert_eq!(opts.region, Some(FunctionRegion::UsEast1));
302 assert_eq!(opts.content_type, Some("text/plain".to_string()));
303 assert_eq!(
304 opts.authorization,
305 Some("Bearer token123".to_string())
306 );
307 }
308
309 #[test]
310 fn invoke_options_body_bytes() {
311 let opts = InvokeOptions::new().body_bytes(vec![1, 2, 3]);
312 assert!(matches!(opts.body, InvokeBody::Bytes(ref b) if b == &[1, 2, 3]));
313 }
314
315 #[test]
316 fn invoke_options_body_text() {
317 let opts = InvokeOptions::new().body_text("hello");
318 assert!(matches!(opts.body, InvokeBody::Text(ref s) if s == "hello"));
319 }
320
321 #[test]
322 fn invoke_options_multiple_headers() {
323 let mut extra = HashMap::new();
324 extra.insert("a".into(), "1".into());
325 extra.insert("b".into(), "2".into());
326 let opts = InvokeOptions::new().header("x", "y").headers(extra);
327 assert_eq!(opts.headers.len(), 3);
328 assert_eq!(opts.headers.get("x"), Some(&"y".to_string()));
329 assert_eq!(opts.headers.get("a"), Some(&"1".to_string()));
330 }
331
332 #[test]
333 fn function_response_json() {
334 let resp = FunctionResponse::new(
335 200,
336 HashMap::new(),
337 br#"{"message":"hello"}"#.to_vec(),
338 );
339 let val: serde_json::Value = resp.json().unwrap();
340 assert_eq!(val["message"], "hello");
341 }
342
343 #[test]
344 fn function_response_text() {
345 let resp = FunctionResponse::new(200, HashMap::new(), b"hello world".to_vec());
346 assert_eq!(resp.text().unwrap(), "hello world");
347 }
348
349 #[test]
350 fn function_response_bytes() {
351 let data = vec![0, 1, 2, 255];
352 let resp = FunctionResponse::new(200, HashMap::new(), data.clone());
353 assert_eq!(resp.bytes(), &data);
354 }
355
356 #[test]
357 fn function_response_header_case_insensitive() {
358 let mut headers = HashMap::new();
359 headers.insert("content-type".into(), "application/json".into());
360 headers.insert("x-custom".into(), "value".into());
361 let resp = FunctionResponse::new(200, headers, vec![]);
362 assert_eq!(resp.header("Content-Type"), Some("application/json"));
363 assert_eq!(resp.header("X-Custom"), Some("value"));
364 assert_eq!(resp.header("missing"), None);
365 }
366
367 #[test]
368 fn function_response_content_type() {
369 let mut headers = HashMap::new();
370 headers.insert("content-type".into(), "text/plain".into());
371 let resp = FunctionResponse::new(200, headers, vec![]);
372 assert_eq!(resp.content_type(), Some("text/plain"));
373 }
374
375 #[test]
376 fn function_response_status() {
377 let resp = FunctionResponse::new(201, HashMap::new(), vec![]);
378 assert_eq!(resp.status(), 201);
379 }
380
381 #[test]
382 fn function_response_into_bytes() {
383 let data = vec![10, 20, 30];
384 let resp = FunctionResponse::new(200, HashMap::new(), data.clone());
385 assert_eq!(resp.into_bytes(), data);
386 }
387}