Skip to main content

hitbox_http/extractors/
query.rs

1//! Query parameter extraction for cache keys.
2//!
3//! Provides [`Query`] extractor with support for name selection, value extraction,
4//! and transformation.
5//!
6//! # Examples
7//!
8//! Extract pagination parameters:
9//!
10//! ```
11//! use hitbox_http::extractors::{Method, query::QueryExtractor};
12//!
13//! # use bytes::Bytes;
14//! # use http_body_util::Empty;
15//! # use hitbox_http::extractors::{NeutralExtractor, query::Query};
16//! let extractor = Method::new()
17//!     .query("page".to_string())
18//!     .query("limit".to_string());
19//! # let _: &Query<Query<Method<NeutralExtractor<Empty<Bytes>>>>> = &extractor;
20//! ```
21
22use async_trait::async_trait;
23use hitbox::{Extractor, KeyPart, KeyParts};
24use regex::Regex;
25
26use super::NeutralExtractor;
27pub use super::transform::Transform;
28use super::transform::apply_transform_chain;
29use crate::CacheableHttpRequest;
30
31/// Selects which query parameters to extract.
32#[derive(Debug, Clone)]
33pub enum NameSelector {
34    /// Match a single parameter by exact name.
35    Exact(String),
36    /// Match all parameters starting with a prefix.
37    ///
38    /// Results are sorted by parameter name for deterministic cache keys.
39    Starts(String),
40}
41
42/// Extracts values from query parameter content.
43#[derive(Debug, Clone)]
44pub enum ValueExtractor {
45    /// Use the full parameter value.
46    Full,
47    /// Extract using regex (returns first capture group, or full match if no groups).
48    Regex(Regex),
49}
50
51/// Extracts query parameters as cache key parts.
52///
53/// Supports flexible parameter selection, value extraction, and transformation.
54/// Array parameters (e.g., `color[]=red&color[]=blue`) are handled correctly.
55///
56/// # Key Parts Generated
57///
58/// For each matched parameter, generates a `KeyPart` with:
59/// - Key: the parameter name
60/// - Value: the extracted (and optionally transformed) value
61///
62/// # Performance
63///
64/// - Query string parsing allocates a `HashMap` for parameter lookup
65/// - When using [`NameSelector::Starts`], results are sorted alphabetically
66///   for deterministic cache keys (O(n log n) where n is matched parameters)
67/// - Regex extraction ([`ValueExtractor::Regex`]) compiles the pattern once
68///   at construction time
69#[derive(Debug)]
70pub struct Query<E> {
71    inner: E,
72    name_selector: NameSelector,
73    value_extractor: ValueExtractor,
74    transforms: Vec<Transform>,
75}
76
77impl<S> Query<NeutralExtractor<S>> {
78    /// Creates a query extractor for a single parameter by exact name.
79    ///
80    /// The parameter value becomes a cache key part with the parameter name
81    /// as key. For more complex extraction (prefix matching, regex, transforms),
82    /// use [`Query::new_with`].
83    ///
84    /// Chain onto existing extractors using [`QueryExtractor::query`] instead
85    /// if you already have an extractor chain.
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use hitbox_http::extractors::query::Query;
91    ///
92    /// # use bytes::Bytes;
93    /// # use http_body_util::Empty;
94    /// # use hitbox_http::extractors::NeutralExtractor;
95    /// // Extract the "page" query parameter
96    /// let extractor = Query::new("page".to_string());
97    /// # let _: &Query<NeutralExtractor<Empty<Bytes>>> = &extractor;
98    /// ```
99    pub fn new(name: String) -> Self {
100        Self {
101            inner: NeutralExtractor::new(),
102            name_selector: NameSelector::Exact(name),
103            value_extractor: ValueExtractor::Full,
104            transforms: Vec::new(),
105        }
106    }
107}
108
109impl<E> Query<E> {
110    /// Creates a query parameter extractor with full configuration options.
111    ///
112    /// This constructor provides complete control over query extraction:
113    /// - Select parameters by exact name or prefix pattern
114    /// - Extract full values or use regex capture groups
115    /// - Apply transformations (hash, lowercase, uppercase)
116    ///
117    /// For simple exact-name extraction without transforms, use [`Query::new`]
118    /// or [`QueryExtractor::query`] instead.
119    pub fn new_with(
120        inner: E,
121        name_selector: NameSelector,
122        value_extractor: ValueExtractor,
123        transforms: Vec<Transform>,
124    ) -> Self {
125        Self {
126            inner,
127            name_selector,
128            value_extractor,
129            transforms,
130        }
131    }
132}
133
134/// Extension trait for adding query parameter extraction to an extractor chain.
135///
136/// # For Callers
137///
138/// Chain this to extract URL query parameters as cache key parts. Each
139/// extracted parameter becomes a key part with the parameter name and value.
140///
141/// # For Implementors
142///
143/// This trait is automatically implemented for all [`Extractor`]
144/// types. You don't need to implement it manually.
145pub trait QueryExtractor: Sized {
146    /// Adds extraction for a single query parameter by name.
147    fn query(self, name: String) -> Query<Self>;
148}
149
150impl<E> QueryExtractor for E
151where
152    E: Extractor,
153{
154    fn query(self, name: String) -> Query<Self> {
155        Query {
156            inner: self,
157            name_selector: NameSelector::Exact(name),
158            value_extractor: ValueExtractor::Full,
159            transforms: Vec::new(),
160        }
161    }
162}
163
164/// Extract value using the value extractor.
165fn extract_value(value: &str, extractor: &ValueExtractor) -> Option<String> {
166    match extractor {
167        ValueExtractor::Full => Some(value.to_string()),
168        ValueExtractor::Regex(regex) => regex
169            .captures(value)
170            .and_then(|caps| caps.get(1).or_else(|| caps.get(0)))
171            .map(|m| m.as_str().to_string()),
172    }
173}
174
175#[async_trait]
176impl<ReqBody, E> Extractor for Query<E>
177where
178    ReqBody: hyper::body::Body + Send + 'static,
179    ReqBody::Error: Send,
180    E: Extractor<Subject = CacheableHttpRequest<ReqBody>> + Send + Sync,
181{
182    type Subject = E::Subject;
183
184    async fn get(&self, subject: Self::Subject) -> KeyParts<Self::Subject> {
185        let query_map = subject
186            .parts()
187            .uri
188            .query()
189            .and_then(crate::query::parse)
190            .unwrap_or_default();
191
192        let mut extracted_parts: Vec<KeyPart> = match &self.name_selector {
193            NameSelector::Exact(name) => query_map
194                .get(name)
195                .map(|v| v.inner())
196                .unwrap_or_default()
197                .into_iter()
198                .filter_map(|value| {
199                    extract_value(&value, &self.value_extractor)
200                        .map(|v| apply_transform_chain(v, &self.transforms))
201                        .map(|v| KeyPart::new(name.clone(), Some(v)))
202                })
203                .collect(),
204
205            NameSelector::Starts(prefix) => {
206                let mut parts: Vec<KeyPart> = query_map
207                    .iter()
208                    .filter(|(name, _)| name.starts_with(prefix.as_str()))
209                    .flat_map(|(name, value)| {
210                        value.inner().into_iter().filter_map(|v| {
211                            extract_value(&v, &self.value_extractor)
212                                .map(|extracted| apply_transform_chain(extracted, &self.transforms))
213                                .map(|extracted| KeyPart::new(name.clone(), Some(extracted)))
214                        })
215                    })
216                    .collect();
217                // Sort by parameter name for deterministic cache keys
218                parts.sort_by(|a, b| a.key().cmp(b.key()));
219                parts
220            }
221        };
222
223        let mut parts = self.inner.get(subject).await;
224        parts.append(&mut extracted_parts);
225        parts
226    }
227}