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}