garde_actix_web/web/
path.rs

1use 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/// Drop in replacement for [actix_web::web::Path](https://docs.rs/actix-web/latest/actix_web/web/struct.Path.html)
16#[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/// Replacement for [actix_web::web::PathConfig](https://docs.rs/actix-web/latest/actix_web/web/struct.PathConfig.html)
70/// Error handler must map from an `garde_actix_web::error::Error`
71#[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}