use std::collections::HashMap;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct HttpMethod(pub String);
impl HttpMethod {
#[must_use]
pub fn new(method: &str) -> Self {
Self(method.to_uppercase())
}
#[must_use]
pub fn matches(&self, other: &str) -> bool {
self.0.eq_ignore_ascii_case(other)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone)]
pub struct HttpTriggerRoute {
pub function_name: String,
pub method: String,
pub path: String,
pub requires_auth: bool,
}
impl HttpTriggerRoute {
#[must_use]
pub fn new(function_name: &str, method: &str, path: &str) -> Self {
Self {
function_name: function_name.to_string(),
method: method.to_string(),
path: path.to_string(),
requires_auth: false,
}
}
#[must_use = "builder method returns modified builder"]
pub const fn with_auth(mut self) -> Self {
self.requires_auth = true;
self
}
#[must_use = "builder method returns modified builder"]
pub const fn without_auth(mut self) -> Self {
self.requires_auth = false;
self
}
#[must_use]
pub fn matches(&self, method: &str, path: &str) -> bool {
self.method.eq_ignore_ascii_case(method) && self.path == path
}
#[must_use]
pub fn pattern_matches(&self, request_path: &str) -> bool {
let route_parts: Vec<&str> = self.path.split('/').collect();
let request_parts: Vec<&str> = request_path.split('/').collect();
if route_parts.len() != request_parts.len() {
return false;
}
route_parts.iter().zip(request_parts.iter()).all(|(route_part, request_part)| {
route_part == request_part || route_part.starts_with(':')
})
}
#[must_use]
pub fn extract_params(&self, request_path: &str) -> HashMap<String, String> {
let mut params = HashMap::new();
let route_parts: Vec<&str> = self.path.split('/').collect();
let request_parts: Vec<&str> = request_path.split('/').collect();
for (route_part, request_part) in route_parts.iter().zip(request_parts.iter()) {
if let Some(param_name) = route_part.strip_prefix(':') {
params.insert(param_name.to_string(), request_part.to_string());
}
}
params
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpTriggerPayload {
pub method: String,
pub path: String,
pub headers: serde_json::Value,
pub query: serde_json::Value,
pub params: serde_json::Value,
pub body: Option<serde_json::Value>,
}
impl HttpTriggerPayload {
#[must_use]
pub fn new(
method: &str,
path: &str,
headers: serde_json::Value,
query: serde_json::Value,
body: Option<serde_json::Value>,
) -> Self {
Self {
method: method.to_string(),
path: path.to_string(),
headers,
query,
params: serde_json::json!({}),
body,
}
}
#[must_use]
pub fn header(&self, name: &str) -> Option<String> {
let name_lower = name.to_lowercase();
if let serde_json::Value::Object(ref obj) = self.headers {
for (key, value) in obj {
if key.to_lowercase() == name_lower {
return value.as_str().map(|s| s.to_string());
}
}
}
None
}
#[must_use]
pub fn query_param(&self, name: &str) -> Option<String> {
self.query.get(name).and_then(|v| v.as_str().map(|s| s.to_string()))
}
#[must_use]
pub fn path_param(&self, name: &str) -> Option<String> {
self.params.get(name).and_then(|v| v.as_str().map(|s| s.to_string()))
}
#[must_use]
pub const fn json_body(&self) -> Option<&serde_json::Value> {
self.body.as_ref()
}
#[must_use]
pub fn is_get(&self) -> bool {
self.method.eq_ignore_ascii_case("GET")
}
#[must_use]
pub fn is_post(&self) -> bool {
self.method.eq_ignore_ascii_case("POST")
}
#[must_use]
pub fn is_put(&self) -> bool {
self.method.eq_ignore_ascii_case("PUT")
}
#[must_use]
pub fn is_delete(&self) -> bool {
self.method.eq_ignore_ascii_case("DELETE")
}
#[must_use]
pub fn is_patch(&self) -> bool {
self.method.eq_ignore_ascii_case("PATCH")
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HttpTriggerResponse {
pub status: u16,
pub headers: serde_json::Value,
pub body: serde_json::Value,
}
impl HttpTriggerResponse {
#[must_use]
pub fn ok(body: serde_json::Value) -> Self {
Self {
status: 200,
headers: serde_json::json!({}),
body,
}
}
#[must_use]
pub fn with_status(status: u16, body: serde_json::Value) -> Self {
Self {
status,
headers: serde_json::json!({}),
body,
}
}
#[must_use]
pub fn created(body: serde_json::Value) -> Self {
Self::with_status(201, body)
}
#[must_use]
pub fn no_content() -> Self {
Self {
status: 204,
headers: serde_json::json!({}),
body: serde_json::json!({}),
}
}
#[must_use]
pub fn bad_request(message: &str) -> Self {
Self::with_status(400, serde_json::json!({"error": message}))
}
#[must_use]
pub fn unauthorized() -> Self {
Self::with_status(401, serde_json::json!({"error": "Unauthorized"}))
}
#[must_use]
pub fn forbidden() -> Self {
Self::with_status(403, serde_json::json!({"error": "Forbidden"}))
}
#[must_use]
pub fn not_found() -> Self {
Self::with_status(404, serde_json::json!({"error": "Not found"}))
}
#[must_use]
pub fn internal_error(message: &str) -> Self {
Self::with_status(500, serde_json::json!({"error": message}))
}
#[must_use = "builder method returns modified builder"]
pub fn with_header(mut self, key: String, value: String) -> Self {
if let serde_json::Value::Object(ref mut map) = self.headers {
map.insert(key, serde_json::Value::String(value));
}
self
}
}
#[derive(Debug, Clone, Default)]
pub struct HttpTriggerMatcher {
routes: Vec<HttpTriggerRoute>,
}
impl HttpTriggerMatcher {
#[must_use]
pub const fn new() -> Self {
Self { routes: Vec::new() }
}
pub fn add(&mut self, route: HttpTriggerRoute) {
self.routes.push(route);
}
#[must_use]
pub fn find(&self, method: &str, path: &str) -> Option<HttpTriggerRoute> {
self.routes
.iter()
.find(|route| route.method.eq_ignore_ascii_case(method) && route.pattern_matches(path))
.cloned()
}
#[must_use]
pub fn routes(&self) -> &[HttpTriggerRoute] {
&self.routes
}
}
#[cfg(test)]
mod tests;