use std::{any::TypeId, collections::BTreeMap, sync::Arc};
use bytes::Bytes;
use switchy_http_models::Method;
use crate::PathParams;
pub type ErasedState = Arc<dyn std::any::Any + Send + Sync>;
pub trait HttpRequestTrait: Send + Sync {
fn path(&self) -> &str;
fn query_string(&self) -> &str;
fn method(&self) -> Method;
fn header(&self, name: &str) -> Option<&str>;
fn headers(&self) -> BTreeMap<String, String>;
fn body(&self) -> Option<&Bytes>;
fn cookie(&self, name: &str) -> Option<String>;
fn cookies(&self) -> BTreeMap<String, String>;
fn remote_addr(&self) -> Option<String>;
fn path_params(&self) -> &PathParams;
fn app_state_any(&self, type_id: TypeId) -> Option<ErasedState>;
}
#[derive(Clone)]
pub struct HttpRequest {
inner: Arc<dyn HttpRequestTrait>,
}
impl std::fmt::Debug for HttpRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpRequest")
.field("path", &self.path())
.field("method", &self.method())
.field("query_string", &self.query_string())
.finish_non_exhaustive()
}
}
impl HttpRequest {
pub fn new<R: HttpRequestTrait + 'static>(request: R) -> Self {
Self {
inner: Arc::new(request),
}
}
#[must_use]
pub fn path(&self) -> &str {
self.inner.path()
}
#[must_use]
pub fn query_string(&self) -> &str {
self.inner.query_string()
}
#[must_use]
pub fn method(&self) -> Method {
self.inner.method()
}
#[must_use]
pub fn header(&self, name: &str) -> Option<&str> {
self.inner.header(name)
}
#[must_use]
pub fn headers(&self) -> BTreeMap<String, String> {
self.inner.headers()
}
#[must_use]
pub fn body(&self) -> Option<&Bytes> {
self.inner.body()
}
#[must_use]
pub fn cookie(&self, name: &str) -> Option<String> {
self.inner.cookie(name)
}
#[must_use]
pub fn cookies(&self) -> BTreeMap<String, String> {
self.inner.cookies()
}
#[must_use]
pub fn remote_addr(&self) -> Option<String> {
self.inner.remote_addr()
}
#[must_use]
pub fn path_params(&self) -> &PathParams {
self.inner.path_params()
}
#[must_use]
pub fn path_param(&self, name: &str) -> Option<&str> {
self.path_params().get(name).map(String::as_str)
}
#[must_use]
pub fn app_state<T: Send + Sync + 'static>(&self) -> Option<Arc<T>> {
self.inner
.app_state_any(TypeId::of::<T>())
.and_then(|erased| erased.downcast::<T>().ok())
}
pub fn parse_query<'a, T: serde::Deserialize<'a>>(
&'a self,
) -> Result<T, serde_querystring::Error> {
serde_querystring::from_str(
self.query_string(),
serde_querystring::ParseMode::UrlEncoded,
)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EmptyRequest;
impl HttpRequestTrait for EmptyRequest {
fn path(&self) -> &'static str {
""
}
fn query_string(&self) -> &'static str {
""
}
fn method(&self) -> Method {
Method::Get
}
fn header(&self, _name: &str) -> Option<&str> {
None
}
fn headers(&self) -> BTreeMap<String, String> {
BTreeMap::new()
}
fn body(&self) -> Option<&Bytes> {
None
}
fn cookie(&self, _name: &str) -> Option<String> {
None
}
fn cookies(&self) -> BTreeMap<String, String> {
BTreeMap::new()
}
fn remote_addr(&self) -> Option<String> {
None
}
fn path_params(&self) -> &PathParams {
static EMPTY: PathParams = BTreeMap::new();
&EMPTY
}
fn app_state_any(&self, _type_id: TypeId) -> Option<ErasedState> {
None
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockRequest {
path: String,
query_string: String,
method: Method,
headers: BTreeMap<String, String>,
body: Option<Bytes>,
cookies: BTreeMap<String, String>,
path_params: PathParams,
}
impl MockRequest {
fn new(path: &str, method: Method) -> Self {
Self {
path: path.to_string(),
query_string: String::new(),
method,
headers: BTreeMap::new(),
body: None,
cookies: BTreeMap::new(),
path_params: BTreeMap::new(),
}
}
}
impl HttpRequestTrait for MockRequest {
fn path(&self) -> &str {
&self.path
}
fn query_string(&self) -> &str {
&self.query_string
}
fn method(&self) -> Method {
self.method
}
fn header(&self, name: &str) -> Option<&str> {
self.headers.get(name).map(String::as_str)
}
fn headers(&self) -> BTreeMap<String, String> {
self.headers.clone()
}
fn body(&self) -> Option<&Bytes> {
self.body.as_ref()
}
fn cookie(&self, name: &str) -> Option<String> {
self.cookies.get(name).cloned()
}
fn cookies(&self) -> BTreeMap<String, String> {
self.cookies.clone()
}
fn remote_addr(&self) -> Option<String> {
None
}
fn path_params(&self) -> &PathParams {
&self.path_params
}
fn app_state_any(&self, _type_id: TypeId) -> Option<ErasedState> {
None
}
}
#[test]
fn test_http_request_delegates_to_inner() {
let mock = MockRequest::new("/api/users", Method::Get);
let request = HttpRequest::new(mock);
assert_eq!(request.path(), "/api/users");
assert_eq!(request.method(), Method::Get);
assert_eq!(request.query_string(), "");
}
#[test]
fn test_http_request_debug() {
let mock = MockRequest::new("/test", Method::Post);
let request = HttpRequest::new(mock);
let debug_str = format!("{request:?}");
assert!(debug_str.contains("/test"));
assert!(debug_str.contains("Post"));
}
}