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