switchy_web_server 0.3.0

Switchy Web Server package
//! Query parameter extractor for HTTP requests.
//!
//! This module provides the [`Query<T>`] extractor for parsing URL query parameters
//! into typed Rust structs using `serde_querystring`.
//!
//! # Overview
//!
//! The query extractor parses the URL query string and deserializes it into the
//! target type. It supports both simple key-value pairs and more complex structures
//! including optional fields and arrays.
//!
//! # Example
//!
//! ```rust,ignore
//! use switchy_web_server::extractors::Query;
//! use serde::Deserialize;
//!
//! #[derive(Deserialize)]
//! struct SearchParams {
//!     q: String,
//!     limit: Option<u32>,
//!     page: Option<u32>,
//! }
//!
//! async fn search(Query(params): Query<SearchParams>) -> Result<HttpResponse, Error> {
//!     // For URL: /search?q=rust&limit=10
//!     println!("Searching for: {}", params.q);
//!     Ok(HttpResponse::ok())
//! }
//! ```

#![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};

/// Extractor for URL query parameters with enhanced error handling
///
/// This extractor parses query parameters from the URL query string using
/// `serde_urlencoded` for robust parsing. It supports both simple and complex
/// query structures including arrays and nested objects.
///
/// # Dual-Mode Support
///
/// * **Actix backend**: Uses synchronous extraction to avoid Send bounds issues
/// * **Simulator backend**: Uses async extraction (delegates to sync implementation)
///
/// # Examples
///
/// ## Simple Query Parameters
///
/// ```rust,ignore
/// use switchy_web_server::extractors::Query;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct SearchParams {
///     q: String,
///     limit: Option<u32>,
/// }
///
/// async fn search(Query(params): Query<SearchParams>) -> Result<HttpResponse, Error> {
///     // For URL: /search?q=hello&limit=10
///     // params.q = "hello"
///     // params.limit = Some(10)
///     Ok(HttpResponse::ok())
/// }
/// ```
///
/// ## Array Parameters
///
/// ```rust,ignore
/// use switchy_web_server::extractors::Query;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct FilterParams {
///     tags: Vec<String>,
///     categories: Option<Vec<String>>,
/// }
///
/// async fn filter(Query(params): Query<FilterParams>) -> Result<HttpResponse, Error> {
///     // For URL: /filter?tags=rust&tags=web&categories=tutorial
///     // params.tags = ["rust", "web"]
///     // params.categories = Some(["tutorial"])
///     Ok(HttpResponse::ok())
/// }
/// ```
///
/// ## Optional Parameters
///
/// ```rust,ignore
/// use switchy_web_server::extractors::Query;
/// use serde::Deserialize;
///
/// #[derive(Deserialize)]
/// struct PaginationParams {
///     page: Option<u32>,
///     per_page: Option<u32>,
///     sort: Option<String>,
/// }
///
/// async fn list_items(Query(params): Query<PaginationParams>) -> Result<HttpResponse, Error> {
///     let page = params.page.unwrap_or(1);
///     let per_page = params.per_page.unwrap_or(20);
///     // Handle pagination...
///     Ok(HttpResponse::ok())
/// }
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Query<T>(pub T);

impl<T> Query<T> {
    /// Extract the inner value
    #[must_use]
    pub fn into_inner(self) -> T {
        self.0
    }

    /// Get a reference to the inner value
    #[must_use]
    pub const fn inner(&self) -> &T {
        &self.0
    }
}

/// Error type for query parameter extraction failures
#[derive(Debug, thiserror::Error)]
pub enum QueryError {
    /// Query string parsing failed
    #[error("Failed to parse query string: {source}")]
    ParseError {
        /// The underlying parsing error
        #[source]
        source: qs::Error,
        /// The query string that failed to parse
        query_string: String,
    },

    /// URL decoding failed
    #[error("Failed to decode URL-encoded query string: {message}")]
    DecodeError {
        /// Error message
        message: String,
        /// The query string that failed to decode
        query_string: String,
    },

    /// Required field is missing
    #[error("Required query parameter '{field}' is missing")]
    MissingField {
        /// Name of the missing field
        field: String,
    },

    /// Field has invalid format
    #[error("Query parameter '{field}' has invalid format: {message}")]
    InvalidFormat {
        /// Name of the field with invalid format
        field: String,
        /// Error message describing the format issue
        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();

        // Handle empty query string
        if query_string.is_empty() {
            // Try to deserialize empty query - this works for structs with all optional fields
            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(),
                }),
            };
        }

        // Parse the query string using serde_querystring
        // Try different parse modes for better array support
        match qs::from_str::<T>(query_string, qs::ParseMode::UrlEncoded) {
            Ok(value) => Ok(Self(value)),
            Err(source) => {
                // For now, we'll use the basic error. Enhanced error parsing can be added later
                // when we understand the structure of qs::Error better
                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 {
        // Note: serde-querystring has limitations with array parsing
        // For now, we'll test with single values that can be parsed as arrays
        tags: String,        // Changed from Vec<String> to String for testing
        ids: Option<String>, // Changed from Vec<u32> to String for testing
    }

    #[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 {
        // For actix-only builds, we can't create a proper test request
        // This is a limitation of the current test setup
        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() {
        // Note: This test demonstrates a limitation of serde-querystring
        // Real array parsing would require a different query string library
        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);

        // Since we're using Ready future, we can use futures::executor::block_on
        // or in this case, we know it's a Ready future so we can handle it directly
        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();

        // Test inner() method
        assert_eq!(query.inner().name, "test");
        assert_eq!(query.inner().age, Some(25));

        // Test into_inner() method
        let params = query.into_inner();
        assert_eq!(params.name, "test");
        assert_eq!(params.age, Some(25));
    }
}