update to 0.5.0
This commit is contained in:
38
Cargo.toml
38
Cargo.toml
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "axum-valid"
|
name = "axum-valid"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
description = "Validation tools for axum using the validator library."
|
description = "Validation tools for axum using the validator library."
|
||||||
authors = ["GengTeng <me@gteng.org>"]
|
authors = ["GengTeng <me@gteng.org>"]
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
@@ -23,15 +23,30 @@ edition = "2021"
|
|||||||
axum = { version = "0.6.18", default-features = false }
|
axum = { version = "0.6.18", default-features = false }
|
||||||
validator = "0.16.0"
|
validator = "0.16.0"
|
||||||
|
|
||||||
|
[dependencies.axum-msgpack]
|
||||||
|
version = "0.3.0"
|
||||||
|
default-features = false
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.axum-yaml]
|
||||||
|
version = "0.3.0"
|
||||||
|
default-features = false
|
||||||
|
optional = true
|
||||||
|
|
||||||
|
[dependencies.axum-extra]
|
||||||
|
version = "0.7.6"
|
||||||
|
default-features = false
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = "1.0.71"
|
anyhow = "1.0.72"
|
||||||
axum = { version = "0.6.18" }
|
axum = { version = "0.6.19" }
|
||||||
tokio = { version = "1.28.2", features = ["full"] }
|
tokio = { version = "1.29.1", features = ["full"] }
|
||||||
hyper = { version = "0.14.26", features = ["full"] }
|
hyper = { version = "0.14.27", features = ["full"] }
|
||||||
reqwest = { version = "0.11.18", features = ["json"] }
|
reqwest = { version = "0.11.18", features = ["json"] }
|
||||||
serde = { version = "1.0.163", features = ["derive"] }
|
serde = { version = "1.0.180", features = ["derive"] }
|
||||||
validator = { version = "0.16.0", features = ["derive"] }
|
validator = { version = "0.16.0", features = ["derive"] }
|
||||||
serde_json = "1.0.103"
|
serde_json = "1.0.104"
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
@@ -39,5 +54,14 @@ default = ["json", "form", "query"]
|
|||||||
json = ["axum/json"]
|
json = ["axum/json"]
|
||||||
form = ["axum/form"]
|
form = ["axum/form"]
|
||||||
query = ["axum/query"]
|
query = ["axum/query"]
|
||||||
|
typed_header = ["axum/headers"]
|
||||||
|
msgpack = ["axum-msgpack"]
|
||||||
|
yaml = ["axum-yaml"]
|
||||||
into_json = ["json"]
|
into_json = ["json"]
|
||||||
422 = []
|
422 = []
|
||||||
|
extra = ["axum-extra"]
|
||||||
|
extra_query = ["axum-extra/query"]
|
||||||
|
extra_form = ["axum-extra/form"]
|
||||||
|
extra_protobuf = ["axum-extra/protobuf"]
|
||||||
|
extra_all = ["extra","extra_query", "extra_form", "extra_protobuf"]
|
||||||
|
all_types = ["json", "form", "query", "typed_header", "msgpack", "yaml", "extra_all"]
|
||||||
|
|||||||
36
README.md
36
README.md
@@ -6,7 +6,9 @@
|
|||||||
[](https://github.com/gengteng/axum-valid/actions/workflows/ci.yml)
|
[](https://github.com/gengteng/axum-valid/actions/workflows/ci.yml)
|
||||||
[](https://coveralls.io/github/gengteng/axum-valid?branch=main)
|
[](https://coveralls.io/github/gengteng/axum-valid?branch=main)
|
||||||
|
|
||||||
This crate provides a `Valid` type that can be used in combination with `Json`, `Path`, `Query`, and `Form` types to validate the entities that implement the `Validate` trait.
|
This crate provides a `Valid` type that can be used in combination with `Json`, `Path`, `Query`, and `Form` types to validate the entities that implement the `Validate` trait from the `validator` crate.
|
||||||
|
|
||||||
|
Additional extractors like `TypedHeader`, `MsgPack`, `Yaml` etc. are supported through optional features.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -46,9 +48,33 @@ pub async fn get_page_by_json(
|
|||||||
|
|
||||||
When validation errors occur, the extractor will automatically return 400 with validation errors as the HTTP message body.
|
When validation errors occur, the extractor will automatically return 400 with validation errors as the HTTP message body.
|
||||||
|
|
||||||
For more usage examples, please refer to the `basic.rs` and `custom.rs` files in the `tests` directory.
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* `422`: Use `422 Unprocessable Entity` instead of `400 Bad Request` as the status code when validation fails.
|
| Feature | Description | Default | Tests |
|
||||||
* `into_json`: When this feature is enabled, validation errors will be serialized into JSON format and returned as the HTTP body.
|
|----------------|------------------------------------------------------------------------------------------------------|---------|-------|
|
||||||
|
| default | Enables support for `Path`, `Query`, `Json` and `Form` | ✅ | ✅ |
|
||||||
|
| json | Enables support for `Json` | ✅ | ✅ |
|
||||||
|
| query | Enables support for `Query` | ✅ | ✅ |
|
||||||
|
| form | Enables support for `Form` | ✅ | ✅ |
|
||||||
|
| typed_header | Enables support for `TypedHeader` | ❌ | ✅ |
|
||||||
|
| msgpack | Enables support for `MsgPack` and `MsgPackRaw` from `axum-msgpack` | ❌ | ❌ |
|
||||||
|
| yaml | Enables support for `Yaml` from `axum-yaml` | ❌ | ❌ |
|
||||||
|
| extra_protobuf | Enables support for `Protobuf` from `axum-extra` | ❌ | ❌ |
|
||||||
|
| extra | Enables support for `Cached`, `WithRejection` from `axum-extra` | ❌ | ✅ |
|
||||||
|
| extra_query | Enables support for `Query` from `axum-extra` | ❌ | ❌ |
|
||||||
|
| extra_form | Enables support for `Form` from `axum-extra` | ❌ | ❌ |
|
||||||
|
| extra_protobuf | Enables support for `Protobuf` from `axum-extra` | ❌ | ❌ |
|
||||||
|
| extra_all | Enables support for all extractors above from `axum-extra` | ❌ | 🚧 |
|
||||||
|
| all_types | Enables support for all extractors above | ❌ | 🚧 |
|
||||||
|
| 422 | Use `422 Unprocessable Entity` instead of `400 Bad Request` as the status code when validation fails | ❌ | ✅ |
|
||||||
|
| into_json | Validation errors will be serialized into JSON format and returned as the HTTP body | ❌ | ✅ |
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under the MIT License.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
* [axum](https://crates.io/crates/axum)
|
||||||
|
* [validator](https://crates.io/crates/validator)
|
||||||
|
* [serde](https://crates.io/crates/serde)
|
||||||
71
src/extra.rs
Normal file
71
src/extra.rs
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
//! # Implementation of the `HasValidate` trait for the extractor in `axum-extra`.
|
||||||
|
//!
|
||||||
|
|
||||||
|
#[cfg(feature = "extra_form")]
|
||||||
|
pub mod form;
|
||||||
|
#[cfg(feature = "extra_protobuf")]
|
||||||
|
pub mod protobuf;
|
||||||
|
#[cfg(feature = "extra_query")]
|
||||||
|
pub mod query;
|
||||||
|
|
||||||
|
use crate::HasValidate;
|
||||||
|
use axum_extra::extract::{Cached, WithRejection};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for Cached<T> {
|
||||||
|
type Validate = T;
|
||||||
|
|
||||||
|
fn get_validate(&self) -> &Self::Validate {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Validate, R> HasValidate for WithRejection<T, R> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::tests::{Rejection, ValidTest};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum_extra::extract::{Cached, WithRejection};
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
|
||||||
|
impl<T: ValidTest> ValidTest for Cached<T> {
|
||||||
|
const ERROR_STATUS_CODE: StatusCode = T::ERROR_STATUS_CODE;
|
||||||
|
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
T::set_valid_request(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
// cached never fails
|
||||||
|
T::set_error_request(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
T::set_invalid_request(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: ValidTest, R: Rejection> ValidTest for WithRejection<T, R> {
|
||||||
|
// just use conflict to test
|
||||||
|
const ERROR_STATUS_CODE: StatusCode = R::STATUS_CODE;
|
||||||
|
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
T::set_valid_request(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
// cached never fails
|
||||||
|
T::set_error_request(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
T::set_invalid_request(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/extra/form.rs
Normal file
13
src/extra/form.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//! # Implementation of the `HasValidate` trait for the `Form` extractor in `axum-extra`.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use crate::HasValidate;
|
||||||
|
use axum_extra::extract::Form;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for Form<T> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/extra/protobuf.rs
Normal file
13
src/extra/protobuf.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//! # Implementation of the `HasValidate` trait for the `Form` extractor.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use crate::HasValidate;
|
||||||
|
use axum_extra::protobuf::Protobuf;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for Protobuf<T> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/extra/query.rs
Normal file
13
src/extra/query.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//! # Implementation of the `HasValidate` trait for the `Query` extractor in `axum-extra`.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use crate::HasValidate;
|
||||||
|
use axum_extra::extract::Query;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for Query<T> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
28
src/form.rs
28
src/form.rs
@@ -1,21 +1,37 @@
|
|||||||
//! # Implementation of the `HasValidate` trait for the `Form` extractor.
|
//! # Implementation of the `HasValidate` trait for the `Form` extractor.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use crate::{HasValidate, ValidRejection};
|
use crate::HasValidate;
|
||||||
use axum::extract::rejection::FormRejection;
|
|
||||||
use axum::Form;
|
use axum::Form;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
impl<T: Validate> HasValidate for Form<T> {
|
impl<T: Validate> HasValidate for Form<T> {
|
||||||
type Validate = T;
|
type Validate = T;
|
||||||
type Rejection = FormRejection;
|
|
||||||
fn get_validate(&self) -> &T {
|
fn get_validate(&self) -> &T {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<FormRejection> for ValidRejection<FormRejection> {
|
#[cfg(test)]
|
||||||
fn from(value: FormRejection) -> Self {
|
mod tests {
|
||||||
Self::Inner(value)
|
use crate::tests::{ValidTest, ValidTestParameter};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::Form;
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
|
||||||
|
impl<T: ValidTestParameter> ValidTest for Form<T> {
|
||||||
|
const ERROR_STATUS_CODE: StatusCode = StatusCode::UNPROCESSABLE_ENTITY;
|
||||||
|
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.form(T::valid())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.form(T::error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.form(T::invalid())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
src/json.rs
28
src/json.rs
@@ -1,21 +1,37 @@
|
|||||||
//! # Implementation of the `HasValidate` trait for the `Json` extractor.
|
//! # Implementation of the `HasValidate` trait for the `Json` extractor.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use crate::{HasValidate, ValidRejection};
|
use crate::HasValidate;
|
||||||
use axum::extract::rejection::JsonRejection;
|
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
impl<T: Validate> HasValidate for Json<T> {
|
impl<T: Validate> HasValidate for Json<T> {
|
||||||
type Validate = T;
|
type Validate = T;
|
||||||
type Rejection = JsonRejection;
|
|
||||||
fn get_validate(&self) -> &T {
|
fn get_validate(&self) -> &T {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<JsonRejection> for ValidRejection<JsonRejection> {
|
#[cfg(test)]
|
||||||
fn from(value: JsonRejection) -> Self {
|
mod tests {
|
||||||
Self::Inner(value)
|
use crate::tests::{ValidTest, ValidTestParameter};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::Json;
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
|
||||||
|
impl<T: ValidTestParameter> ValidTest for Json<T> {
|
||||||
|
const ERROR_STATUS_CODE: StatusCode = StatusCode::UNPROCESSABLE_ENTITY;
|
||||||
|
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.json(T::valid())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.json(T::error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.json(T::invalid())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
99
src/lib.rs
99
src/lib.rs
@@ -1,19 +1,30 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
#![deny(unsafe_code, missing_docs, clippy::unwrap_used)]
|
#![deny(unsafe_code, missing_docs, clippy::unwrap_used)]
|
||||||
|
|
||||||
|
#[cfg(feature = "extra")]
|
||||||
|
pub mod extra;
|
||||||
#[cfg(feature = "form")]
|
#[cfg(feature = "form")]
|
||||||
pub mod form;
|
pub mod form;
|
||||||
#[cfg(feature = "json")]
|
#[cfg(feature = "json")]
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
#[cfg(feature = "msgpack")]
|
||||||
|
pub mod msgpack;
|
||||||
pub mod path;
|
pub mod path;
|
||||||
#[cfg(feature = "query")]
|
#[cfg(feature = "query")]
|
||||||
pub mod query;
|
pub mod query;
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod test;
|
||||||
|
#[cfg(feature = "typed_header")]
|
||||||
|
pub mod typed_header;
|
||||||
|
#[cfg(feature = "yaml")]
|
||||||
|
pub mod yaml;
|
||||||
|
|
||||||
use axum::async_trait;
|
use axum::async_trait;
|
||||||
use axum::extract::{FromRequest, FromRequestParts};
|
use axum::extract::{FromRequest, FromRequestParts};
|
||||||
use axum::http::request::Parts;
|
use axum::http::request::Parts;
|
||||||
use axum::http::{Request, StatusCode};
|
use axum::http::{Request, StatusCode};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use std::ops::{Deref, DerefMut};
|
||||||
use validator::{Validate, ValidationErrors};
|
use validator::{Validate, ValidationErrors};
|
||||||
|
|
||||||
/// Http status code returned when there are validation errors.
|
/// Http status code returned when there are validation errors.
|
||||||
@@ -25,7 +36,21 @@ pub const VALIDATION_ERROR_STATUS: StatusCode = StatusCode::BAD_REQUEST;
|
|||||||
|
|
||||||
/// Valid entity extractor
|
/// Valid entity extractor
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub struct Valid<T>(pub T);
|
pub struct Valid<E>(pub E);
|
||||||
|
|
||||||
|
impl<E> Deref for Valid<E> {
|
||||||
|
type Target = E;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> DerefMut for Valid<E> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// If the valid extractor fails it'll use this "rejection" type.
|
/// If the valid extractor fails it'll use this "rejection" type.
|
||||||
/// This rejection type can be converted into a response.
|
/// This rejection type can be converted into a response.
|
||||||
@@ -64,44 +89,84 @@ impl<E: IntoResponse> IntoResponse for ValidRejection<E> {
|
|||||||
pub trait HasValidate {
|
pub trait HasValidate {
|
||||||
/// Inner type that can be validated for correctness
|
/// Inner type that can be validated for correctness
|
||||||
type Validate: Validate;
|
type Validate: Validate;
|
||||||
/// If the inner extractor fails it'll use this "rejection" type.
|
/// Get the inner value
|
||||||
/// A rejection is a kind of error that can be converted into a response.
|
|
||||||
type Rejection: IntoResponse;
|
|
||||||
/// get the inner type
|
|
||||||
fn get_validate(&self) -> &Self::Validate;
|
fn get_validate(&self) -> &Self::Validate;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S, B, T> FromRequest<S, B> for Valid<T>
|
impl<S, B, E> FromRequest<S, B> for Valid<E>
|
||||||
where
|
where
|
||||||
S: Send + Sync + 'static,
|
S: Send + Sync + 'static,
|
||||||
B: Send + Sync + 'static,
|
B: Send + Sync + 'static,
|
||||||
T: HasValidate + FromRequest<S, B>,
|
E: HasValidate + FromRequest<S, B>,
|
||||||
T::Validate: Validate,
|
E::Validate: Validate,
|
||||||
ValidRejection<<T as HasValidate>::Rejection>: From<<T as FromRequest<S, B>>::Rejection>,
|
|
||||||
{
|
{
|
||||||
type Rejection = ValidRejection<<T as HasValidate>::Rejection>;
|
type Rejection = ValidRejection<<E as FromRequest<S, B>>::Rejection>;
|
||||||
|
|
||||||
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request(req: Request<B>, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
let inner = T::from_request(req, state).await?;
|
let inner = E::from_request(req, state)
|
||||||
|
.await
|
||||||
|
.map_err(ValidRejection::Inner)?;
|
||||||
inner.get_validate().validate()?;
|
inner.get_validate().validate()?;
|
||||||
Ok(Valid(inner))
|
Ok(Valid(inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl<S, T> FromRequestParts<S> for Valid<T>
|
impl<S, E> FromRequestParts<S> for Valid<E>
|
||||||
where
|
where
|
||||||
S: Send + Sync + 'static,
|
S: Send + Sync + 'static,
|
||||||
T: HasValidate + FromRequestParts<S>,
|
E: HasValidate + FromRequestParts<S>,
|
||||||
T::Validate: Validate,
|
E::Validate: Validate,
|
||||||
ValidRejection<<T as HasValidate>::Rejection>: From<<T as FromRequestParts<S>>::Rejection>,
|
|
||||||
{
|
{
|
||||||
type Rejection = ValidRejection<<T as HasValidate>::Rejection>;
|
type Rejection = ValidRejection<<E as FromRequestParts<S>>::Rejection>;
|
||||||
|
|
||||||
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||||
let inner = T::from_request_parts(parts, state).await?;
|
let inner = E::from_request_parts(parts, state)
|
||||||
|
.await
|
||||||
|
.map_err(ValidRejection::Inner)?;
|
||||||
inner.get_validate().validate()?;
|
inner.get_validate().validate()?;
|
||||||
Ok(Valid(inner))
|
Ok(Valid(inner))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub mod tests {
|
||||||
|
use reqwest::{RequestBuilder, StatusCode};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
/// # Valid test parameter
|
||||||
|
pub trait ValidTestParameter: Serialize + 'static {
|
||||||
|
/// Create a valid parameter
|
||||||
|
fn valid() -> &'static Self;
|
||||||
|
/// Create an error serializable array
|
||||||
|
fn error() -> &'static [(&'static str, &'static str)];
|
||||||
|
/// Create a invalid parameter
|
||||||
|
fn invalid() -> &'static Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Valid Tests
|
||||||
|
///
|
||||||
|
/// This trait defines three test cases to check
|
||||||
|
/// if an extractor combined with the Valid type works properly.
|
||||||
|
///
|
||||||
|
/// 1. For a valid request, the server should return `200 OK`.
|
||||||
|
/// 2. For an invalid request according to the extractor, the server should return the error HTTP status code defined by the extractor itself.
|
||||||
|
/// 3. For an invalid request according to Valid, the server should return VALIDATION_ERROR_STATUS as the error code.
|
||||||
|
///
|
||||||
|
pub trait ValidTest {
|
||||||
|
/// Http status code when inner extractor failed
|
||||||
|
const ERROR_STATUS_CODE: StatusCode;
|
||||||
|
/// Build a valid request, the server should return `200 OK`.
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder;
|
||||||
|
/// Build an invalid request according to the extractor, the server should return `Self::ERROR_STATUS_CODE`
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder;
|
||||||
|
/// Build an invalid request according to Valid, the server should return VALIDATION_ERROR_STATUS
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "extra")]
|
||||||
|
pub trait Rejection {
|
||||||
|
const STATUS_CODE: StatusCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
20
src/msgpack.rs
Normal file
20
src/msgpack.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
//! # Implementation of the `HasValidate` trait for the `MsgPack` extractor.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use crate::HasValidate;
|
||||||
|
use axum_msgpack::{MsgPack, MsgPackRaw};
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for MsgPack<T> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for MsgPackRaw<T> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/path.rs
10
src/path.rs
@@ -1,21 +1,13 @@
|
|||||||
//! # Implementation of the `HasValidate` trait for the `Path` extractor.
|
//! # Implementation of the `HasValidate` trait for the `Path` extractor.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use crate::{HasValidate, ValidRejection};
|
use crate::HasValidate;
|
||||||
use axum::extract::rejection::PathRejection;
|
|
||||||
use axum::extract::Path;
|
use axum::extract::Path;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
impl<T: Validate> HasValidate for Path<T> {
|
impl<T: Validate> HasValidate for Path<T> {
|
||||||
type Validate = T;
|
type Validate = T;
|
||||||
type Rejection = PathRejection;
|
|
||||||
fn get_validate(&self) -> &T {
|
fn get_validate(&self) -> &T {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<PathRejection> for ValidRejection<PathRejection> {
|
|
||||||
fn from(value: PathRejection) -> Self {
|
|
||||||
Self::Inner(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
28
src/query.rs
28
src/query.rs
@@ -1,21 +1,37 @@
|
|||||||
//! # Implementation of the `HasValidate` trait for the `Query` extractor.
|
//! # Implementation of the `HasValidate` trait for the `Query` extractor.
|
||||||
//!
|
//!
|
||||||
|
|
||||||
use crate::{HasValidate, ValidRejection};
|
use crate::HasValidate;
|
||||||
use axum::extract::rejection::QueryRejection;
|
|
||||||
use axum::extract::Query;
|
use axum::extract::Query;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
impl<T: Validate> HasValidate for Query<T> {
|
impl<T: Validate> HasValidate for Query<T> {
|
||||||
type Validate = T;
|
type Validate = T;
|
||||||
type Rejection = QueryRejection;
|
|
||||||
fn get_validate(&self) -> &T {
|
fn get_validate(&self) -> &T {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<QueryRejection> for ValidRejection<QueryRejection> {
|
#[cfg(test)]
|
||||||
fn from(value: QueryRejection) -> Self {
|
mod tests {
|
||||||
Self::Inner(value)
|
use crate::tests::{ValidTest, ValidTestParameter};
|
||||||
|
use axum::extract::Query;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
|
||||||
|
impl<T: ValidTestParameter> ValidTest for Query<T> {
|
||||||
|
const ERROR_STATUS_CODE: StatusCode = StatusCode::BAD_REQUEST;
|
||||||
|
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.query(&T::valid())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.query(T::error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.query(&T::invalid())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
430
src/test.rs
Normal file
430
src/test.rs
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
use crate::tests::{ValidTest, ValidTestParameter};
|
||||||
|
use crate::{Valid, VALIDATION_ERROR_STATUS};
|
||||||
|
use axum::extract::{Path, Query};
|
||||||
|
use axum::routing::{get, post};
|
||||||
|
use axum::{Form, Json, Router};
|
||||||
|
use hyper::Method;
|
||||||
|
use reqwest::{StatusCode, Url};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::any::type_name;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::net::SocketAddr;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Deserialize, Serialize, Validate, Eq, PartialEq)]
|
||||||
|
pub struct Parameters {
|
||||||
|
#[validate(range(min = 5, max = 10))]
|
||||||
|
v0: i32,
|
||||||
|
#[validate(length(min = 1, max = 10))]
|
||||||
|
v1: Cow<'static, str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static VALID_PARAMETERS: Parameters = Parameters {
|
||||||
|
v0: 5,
|
||||||
|
v1: Cow::Borrowed("0123456789"),
|
||||||
|
};
|
||||||
|
|
||||||
|
static INVALID_PARAMETERS: Parameters = Parameters {
|
||||||
|
v0: 6,
|
||||||
|
v1: Cow::Borrowed("01234567890"),
|
||||||
|
};
|
||||||
|
|
||||||
|
impl ValidTestParameter for Parameters {
|
||||||
|
fn valid() -> &'static Self {
|
||||||
|
&VALID_PARAMETERS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn error() -> &'static [(&'static str, &'static str)] {
|
||||||
|
&[("not_v0_or_v1", "value")]
|
||||||
|
}
|
||||||
|
|
||||||
|
fn invalid() -> &'static Self {
|
||||||
|
&INVALID_PARAMETERS
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn test_main() -> anyhow::Result<()> {
|
||||||
|
let router = Router::new()
|
||||||
|
.route(route::PATH, get(extract_path))
|
||||||
|
.route(route::QUERY, get(extract_query))
|
||||||
|
.route(route::FORM, post(extract_form))
|
||||||
|
.route(route::JSON, post(extract_json));
|
||||||
|
|
||||||
|
#[cfg(feature = "typed_header")]
|
||||||
|
let router = router.route(
|
||||||
|
typed_header::route::TYPED_HEADER,
|
||||||
|
post(typed_header::extract_typed_header),
|
||||||
|
);
|
||||||
|
|
||||||
|
#[cfg(feature = "extra")]
|
||||||
|
let router = router
|
||||||
|
.route(route::extra::CACHED, post(extra::extract_cached))
|
||||||
|
.route(
|
||||||
|
route::extra::WITH_REJECTION,
|
||||||
|
post(extra::extract_with_rejection),
|
||||||
|
);
|
||||||
|
|
||||||
|
let server = axum::Server::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16)))
|
||||||
|
.serve(router.into_make_service());
|
||||||
|
let server_addr = server.local_addr();
|
||||||
|
println!("Axum server address: {}.", server_addr);
|
||||||
|
|
||||||
|
let (server_guard, close) = tokio::sync::oneshot::channel::<()>();
|
||||||
|
let server_handle = tokio::spawn(server.with_graceful_shutdown(async move {
|
||||||
|
let _ = close.await;
|
||||||
|
}));
|
||||||
|
|
||||||
|
let server_url = format!("http://{}", server_addr);
|
||||||
|
let test_executor = TestExecutor::from(Url::parse(&format!("http://{}", server_addr))?);
|
||||||
|
|
||||||
|
// Valid<Path<...>>
|
||||||
|
let valid_path_response = test_executor
|
||||||
|
.client()
|
||||||
|
.get(format!(
|
||||||
|
"{}/path/{}/{}",
|
||||||
|
server_url, VALID_PARAMETERS.v0, VALID_PARAMETERS.v1
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
assert_eq!(valid_path_response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let invalid_path_response = test_executor
|
||||||
|
.client()
|
||||||
|
.get(format!("{}/path/not_i32/path", server_url))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
assert_eq!(invalid_path_response.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
||||||
|
let invalid_path_response = test_executor
|
||||||
|
.client()
|
||||||
|
.get(format!(
|
||||||
|
"{}/path/{}/{}",
|
||||||
|
server_url, INVALID_PARAMETERS.v0, INVALID_PARAMETERS.v1
|
||||||
|
))
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
assert_eq!(invalid_path_response.status(), VALIDATION_ERROR_STATUS);
|
||||||
|
#[cfg(feature = "into_json")]
|
||||||
|
check_json(invalid_path_response).await;
|
||||||
|
println!("Valid<Path<...>> works.");
|
||||||
|
|
||||||
|
test_executor
|
||||||
|
.execute::<Query<Parameters>>(Method::GET, route::QUERY)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test_executor
|
||||||
|
.execute::<Form<Parameters>>(Method::POST, route::FORM)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
test_executor
|
||||||
|
.execute::<Json<Parameters>>(Method::POST, route::JSON)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
#[cfg(feature = "typed_header")]
|
||||||
|
{
|
||||||
|
use axum::TypedHeader;
|
||||||
|
test_executor
|
||||||
|
.execute::<TypedHeader<Parameters>>(Method::POST, typed_header::route::TYPED_HEADER)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "extra")]
|
||||||
|
{
|
||||||
|
use axum_extra::extract::{Cached, WithRejection};
|
||||||
|
use extra::TestRejection;
|
||||||
|
test_executor
|
||||||
|
.execute::<Cached<Parameters>>(Method::POST, route::extra::CACHED)
|
||||||
|
.await?;
|
||||||
|
test_executor
|
||||||
|
.execute::<WithRejection<Parameters, TestRejection>>(
|
||||||
|
Method::POST,
|
||||||
|
route::extra::WITH_REJECTION,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
drop(server_guard);
|
||||||
|
server_handle.await??;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct TestExecutor {
|
||||||
|
client: reqwest::Client,
|
||||||
|
server_url: Url,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Url> for TestExecutor {
|
||||||
|
fn from(server_url: Url) -> Self {
|
||||||
|
Self {
|
||||||
|
client: Default::default(),
|
||||||
|
server_url,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestExecutor {
|
||||||
|
/// Execute all tests
|
||||||
|
pub async fn execute<T: ValidTest>(&self, method: Method, route: &str) -> anyhow::Result<()> {
|
||||||
|
let url = {
|
||||||
|
let mut url_builder = self.server_url.clone();
|
||||||
|
url_builder.set_path(route);
|
||||||
|
url_builder
|
||||||
|
};
|
||||||
|
|
||||||
|
let valid_builder = self.client.request(method.clone(), url.clone());
|
||||||
|
let valid_response = T::set_valid_request(valid_builder).send().await?;
|
||||||
|
assert_eq!(valid_response.status(), StatusCode::OK);
|
||||||
|
|
||||||
|
let error_builder = self.client.request(method.clone(), url.clone());
|
||||||
|
let error_response = T::set_error_request(error_builder).send().await?;
|
||||||
|
assert_eq!(error_response.status(), T::ERROR_STATUS_CODE);
|
||||||
|
|
||||||
|
let invalid_builder = self.client.request(method, url);
|
||||||
|
let invalid_response = T::set_invalid_request(invalid_builder).send().await?;
|
||||||
|
assert_eq!(invalid_response.status(), VALIDATION_ERROR_STATUS);
|
||||||
|
#[cfg(feature = "into_json")]
|
||||||
|
check_json(invalid_response).await;
|
||||||
|
println!("{} works.", type_name::<T>());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn client(&self) -> &reqwest::Client {
|
||||||
|
&self.client
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the response is a json response
|
||||||
|
#[cfg(feature = "into_json")]
|
||||||
|
pub async fn check_json(response: reqwest::Response) {
|
||||||
|
assert_eq!(
|
||||||
|
response.headers()[axum::http::header::CONTENT_TYPE],
|
||||||
|
axum::http::HeaderValue::from_static(mime::APPLICATION_JSON.as_ref())
|
||||||
|
);
|
||||||
|
assert!(response.json::<serde_json::Value>().await.is_ok());
|
||||||
|
}
|
||||||
|
|
||||||
|
mod route {
|
||||||
|
pub const PATH: &str = "/path/:v0/:v1";
|
||||||
|
pub const QUERY: &str = "/query";
|
||||||
|
pub const FORM: &str = "/form";
|
||||||
|
pub const JSON: &str = "/json";
|
||||||
|
|
||||||
|
#[cfg(feature = "extra")]
|
||||||
|
pub mod extra {
|
||||||
|
pub const CACHED: &str = "/cached";
|
||||||
|
pub const WITH_REJECTION: &str = "/with_rejection";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_path(Valid(Path(parameters)): Valid<Path<Parameters>>) -> StatusCode {
|
||||||
|
validate_again(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_query(Valid(Query(parameters)): Valid<Query<Parameters>>) -> StatusCode {
|
||||||
|
validate_again(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_form(Valid(Form(parameters)): Valid<Form<Parameters>>) -> StatusCode {
|
||||||
|
validate_again(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn extract_json(Valid(Json(parameters)): Valid<Json<Parameters>>) -> StatusCode {
|
||||||
|
validate_again(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_again(parameters: Parameters) -> StatusCode {
|
||||||
|
// The `Valid` extractor has validated the `parameters` once,
|
||||||
|
// it should have returned `400 BAD REQUEST` if the `parameters` were invalid,
|
||||||
|
// Let's validate them again to check if the `Valid` extractor works well.
|
||||||
|
// If it works properly, this function will never return `500 INTERNAL SERVER ERROR`
|
||||||
|
match parameters.validate() {
|
||||||
|
Ok(_) => StatusCode::OK,
|
||||||
|
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "typed_header")]
|
||||||
|
mod typed_header {
|
||||||
|
|
||||||
|
pub(crate) mod route {
|
||||||
|
pub const TYPED_HEADER: &str = "/typedHeader";
|
||||||
|
}
|
||||||
|
|
||||||
|
use super::{validate_again, Parameters};
|
||||||
|
use crate::Valid;
|
||||||
|
use axum::headers::{Error, Header, HeaderName, HeaderValue};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::TypedHeader;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
pub static AXUM_VALID_PARAMETERS: HeaderName = HeaderName::from_static("axum-valid-parameters");
|
||||||
|
|
||||||
|
pub(super) async fn extract_typed_header(
|
||||||
|
Valid(TypedHeader(parameters)): Valid<TypedHeader<Parameters>>,
|
||||||
|
) -> StatusCode {
|
||||||
|
validate_again(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Header for Parameters {
|
||||||
|
fn name() -> &'static HeaderName {
|
||||||
|
&AXUM_VALID_PARAMETERS
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode<'i, I>(values: &mut I) -> Result<Self, Error>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
I: Iterator<Item = &'i HeaderValue>,
|
||||||
|
{
|
||||||
|
let value = values.next().ok_or_else(Error::invalid)?;
|
||||||
|
let src = std::str::from_utf8(value.as_bytes()).map_err(|_| Error::invalid())?;
|
||||||
|
let split = src.split(',').collect::<Vec<_>>();
|
||||||
|
match split.as_slice() {
|
||||||
|
[v0, v1] => Ok(Parameters {
|
||||||
|
v0: v0.parse().map_err(|_| Error::invalid())?,
|
||||||
|
v1: Cow::Owned(v1.to_string()),
|
||||||
|
}),
|
||||||
|
_ => Err(Error::invalid()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn encode<E: Extend<HeaderValue>>(&self, values: &mut E) {
|
||||||
|
let v0 = self.v0.to_string();
|
||||||
|
let mut vec = Vec::with_capacity(v0.len() + 1 + self.v1.len());
|
||||||
|
vec.extend_from_slice(v0.as_bytes());
|
||||||
|
vec.push(b',');
|
||||||
|
vec.extend_from_slice(self.v1.as_bytes());
|
||||||
|
let value = HeaderValue::from_bytes(&vec).expect("Failed to build header");
|
||||||
|
values.extend(::std::iter::once(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parameter_is_header() -> anyhow::Result<()> {
|
||||||
|
let parameter = Parameters {
|
||||||
|
v0: 123456,
|
||||||
|
v1: Cow::Owned("111111".to_string()),
|
||||||
|
};
|
||||||
|
let mut vec = Vec::new();
|
||||||
|
parameter.encode(&mut vec);
|
||||||
|
let mut iter = vec.iter();
|
||||||
|
assert_eq!(parameter, Parameters::decode(&mut iter)?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "extra")]
|
||||||
|
mod extra {
|
||||||
|
use crate::test::{validate_again, Parameters};
|
||||||
|
use crate::tests::{Rejection, ValidTest, ValidTestParameter};
|
||||||
|
use crate::Valid;
|
||||||
|
use axum::extract::FromRequestParts;
|
||||||
|
use axum::http::request::Parts;
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::response::{IntoResponse, Response};
|
||||||
|
use axum_extra::extract::{Cached, WithRejection};
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
|
||||||
|
pub const PARAMETERS_HEADER: &str = "parameters-header";
|
||||||
|
pub const CACHED_REJECTION_STATUS: StatusCode = StatusCode::FORBIDDEN;
|
||||||
|
|
||||||
|
// 1.2. Define you own `Rejection` type and implement `IntoResponse` for it.
|
||||||
|
pub enum ParametersRejection {
|
||||||
|
Null,
|
||||||
|
InvalidJson(serde_json::error::Error),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for ParametersRejection {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
match self {
|
||||||
|
ParametersRejection::Null => {
|
||||||
|
(CACHED_REJECTION_STATUS, "My-Data header is missing").into_response()
|
||||||
|
}
|
||||||
|
ParametersRejection::InvalidJson(e) => (
|
||||||
|
CACHED_REJECTION_STATUS,
|
||||||
|
format!("My-Data is not valid json string: {e}"),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1.3. Implement your extractor (`FromRequestParts` or `FromRequest`)
|
||||||
|
#[axum::async_trait]
|
||||||
|
impl<S> FromRequestParts<S> for Parameters
|
||||||
|
where
|
||||||
|
S: Send + Sync,
|
||||||
|
{
|
||||||
|
type Rejection = ParametersRejection;
|
||||||
|
|
||||||
|
async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
|
||||||
|
let Some(value) = parts.headers.get(PARAMETERS_HEADER) else {
|
||||||
|
return Err(ParametersRejection::Null);
|
||||||
|
};
|
||||||
|
|
||||||
|
serde_json::from_slice(value.as_bytes()).map_err(ParametersRejection::InvalidJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValidTest for Parameters {
|
||||||
|
const ERROR_STATUS_CODE: StatusCode = CACHED_REJECTION_STATUS;
|
||||||
|
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.header(
|
||||||
|
PARAMETERS_HEADER,
|
||||||
|
serde_json::to_string(Parameters::valid()).expect("Failed to serialize parameters"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.header(
|
||||||
|
PARAMETERS_HEADER,
|
||||||
|
serde_json::to_string(Parameters::error()).expect("Failed to serialize parameters"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder.header(
|
||||||
|
PARAMETERS_HEADER,
|
||||||
|
serde_json::to_string(Parameters::invalid())
|
||||||
|
.expect("Failed to serialize parameters"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct TestRejection {
|
||||||
|
_inner: ParametersRejection,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Rejection for TestRejection {
|
||||||
|
const STATUS_CODE: StatusCode = StatusCode::CONFLICT;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoResponse for TestRejection {
|
||||||
|
fn into_response(self) -> Response {
|
||||||
|
Self::STATUS_CODE.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// satisfy the `WithRejection`'s extractor trait bound
|
||||||
|
// R: From<E::Rejection> + IntoResponse
|
||||||
|
impl From<ParametersRejection> for TestRejection {
|
||||||
|
fn from(_inner: ParametersRejection) -> Self {
|
||||||
|
Self { _inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extract_cached(
|
||||||
|
Valid(Cached(parameters)): Valid<Cached<Parameters>>,
|
||||||
|
) -> StatusCode {
|
||||||
|
validate_again(parameters)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn extract_with_rejection(
|
||||||
|
Valid(WithRejection(parameters, _)): Valid<WithRejection<Parameters, TestRejection>>,
|
||||||
|
) -> StatusCode {
|
||||||
|
validate_again(parameters)
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/typed_header.rs
Normal file
43
src/typed_header.rs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
//! # Implementation of the `HasValidate` trait for the `TypedHeader` extractor.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use crate::HasValidate;
|
||||||
|
use axum::TypedHeader;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for TypedHeader<T> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::tests::{ValidTest, ValidTestParameter};
|
||||||
|
use axum::headers::{Header, HeaderMapExt};
|
||||||
|
use axum::http::StatusCode;
|
||||||
|
use axum::TypedHeader;
|
||||||
|
use reqwest::header::HeaderMap;
|
||||||
|
use reqwest::RequestBuilder;
|
||||||
|
|
||||||
|
impl<T: ValidTestParameter + Header + Clone> ValidTest for TypedHeader<T> {
|
||||||
|
const ERROR_STATUS_CODE: StatusCode = StatusCode::BAD_REQUEST;
|
||||||
|
|
||||||
|
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
let mut headers = HeaderMap::default();
|
||||||
|
headers.typed_insert(T::valid().clone());
|
||||||
|
builder.headers(headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
builder
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
|
||||||
|
let mut headers = HeaderMap::default();
|
||||||
|
headers.typed_insert(T::invalid().clone());
|
||||||
|
builder.headers(headers)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
13
src/yaml.rs
Normal file
13
src/yaml.rs
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//! # Implementation of the `HasValidate` trait for the `Yaml` extractor.
|
||||||
|
//!
|
||||||
|
|
||||||
|
use crate::HasValidate;
|
||||||
|
use axum_yaml::Yaml;
|
||||||
|
use validator::Validate;
|
||||||
|
|
||||||
|
impl<T: Validate> HasValidate for Yaml<T> {
|
||||||
|
type Validate = T;
|
||||||
|
fn get_validate(&self) -> &T {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,13 @@
|
|||||||
[feature_default]
|
[feature_default]
|
||||||
|
|
||||||
|
[feature_all_types]
|
||||||
|
features = "all_types"
|
||||||
|
|
||||||
[feature_into_json]
|
[feature_into_json]
|
||||||
features = "into_json"
|
features = "all_types into_json"
|
||||||
|
|
||||||
[feature_422]
|
[feature_422]
|
||||||
features = "422"
|
features = "all_types 422"
|
||||||
|
|
||||||
[feature_422_into_json]
|
[feature_422_into_json]
|
||||||
features = "422 into_json"
|
features = "all_types 422 into_json"
|
||||||
210
tests/basic.rs
210
tests/basic.rs
@@ -1,210 +0,0 @@
|
|||||||
//! # Basic extractors validation
|
|
||||||
//!
|
|
||||||
//! * `Path`
|
|
||||||
//! * `Query`
|
|
||||||
//! * `Form`
|
|
||||||
//! * `Json`
|
|
||||||
|
|
||||||
use axum::extract::{Path, Query};
|
|
||||||
use axum::http::StatusCode;
|
|
||||||
use axum::routing::{get, post};
|
|
||||||
use axum::{Form, Json, Router};
|
|
||||||
use axum_valid::{Valid, VALIDATION_ERROR_STATUS};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::json;
|
|
||||||
use std::net::SocketAddr;
|
|
||||||
use validator::Validate;
|
|
||||||
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
mod route {
|
|
||||||
pub const PATH: &str = "/path/:v0/:v1";
|
|
||||||
pub const QUERY: &str = "/query";
|
|
||||||
pub const FORM: &str = "/form";
|
|
||||||
pub const JSON: &str = "/json";
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn main() -> anyhow::Result<()> {
|
|
||||||
let router = Router::new()
|
|
||||||
.route(route::PATH, get(extract_path))
|
|
||||||
.route(route::QUERY, get(extract_query))
|
|
||||||
.route(route::FORM, post(extract_form))
|
|
||||||
.route(route::JSON, post(extract_json));
|
|
||||||
|
|
||||||
let server = axum::Server::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16)))
|
|
||||||
.serve(router.into_make_service());
|
|
||||||
let server_addr = server.local_addr();
|
|
||||||
println!("Axum server address: {}.", server_addr);
|
|
||||||
|
|
||||||
let (server_guard, close) = tokio::sync::oneshot::channel::<()>();
|
|
||||||
let server_handle = tokio::spawn(server.with_graceful_shutdown(async move {
|
|
||||||
let _ = close.await;
|
|
||||||
}));
|
|
||||||
|
|
||||||
let server_url = format!("http://{}", server_addr);
|
|
||||||
let client = reqwest::Client::default();
|
|
||||||
|
|
||||||
let valid_parameters = Parameters {
|
|
||||||
v0: 5,
|
|
||||||
v1: "0123456789".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let invalid_parameters = Parameters {
|
|
||||||
v0: 6,
|
|
||||||
v1: "01234567890".to_string(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Valid<Path<...>>
|
|
||||||
let valid_path_response = client
|
|
||||||
.get(format!(
|
|
||||||
"{}/path/{}/{}",
|
|
||||||
server_url, valid_parameters.v0, valid_parameters.v1
|
|
||||||
))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(valid_path_response.status(), StatusCode::OK);
|
|
||||||
|
|
||||||
let invalid_path_response = client
|
|
||||||
.get(format!("{}/path/invalid/path", server_url))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(invalid_path_response.status(), StatusCode::BAD_REQUEST);
|
|
||||||
|
|
||||||
let invalid_path_response = client
|
|
||||||
.get(format!(
|
|
||||||
"{}/path/{}/{}",
|
|
||||||
server_url, invalid_parameters.v0, invalid_parameters.v1
|
|
||||||
))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(invalid_path_response.status(), VALIDATION_ERROR_STATUS);
|
|
||||||
#[cfg(feature = "into_json")]
|
|
||||||
utils::check_json(invalid_path_response).await;
|
|
||||||
println!("Valid<Path<...>> works.");
|
|
||||||
|
|
||||||
// Valid<Query<...>>
|
|
||||||
let query_url = format!("{}{}", server_url, route::QUERY);
|
|
||||||
let valid_query_response = client
|
|
||||||
.get(&query_url)
|
|
||||||
.query(&valid_parameters)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(valid_query_response.status(), StatusCode::OK);
|
|
||||||
|
|
||||||
let invalid_query_response = client
|
|
||||||
.get(&query_url)
|
|
||||||
.query(&[("invalid", "query")])
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(invalid_query_response.status(), StatusCode::BAD_REQUEST);
|
|
||||||
|
|
||||||
let invalid_query_response = client
|
|
||||||
.get(&query_url)
|
|
||||||
.query(&invalid_parameters)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(invalid_query_response.status(), VALIDATION_ERROR_STATUS);
|
|
||||||
#[cfg(feature = "into_json")]
|
|
||||||
utils::check_json(invalid_query_response).await;
|
|
||||||
println!("Valid<Query<...>> works.");
|
|
||||||
|
|
||||||
// Valid<Form<...>>
|
|
||||||
let form_url = format!("{}{}", server_url, route::FORM);
|
|
||||||
let valid_form_response = client
|
|
||||||
.post(&form_url)
|
|
||||||
.form(&valid_parameters)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(valid_form_response.status(), StatusCode::OK);
|
|
||||||
|
|
||||||
let invalid_form_response = client
|
|
||||||
.post(&form_url)
|
|
||||||
.form(&[("invalid", "form")])
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
invalid_form_response.status(),
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY
|
|
||||||
);
|
|
||||||
|
|
||||||
let invalid_form_response = client
|
|
||||||
.post(&form_url)
|
|
||||||
.form(&invalid_parameters)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(invalid_form_response.status(), VALIDATION_ERROR_STATUS);
|
|
||||||
#[cfg(feature = "into_json")]
|
|
||||||
utils::check_json(invalid_form_response).await;
|
|
||||||
println!("Valid<Form<...>> works.");
|
|
||||||
|
|
||||||
// Valid<Json<...>>
|
|
||||||
let json_url = format!("{}{}", server_url, route::JSON);
|
|
||||||
let valid_json_response = client
|
|
||||||
.post(&json_url)
|
|
||||||
.json(&valid_parameters)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(valid_json_response.status(), StatusCode::OK);
|
|
||||||
|
|
||||||
let invalid_json_response = client
|
|
||||||
.post(&json_url)
|
|
||||||
.json(&json!({"invalid": "json"}))
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(
|
|
||||||
invalid_json_response.status(),
|
|
||||||
StatusCode::UNPROCESSABLE_ENTITY
|
|
||||||
);
|
|
||||||
|
|
||||||
let invalid_json_response = client
|
|
||||||
.post(&json_url)
|
|
||||||
.json(&invalid_parameters)
|
|
||||||
.send()
|
|
||||||
.await?;
|
|
||||||
assert_eq!(invalid_json_response.status(), VALIDATION_ERROR_STATUS);
|
|
||||||
#[cfg(feature = "into_json")]
|
|
||||||
utils::check_json(invalid_json_response).await;
|
|
||||||
println!("Valid<Json<...>> works.");
|
|
||||||
|
|
||||||
drop(server_guard);
|
|
||||||
server_handle.await??;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Implement `Deserialize` and `Validate` for `Parameters`,
|
|
||||||
// then `Valid` will work as you expect.
|
|
||||||
#[derive(Debug, Deserialize, Serialize, Validate)]
|
|
||||||
struct Parameters {
|
|
||||||
#[validate(range(min = 5, max = 10))]
|
|
||||||
v0: i32,
|
|
||||||
#[validate(length(min = 1, max = 10))]
|
|
||||||
v1: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extract_path(Valid(Path(parameters)): Valid<Path<Parameters>>) -> StatusCode {
|
|
||||||
validate_again(parameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extract_query(Valid(Query(parameters)): Valid<Query<Parameters>>) -> StatusCode {
|
|
||||||
validate_again(parameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extract_form(Valid(Form(parameters)): Valid<Form<Parameters>>) -> StatusCode {
|
|
||||||
validate_again(parameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn extract_json(Valid(Json(parameters)): Valid<Json<Parameters>>) -> StatusCode {
|
|
||||||
validate_again(parameters)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_again(parameters: Parameters) -> StatusCode {
|
|
||||||
// The `Valid` extractor has validated the `parameters` once,
|
|
||||||
// it should have returned `400 BAD REQUEST` if the `parameters` were invalid,
|
|
||||||
// Let's validate them again to check if the `Valid` extractor works well.
|
|
||||||
// If it works properly, this function will never return `500 INTERNAL SERVER ERROR`
|
|
||||||
match parameters.validate() {
|
|
||||||
Ok(_) => StatusCode::OK,
|
|
||||||
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,14 +6,12 @@ use axum::http::request::Parts;
|
|||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use axum::routing::get;
|
use axum::routing::get;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
use axum_valid::{HasValidate, Valid, ValidRejection, VALIDATION_ERROR_STATUS};
|
use axum_valid::{HasValidate, Valid, VALIDATION_ERROR_STATUS};
|
||||||
use hyper::StatusCode;
|
use hyper::StatusCode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
|
|
||||||
mod utils;
|
|
||||||
|
|
||||||
const MY_DATA_HEADER: &str = "My-Data";
|
const MY_DATA_HEADER: &str = "My-Data";
|
||||||
|
|
||||||
// 1. Implement your own extractor.
|
// 1. Implement your own extractor.
|
||||||
@@ -66,20 +64,11 @@ where
|
|||||||
// 2.1. Implement `HasValidate` for your extractor
|
// 2.1. Implement `HasValidate` for your extractor
|
||||||
impl HasValidate for MyData {
|
impl HasValidate for MyData {
|
||||||
type Validate = Self;
|
type Validate = Self;
|
||||||
type Rejection = MyDataRejection;
|
|
||||||
|
|
||||||
fn get_validate(&self) -> &Self::Validate {
|
fn get_validate(&self) -> &Self::Validate {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2.2. Implement `From<MyDataRejection>` for `ValidRejection<MyDataRejection>`.
|
|
||||||
impl From<MyDataRejection> for ValidRejection<MyDataRejection> {
|
|
||||||
fn from(value: MyDataRejection) -> Self {
|
|
||||||
Self::Inner(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn main() -> anyhow::Result<()> {
|
async fn main() -> anyhow::Result<()> {
|
||||||
let router = Router::new().route("/", get(handler));
|
let router = Router::new().route("/", get(handler));
|
||||||
@@ -125,8 +114,8 @@ async fn main() -> anyhow::Result<()> {
|
|||||||
.send()
|
.send()
|
||||||
.await?;
|
.await?;
|
||||||
assert_eq!(invalid_my_data_response.status(), VALIDATION_ERROR_STATUS);
|
assert_eq!(invalid_my_data_response.status(), VALIDATION_ERROR_STATUS);
|
||||||
#[cfg(feature = "into_json")]
|
// #[cfg(feature = "into_json")]
|
||||||
utils::check_json(invalid_my_data_response).await;
|
// test::check_json(invalid_my_data_response).await;
|
||||||
println!("Valid<MyData> works.");
|
println!("Valid<MyData> works.");
|
||||||
|
|
||||||
drop(server_guard);
|
drop(server_guard);
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
/// Check if the response is a json response
|
|
||||||
#[cfg(feature = "into_json")]
|
|
||||||
pub async fn check_json(response: reqwest::Response) {
|
|
||||||
assert_eq!(
|
|
||||||
response.headers()[axum::http::header::CONTENT_TYPE],
|
|
||||||
axum::http::HeaderValue::from_static(mime::APPLICATION_JSON.as_ref())
|
|
||||||
);
|
|
||||||
assert!(response.json::<serde_json::Value>().await.is_ok());
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user