Skip to main content

hitbox_http/extractors/
path.rs

1use actix_router::ResourceDef;
2use async_trait::async_trait;
3use hitbox::{Extractor, KeyPart, KeyParts};
4
5use super::NeutralExtractor;
6use crate::CacheableHttpRequest;
7
8/// Extracts path parameters as cache key parts.
9///
10/// Uses [actix-router](https://docs.rs/actix-router) patterns to match and
11/// extract named segments from the request path.
12///
13/// # Type Parameters
14///
15/// * `E` - The inner extractor to chain with. Use [`Path::new`] to start
16///   a new extractor chain (uses [`NeutralExtractor`] internally), or use the
17///   [`PathExtractor`] extension trait to chain onto an existing extractor.
18///
19/// # Pattern Syntax
20///
21/// - `{name}` — captures a path segment (characters until `/`)
22/// - `{name:regex}` — captures with regex constraint (e.g., `{id:\d+}`)
23/// - `{tail}*` — captures remaining path (e.g., `/blob/{path}*` matches `/blob/a/b/c`)
24///
25/// # Examples
26///
27/// ```
28/// use hitbox_http::extractors::Path;
29///
30/// # use bytes::Bytes;
31/// # use http_body_util::Empty;
32/// # use hitbox_http::extractors::NeutralExtractor;
33/// // Extract user_id and post_id from "/users/42/posts/123"
34/// let extractor = Path::new("/users/{user_id}/posts/{post_id}");
35/// # let _: &Path<NeutralExtractor<Empty<Bytes>>> = &extractor;
36/// ```
37///
38/// Using the builder pattern:
39///
40/// ```
41/// use hitbox_http::extractors::{Method, path::PathExtractor};
42///
43/// # use bytes::Bytes;
44/// # use http_body_util::Empty;
45/// # use hitbox_http::extractors::{NeutralExtractor, Path};
46/// let extractor = Method::new()
47///     .path("/api/v1/users/{user_id}");
48/// # let _: &Path<Method<NeutralExtractor<Empty<Bytes>>>> = &extractor;
49/// ```
50///
51/// # Key Parts Generated
52///
53/// For path `/users/42/posts/123` with pattern `/users/{user_id}/posts/{post_id}`:
54/// - `KeyPart { key: "user_id", value: Some("42") }`
55/// - `KeyPart { key: "post_id", value: Some("123") }`
56///
57/// # Format Examples
58///
59/// | Request Path | Pattern | Generated Key Parts |
60/// |--------------|---------|---------------------|
61/// | `/users/42` | `/users/{id}` | `id=42` |
62/// | `/api/v2/items` | `/api/{version}/items` | `version=v2` |
63/// | `/files/docs/report.pdf` | `/files/{path}*` | `path=docs/report.pdf` |
64/// | `/orders/123/items/456` | `/orders/{order_id}/items/{item_id}` | `order_id=123&item_id=456` |
65#[derive(Debug)]
66pub struct Path<E> {
67    inner: E,
68    resource: ResourceDef,
69}
70
71impl<S> Path<NeutralExtractor<S>> {
72    /// Creates a path extractor that captures named segments from request paths.
73    ///
74    /// Each captured segment becomes a cache key part with the segment name
75    /// as key. See the struct documentation for pattern syntax.
76    ///
77    /// Chain onto existing extractors using [`PathExtractor::path`] instead
78    /// if you already have an extractor chain.
79    pub fn new(resource: &str) -> Self {
80        Self {
81            inner: NeutralExtractor::new(),
82            resource: ResourceDef::from(resource),
83        }
84    }
85}
86
87/// Extension trait for adding path extraction to an extractor chain.
88///
89/// # For Callers
90///
91/// Chain this to extract named segments from the request path. Each captured
92/// segment becomes a cache key part. Use patterns like `/users/{user_id}` to
93/// capture dynamic path segments.
94///
95/// # For Implementors
96///
97/// This trait is automatically implemented for all [`Extractor`]
98/// types. You don't need to implement it manually.
99pub trait PathExtractor: Sized {
100    /// Adds path parameter extraction with the given pattern.
101    ///
102    /// See [`Path`] for pattern syntax documentation.
103    fn path(self, resource: &str) -> Path<Self>;
104}
105
106impl<E> PathExtractor for E
107where
108    E: Extractor,
109{
110    fn path(self, resource: &str) -> Path<Self> {
111        Path {
112            inner: self,
113            resource: ResourceDef::from(resource),
114        }
115    }
116}
117
118#[async_trait]
119impl<ReqBody, E> Extractor for Path<E>
120where
121    ReqBody: hyper::body::Body + Send + 'static,
122    ReqBody::Error: Send,
123    E: Extractor<Subject = CacheableHttpRequest<ReqBody>> + Send + Sync,
124{
125    type Subject = E::Subject;
126
127    async fn get(&self, subject: Self::Subject) -> KeyParts<Self::Subject> {
128        let mut path = actix_router::Path::new(subject.parts().uri.path());
129        self.resource.capture_match_info(&mut path);
130        let mut matched_parts = path
131            .iter()
132            .map(|(key, value)| KeyPart::new(key, Some(value)))
133            .collect::<Vec<_>>();
134        let mut parts = self.inner.get(subject).await;
135        parts.append(&mut matched_parts);
136        parts
137    }
138}