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}