use std::collections::HashMap;
use std::fmt;
use serde::de::DeserializeOwned;
use serde_json::Value;
use crate::error::FunctionsError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HttpMethod {
Get,
Post,
Put,
Patch,
Delete,
Options,
Head,
}
impl fmt::Display for HttpMethod {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Get => write!(f, "GET"),
Self::Post => write!(f, "POST"),
Self::Put => write!(f, "PUT"),
Self::Patch => write!(f, "PATCH"),
Self::Delete => write!(f, "DELETE"),
Self::Options => write!(f, "OPTIONS"),
Self::Head => write!(f, "HEAD"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum FunctionRegion {
UsEast1,
UsWest1,
UsCentral1,
EuWest1,
EuWest2,
EuWest3,
EuCentral1,
EuCentral2,
ApSoutheast1,
ApSoutheast2,
ApNortheast1,
ApNortheast2,
ApSouth1,
SaEast1,
CaCentral1,
MeSouth1,
AfSouth1,
Any,
Custom(String),
}
impl fmt::Display for FunctionRegion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UsEast1 => write!(f, "us-east-1"),
Self::UsWest1 => write!(f, "us-west-1"),
Self::UsCentral1 => write!(f, "us-central-1"),
Self::EuWest1 => write!(f, "eu-west-1"),
Self::EuWest2 => write!(f, "eu-west-2"),
Self::EuWest3 => write!(f, "eu-west-3"),
Self::EuCentral1 => write!(f, "eu-central-1"),
Self::EuCentral2 => write!(f, "eu-central-2"),
Self::ApSoutheast1 => write!(f, "ap-southeast-1"),
Self::ApSoutheast2 => write!(f, "ap-southeast-2"),
Self::ApNortheast1 => write!(f, "ap-northeast-1"),
Self::ApNortheast2 => write!(f, "ap-northeast-2"),
Self::ApSouth1 => write!(f, "ap-south-1"),
Self::SaEast1 => write!(f, "sa-east-1"),
Self::CaCentral1 => write!(f, "ca-central-1"),
Self::MeSouth1 => write!(f, "me-south-1"),
Self::AfSouth1 => write!(f, "af-south-1"),
Self::Any => write!(f, "any"),
Self::Custom(s) => write!(f, "{}", s),
}
}
}
#[derive(Debug, Clone)]
pub enum InvokeBody {
Json(Value),
Bytes(Vec<u8>),
Text(String),
None,
}
#[derive(Debug, Clone)]
pub struct InvokeOptions {
pub(crate) body: InvokeBody,
pub(crate) method: HttpMethod,
pub(crate) headers: HashMap<String, String>,
pub(crate) region: Option<FunctionRegion>,
pub(crate) content_type: Option<String>,
pub(crate) authorization: Option<String>,
}
impl Default for InvokeOptions {
fn default() -> Self {
Self::new()
}
}
impl InvokeOptions {
pub fn new() -> Self {
Self {
body: InvokeBody::None,
method: HttpMethod::Post,
headers: HashMap::new(),
region: None,
content_type: None,
authorization: None,
}
}
pub fn body(mut self, value: Value) -> Self {
self.body = InvokeBody::Json(value);
self
}
pub fn body_bytes(mut self, bytes: Vec<u8>) -> Self {
self.body = InvokeBody::Bytes(bytes);
self
}
pub fn body_text(mut self, text: impl Into<String>) -> Self {
self.body = InvokeBody::Text(text.into());
self
}
pub fn method(mut self, method: HttpMethod) -> Self {
self.method = method;
self
}
pub fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.insert(key.into(), value.into());
self
}
pub fn headers(mut self, headers: HashMap<String, String>) -> Self {
self.headers.extend(headers);
self
}
pub fn region(mut self, region: FunctionRegion) -> Self {
self.region = Some(region);
self
}
pub fn content_type(mut self, ct: impl Into<String>) -> Self {
self.content_type = Some(ct.into());
self
}
pub fn authorization(mut self, auth: impl Into<String>) -> Self {
self.authorization = Some(auth.into());
self
}
}
#[derive(Debug, Clone)]
pub struct FunctionResponse {
status: u16,
headers: HashMap<String, String>,
body: Vec<u8>,
}
impl FunctionResponse {
pub(crate) fn new(status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Self {
Self {
status,
headers,
body,
}
}
pub fn status(&self) -> u16 {
self.status
}
pub fn headers(&self) -> &HashMap<String, String> {
&self.headers
}
pub fn header(&self, name: &str) -> Option<&str> {
let lower = name.to_lowercase();
self.headers.get(&lower).map(|s| s.as_str())
}
pub fn json<T: DeserializeOwned>(&self) -> Result<T, FunctionsError> {
serde_json::from_slice(&self.body).map_err(FunctionsError::from)
}
pub fn text(&self) -> Result<String, FunctionsError> {
String::from_utf8(self.body.clone()).map_err(|e| {
FunctionsError::InvalidConfig(format!("Response body is not valid UTF-8: {}", e))
})
}
pub fn bytes(&self) -> &[u8] {
&self.body
}
pub fn into_bytes(self) -> Vec<u8> {
self.body
}
pub fn content_type(&self) -> Option<&str> {
self.header("content-type")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn http_method_display() {
assert_eq!(HttpMethod::Get.to_string(), "GET");
assert_eq!(HttpMethod::Post.to_string(), "POST");
assert_eq!(HttpMethod::Put.to_string(), "PUT");
assert_eq!(HttpMethod::Patch.to_string(), "PATCH");
assert_eq!(HttpMethod::Delete.to_string(), "DELETE");
assert_eq!(HttpMethod::Options.to_string(), "OPTIONS");
assert_eq!(HttpMethod::Head.to_string(), "HEAD");
}
#[test]
fn function_region_display() {
assert_eq!(FunctionRegion::UsEast1.to_string(), "us-east-1");
assert_eq!(FunctionRegion::EuWest1.to_string(), "eu-west-1");
assert_eq!(FunctionRegion::ApNortheast1.to_string(), "ap-northeast-1");
assert_eq!(FunctionRegion::Any.to_string(), "any");
assert_eq!(
FunctionRegion::Custom("my-region".into()).to_string(),
"my-region"
);
}
#[test]
fn invoke_options_defaults() {
let opts = InvokeOptions::new();
assert!(matches!(opts.body, InvokeBody::None));
assert_eq!(opts.method, HttpMethod::Post);
assert!(opts.headers.is_empty());
assert!(opts.region.is_none());
assert!(opts.content_type.is_none());
assert!(opts.authorization.is_none());
}
#[test]
fn invoke_options_builder() {
let opts = InvokeOptions::new()
.body(serde_json::json!({"key": "value"}))
.method(HttpMethod::Put)
.header("x-custom", "test")
.region(FunctionRegion::UsEast1)
.content_type("text/plain")
.authorization("Bearer token123");
assert!(matches!(opts.body, InvokeBody::Json(_)));
assert_eq!(opts.method, HttpMethod::Put);
assert_eq!(opts.headers.get("x-custom"), Some(&"test".to_string()));
assert_eq!(opts.region, Some(FunctionRegion::UsEast1));
assert_eq!(opts.content_type, Some("text/plain".to_string()));
assert_eq!(
opts.authorization,
Some("Bearer token123".to_string())
);
}
#[test]
fn invoke_options_body_bytes() {
let opts = InvokeOptions::new().body_bytes(vec![1, 2, 3]);
assert!(matches!(opts.body, InvokeBody::Bytes(ref b) if b == &[1, 2, 3]));
}
#[test]
fn invoke_options_body_text() {
let opts = InvokeOptions::new().body_text("hello");
assert!(matches!(opts.body, InvokeBody::Text(ref s) if s == "hello"));
}
#[test]
fn invoke_options_multiple_headers() {
let mut extra = HashMap::new();
extra.insert("a".into(), "1".into());
extra.insert("b".into(), "2".into());
let opts = InvokeOptions::new().header("x", "y").headers(extra);
assert_eq!(opts.headers.len(), 3);
assert_eq!(opts.headers.get("x"), Some(&"y".to_string()));
assert_eq!(opts.headers.get("a"), Some(&"1".to_string()));
}
#[test]
fn function_response_json() {
let resp = FunctionResponse::new(
200,
HashMap::new(),
br#"{"message":"hello"}"#.to_vec(),
);
let val: serde_json::Value = resp.json().unwrap();
assert_eq!(val["message"], "hello");
}
#[test]
fn function_response_text() {
let resp = FunctionResponse::new(200, HashMap::new(), b"hello world".to_vec());
assert_eq!(resp.text().unwrap(), "hello world");
}
#[test]
fn function_response_bytes() {
let data = vec![0, 1, 2, 255];
let resp = FunctionResponse::new(200, HashMap::new(), data.clone());
assert_eq!(resp.bytes(), &data);
}
#[test]
fn function_response_header_case_insensitive() {
let mut headers = HashMap::new();
headers.insert("content-type".into(), "application/json".into());
headers.insert("x-custom".into(), "value".into());
let resp = FunctionResponse::new(200, headers, vec![]);
assert_eq!(resp.header("Content-Type"), Some("application/json"));
assert_eq!(resp.header("X-Custom"), Some("value"));
assert_eq!(resp.header("missing"), None);
}
#[test]
fn function_response_content_type() {
let mut headers = HashMap::new();
headers.insert("content-type".into(), "text/plain".into());
let resp = FunctionResponse::new(200, headers, vec![]);
assert_eq!(resp.content_type(), Some("text/plain"));
}
#[test]
fn function_response_status() {
let resp = FunctionResponse::new(201, HashMap::new(), vec![]);
assert_eq!(resp.status(), 201);
}
#[test]
fn function_response_into_bytes() {
let data = vec![10, 20, 30];
let resp = FunctionResponse::new(200, HashMap::new(), data.clone());
assert_eq!(resp.into_bytes(), data);
}
}