use std::{collections::HashMap, future::Future};
use crate::{Error, HttpRequest, Method};
#[cfg(feature = "serde")]
use serde::de::DeserializeOwned;
pub trait IntoHandlerError {
fn into_handler_error(self) -> Error;
}
impl IntoHandlerError for Error {
fn into_handler_error(self) -> Error {
self
}
}
pub trait FromRequest: Sized {
type Error: IntoHandlerError;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error>;
type Future: Future<Output = Result<Self, Self::Error>>;
fn from_request_async(req: HttpRequest) -> Self::Future;
}
impl FromRequest for HttpRequest {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(_req: &HttpRequest) -> Result<Self, Self::Error> {
Err(Error::bad_request(
"Cannot extract HttpRequest directly due to Clone limitations. Extract specific data instead.",
))
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Ok(req))
}
}
#[derive(Debug, Clone)]
pub struct RequestData {
pub method: Method,
pub path: String,
pub query: String,
pub headers: HashMap<String, String>,
pub remote_addr: Option<String>,
pub user_agent: Option<String>,
pub content_type: Option<String>,
}
impl RequestData {
#[must_use]
pub fn header(&self, name: &str) -> Option<&String> {
self.headers.get(name)
}
#[must_use]
pub fn has_header(&self, name: &str) -> bool {
self.headers.contains_key(name)
}
#[must_use]
pub fn content_length(&self) -> Option<usize> {
self.header("content-length").and_then(|v| v.parse().ok())
}
}
impl FromRequest for RequestData {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
let method = req.method();
let path = req.path().to_string();
let query = req.query_string().to_string();
let remote_addr = req.remote_addr();
let mut headers = HashMap::new();
let cookies = req.cookies();
for (name, value) in &cookies {
headers.insert(format!("cookie-{name}"), value.clone());
}
let user_agent = req
.header("user-agent")
.map(std::string::ToString::to_string);
if let Some(ua) = &user_agent {
headers.insert("user-agent".to_string(), ua.clone());
}
let content_type = req
.header("content-type")
.map(std::string::ToString::to_string);
if let Some(ct) = &content_type {
headers.insert("content-type".to_string(), ct.clone());
}
if let Some(auth) = req.header("authorization") {
headers.insert("authorization".to_string(), auth.to_string());
}
if let Some(accept) = req.header("accept") {
headers.insert("accept".to_string(), accept.to_string());
}
Ok(Self {
method,
path,
query,
headers,
remote_addr,
user_agent,
content_type,
})
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
impl FromRequest for String {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
Ok(req.query_string().to_string())
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
impl FromRequest for u32 {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
let query = req.query_string();
query
.parse::<Self>()
.map_err(|e| Error::bad_request(format!("Failed to parse query as u32: {e}")))
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
impl FromRequest for i32 {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
let query = req.query_string();
query
.parse::<Self>()
.map_err(|e| Error::bad_request(format!("Failed to parse query as i32: {e}")))
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
impl FromRequest for bool {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
let query = req.query_string().to_lowercase();
let value = matches!(query.as_str(), "true" | "1" | "yes" | "on");
Ok(value)
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone)]
pub struct Query<T>(pub T);
#[cfg(feature = "serde")]
impl<T> FromRequest for Query<T>
where
T: DeserializeOwned + Send + 'static,
{
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
req.parse_query::<T>()
.map(Query)
.map_err(|e| Error::bad_request(format!("Failed to parse query parameters: {e}")))
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone)]
pub struct Json<T>(pub T);
#[cfg(feature = "serde")]
impl<T> FromRequest for Json<T>
where
T: DeserializeOwned + Send + 'static,
{
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
req.body().map_or_else(|| Err(Error::bad_request(
"JSON body not available. For Actix backend, body must be pre-extracted. For Simulator backend, ensure body is set on the request."
)), |body| match serde_json::from_slice::<T>(body) {
Ok(value) => Ok(Self(value)),
Err(e) => Err(Error::bad_request(format!("Failed to parse JSON body: {e}"))),
})
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
#[derive(Debug, Clone)]
pub struct Headers {
headers: HashMap<String, String>,
}
impl Headers {
#[must_use]
pub fn get(&self, name: &str) -> Option<&String> {
self.headers.get(&name.to_lowercase())
}
#[must_use]
pub fn contains(&self, name: &str) -> bool {
self.headers.contains_key(&name.to_lowercase())
}
#[must_use]
pub const fn all(&self) -> &HashMap<String, String> {
&self.headers
}
#[must_use]
pub fn authorization(&self) -> Option<&String> {
self.get("authorization")
}
#[must_use]
pub fn content_type(&self) -> Option<&String> {
self.get("content-type")
}
#[must_use]
pub fn user_agent(&self) -> Option<&String> {
self.get("user-agent")
}
}
impl FromRequest for Headers {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
let mut headers = HashMap::new();
if let Some(auth) = req.header("authorization") {
headers.insert("authorization".to_string(), auth.to_string());
}
if let Some(ct) = req.header("content-type") {
headers.insert("content-type".to_string(), ct.to_string());
}
if let Some(ua) = req.header("user-agent") {
headers.insert("user-agent".to_string(), ua.to_string());
}
if let Some(accept) = req.header("accept") {
headers.insert("accept".to_string(), accept.to_string());
}
if let Some(host) = req.header("host") {
headers.insert("host".to_string(), host.to_string());
}
Ok(Self { headers })
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
#[derive(Debug, Clone)]
pub struct RequestInfo {
pub method: Method,
pub path: String,
pub query: String,
pub remote_addr: Option<String>,
}
impl FromRequest for RequestInfo {
type Error = Error;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
Ok(Self {
method: req.method(),
path: req.path().to_string(),
query: req.query_string().to_string(),
remote_addr: req.remote_addr(),
})
}
fn from_request_async(req: HttpRequest) -> Self::Future {
std::future::ready(Self::from_request_sync(&req))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(any(feature = "simulator", not(feature = "actix")))]
use crate::{Method, simulator::SimulationRequest};
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn create_test_request() -> HttpRequest {
let sim_req = SimulationRequest::new(Method::Get, "/api/users")
.with_query_string("page=1&limit=20")
.with_header("user-agent", "TestAgent/1.0")
.with_header("content-type", "application/json")
.with_header("authorization", "Bearer token123")
.with_header("accept", "application/json");
HttpRequest::new(crate::simulator::SimulationStub::from(sim_req))
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_request_data_extraction() {
let req = create_test_request();
let result = RequestData::from_request_sync(&req);
assert!(result.is_ok());
let data = result.unwrap();
assert_eq!(data.method, Method::Get);
assert_eq!(data.path, "/api/users");
assert_eq!(data.query, "page=1&limit=20");
assert_eq!(data.user_agent, Some("TestAgent/1.0".to_string()));
assert_eq!(data.content_type, Some("application/json".to_string()));
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_request_data_header_methods() {
let req = create_test_request();
let data = RequestData::from_request_sync(&req).unwrap();
assert!(data.has_header("user-agent"));
assert!(data.has_header("content-type"));
assert!(!data.has_header("non-existent"));
assert_eq!(
data.header("authorization"),
Some(&"Bearer token123".to_string())
);
assert_eq!(data.header("accept"), Some(&"application/json".to_string()));
assert_eq!(data.header("non-existent"), None);
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_request_info_extraction() {
let req = create_test_request();
let result = RequestInfo::from_request_sync(&req);
assert!(result.is_ok());
let info = result.unwrap();
assert_eq!(info.method, Method::Get);
assert_eq!(info.path, "/api/users");
assert_eq!(info.query, "page=1&limit=20");
assert!(info.remote_addr.is_none());
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_headers_extraction() {
let req = create_test_request();
let result = Headers::from_request_sync(&req);
assert!(result.is_ok());
let headers = result.unwrap();
assert!(headers.contains("authorization"));
assert!(headers.contains("content-type"));
assert!(headers.contains("user-agent"));
assert!(!headers.contains("non-existent"));
assert_eq!(
headers.authorization(),
Some(&"Bearer token123".to_string())
);
assert_eq!(
headers.content_type(),
Some(&"application/json".to_string())
);
assert_eq!(headers.user_agent(), Some(&"TestAgent/1.0".to_string()));
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_headers_all() {
let req = create_test_request();
let headers = Headers::from_request_sync(&req).unwrap();
let all_headers = headers.all();
assert!(!all_headers.is_empty());
assert!(all_headers.contains_key("authorization"));
assert!(all_headers.contains_key("content-type"));
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_string_from_request() {
let req = create_test_request();
let result = String::from_request_sync(&req);
assert!(result.is_ok());
let query = result.unwrap();
assert_eq!(query, "page=1&limit=20");
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_u32_from_request_valid() {
let sim_req = SimulationRequest::new(Method::Get, "/test").with_query_string("42");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let result = u32::from_request_sync(&req);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_u32_from_request_invalid() {
let sim_req =
SimulationRequest::new(Method::Get, "/test").with_query_string("not_a_number");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let result = u32::from_request_sync(&req);
assert!(result.is_err());
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_i32_from_request_valid() {
let sim_req = SimulationRequest::new(Method::Get, "/test").with_query_string("-123");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let result = i32::from_request_sync(&req);
assert!(result.is_ok());
assert_eq!(result.unwrap(), -123);
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_bool_from_request_true_values() {
for true_value in &["true", "1", "yes", "on", "TRUE", "YES", "ON"] {
let sim_req =
SimulationRequest::new(Method::Get, "/test").with_query_string(*true_value);
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let result = bool::from_request_sync(&req);
assert!(result.is_ok());
assert!(result.unwrap(), "Failed for value: {true_value}");
}
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_bool_from_request_false_values() {
for false_value in &[
"false",
"0",
"no",
"off",
"FALSE",
"NO",
"OFF",
"anything_else",
] {
let sim_req =
SimulationRequest::new(Method::Get, "/test").with_query_string(*false_value);
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let result = bool::from_request_sync(&req);
assert!(result.is_ok());
assert!(!result.unwrap(), "Failed for value: {false_value}");
}
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_http_request_from_request_error() {
let req = create_test_request();
let result = HttpRequest::from_request_sync(&req);
assert!(result.is_err());
}
#[test_log::test]
fn test_into_handler_error() {
let error = Error::bad_request("Test error");
let handler_error = error.into_handler_error();
match handler_error {
Error::Http { status_code, .. } => {
assert_eq!(status_code, crate::StatusCode::BadRequest);
}
}
}
#[test_log::test]
fn test_request_data_content_length_when_header_present() {
let mut headers = HashMap::new();
headers.insert("content-length".to_string(), "1024".to_string());
let data = RequestData {
method: Method::Post,
path: "/api/data".to_string(),
query: String::new(),
headers,
remote_addr: None,
user_agent: None,
content_type: None,
};
assert_eq!(data.content_length(), Some(1024));
}
#[test_log::test]
fn test_request_data_content_length_zero() {
let mut headers = HashMap::new();
headers.insert("content-length".to_string(), "0".to_string());
let data = RequestData {
method: Method::Post,
path: "/api/data".to_string(),
query: String::new(),
headers,
remote_addr: None,
user_agent: None,
content_type: None,
};
assert_eq!(data.content_length(), Some(0));
}
#[test_log::test]
fn test_request_data_content_length_missing() {
let data = RequestData {
method: Method::Get,
path: "/api/data".to_string(),
query: String::new(),
headers: HashMap::new(),
remote_addr: None,
user_agent: None,
content_type: None,
};
assert_eq!(data.content_length(), None);
}
#[test_log::test]
fn test_request_data_content_length_invalid_not_number() {
let mut headers = HashMap::new();
headers.insert("content-length".to_string(), "not-a-number".to_string());
let data = RequestData {
method: Method::Post,
path: "/api/data".to_string(),
query: String::new(),
headers,
remote_addr: None,
user_agent: None,
content_type: None,
};
assert_eq!(data.content_length(), None);
}
#[test_log::test]
fn test_request_data_content_length_large_value() {
let mut headers = HashMap::new();
headers.insert("content-length".to_string(), "999999999".to_string());
let data = RequestData {
method: Method::Post,
path: "/api/data".to_string(),
query: String::new(),
headers,
remote_addr: None,
user_agent: None,
content_type: None,
};
assert_eq!(data.content_length(), Some(999_999_999));
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_request_data_remote_addr_present() {
let sim_req =
SimulationRequest::new(Method::Get, "/test").with_remote_addr("192.168.1.50:43210");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let data = RequestData::from_request_sync(&req).unwrap();
assert_eq!(data.remote_addr, Some("192.168.1.50:43210".to_string()));
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_request_data_remote_addr_absent() {
let sim_req = SimulationRequest::new(Method::Get, "/test");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let data = RequestData::from_request_sync(&req).unwrap();
assert_eq!(data.remote_addr, None);
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_request_info_remote_addr_present() {
let sim_req =
SimulationRequest::new(Method::Get, "/api/info").with_remote_addr("10.0.0.1:8080");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let info = RequestInfo::from_request_sync(&req).unwrap();
assert_eq!(info.remote_addr, Some("10.0.0.1:8080".to_string()));
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_headers_extracts_common_headers() {
let sim_req = SimulationRequest::new(Method::Get, "/test")
.with_header("authorization", "Bearer token123")
.with_header("content-type", "application/json")
.with_header("user-agent", "TestAgent/1.0")
.with_header("accept", "text/html")
.with_header("host", "example.com");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let headers = Headers::from_request_sync(&req).unwrap();
assert_eq!(
headers.authorization(),
Some(&"Bearer token123".to_string())
);
assert_eq!(
headers.content_type(),
Some(&"application/json".to_string())
);
assert_eq!(headers.user_agent(), Some(&"TestAgent/1.0".to_string()));
assert_eq!(headers.get("accept"), Some(&"text/html".to_string()));
assert_eq!(headers.get("host"), Some(&"example.com".to_string()));
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_headers_custom_headers_not_extracted() {
let sim_req = SimulationRequest::new(Method::Get, "/test")
.with_header("x-custom-header", "custom-value")
.with_header("x-request-id", "req-12345");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let headers = Headers::from_request_sync(&req).unwrap();
assert_eq!(headers.get("x-custom-header"), None);
assert_eq!(headers.get("x-request-id"), None);
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_headers_accept_via_get() {
let sim_req = SimulationRequest::new(Method::Get, "/test")
.with_header("accept", "text/html,application/xhtml+xml");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let headers = Headers::from_request_sync(&req).unwrap();
assert_eq!(
headers.get("accept"),
Some(&"text/html,application/xhtml+xml".to_string())
);
}
#[test_log::test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_headers_empty_request() {
let sim_req = SimulationRequest::new(Method::Get, "/test");
let req = HttpRequest::new(crate::simulator::SimulationStub::from(sim_req));
let headers = Headers::from_request_sync(&req).unwrap();
assert!(headers.authorization().is_none());
assert!(headers.content_type().is_none());
assert!(headers.user_agent().is_none());
assert!(headers.get("accept").is_none());
}
}