actix_web_lab/
path.rs

1//! For path segment extractor documentation, see [`Path`].
2
3use actix_router::PathDeserializer;
4use actix_utils::future::{ready, Ready};
5use actix_web::{
6    dev::Payload,
7    error::{Error, ErrorNotFound},
8    FromRequest, HttpRequest,
9};
10use derive_more::{AsRef, Display, From};
11use serde::de;
12use tracing::debug;
13
14/// Extract typed data from request path segments.
15///
16/// Alternative to `web::Path` extractor from Actix Web that allows deconstruction, but omits the
17/// implementation of `Deref`.
18///
19/// Unlike, [`HttpRequest::match_info`], this extractor will fully percent-decode dynamic segments,
20/// including `/`, `%`, and `+`.
21///
22/// # Examples
23/// ```
24/// use actix_web::get;
25/// use actix_web_lab::extract::Path;
26///
27/// // extract path info from "/{name}/{count}/index.html" into tuple
28/// // {name}  - deserialize a String
29/// // {count} - deserialize a u32
30/// #[get("/{name}/{count}/index.html")]
31/// async fn index(Path((name, count)): Path<(String, u32)>) -> String {
32///     format!("Welcome {}! {}", name, count)
33/// }
34/// ```
35///
36/// Path segments also can be deserialized into any type that implements [`serde::Deserialize`].
37/// Path segment labels will be matched with struct field names.
38///
39/// ```
40/// use actix_web::get;
41/// use actix_web_lab::extract::Path;
42/// use serde::Deserialize;
43///
44/// #[derive(Deserialize)]
45/// struct Info {
46///     name: String,
47/// }
48///
49/// // extract `Info` from a path using serde
50/// #[get("/{name}")]
51/// async fn index(info: Path<Info>) -> String {
52///     let info = info.into_inner();
53///     format!("Welcome {}!", info.name)
54/// }
55/// ```
56#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, AsRef, Display, From)]
57pub struct Path<T>(pub T);
58
59impl<T> Path<T> {
60    /// Unwrap into inner `T` value.
61    pub fn into_inner(self) -> T {
62        self.0
63    }
64}
65
66/// See [here](#Examples) for example of usage as an extractor.
67impl<T> FromRequest for Path<T>
68where
69    T: de::DeserializeOwned,
70{
71    type Error = Error;
72    type Future = Ready<Result<Self, Self::Error>>;
73
74    #[inline]
75    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
76        ready(
77            de::Deserialize::deserialize(PathDeserializer::new(req.match_info()))
78                .map(Path)
79                .map_err(move |err| {
80                    debug!(
81                        "Failed during Path extractor deserialization. \
82                         Request path: {:?}",
83                        req.path()
84                    );
85
86                    ErrorNotFound(err)
87                }),
88        )
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use actix_web::{dev::ResourceDef, test::TestRequest};
95    use derive_more::Display;
96    use serde::Deserialize;
97
98    use super::*;
99
100    #[derive(Deserialize, Debug, Display)]
101    #[display(fmt = "MyStruct({key}, {value})")]
102    struct MyStruct {
103        key: String,
104        value: String,
105    }
106
107    #[derive(Deserialize)]
108    struct Test2 {
109        key: String,
110        value: u32,
111    }
112
113    #[actix_web::test]
114    async fn test_extract_path_single() {
115        let resource = ResourceDef::new("/{value}/");
116
117        let mut req = TestRequest::with_uri("/32/").to_srv_request();
118        resource.capture_match_info(req.match_info_mut());
119
120        let (req, mut pl) = req.into_parts();
121        assert_eq!(
122            Path::<i8>::from_request(&req, &mut pl)
123                .await
124                .unwrap()
125                .into_inner(),
126            32
127        );
128        assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err());
129    }
130
131    #[actix_web::test]
132    async fn test_tuple_extract() {
133        let resource = ResourceDef::new("/{key}/{value}/");
134
135        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
136        resource.capture_match_info(req.match_info_mut());
137
138        let (req, mut pl) = req.into_parts();
139        let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl)
140            .await
141            .unwrap();
142        assert_eq!(res.0, "name");
143        assert_eq!(res.1, "user1");
144
145        let (Path(a), Path(b)) =
146            <(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl)
147                .await
148                .unwrap();
149        assert_eq!(a.0, "name");
150        assert_eq!(a.1, "user1");
151        assert_eq!(b.0, "name");
152        assert_eq!(b.1, "user1");
153
154        <()>::from_request(&req, &mut pl).await.unwrap();
155    }
156
157    #[actix_web::test]
158    async fn test_request_extract() {
159        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
160
161        let resource = ResourceDef::new("/{key}/{value}/");
162        resource.capture_match_info(req.match_info_mut());
163
164        let (req, mut pl) = req.into_parts();
165        let s = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
166        assert_eq!(format!("{s}"), "MyStruct(name, user1)");
167        assert_eq!(
168            format!("{s:?}"),
169            "Path(MyStruct { key: \"name\", value: \"user1\" })"
170        );
171        let mut s = s.into_inner();
172        assert_eq!(s.key, "name");
173        assert_eq!(s.value, "user1");
174        s.value = "user2".to_string();
175        assert_eq!(s.value, "user2");
176
177        let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl)
178            .await
179            .unwrap();
180        assert_eq!(s.0, "name");
181        assert_eq!(s.1, "user1");
182
183        let mut req = TestRequest::with_uri("/name/32/").to_srv_request();
184        let resource = ResourceDef::new("/{key}/{value}/");
185        resource.capture_match_info(req.match_info_mut());
186
187        let (req, mut pl) = req.into_parts();
188        let s = Path::<Test2>::from_request(&req, &mut pl).await.unwrap();
189        assert_eq!(s.as_ref().key, "name");
190        let s = s.into_inner();
191        assert_eq!(s.value, 32);
192
193        let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl)
194            .await
195            .unwrap();
196        assert_eq!(s.0, "name");
197        assert_eq!(s.1, 32);
198
199        let s = Path::<Vec<String>>::from_request(&req, &mut pl)
200            .await
201            .unwrap();
202        let s = s.into_inner();
203        assert_eq!(s[0], "name".to_owned());
204        assert_eq!(s[1], "32".to_owned());
205    }
206
207    #[actix_web::test]
208    async fn paths_decoded() {
209        let resource = ResourceDef::new("/{key}/{value}");
210        let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%254%32").to_srv_request();
211        resource.capture_match_info(req.match_info_mut());
212
213        let (req, mut pl) = req.into_parts();
214        let path_items = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
215        let path_items = path_items.into_inner();
216        assert_eq!(path_items.key, "na+me");
217        assert_eq!(path_items.value, "us/er%42");
218        assert_eq!(req.match_info().as_str(), "/na%2Bme/us%2Fer%2542");
219    }
220}