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}