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    FromRequest, HttpRequest,
7    dev::Payload,
8    error::{Error, ErrorNotFound},
9};
10use derive_more::Display;
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, Clone, PartialEq, Eq, PartialOrd, Ord, Display)]
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
66impl_more::impl_as_ref!(Path<T> => T);
67impl_more::impl_from!(<T> in T => Path<T>);
68
69/// See [here](#Examples) for example of usage as an extractor.
70impl<T> FromRequest for Path<T>
71where
72    T: de::DeserializeOwned,
73{
74    type Error = Error;
75    type Future = Ready<Result<Self, Self::Error>>;
76
77    #[inline]
78    fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
79        ready(
80            de::Deserialize::deserialize(PathDeserializer::new(req.match_info()))
81                .map(Path)
82                .map_err(move |err| {
83                    debug!(
84                        "Failed during Path extractor deserialization. \
85                         Request path: {:?}",
86                        req.path()
87                    );
88
89                    ErrorNotFound(err)
90                }),
91        )
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use actix_web::{dev::ResourceDef, test::TestRequest};
98    use derive_more::Display;
99    use serde::Deserialize;
100
101    use super::*;
102
103    #[derive(Deserialize, Debug, Display)]
104    #[display("MyStruct({key}, {value})")]
105    struct MyStruct {
106        key: String,
107        value: String,
108    }
109
110    #[derive(Deserialize)]
111    struct Test2 {
112        key: String,
113        value: u32,
114    }
115
116    #[actix_web::test]
117    async fn test_extract_path_single() {
118        let resource = ResourceDef::new("/{value}/");
119
120        let mut req = TestRequest::with_uri("/32/").to_srv_request();
121        resource.capture_match_info(req.match_info_mut());
122
123        let (req, mut pl) = req.into_parts();
124        assert_eq!(
125            Path::<i8>::from_request(&req, &mut pl)
126                .await
127                .unwrap()
128                .into_inner(),
129            32
130        );
131        assert!(Path::<MyStruct>::from_request(&req, &mut pl).await.is_err());
132    }
133
134    #[actix_web::test]
135    async fn test_tuple_extract() {
136        let resource = ResourceDef::new("/{key}/{value}/");
137
138        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
139        resource.capture_match_info(req.match_info_mut());
140
141        let (req, mut pl) = req.into_parts();
142        let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl)
143            .await
144            .unwrap();
145        assert_eq!(res.0, "name");
146        assert_eq!(res.1, "user1");
147
148        let (Path(a), Path(b)) =
149            <(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl)
150                .await
151                .unwrap();
152        assert_eq!(a.0, "name");
153        assert_eq!(a.1, "user1");
154        assert_eq!(b.0, "name");
155        assert_eq!(b.1, "user1");
156
157        <()>::from_request(&req, &mut pl).await.unwrap();
158    }
159
160    #[actix_web::test]
161    async fn test_request_extract() {
162        let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
163
164        let resource = ResourceDef::new("/{key}/{value}/");
165        resource.capture_match_info(req.match_info_mut());
166
167        let (req, mut pl) = req.into_parts();
168        let s = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
169        assert_eq!(format!("{s}"), "MyStruct(name, user1)");
170        assert_eq!(
171            format!("{s:?}"),
172            "Path(MyStruct { key: \"name\", value: \"user1\" })"
173        );
174        let mut s = s.into_inner();
175        assert_eq!(s.key, "name");
176        assert_eq!(s.value, "user1");
177        s.value = "user2".to_string();
178        assert_eq!(s.value, "user2");
179
180        let Path(s) = Path::<(String, String)>::from_request(&req, &mut pl)
181            .await
182            .unwrap();
183        assert_eq!(s.0, "name");
184        assert_eq!(s.1, "user1");
185
186        let mut req = TestRequest::with_uri("/name/32/").to_srv_request();
187        let resource = ResourceDef::new("/{key}/{value}/");
188        resource.capture_match_info(req.match_info_mut());
189
190        let (req, mut pl) = req.into_parts();
191        let s = Path::<Test2>::from_request(&req, &mut pl).await.unwrap();
192        assert_eq!(s.as_ref().key, "name");
193        let s = s.into_inner();
194        assert_eq!(s.value, 32);
195
196        let Path(s) = Path::<(String, u8)>::from_request(&req, &mut pl)
197            .await
198            .unwrap();
199        assert_eq!(s.0, "name");
200        assert_eq!(s.1, 32);
201
202        let s = Path::<Vec<String>>::from_request(&req, &mut pl)
203            .await
204            .unwrap();
205        let s = s.into_inner();
206        assert_eq!(s[0], "name".to_owned());
207        assert_eq!(s[1], "32".to_owned());
208    }
209
210    #[actix_web::test]
211    async fn paths_decoded() {
212        let resource = ResourceDef::new("/{key}/{value}");
213        let mut req = TestRequest::with_uri("/na%2Bme/us%2Fer%254%32").to_srv_request();
214        resource.capture_match_info(req.match_info_mut());
215
216        let (req, mut pl) = req.into_parts();
217        let path_items = Path::<MyStruct>::from_request(&req, &mut pl).await.unwrap();
218        let path_items = path_items.into_inner();
219        assert_eq!(path_items.key, "na+me");
220        assert_eq!(path_items.value, "us/er%42");
221        assert_eq!(req.match_info().as_str(), "/na%2Bme/us%2Fer%2542");
222    }
223}