Skip to main content

hitbox_http/predicates/request/
path.rs

1//! Path pattern matching predicate.
2//!
3//! Provides [`Path`] predicate for matching request paths against
4//! [actix-router](https://docs.rs/actix-router) patterns.
5
6use crate::CacheableHttpRequest;
7use actix_router::ResourceDef;
8use async_trait::async_trait;
9use hitbox::Neutral;
10use hitbox::predicate::{Predicate, PredicateResult};
11
12/// A predicate that matches request paths against a pattern.
13///
14/// Uses [actix-router](https://docs.rs/actix-router) for pattern matching.
15/// The predicate returns [`Cacheable`](PredicateResult::Cacheable) when the
16/// request path matches the pattern.
17///
18/// # Type Parameters
19///
20/// * `P` - The inner predicate to chain with. Use [`Path::new`] to start
21///   a new predicate chain (uses [`Neutral`] internally), or use the
22///   [`PathPredicate`] extension trait to chain onto an existing predicate.
23///
24/// # Pattern Syntax
25///
26/// - `{name}` — matches a path segment
27/// - `{name:regex}` — matches with regex constraint
28/// - `{tail}*` — matches remaining path segments
29///
30/// # Examples
31///
32/// ```
33/// use hitbox_http::predicates::request::{Path, PathPredicate};
34/// use actix_router::ResourceDef;
35///
36/// # use bytes::Bytes;
37/// # use http_body_util::Empty;
38/// # use hitbox::Neutral;
39/// # use hitbox_http::CacheableHttpRequest;
40/// # type Subject = CacheableHttpRequest<Empty<Bytes>>;
41/// // Match requests to /api/users/{id}
42/// let predicate = Path::new(ResourceDef::new("/api/users/{id}"));
43/// # let _: &Path<Neutral<Subject>> = &predicate;
44/// ```
45#[derive(Debug)]
46pub struct Path<P> {
47    resource: ResourceDef,
48    inner: P,
49}
50
51impl<S> Path<Neutral<S>> {
52    /// Creates a predicate that matches request paths against a pattern.
53    ///
54    /// Returns [`Cacheable`](hitbox::predicate::PredicateResult::Cacheable) when
55    /// the request path matches the pattern, [`NonCacheable`](hitbox::predicate::PredicateResult::NonCacheable) otherwise.
56    ///
57    /// Chain onto existing predicates using [`PathPredicate::path`] instead
58    /// if you already have a predicate chain.
59    pub fn new(resource: ResourceDef) -> Self {
60        Self {
61            resource,
62            inner: Neutral::new(),
63        }
64    }
65}
66
67/// Extension trait for adding path matching to a predicate chain.
68///
69/// # For Callers
70///
71/// Chain this to match requests by their URL path. The path is matched
72/// against an [actix-router](https://docs.rs/actix-router) pattern supporting
73/// dynamic segments like `{id}` and wildcards like `{tail}*`.
74///
75/// # For Implementors
76///
77/// This trait is automatically implemented for all [`Predicate`]
78/// types. You don't need to implement it manually.
79pub trait PathPredicate: Sized {
80    /// Adds a path pattern match to this predicate chain.
81    ///
82    /// The pattern is compiled into a [`ResourceDef`].
83    fn path(self, resource: String) -> Path<Self>;
84}
85
86impl<P> PathPredicate for P
87where
88    P: Predicate,
89{
90    fn path(self, resource: String) -> Path<Self> {
91        Path {
92            resource: ResourceDef::from(resource),
93            inner: self,
94        }
95    }
96}
97
98#[async_trait]
99impl<P, ReqBody> Predicate for Path<P>
100where
101    P: Predicate<Subject = CacheableHttpRequest<ReqBody>> + Send + Sync,
102    ReqBody: hyper::body::Body + Send + 'static,
103    ReqBody::Error: Send,
104{
105    type Subject = P::Subject;
106
107    async fn check(&self, request: Self::Subject) -> PredicateResult<Self::Subject> {
108        match self.inner.check(request).await {
109            PredicateResult::Cacheable(request) => {
110                if self.resource.is_match(request.parts().uri.path()) {
111                    PredicateResult::Cacheable(request)
112                } else {
113                    PredicateResult::NonCacheable(request)
114                }
115            }
116            PredicateResult::NonCacheable(request) => PredicateResult::NonCacheable(request),
117        }
118    }
119}