rocket_validation/
lib.rs

1//! # Rocket Validation
2//!
3//! Welcome to the Rocket Validation crate. If you are looking to validate your Json, Form or Query Structs using Rocket you have come to the right place!
4//!
5//! ## Why
6//! Rocket is using Rusts powerful typing system. Which is amazing because you can be sure its what you want. But is it? How about kebab-case strings or phone number inputs, these aren’t really types.
7//! You could implement a [custom deserializer](https://docs.serde.rs/serde/de/trait.Deserializer.html) for a wrapped type or write custom logic to validate it on endpoint calls, thats error prone and not ergonomic and doesn't allow you to return meaningful and contextual errors.
8//!
9//! If you are coming from TypeScript you might have heard of [class-validator](https://github.com/typestack/class-validator) which is simple, declarative and can be implemented into middleware. Using [validator](https://github.com/Keats/validator) this crate achieves a similar result using rockets [guard](https://rocket.rs/v0.5-rc/guide/requests/#request-guards) mechanism.
10//! > Anything implementing [Json](https://rocket.rs/v0.5-rc/guide/requests/#json), [FromRequest](https://rocket.rs/v0.5-rc/guide/requests/#custom-guards) or [FromForm](https://rocket.rs/v0.5-rc/guide/requests/#forms) as well as [`Validate`](https://docs.rs/validator/latest/validator/#example) are able to use the `Validated` guard of this crate, so you can be sure your data is validated once you receive it in your handler.
11//!
12//! > Using rockets [catchers](https://rocket.rs/v0.5-rc/guide/requests/#error-catchers) you are able to route errors which occurs during validation to your user.
13//!
14//! Current validation in rocket: Rocket has validation for FromForm structs but for nothing else.
15//!
16//! ## Usage
17//!
18//! In order to get going, you need to depend on the `rocket-validation`.
19//!
20//! Add this to your `Cargo.toml`
21//! ```toml
22//! [dependencies]
23//! rocket-validation = "0.1.0"
24//! validator="?"
25//! ```
26//! > `validator` is needed as the derive macros of the crate `validator` generate code dependent on it being available in a global scope
27//!
28//! Now you can go on and implement your Validation
29//! ```rust
30//! # #[macro_use] extern crate rocket;
31//! ///  Some types for Json types
32//! use rocket::serde::{json::Json, Deserialize, Serialize};
33//!
34//! ///  Will be important for validation....
35//! use rocket_validation::{Validate, Validated};
36//!
37//! #[derive(Debug, Deserialize, Serialize, Validate)]
38//! ///  Implements `Validate`
39//! #[serde(crate = "rocket::serde")]
40//! pub struct HelloData {
41//!     #[validate(length(min = 1))]
42//!     ///  Your validation annotation
43//!     name: String,
44//!     #[validate(range(min = 0, max = 100))]
45//!     ///  Your validation annotation
46//!     age: u8,
47//! }
48//!
49//! #[post("/hello", format = "application/json", data = "<data>")]
50//! fn validated_hello(
51//!     data: /* Uses the `Validated` type */ Validated<Json<HelloData>>,
52//! ) -> Json<HelloData> {
53//!     Json(data.0 .0)
54//! }
55//!
56//! #[launch]
57//! fn rocket() -> _ {
58//!     rocket::build().mount("/", routes![validated_hello])
59//! }
60//! ```
61//! ### Exposing errors to clients
62//!
63//! > Before you use the following, you should be aware of what errors you expose to your clients as well as what that means for security.
64//!
65//! If you would like to respond invalid requests with some custom messages, you can implement the `validation_catcher` catcher to do so.
66//! ```rust
67//! # #[macro_use] extern crate rocket;
68//! #[launch]
69//! fn rocket() -> _ {
70//!     rocket::build()
71//!         .mount("/", routes![/*validated_hello*/])
72//!         .register("/", catchers![rocket_validation::validation_catcher])
73//! }
74//! ```
75#![deny(clippy::all, clippy::cargo)]
76#![forbid(unsafe_code)]
77
78#[allow(unused_imports)]
79#[macro_use]
80pub extern crate validator;
81
82#[macro_use]
83extern crate rocket;
84
85use rocket::{
86    data::{Data, FromData, Outcome as DataOutcome},
87    form,
88    form::{DataField, FromForm, ValueField},
89    http::Status,
90    outcome::Outcome,
91    request::{FromRequest, Request},
92    serde::{json::Json, Serialize},
93};
94use std::fmt::Debug;
95pub use validator::{Validate, ValidationErrors};
96
97///  Struct used for Request Guards
98#[derive(Clone, Debug)]
99pub struct Validated<T>(pub T);
100
101///  Impl to get type T of `Json`
102impl<T> Validated<Json<T>> {
103    #[inline]
104    pub fn into_deep_inner(self) -> T {
105        self.0 .0
106    }
107}
108
109///  Impl to get type T
110impl<T> Validated<T> {
111    #[inline]
112    pub fn into_inner(self) -> T {
113        self.0
114    }
115}
116
117///  Struct representing errors sent by the catcher
118#[derive(Serialize)]
119#[serde(crate = "rocket::serde")]
120pub struct Error<'a> {
121    code: u128,
122    message: &'a str,
123    errors: Option<&'a ValidationErrors>,
124}
125
126///  Catcher to return validation errors to the client
127///  ```rust
128///  # #[macro_use] extern crate rocket;
129///  #[launch]
130///  fn rocket() -> _ {
131///      rocket::build()
132///          .mount("/", routes![/*validated_hello*/])
133///  /* right here ---->*/.register("/", catchers![rocket_validation::validation_catcher])
134///  }
135///  ```
136#[catch(422)]
137pub fn validation_catcher<'a>(req: &'a Request) -> Json<Error<'a>> {
138    Json(Error {
139        code: 422,
140        message: "Unprocessable Entity. The request was well-formed but was unable to be followed \
141                  due to semantic errors.",
142        errors: req.local_cache(|| CachedValidationErrors(None)).0.as_ref(),
143    })
144}
145
146///  Wrapper used to store `ValidationErrors` within the scope of the request
147#[derive(Clone)]
148pub struct CachedValidationErrors(pub Option<ValidationErrors>);
149
150///  Implementation of `Validated` for `Json`
151//
152///  An example with `Json`
153///  ```rust
154///  # #[macro_use] extern crate rocket;
155///  use rocket::serde::{json::Json, Deserialize, Serialize};
156///  use rocket_validation::{Validate, Validated};
157///  
158///  #[derive(Debug, Deserialize, Serialize, Validate)]
159///  #[serde(crate = "rocket::serde")]
160///  pub struct HelloData {
161///      #[validate(length(min = 1))]
162///      name: String,
163///      #[validate(range(min = 0, max = 100))]
164///      age: u8,
165///  }
166//
167///  #[post("/hello", format = "application/json", data = "<data>")]
168///  fn validated_hello(data: Validated<Json<HelloData>>) -> Json<HelloData> {
169///      Json(data.into_deep_inner())
170///  }
171///  
172///  #[launch]
173///  fn rocket() -> _ {
174///      rocket::build()
175///          .mount("/", routes![validated_hello])
176///          .register("/", catchers![rocket_validation::validation_catcher])
177///  }
178///  ```
179#[rocket::async_trait]
180impl<'r, D: Validate + rocket::serde::Deserialize<'r>> FromData<'r> for Validated<Json<D>> {
181    type Error = Result<ValidationErrors, rocket::serde::json::Error<'r>>;
182
183    async fn from_data(req: &'r Request<'_>, data: Data<'r>) -> DataOutcome<'r, Self> {
184        let data_outcome = <Json<D> as FromData<'r>>::from_data(req, data).await;
185
186        match data_outcome {
187            Outcome::Error((status, err)) => Outcome::Error((status, Err(err))),
188            Outcome::Forward(err) => Outcome::Forward(err),
189            Outcome::Success(data) => match data.validate() {
190                Ok(_) => Outcome::Success(Validated(data)),
191                Err(err) => {
192                    req.local_cache(|| CachedValidationErrors(Some(err.to_owned())));
193                    Outcome::Error((Status::UnprocessableEntity, Ok(err)))
194                }
195            },
196        }
197    }
198}
199
200///  Implementation of `Validated` for `FromRequest` implementing `Validate`
201//
202///  Anything you implement `FromRequest` for as well as `Validate`
203#[rocket::async_trait]
204impl<'r, D: Validate + FromRequest<'r>> FromRequest<'r> for Validated<D> {
205    type Error = Result<ValidationErrors, D::Error>;
206    async fn from_request(req: &'r Request<'_>) -> rocket::request::Outcome<Self, Self::Error> {
207        let data_outcome = D::from_request(req).await;
208
209        match data_outcome {
210            Outcome::Error((status, err)) => Outcome::Error((status, Err(err))),
211            Outcome::Forward(err) => Outcome::Forward(err),
212            Outcome::Success(data) => match data.validate() {
213                Ok(_) => Outcome::Success(Validated(data)),
214                Err(err) => {
215                    req.local_cache(|| CachedValidationErrors(Some(err.to_owned())));
216                    Outcome::Error((Status::UnprocessableEntity, Ok(err)))
217                }
218            },
219        }
220    }
221}
222
223///  Implementation of `Validated` for `FromForm`
224///
225///  An example validating a query struct
226///  ```rust
227///  # #[macro_use] extern crate rocket;
228///  use rocket::serde::{json::Json, Deserialize, Serialize};
229///  use rocket_validation::{Validate, Validated};
230///  
231///  #[derive(Debug, Deserialize, Serialize, Validate, FromForm)]
232///  #[serde(crate = "rocket::serde")]
233///  pub struct HelloData {
234///      #[validate(length(min = 1))]
235///      name: String,
236///      #[validate(range(min = 0, max = 100))]
237///      age: u8,
238///  }
239//
240///  #[get("/validated-hello?<params..>", format = "application/json")]
241///  fn validated_hello(params: Validated<HelloData>) -> Json<HelloData> {
242///      Json(params.into_inner())
243///  }
244///  
245///  #[launch]
246///  fn rocket() -> _ {
247///      rocket::build()
248///          .mount("/", routes![validated_hello])
249///          .register("/", catchers![rocket_validation::validation_catcher])
250///  }
251///  ```
252#[rocket::async_trait]
253impl<'r, T: Validate + FromForm<'r>> FromForm<'r> for Validated<T> {
254    type Context = T::Context;
255
256    #[inline]
257    fn init(opts: form::Options) -> Self::Context {
258        T::init(opts)
259    }
260
261    #[inline]
262    fn push_value(ctxt: &mut Self::Context, field: ValueField<'r>) {
263        T::push_value(ctxt, field)
264    }
265
266    #[inline]
267    async fn push_data(ctxt: &mut Self::Context, field: DataField<'r, '_>) {
268        T::push_data(ctxt, field).await
269    }
270
271    fn finalize(this: Self::Context) -> form::Result<'r, Self> {
272        match T::finalize(this) {
273            Err(err) => Err(err),
274            Ok(data) => match data.validate() {
275                Ok(_) => Ok(Validated(data)),
276                Err(err) => Err(err
277                    .into_errors()
278                    .into_iter()
279                    .map(|e| form::Error {
280                        name: Some(e.0.into()),
281                        kind: form::error::ErrorKind::Validation(std::borrow::Cow::Borrowed(e.0)),
282                        value: None,
283                        entity: form::error::Entity::Value,
284                    })
285                    .collect::<Vec<_>>()
286                    .into()),
287            },
288        }
289    }
290}