#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
#![allow(clippy::multiple_crate_versions)]
use serde::de::DeserializeOwned;
use crate::{Error, HttpRequest, from_request::FromRequest, qs};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Query<T>(pub T);
impl<T> Query<T> {
#[must_use]
pub fn into_inner(self) -> T {
self.0
}
#[must_use]
pub const fn inner(&self) -> &T {
&self.0
}
}
#[derive(Debug, thiserror::Error)]
pub enum QueryError {
#[error("Failed to parse query string: {source}")]
ParseError {
#[source]
source: qs::Error,
query_string: String,
},
#[error("Failed to decode URL-encoded query string: {message}")]
DecodeError {
message: String,
query_string: String,
},
#[error("Required query parameter '{field}' is missing")]
MissingField {
field: String,
},
#[error("Query parameter '{field}' has invalid format: {message}")]
InvalidFormat {
field: String,
message: String,
},
}
impl crate::from_request::IntoHandlerError for QueryError {
fn into_handler_error(self) -> Error {
Error::bad_request(self)
}
}
impl<T> FromRequest for Query<T>
where
T: DeserializeOwned + Send + 'static,
{
type Error = QueryError;
type Future = std::future::Ready<Result<Self, Self::Error>>;
fn from_request_sync(req: &HttpRequest) -> Result<Self, Self::Error> {
let query_string = req.query_string();
if query_string.is_empty() {
return match qs::from_str::<T>("", qs::ParseMode::UrlEncoded) {
Ok(value) => Ok(Self(value)),
Err(source) => Err(QueryError::ParseError {
source,
query_string: query_string.to_string(),
}),
};
}
match qs::from_str::<T>(query_string, qs::ParseMode::UrlEncoded) {
Ok(value) => Ok(Self(value)),
Err(source) => {
Err(QueryError::ParseError {
source,
query_string: query_string.to_string(),
})
}
}
}
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 serde::Deserialize;
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
struct SimpleParams {
name: String,
age: Option<u32>,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
struct ArrayParams {
tags: String, ids: Option<String>, }
#[derive(Debug, Deserialize, PartialEq, Eq)]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
struct OptionalParams {
page: Option<u32>,
limit: Option<u32>,
}
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn create_test_request(query: &str) -> HttpRequest {
use crate::{Method, simulator::SimulationRequest};
let sim_request =
SimulationRequest::new(Method::Get, "/test").with_query_string(query.to_string());
HttpRequest::new(crate::simulator::SimulationStub::from(sim_request))
}
#[cfg(all(feature = "actix", not(feature = "simulator")))]
#[allow(dead_code)]
fn create_test_request(_query: &str) -> HttpRequest {
HttpRequest::new(crate::EmptyRequest)
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_simple_query_extraction() {
let req = create_test_request("name=john&age=30");
let result = Query::<SimpleParams>::from_request_sync(&req);
assert!(result.is_ok());
let Query(params) = result.unwrap();
assert_eq!(params.name, "john");
assert_eq!(params.age, Some(30));
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_optional_parameters() {
let req = create_test_request("name=alice");
let result = Query::<SimpleParams>::from_request_sync(&req);
assert!(result.is_ok());
let Query(params) = result.unwrap();
assert_eq!(params.name, "alice");
assert_eq!(params.age, None);
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_array_parameters() {
let req = create_test_request("tags=rust&ids=123");
let result = Query::<ArrayParams>::from_request_sync(&req);
assert!(result.is_ok());
let Query(params) = result.unwrap();
assert_eq!(params.tags, "rust");
assert_eq!(params.ids, Some("123".to_string()));
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_empty_query_string() {
let req = create_test_request("");
let result = Query::<OptionalParams>::from_request_sync(&req);
assert!(result.is_ok());
let Query(params) = result.unwrap();
assert_eq!(params.page, None);
assert_eq!(params.limit, None);
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_url_encoded_values() {
let req = create_test_request("name=john%20doe&age=25");
let result = Query::<SimpleParams>::from_request_sync(&req);
assert!(result.is_ok());
let Query(params) = result.unwrap();
assert_eq!(params.name, "john doe");
assert_eq!(params.age, Some(25));
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_missing_required_field() {
let req = create_test_request("age=30");
let result = Query::<SimpleParams>::from_request_sync(&req);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, QueryError::ParseError { .. }));
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_invalid_number_format() {
let req = create_test_request("name=john&age=not_a_number");
let result = Query::<SimpleParams>::from_request_sync(&req);
assert!(result.is_err());
let error = result.unwrap_err();
assert!(matches!(error, QueryError::ParseError { .. }));
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_async_extraction() {
use std::future::Future;
use std::task::{Context, Poll};
let req = create_test_request("name=async_test&age=42");
let future = Query::<SimpleParams>::from_request_async(req);
let mut future = Box::pin(future);
let waker = std::task::Waker::noop();
let mut context = Context::from_waker(waker);
let result = match future.as_mut().poll(&mut context) {
Poll::Ready(result) => result,
Poll::Pending => panic!("Future should be ready immediately"),
};
assert!(result.is_ok());
let Query(params) = result.unwrap();
assert_eq!(params.name, "async_test");
assert_eq!(params.age, Some(42));
}
#[test]
#[cfg(any(feature = "simulator", not(feature = "actix")))]
fn test_query_methods() {
let req = create_test_request("name=test&age=25");
let query = Query::<SimpleParams>::from_request_sync(&req).unwrap();
assert_eq!(query.inner().name, "test");
assert_eq!(query.inner().age, Some(25));
let params = query.into_inner();
assert_eq!(params.name, "test");
assert_eq!(params.age, Some(25));
}
}