garde_actix_web/web/
json.rs

1use crate::validate_for_request;
2use actix_web::dev::{JsonBody, Payload};
3use actix_web::{Error, FromRequest, HttpRequest, web};
4use futures::FutureExt;
5use futures::future::LocalBoxFuture;
6use garde::Validate;
7use serde::de::DeserializeOwned;
8use std::sync::Arc;
9use std::{fmt, ops};
10
11/// Drop in replacement for [actix_web::web::Json](https://docs.rs/actix-web/latest/actix_web/web/struct.Json.html)
12#[derive(Debug)]
13pub struct Json<T>(pub T);
14
15impl<T> Json<T> {
16  pub fn into_inner(self) -> T {
17    self.0
18  }
19}
20
21impl<T> ops::Deref for Json<T> {
22  type Target = T;
23
24  fn deref(&self) -> &T {
25    &self.0
26  }
27}
28
29impl<T> ops::DerefMut for Json<T> {
30  fn deref_mut(&mut self) -> &mut T {
31    &mut self.0
32  }
33}
34
35impl<T: fmt::Display> fmt::Display for Json<T> {
36  fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37    fmt::Display::fmt(&self.0, f)
38  }
39}
40
41impl<T> FromRequest for Json<T>
42where
43  T: DeserializeOwned + Validate + 'static,
44  T::Context: Default,
45{
46  type Error = Error;
47  type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
48
49  #[inline]
50  fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
51    let req_copy = req.clone();
52    let req_copy2 = req.clone();
53
54    let config = JsonConfig::from_req(req);
55
56    let limit = config.limit;
57    let ctype_required = config.content_type_required;
58    let ctype_fn = config.content_type.as_deref();
59    let err_handler = config.err_handler.clone();
60
61    JsonBody::new(req, payload, ctype_fn, ctype_required)
62      .limit(limit)
63      .map(move |res: Result<T, _>| match res {
64        Ok(data) => {
65          let req = req_copy;
66          validate_for_request(data, &req)
67        }
68        Err(e) => Err(e.into()),
69      })
70      .map(move |res| match res {
71        Err(err) => {
72          log::debug!(
73            "Failed to deserialize Json from payload. \
74                         Request path: {}",
75            req_copy2.path()
76          );
77
78          if let Some(err_handler) = err_handler.as_ref() {
79            Err((*err_handler)(err, &req_copy2))
80          } else {
81            Err(err.into())
82          }
83        }
84        Ok(data) => Ok(Json(data)),
85      })
86      .boxed_local()
87  }
88}
89
90type JsonErrorHandler = Option<Arc<dyn Fn(crate::error::Error, &HttpRequest) -> Error + Send + Sync>>;
91
92/// Replacement for [actix_web::web::JsonConfig](https://docs.rs/actix-web/latest/actix_web/web/struct.JsonConfig.html)
93/// Error handler must map from an `garde_actix_web::error::Error`
94#[derive(Clone)]
95pub struct JsonConfig {
96  limit: usize,
97  err_handler: JsonErrorHandler,
98  content_type: Option<Arc<dyn Fn(mime::Mime) -> bool + Send + Sync>>,
99  content_type_required: bool,
100}
101
102impl JsonConfig {
103  pub fn limit(mut self, limit: usize) -> Self {
104    self.limit = limit;
105    self
106  }
107
108  pub fn error_handler<F>(mut self, f: F) -> Self
109  where
110    F: Fn(crate::error::Error, &HttpRequest) -> Error + Send + Sync + 'static,
111  {
112    self.err_handler = Some(Arc::new(f));
113    self
114  }
115
116  pub fn content_type<F>(mut self, predicate: F) -> Self
117  where
118    F: Fn(mime::Mime) -> bool + Send + Sync + 'static,
119  {
120    self.content_type = Some(Arc::new(predicate));
121    self
122  }
123
124  pub fn content_type_required(mut self, content_type_required: bool) -> Self {
125    self.content_type_required = content_type_required;
126    self
127  }
128
129  pub fn from_req(req: &HttpRequest) -> &Self {
130    req
131      .app_data::<Self>()
132      .or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
133      .unwrap_or(&DEFAULT_CONFIG)
134  }
135}
136
137const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb
138
139const DEFAULT_CONFIG: JsonConfig = JsonConfig {
140  limit: DEFAULT_LIMIT,
141  err_handler: None,
142  content_type: None,
143  content_type_required: true,
144};
145
146impl Default for JsonConfig {
147  fn default() -> Self {
148    DEFAULT_CONFIG
149  }
150}
151
152#[cfg(test)]
153mod test {
154  use crate::web::{Json, JsonConfig};
155  use actix_http::StatusCode;
156  use actix_web::error::InternalError;
157  use actix_web::test::{TestRequest, call_service, init_service};
158  use actix_web::web::{post, resource};
159  use actix_web::{App, HttpResponse};
160  use garde::Validate;
161  use serde::{Deserialize, Serialize};
162
163  #[derive(Debug, PartialEq, Validate, Serialize, Deserialize)]
164  struct JsonData {
165    #[garde(range(min = 18, max = 28))]
166    age: u8,
167  }
168
169  #[derive(Debug, PartialEq, Validate, Serialize, Deserialize)]
170  #[garde(context(NumberContext))]
171  struct JsonDataWithContext {
172    #[garde(custom(is_big_enough))]
173    age: u8,
174  }
175
176  #[derive(Default, Debug)]
177  struct NumberContext {
178    min: u8,
179  }
180
181  fn is_big_enough(value: &u8, context: &NumberContext) -> garde::Result {
182    if value < &context.min {
183      return Err(garde::Error::new("Number is too low"));
184    }
185    Ok(())
186  }
187
188  async fn test_handler(_: Json<JsonData>) -> HttpResponse {
189    HttpResponse::Ok().finish()
190  }
191
192  async fn test_handler_with_context(_: Json<JsonDataWithContext>) -> HttpResponse {
193    HttpResponse::Ok().finish()
194  }
195
196  #[tokio::test]
197  async fn test_simple_json_validation() {
198    let app = init_service(App::new().service(resource("/").route(post().to(test_handler)))).await;
199
200    let req = TestRequest::post()
201      .uri("/")
202      .set_json(&JsonData { age: 24 })
203      .to_request();
204    let resp = call_service(&app, req).await;
205    assert_eq!(resp.status(), StatusCode::OK);
206
207    let req = TestRequest::post()
208      .uri("/")
209      .set_json(&JsonData { age: 30 })
210      .to_request();
211    let resp = call_service(&app, req).await;
212    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
213  }
214
215  #[tokio::test]
216  async fn test_json_validation_custom_config() {
217    let app = init_service(
218      App::new()
219        .app_data(
220          JsonConfig::default()
221            .error_handler(|err, _req| InternalError::from_response(err, HttpResponse::Conflict().finish()).into()),
222        )
223        .service(resource("/").route(post().to(test_handler))),
224    )
225    .await;
226
227    let req = TestRequest::post()
228      .uri("/")
229      .set_json(&JsonData { age: 24 })
230      .to_request();
231    let resp = call_service(&app, req).await;
232    assert_eq!(resp.status(), StatusCode::OK);
233
234    let req = TestRequest::post()
235      .uri("/")
236      .set_json(&JsonData { age: 30 })
237      .to_request();
238    let resp = call_service(&app, req).await;
239    assert_eq!(resp.status(), StatusCode::CONFLICT);
240  }
241
242  #[tokio::test]
243  async fn test_json_validation_with_context() {
244    let number_context = NumberContext { min: 25 };
245    let app = init_service(
246      App::new()
247        .app_data(number_context)
248        .service(resource("/").route(post().to(test_handler_with_context))),
249    )
250    .await;
251
252    let req = TestRequest::post()
253      .uri("/")
254      .set_json(&JsonData { age: 24 })
255      .to_request();
256    let resp = call_service(&app, req).await;
257    assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
258
259    let req = TestRequest::post()
260      .uri("/")
261      .set_json(&JsonData { age: 30 })
262      .to_request();
263    let resp = call_service(&app, req).await;
264    assert_eq!(resp.status(), StatusCode::OK);
265  }
266
267  #[tokio::test]
268  async fn test_json_validation_with_missing_context() {
269    let app = init_service(App::new().service(resource("/").route(post().to(test_handler_with_context)))).await;
270
271    let req = TestRequest::post()
272      .uri("/")
273      .set_json(&JsonData { age: 24 })
274      .to_request();
275    let resp = call_service(&app, req).await;
276    assert_eq!(resp.status(), StatusCode::OK);
277
278    let req = TestRequest::post()
279      .uri("/")
280      .set_json(&JsonData { age: 30 })
281      .to_request();
282    let resp = call_service(&app, req).await;
283    assert_eq!(resp.status(), StatusCode::OK);
284  }
285}