Skip to main content

actix_web_lab/
path.rs

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