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}