use crate::errors::{HttpError, HttpResult};
use crate::request::ElifRequest;
use serde::de::DeserializeOwned;
use std::collections::HashMap;
use std::str::FromStr;
#[derive(Debug)]
pub struct ElifQuery<T>(pub T);
impl<T: DeserializeOwned> ElifQuery<T> {
pub fn from_request(request: &ElifRequest) -> HttpResult<Self> {
let query_str = request.query_string().unwrap_or("");
let data = serde_urlencoded::from_str::<T>(query_str)
.map_err(|e| HttpError::bad_request(format!("Invalid query parameters: {}", e)))?;
Ok(ElifQuery(data))
}
}
#[derive(Debug)]
pub struct ElifPath<T>(pub T);
impl<T: DeserializeOwned> ElifPath<T> {
pub fn from_request(request: &ElifRequest) -> HttpResult<Self> {
let data = request.path_params::<T>()?;
Ok(ElifPath(data))
}
}
#[derive(Debug)]
pub struct ElifState<T>(pub T);
impl<T: Clone> ElifState<T> {
pub fn new(state: T) -> Self {
ElifState(state)
}
pub fn inner(&self) -> &T {
&self.0
}
pub fn into_inner(self) -> T {
self.0
}
}
impl ElifRequest {
pub fn input<T>(&self, key: &str, default: T) -> T
where
T: FromStr + Clone,
T::Err: std::fmt::Debug,
{
self.query_param(key)
.or_else(|| self.path_param(key))
.and_then(|s| s.parse().ok())
.unwrap_or(default)
}
pub fn input_optional<T>(&self, key: &str) -> Option<T>
where
T: FromStr,
T::Err: std::fmt::Debug,
{
self.query_param(key)
.or_else(|| self.path_param(key))
.and_then(|s| s.parse().ok())
}
pub fn string(&self, key: &str, default: &str) -> String {
self.query_param(key)
.or_else(|| self.path_param(key))
.cloned()
.unwrap_or_else(|| default.to_string())
}
pub fn string_optional(&self, key: &str) -> Option<String> {
self.query_param(key)
.or_else(|| self.path_param(key))
.cloned()
}
pub fn integer(&self, key: &str, default: i64) -> i64 {
self.input(key, default)
}
pub fn integer_optional(&self, key: &str) -> Option<i64> {
self.input_optional(key)
}
pub fn boolean(&self, key: &str, default: bool) -> bool {
self.query_param(key)
.or_else(|| self.path_param(key))
.map(|s| match s.to_lowercase().as_str() {
"true" | "1" | "on" | "yes" => true,
"false" | "0" | "off" | "no" => false,
_ => default,
})
.unwrap_or(default)
}
pub fn inputs(&self, keys: &[&str]) -> HashMap<String, String> {
keys.iter()
.filter_map(|&key| {
self.query_param(key)
.or_else(|| self.path_param(key))
.map(|val| (key.to_string(), val.clone()))
})
.collect()
}
pub fn all_query(&self) -> HashMap<String, String> {
self.query_params
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect()
}
pub fn has(&self, key: &str) -> bool {
self.query_param(key).is_some() || self.path_param(key).is_some()
}
pub fn filled(&self, key: &str) -> bool {
self.query_param(key)
.or_else(|| self.path_param(key))
.map(|s| !s.trim().is_empty())
.unwrap_or(false)
}
pub fn array(&self, key: &str) -> Vec<String> {
if let Some(value) = self.query_param(key).or_else(|| self.path_param(key)) {
value
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
} else {
Vec::new()
}
}
pub fn pagination(&self) -> (u32, u32) {
let page = self.input("page", 1u32).max(1);
let per_page = self.input("per_page", 10u32).clamp(1, 100);
(page, per_page)
}
pub fn sorting(&self, default_field: &str) -> (String, String) {
let sort = self.string("sort", default_field);
let order = self.string("order", "asc");
let direction = match order.to_lowercase().as_str() {
"desc" | "descending" | "down" => "desc".to_string(),
_ => "asc".to_string(),
};
(sort, direction)
}
pub fn filters(&self) -> HashMap<String, String> {
self.inputs(&[
"search", "q", "query", "status", "state", "category", "type", "filter", "filters",
])
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::request::ElifRequest;
use serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq)]
struct TestQuery {
name: String,
age: Option<u32>,
active: Option<bool>,
}
#[derive(Debug, Deserialize, PartialEq)]
struct TestPath {
id: u32,
slug: String,
}
#[derive(Debug, Clone, PartialEq)]
struct TestAppState {
database_url: String,
api_key: String,
}
fn create_test_request_with_query(query: &str) -> ElifRequest {
use axum::body::Body;
use axum::extract::Request;
let uri = if query.is_empty() {
"/test".to_string()
} else {
format!("/test?{}", query)
};
let request = Request::builder()
.method("GET")
.uri(uri)
.body(Body::empty())
.unwrap();
let (parts, _body) = request.into_parts();
ElifRequest::extract_elif_request(
crate::request::ElifMethod::from_axum(parts.method),
parts.uri,
crate::response::headers::ElifHeaderMap::from_axum(parts.headers),
None,
)
}
#[test]
fn test_elif_query_extraction_success() {
let request = create_test_request_with_query("name=John&age=30&active=true");
let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
assert!(result.is_ok());
let query = result.unwrap();
assert_eq!(query.0.name, "John");
assert_eq!(query.0.age, Some(30));
assert_eq!(query.0.active, Some(true));
}
#[test]
fn test_elif_query_extraction_partial() {
let request = create_test_request_with_query("name=Alice");
let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
assert!(result.is_ok());
let query = result.unwrap();
assert_eq!(query.0.name, "Alice");
assert_eq!(query.0.age, None);
assert_eq!(query.0.active, None);
}
#[test]
fn test_elif_query_extraction_empty() {
let request = create_test_request_with_query("");
let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HttpError::BadRequest { .. }));
}
#[test]
fn test_elif_query_extraction_invalid_format() {
let request = create_test_request_with_query("name=John&age=not_a_number");
let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), HttpError::BadRequest { .. }));
}
#[test]
fn test_elif_query_url_decoding() {
let request = create_test_request_with_query("name=John%20Doe&active=true");
let result: Result<ElifQuery<TestQuery>, _> = ElifQuery::from_request(&request);
assert!(result.is_ok());
let query = result.unwrap();
assert_eq!(query.0.name, "John Doe");
}
#[test]
fn test_elif_state_creation_and_access() {
let state = TestAppState {
database_url: "postgres://localhost:5432/test".to_string(),
api_key: "secret_key_123".to_string(),
};
let elif_state = ElifState::new(state.clone());
assert_eq!(elif_state.inner(), &state);
let recovered_state = elif_state.into_inner();
assert_eq!(recovered_state, state);
}
#[test]
fn test_elif_state_clone_requirement() {
#[derive(Clone, PartialEq, Debug)]
struct CloneableState {
value: i32,
}
let state = CloneableState { value: 42 };
let elif_state = ElifState::new(state.clone());
assert_eq!(elif_state.inner().value, 42);
assert_eq!(elif_state.into_inner(), state);
}
#[test]
fn test_elif_query_debug_impl() {
let query = ElifQuery(TestQuery {
name: "Test".to_string(),
age: Some(25),
active: Some(false),
});
let debug_string = format!("{:?}", query);
assert!(debug_string.contains("ElifQuery"));
assert!(debug_string.contains("Test"));
}
#[test]
fn test_elif_path_debug_impl() {
let path = ElifPath(TestPath {
id: 123,
slug: "test-slug".to_string(),
});
let debug_string = format!("{:?}", path);
assert!(debug_string.contains("ElifPath"));
assert!(debug_string.contains("123"));
assert!(debug_string.contains("test-slug"));
}
#[test]
fn test_elif_state_debug_impl() {
let state = ElifState::new(TestAppState {
database_url: "postgres://localhost:5432/test".to_string(),
api_key: "secret_key_123".to_string(),
});
let debug_string = format!("{:?}", state);
assert!(debug_string.contains("ElifState"));
}
}