Skip to main content

hitbox_http/predicates/request/
query.rs

1//! Query parameter matching predicate.
2//!
3//! Provides [`Query`] predicate and [`Operation`] for matching URL query parameters.
4
5use crate::CacheableHttpRequest;
6use async_trait::async_trait;
7use hitbox::Neutral;
8use hitbox::predicate::{Predicate, PredicateResult};
9
10/// Operations for matching query parameters.
11///
12/// # Variants
13///
14/// - [`Eq`](Self::Eq) — Parameter must equal a specific value
15/// - [`Exist`](Self::Exist) — Parameter must be present
16/// - [`In`](Self::In) — Parameter value must be one of the specified values
17#[derive(Debug)]
18pub enum Operation {
19    /// Match if the parameter equals the value. Format: `(name, expected_value)`.
20    Eq(String, String),
21    /// Match if the parameter exists (regardless of value).
22    Exist(String),
23    /// Match if the parameter value is one of these values. Format: `(name, allowed_values)`.
24    In(String, Vec<String>),
25}
26
27/// A predicate that matches requests by query parameters.
28///
29/// Returns [`Cacheable`](PredicateResult::Cacheable) when the query parameter
30/// satisfies the operation, [`NonCacheable`](PredicateResult::NonCacheable) otherwise.
31///
32/// # Type Parameters
33///
34/// * `P` - The inner predicate to chain with. Use [`Query::new`] to start
35///   a new predicate chain (uses [`Neutral`] internally), or use the
36///   [`QueryPredicate`] extension trait to chain onto an existing predicate.
37///
38/// # Examples
39///
40/// ```
41/// use hitbox_http::predicates::request::query::{Query, Operation};
42///
43/// # use bytes::Bytes;
44/// # use http_body_util::Empty;
45/// # use hitbox::Neutral;
46/// # use hitbox_http::CacheableHttpRequest;
47/// # type Subject = CacheableHttpRequest<Empty<Bytes>>;
48/// // Cache only when "format" query parameter is "json"
49/// let predicate = Query::new(Operation::Eq("format".into(), "json".into()));
50/// # let _: &Query<Neutral<Subject>> = &predicate;
51/// ```
52#[derive(Debug)]
53pub struct Query<P> {
54    /// The operation to perform on the query parameter.
55    pub operation: Operation,
56    inner: P,
57}
58
59impl<S> Query<Neutral<S>> {
60    /// Creates a predicate that matches query parameters against the operation.
61    ///
62    /// Returns [`Cacheable`](hitbox::predicate::PredicateResult::Cacheable) when
63    /// the query parameter satisfies the operation, [`NonCacheable`](hitbox::predicate::PredicateResult::NonCacheable) otherwise.
64    ///
65    /// Chain onto existing predicates using [`QueryPredicate::query`] instead
66    /// if you already have a predicate chain.
67    pub fn new(operation: Operation) -> Self {
68        Self {
69            operation,
70            inner: Neutral::new(),
71        }
72    }
73}
74
75/// Extension trait for adding query parameter matching to a predicate chain.
76///
77/// # For Callers
78///
79/// Chain this to match requests by their URL query parameters. Use the
80/// [`Operation`] enum to specify exact matches, existence checks, or
81/// set membership.
82///
83/// # For Implementors
84///
85/// This trait is automatically implemented for all [`Predicate`]
86/// types. You don't need to implement it manually.
87pub trait QueryPredicate: Sized {
88    /// Adds a query parameter match to this predicate chain.
89    fn query(self, operation: Operation) -> Query<Self>;
90}
91
92impl<P> QueryPredicate for P
93where
94    P: Predicate,
95{
96    fn query(self, operation: Operation) -> Query<Self> {
97        Query {
98            operation,
99            inner: self,
100        }
101    }
102}
103
104#[async_trait]
105impl<P, ReqBody> Predicate for Query<P>
106where
107    ReqBody: hyper::body::Body + Send + 'static,
108    ReqBody::Error: Send,
109    P: Predicate<Subject = CacheableHttpRequest<ReqBody>> + Send + Sync,
110{
111    type Subject = P::Subject;
112
113    async fn check(&self, request: Self::Subject) -> PredicateResult<Self::Subject> {
114        match self.inner.check(request).await {
115            PredicateResult::Cacheable(request) => {
116                let is_cacheable = match request.parts().uri.query().and_then(crate::query::parse) {
117                    Some(query_map) => match &self.operation {
118                        Operation::Eq(name, value) => query_map
119                            .get(name)
120                            .map(|v| v.contains(value))
121                            .unwrap_or_default(),
122                        Operation::Exist(name) => {
123                            query_map.get(name).map(|_| true).unwrap_or_default()
124                        }
125                        Operation::In(name, values) => query_map
126                            .get(name)
127                            .and_then(|value| values.iter().find(|v| value.contains(v)))
128                            .map(|_| true)
129                            .unwrap_or_default(),
130                    },
131                    None => false,
132                };
133                if is_cacheable {
134                    PredicateResult::Cacheable(request)
135                } else {
136                    PredicateResult::NonCacheable(request)
137                }
138            }
139            PredicateResult::NonCacheable(request) => PredicateResult::NonCacheable(request),
140        }
141    }
142}