garde_actix_web/web/
path.rs1use actix_router::PathDeserializer;
2use actix_web::dev::Payload;
3use actix_web::error::{ErrorNotFound, PathError};
4use actix_web::web::Data;
5use actix_web::{Error, FromRequest, HttpRequest};
6use std::sync::Arc;
7
8use crate::validate_for_request;
9use derive_more::{AsRef, Deref, DerefMut, Display, From};
10use futures::future::{Ready, err, ok};
11use garde::Validate;
12use serde::Deserialize;
13use serde::de::DeserializeOwned;
14
15#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut, AsRef, Display, From)]
17pub struct Path<T>(T);
18
19impl<T> Path<T> {
20 pub fn into_inner(self) -> T {
21 self.0
22 }
23}
24
25impl<T> FromRequest for Path<T>
26where
27 T: DeserializeOwned + Validate + 'static,
28 T::Context: Default,
29{
30 type Error = Error;
31 type Future = Ready<Result<Self, Self::Error>>;
32
33 #[inline]
34 fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
35 let req_copy = req.clone();
36 let error_handler = req
37 .app_data::<PathConfig>()
38 .or_else(|| req.app_data::<Data<PathConfig>>().map(Data::get_ref))
39 .and_then(|c| c.err_handler.clone());
40
41 Deserialize::deserialize(PathDeserializer::new(req.match_info()))
42 .map_err(|e| {
43 let e = PathError::Deserialize(e);
44 crate::error::Error::PathError(e)
45 })
46 .and_then(|data: T| {
47 let req = req_copy;
48 validate_for_request(data, &req)
49 })
50 .map(|val| ok(Path(val)))
51 .unwrap_or_else(move |e| {
52 log::debug!(
53 "Failed during Path extractor deserialization. \
54 Request path: {:?}",
55 req.path()
56 );
57
58 let e = if let Some(error_handler) = error_handler {
59 (error_handler)(e, req)
60 } else {
61 ErrorNotFound(e)
62 };
63
64 err(e)
65 })
66 }
67}
68
69#[derive(Clone, Default)]
72pub struct PathConfig {
73 #[allow(clippy::type_complexity)]
74 err_handler: Option<Arc<dyn Fn(crate::error::Error, &HttpRequest) -> Error + Send + Sync>>,
75}
76
77impl PathConfig {
78 pub fn error_handler<F>(mut self, f: F) -> Self
79 where
80 F: Fn(crate::error::Error, &HttpRequest) -> Error + Send + Sync + 'static,
81 {
82 self.err_handler = Some(Arc::new(f));
83 self
84 }
85}
86
87#[cfg(test)]
88mod test {
89 use crate::web::{Path, PathConfig};
90 use actix_http::StatusCode;
91 use actix_web::error::InternalError;
92 use actix_web::test::{TestRequest, call_service, init_service};
93 use actix_web::web::{post, resource};
94 use actix_web::{App, HttpResponse};
95 use garde::Validate;
96 use serde::{Deserialize, Serialize};
97 use std::fmt;
98
99 #[derive(Debug, PartialEq, Validate, Serialize, Deserialize)]
100 struct PathData {
101 #[garde(range(min = 18, max = 28))]
102 age: u8,
103 }
104
105 impl fmt::Display for PathData {
106 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
107 write!(f, "{{ age: {} }}", self.age)
108 }
109 }
110
111 #[derive(Debug, PartialEq, Validate, Serialize, Deserialize)]
112 #[garde(context(NumberContext))]
113 struct PathDataWithContext {
114 #[garde(custom(is_big_enough))]
115 age: u8,
116 }
117
118 impl fmt::Display for PathDataWithContext {
119 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120 write!(f, "{{ age: {} }}", self.age)
121 }
122 }
123
124 #[derive(Default, Debug)]
125 struct NumberContext {
126 min: u8,
127 }
128
129 fn is_big_enough(value: &u8, context: &NumberContext) -> garde::Result {
130 if value < &context.min {
131 return Err(garde::Error::new("Number is too low"));
132 }
133 Ok(())
134 }
135
136 async fn test_handler(_: Path<PathData>) -> HttpResponse {
137 HttpResponse::Ok().finish()
138 }
139
140 async fn test_handler_with_context(_: Path<PathDataWithContext>) -> HttpResponse {
141 HttpResponse::Ok().finish()
142 }
143
144 #[tokio::test]
145 async fn test_simple_path_validation() {
146 let app = init_service(App::new().service(resource("/{age}/").route(post().to(test_handler)))).await;
147
148 let req = TestRequest::post().uri("/24/").to_request();
149 let resp = call_service(&app, req).await;
150 assert_eq!(resp.status(), StatusCode::OK);
151
152 let req = TestRequest::post().uri("/30/").to_request();
153 let resp = call_service(&app, req).await;
154 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
155 }
156
157 #[tokio::test]
158 async fn test_path_validation_custom_config() {
159 let app = init_service(
160 App::new()
161 .app_data(
162 PathConfig::default()
163 .error_handler(|err, _req| InternalError::from_response(err, HttpResponse::Conflict().finish()).into()),
164 )
165 .service(resource("/{age}/").route(post().to(test_handler))),
166 )
167 .await;
168
169 let req = TestRequest::post().uri("/24/").to_request();
170 let resp = call_service(&app, req).await;
171 assert_eq!(resp.status(), StatusCode::OK);
172
173 let req = TestRequest::post().uri("/30/").to_request();
174 let resp = call_service(&app, req).await;
175 assert_eq!(resp.status(), StatusCode::CONFLICT);
176 }
177
178 #[tokio::test]
179 async fn test_path_validation_with_context() {
180 let number_context = NumberContext { min: 25 };
181 let app = init_service(
182 App::new()
183 .app_data(number_context)
184 .service(resource("/{age}/").route(post().to(test_handler_with_context))),
185 )
186 .await;
187
188 let req = TestRequest::post().uri("/24/").to_request();
189 let resp = call_service(&app, req).await;
190 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
191
192 let req = TestRequest::post().uri("/30/").to_request();
193 let resp = call_service(&app, req).await;
194 assert_eq!(resp.status(), StatusCode::OK);
195 }
196
197 #[tokio::test]
198 async fn test_path_validation_with_missing_context() {
199 let app = init_service(App::new().service(resource("/{age}/").route(post().to(test_handler_with_context)))).await;
200
201 let req = TestRequest::post().uri("/24/").to_request();
202 let resp = call_service(&app, req).await;
203 assert_eq!(resp.status(), StatusCode::OK);
204
205 let req = TestRequest::post().uri("/30/").to_request();
206 let resp = call_service(&app, req).await;
207 assert_eq!(resp.status(), StatusCode::OK);
208 }
209}