add support for validify

This commit is contained in:
gengteng
2023-10-19 23:12:32 +08:00
parent 61bf3c5b23
commit b2d2c3d7ca
14 changed files with 1139 additions and 354 deletions

View File

@@ -8,6 +8,17 @@
### Fixed ### Fixed
## axum-valid 0.11.0 (2023-10-20)
### Added
* Added support for `validify`.
* Introduced four new extractors using validify: `Validated`, `Modified`, `Validified`, and `ValidifiedByRef`.
### Changed
### Fixed
## axum-valid 0.10.1 (2023-10-10) ## axum-valid 0.10.1 (2023-10-10)
### Added ### Added

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "axum-valid" name = "axum-valid"
version = "0.10.1" version = "0.11.0"
description = "Provides validation extractors for your Axum application to validate data using validator, garde, or both." description = "Provides validation extractors for your Axum application, allowing you to validate data using validator, garde, validify or all of them."
authors = ["GengTeng <me@gteng.org>"] authors = ["GengTeng <me@gteng.org>"]
license = "MIT" license = "MIT"
homepage = "https://github.com/gengteng/axum-valid" homepage = "https://github.com/gengteng/axum-valid"
@@ -11,7 +11,8 @@ keywords = [
"axum", "axum",
"validator", "validator",
"extractor", "extractor",
"web", "garde",
"validify",
] ]
categories = [ categories = [
"asynchronous", "asynchronous",
@@ -21,11 +22,11 @@ categories = [
edition = "2021" edition = "2021"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = ["all_types", "garde"] features = ["full"]
[dependencies] [dependencies]
axum = { version = "0.6.20", default-features = false } axum = { version = "0.6.20", default-features = false }
garde = { version = "0.15.0", optional = true } garde = { version = "0.16.0", optional = true }
validator = { version = "0.16.1", optional = true} validator = { version = "0.16.1", optional = true}
validify = { version = "1.0.12", optional = true } validify = { version = "1.0.12", optional = true }
@@ -50,18 +51,18 @@ default-features = false
optional = true optional = true
[dependencies.serde] [dependencies.serde]
version = "1.0.188" version = "1.0.189"
optional = true optional = true
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.72" anyhow = "1.0.72"
axum = { version = "0.6.20", features = ["macros"] } axum = { version = "0.6.20", features = ["macros"] }
tokio = { version = "1.29.1", features = ["full"] } tokio = { version = "1.33.0", features = ["full"] }
hyper = { version = "0.14.27", features = ["full"] } hyper = { version = "0.14.27", features = ["full"] }
reqwest = { version = "0.11.18", features = ["json", "multipart"] } reqwest = { version = "0.11.22", features = ["json", "multipart"] }
serde = { version = "1.0.188", features = ["derive"] } serde = { version = "1.0.189", features = ["derive"] }
validator = { version = "0.16.0", features = ["derive"] } validator = { version = "0.16.1", features = ["derive"] }
serde_json = "1.0.104" serde_json = "1.0.107"
serde_yaml = "0.9.25" serde_yaml = "0.9.25"
mime = "0.3.17" mime = "0.3.17"
prost = "0.12.1" prost = "0.12.1"
@@ -90,6 +91,7 @@ extra_form = ["extra", "axum-extra/form"]
extra_protobuf = ["extra", "axum-extra/protobuf"] extra_protobuf = ["extra", "axum-extra/protobuf"]
all_extra_types = ["extra", "extra_typed_path", "extra_query", "extra_form", "extra_protobuf"] all_extra_types = ["extra", "extra_typed_path", "extra_query", "extra_form", "extra_protobuf"]
all_types = ["json", "form", "query", "typed_header", "typed_multipart", "msgpack", "yaml", "all_extra_types"] all_types = ["json", "form", "query", "typed_header", "typed_multipart", "msgpack", "yaml", "all_extra_types"]
full = ["validator", "all_types", "422", "into_json"] full_validator = ["validator", "all_types", "422", "into_json"]
full_garde = ["garde", "all_types", "422", "into_json"] full_garde = ["garde", "all_types", "422", "into_json"]
full_validify = ["validify", "all_types", "422", "into_json"] full_validify = ["validify", "all_types", "422", "into_json"]
full = ["full_validator", "full_garde", "full_validify"]

610
README.md
View File

@@ -7,110 +7,61 @@
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gengteng/axum-valid/.github/workflows/main.yml?branch=main)](https://github.com/gengteng/axum-valid/actions/workflows/ci.yml) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gengteng/axum-valid/.github/workflows/main.yml?branch=main)](https://github.com/gengteng/axum-valid/actions/workflows/ci.yml)
[![Coverage Status](https://coveralls.io/repos/github/gengteng/axum-valid/badge.svg?branch=main)](https://coveralls.io/github/gengteng/axum-valid?branch=main) [![Coverage Status](https://coveralls.io/repos/github/gengteng/axum-valid/badge.svg?branch=main)](https://coveralls.io/github/gengteng/axum-valid?branch=main)
This crate provides data validation capabilities for Axum based on the `validator` and `garde` crates. It offers the `Valid`, `ValidEx` and `Garde` types to enable validation for extractors like `Json`, `Path`, `Query` and `Form`. ## 📑 Overview
`validator` support is included by default. To use `garde`, enable it via the `garde` feature. `garde` alone can be enabled with `default-features = false`. axum-valid is a library that provides data validation extractors for the Axum web framework. It integrates several popular validation crates in the Rust ecosystem to offer convenient validation and data handling extractors for Axum applications.
The `Valid` type performs validation using `validator`. The `ValidEx` type supports validations requiring extra arguments. The `Garde` type unifies both argument and non-argument validations using `garde`. ## 🚀 Basic usage
Additional extractors like `TypedHeader`, `MsgPack` and `Yaml` are also supported through optional features. Refer to `Features` for details. ### 📦 `Valid<E>`
## Basic usage * Install
```shell ```shell
cargo add validator --features derive
cargo add axum-valid cargo add axum-valid
# validator is enabled by default
``` ```
```rust,no_run * Example
#[cfg(feature = "validator")]
mod validator_example { ```rust,ignore
use validator::Validate; use validator::Validate;
use serde::Deserialize; use serde::Deserialize;
use axum_valid::Valid; use axum_valid::Valid;
use axum::extract::Query; use axum::extract::Query;
use axum::{Json, Router}; use axum::{Json, Router};
use axum::routing::{get, post}; use axum::routing::{get, post};
#[derive(Debug, Validate, Deserialize)] #[derive(Debug, Validate, Deserialize)]
pub struct Pager { pub struct Pager {
#[validate(range(min = 1, max = 50))] #[validate(range(min = 1, max = 50))]
pub page_size: usize, pub page_size: usize,
#[validate(range(min = 1))] #[validate(range(min = 1))]
pub page_no: usize, pub page_no: usize,
}
pub async fn pager_from_query(
Valid(Query(pager)): Valid<Query<Pager>>,
) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
}
pub async fn pager_from_json(
pager: Valid<Json<Pager>>,
) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
// NOTE: support automatic dereferencing
println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size);
}
pub fn router() -> Router {
Router::new()
.route("/query", get(pager_from_query))
.route("/json", post(pager_from_json))
}
} }
#[cfg(feature = "garde")] pub async fn pager_from_query(
mod garde_example { Valid(Query(pager)): Valid<Query<Pager>>,
use garde::Validate; ) {
use serde::Deserialize; assert!((1..=50).contains(&pager.page_size));
use axum_valid::Garde; assert!((1..).contains(&pager.page_no));
use axum::extract::Query; }
use axum::{Json, Router};
use axum::routing::{get, post}; pub async fn pager_from_json(
pager: Valid<Json<Pager>>,
#[derive(Debug, Validate, Deserialize)] ) {
pub struct Pager { assert!((1..=50).contains(&pager.page_size));
#[garde(range(min = 1, max = 50))] assert!((1..).contains(&pager.page_no));
pub page_size: usize, // NOTE: support automatic dereferencing
#[garde(range(min = 1))] println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size);
pub page_no: usize,
}
pub async fn pager_from_query(
Garde(Query(pager)): Garde<Query<Pager>>,
) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
}
pub async fn pager_from_json(
pager: Garde<Json<Pager>>,
) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
// NOTE: support automatic dereferencing
println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size);
}
pub fn router() -> Router {
Router::new()
.route("/query", get(pager_from_query))
.route("/json", post(pager_from_json))
}
} }
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
use axum::Router; let router = Router::new()
.route("/query", get(pager_from_query))
let router = Router::new(); .route("/json", post(pager_from_json));
#[cfg(feature = "validator")]
let router = router.nest("/validator", validator_example::router());
#[cfg(feature = "garde")]
let router = router.nest("/garde", garde_example::router());
axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
.serve(router.into_make_service()) .serve(router.into_make_service())
.await?; .await?;
@@ -118,151 +69,338 @@ async fn main() -> anyhow::Result<()> {
} }
``` ```
When validation errors occur, the extractor will automatically return 400 with validation errors as the HTTP message body. In case of inner extractor errors, it will first return the Rejection from the inner extractor. When validation errors occur, the outer extractor will automatically return 400 with validation errors as the HTTP message body.
To see how each extractor can be used with `Valid`, please refer to the example in the [documentation](https://docs.rs/axum-valid) of the corresponding module. ### 📦 `Garde<E>`
## Argument-Based Validation * Install
Here are the examples of using the `ValidEx` / `Garde` extractor to validate data in a `Form` using arguments: ```shell
cargo add garde
cargo add axum-valid --features garde,basic --no-default-features
# excluding validator
```
```rust,no_run * Example
#[cfg(feature = "validator")]
mod validator_example { ```rust,ignore
use axum::routing::post; use axum::extract::{FromRef, Query, State};
use axum::{Form, Router}; use axum::routing::{get, post};
use axum_valid::{Arguments, ValidEx}; use axum::{Json, Router};
use serde::Deserialize; use axum_valid::Garde;
use std::ops::{RangeFrom, RangeInclusive}; use garde::Validate;
use validator::{Validate, ValidateArgs, ValidationError}; use serde::Deserialize;
// NOTE: When some fields use custom validation functions with arguments, #[derive(Debug, Validate, Deserialize)]
// `#[derive(Validate)]` will implement `ValidateArgs` instead of `Validate` for the type. pub struct Pager {
// The validation arguments will be a tuple of all the field validation args. #[garde(range(min = 1, max = 50))]
// In this example it is (&RangeInclusive<usize>, &RangeFrom<usize>). pub page_size: usize,
// For more detailed information and understanding of `ValidateArgs` and their argument types, #[garde(range(min = 1))]
// please refer to the `validator` crate documentation. pub page_no: usize,
#[derive(Debug, Validate, Deserialize)]
pub struct Pager {
#[validate(custom(function = "validate_page_size", arg = "&'v_a RangeInclusive<usize>"))]
pub page_size: usize,
#[validate(custom(function = "validate_page_no", arg = "&'v_a RangeFrom<usize>"))]
pub page_no: usize,
}
fn validate_page_size(v: usize, args: &RangeInclusive<usize>) -> Result<(), ValidationError> {
args.contains(&v)
.then_some(())
.ok_or_else(|| ValidationError::new("page_size is out of range"))
}
fn validate_page_no(v: usize, args: &RangeFrom<usize>) -> Result<(), ValidationError> {
args.contains(&v)
.then_some(())
.ok_or_else(|| ValidationError::new("page_no is out of range"))
}
// NOTE: Clone is required
#[derive(Debug, Clone)]
pub struct PagerValidArgs {
page_size_range: RangeInclusive<usize>,
page_no_range: RangeFrom<usize>,
}
// NOTE: This implementation allows PagerValidArgs to be the second member of ValidEx, and provides arguments for actual validation.
// The type mapping <Pager as ValidateArgs<'a>>::Args represents the combination of validators applied on each field of Pager.
// get() method returns the validating arguments to be used during validation.
impl<'a> Arguments<'a> for PagerValidArgs {
type T = Pager;
// NOTE: <Pager as ValidateArgs<'a>>::Args == (&RangeInclusive<usize>, &RangeFrom<usize>)
fn get(&'a self) -> <Pager as ValidateArgs<'a>>::Args {
(&self.page_size_range, &self.page_no_range)
}
}
pub async fn pager_from_form_ex(ValidEx(Form(pager), _): ValidEx<Form<Pager>, PagerValidArgs>) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
}
pub fn router() -> Router {
Router::new()
.route("/form", post(pager_from_form_ex))
.with_state(PagerValidArgs {
page_size_range: 1..=50,
page_no_range: 1..,
})
// NOTE: The PagerValidArgs can also be stored in a XxxState,
// make sure it implements FromRef<XxxState>.
// Consider using Arc to reduce deep copying costs.
}
} }
#[cfg(feature = "garde")] pub async fn pager_from_query(Garde(Query(pager)): Garde<Query<Pager>>) {
mod garde_example { assert!((1..=50).contains(&pager.page_size));
use axum::routing::post; assert!((1..).contains(&pager.page_no));
use axum::{Form, Router}; }
use axum_valid::Garde;
use garde::Validate;
use serde::Deserialize;
use std::ops::{RangeFrom, RangeInclusive};
#[derive(Debug, Validate, Deserialize)] pub async fn pager_from_json(pager: Garde<Json<Pager>>) {
#[garde(context(PagerValidContext))] assert!((1..=50).contains(&pager.page_size));
pub struct Pager { assert!((1..).contains(&pager.page_no));
#[garde(custom(validate_page_size))] // NOTE: support automatic dereferencing
pub page_size: usize, println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size);
#[garde(custom(validate_page_no))] }
pub page_no: usize,
}
fn validate_page_size(v: &usize, args: &PagerValidContext) -> garde::Result { pub async fn get_state(_state: State<MyState>) {}
args.page_size_range
.contains(&v)
.then_some(())
.ok_or_else(|| garde::Error::new("page_size is out of range"))
}
fn validate_page_no(v: &usize, args: &PagerValidContext) -> garde::Result { #[derive(Debug, Clone, FromRef)]
args.page_no_range pub struct MyState {
.contains(&v) state_field: i32,
.then_some(()) without_validation_arguments: (),
.ok_or_else(|| garde::Error::new("page_no is out of range"))
}
#[derive(Debug, Clone)]
pub struct PagerValidContext {
page_size_range: RangeInclusive<usize>,
page_no_range: RangeFrom<usize>,
}
pub async fn pager_from_form_garde(Garde(Form(pager)): Garde<Form<Pager>>) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
}
pub fn router() -> Router {
Router::new()
.route("/form", post(pager_from_form_garde))
.with_state(PagerValidContext {
page_size_range: 1..=50,
page_no_range: 1..,
})
// NOTE: The PagerValidContext can also be stored in a XxxState,
// make sure it implements FromRef<XxxState>.
// Consider using Arc to reduce deep copying costs.
}
} }
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
use axum::Router; let router = Router::new()
let router = Router::new(); .route("/query", get(pager_from_query))
#[cfg(feature = "validator")] .route("/json", post(pager_from_json));
let router = router.nest("/validator", validator_example::router());
#[cfg(feature = "garde")] // WARNING: If you are using Garde and also have a state,
let router = router.nest("/garde", garde_example::router()); // even if that state is unrelated to Garde,
// you still need to implement FromRef<StateType> for ().
// Tip: You can add an () field to your state and derive FromRef for it.
let router = router.route("/state", get(get_state)).with_state(MyState {
state_field: 1,
without_validation_arguments: (),
});
axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
.serve(router.into_make_service())
.await?;
Ok(())
}
```
### 📦 `Validated<E>`, `Modified<E>`, `Validified<E>` and `ValidifiedByRef<E>`
* Install
```shell
cargo add validify
cargo add axum-valid --features validify,basic --no-default-features
```
* Example
Extra dependencies of this example:
```shell
cargo add axum_typed_multipart
cargo add axum-valid --features validify,basic,typed_multipart --no-default-features
```
```rust,ignore
use axum::extract::Query;
use axum::routing::{get, post};
use axum::{Form, Json, Router};
use axum_typed_multipart::{TryFromMultipart, TypedMultipart};
use axum_valid::{Modified, Validated, Validified, ValidifiedByRef};
use serde::Deserialize;
use validify::{Validate, Validify};
#[derive(Debug, Validify, Deserialize)]
pub struct Pager {
#[validate(range(min = 1.0, max = 50.0))]
pub page_size: usize,
#[validate(range(min = 1.0))]
pub page_no: usize,
}
pub async fn pager_from_query(Validated(Query(pager)): Validated<Query<Pager>>) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
}
#[derive(Debug, Validify, Deserialize)]
pub struct Parameters {
#[modify(lowercase)]
#[validate(length(min = 1, max = 50))]
pub v0: String,
#[modify(trim)]
#[validate(length(min = 1, max = 100))]
pub v1: String,
}
pub async fn parameters_from_json(modified_parameters: Modified<Json<Parameters>>) {
// NOTE: support automatic dereferencing
assert_eq!(
modified_parameters.v0,
modified_parameters.v0.to_lowercase()
);
assert_eq!(modified_parameters.v1, modified_parameters.v1.trim())
// but modified_parameters may be invalid
}
// NOTE: missing required fields will be treated as validation errors.
pub async fn parameters_from_form(parameters: Validified<Form<Parameters>>) {
// NOTE: support automatic dereferencing
assert_eq!(parameters.v0, parameters.v0.to_lowercase());
assert_eq!(parameters.v1, parameters.v1.trim());
assert!(parameters.validate().is_ok());
}
// NOTE: TypedMultipart doesn't using serde::Deserialize to construct data
// we should use ValidifiedByRef instead of Validified
#[derive(Debug, Validify, TryFromMultipart)]
pub struct FormData {
#[modify(lowercase)]
#[validate(length(min = 1, max = 50))]
pub v0: String,
#[modify(trim)]
#[validate(length(min = 1, max = 100))]
pub v1: String,
}
pub async fn parameters_from_typed_multipart(
ValidifiedByRef(TypedMultipart(data)): ValidifiedByRef<TypedMultipart<FormData>>,
) {
// NOTE: support automatic dereferencing
assert_eq!(data.v0, data.v0.to_lowercase());
assert_eq!(data.v1, data.v1.trim());
assert!(data.validate().is_ok());
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let router = Router::new()
.route("/validated", get(pager_from_query))
.route("/modified", post(parameters_from_json))
.route("/validified", post(parameters_from_form))
.route("/validified_by_ref", post(parameters_from_typed_multipart));
axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
.serve(router.into_make_service())
.await?;
Ok(())
}
```
To see how each inner extractor can be used with validation extractors, please refer to the example in the [documentation](https://docs.rs/axum-valid) of the corresponding module.
## 🚀 Argument-Based Validation
### 📦 `ValidEx<E, A>`
* Install
```shell
cargo add validator --features derive
cargo add axum-valid
# validator is enabled by default
```
* Example
```rust,ignore
use axum::routing::post;
use axum::{Form, Router};
use axum_valid::{Arguments, ValidEx};
use serde::Deserialize;
use std::ops::{RangeFrom, RangeInclusive};
use validator::{Validate, ValidateArgs, ValidationError};
// NOTE: When some fields use custom validation functions with arguments,
// `#[derive(Validate)]` will implement `ValidateArgs` instead of `Validate` for the type.
// The validation arguments will be a tuple of all the field validation args.
// In this example it is (&RangeInclusive<usize>, &RangeFrom<usize>).
// For more detailed information and understanding of `ValidateArgs` and their argument types,
// please refer to the `validator` crate documentation.
#[derive(Debug, Validate, Deserialize)]
pub struct Pager {
#[validate(custom(function = "validate_page_size", arg = "&'v_a RangeInclusive<usize>"))]
pub page_size: usize,
#[validate(custom(function = "validate_page_no", arg = "&'v_a RangeFrom<usize>"))]
pub page_no: usize,
}
fn validate_page_size(v: usize, args: &RangeInclusive<usize>) -> Result<(), ValidationError> {
args.contains(&v)
.then_some(())
.ok_or_else(|| ValidationError::new("page_size is out of range"))
}
fn validate_page_no(v: usize, args: &RangeFrom<usize>) -> Result<(), ValidationError> {
args.contains(&v)
.then_some(())
.ok_or_else(|| ValidationError::new("page_no is out of range"))
}
// NOTE: Clone is required, consider using Arc to reduce deep copying costs.
#[derive(Debug, Clone)]
pub struct PagerValidArgs {
page_size_range: RangeInclusive<usize>,
page_no_range: RangeFrom<usize>,
}
// NOTE: This implementation allows PagerValidArgs to be the second member of ValidEx, and provides arguments for actual validation.
// get() method returns the actual validation arguments to be used during validation.
impl<'a> Arguments<'a> for PagerValidArgs {
type T = Pager;
// NOTE: <Pager as ValidateArgs<'a>>::Args == (&RangeInclusive<usize>, &RangeFrom<usize>)
fn get(&'a self) -> <Pager as ValidateArgs<'a>>::Args {
(&self.page_size_range, &self.page_no_range)
}
}
pub async fn pager_from_form_ex(ValidEx(Form(pager), _): ValidEx<Form<Pager>, PagerValidArgs>) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let router = Router::new()
.route("/form", post(pager_from_form_ex))
.with_state(PagerValidArgs {
page_size_range: 1..=50,
page_no_range: 1..,
});
// NOTE: The PagerValidArgs can also be stored in a XxxState,
// make sure it implements FromRef<XxxState>.
axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
.serve(router.into_make_service())
.await?;
Ok(())
}
```
### 📦 `Garde<E>`
* Install
```shell
cargo add validator --features derive
cargo add axum-valid
# validator is enabled by default
```
* Example
```rust,ignore
use axum::routing::post;
use axum::{Form, Router};
use axum_valid::Garde;
use garde::Validate;
use serde::Deserialize;
use std::ops::{RangeFrom, RangeInclusive};
#[derive(Debug, Validate, Deserialize)]
#[garde(context(PagerValidContext))]
pub struct Pager {
#[garde(custom(validate_page_size))]
pub page_size: usize,
#[garde(custom(validate_page_no))]
pub page_no: usize,
}
fn validate_page_size(v: &usize, args: &PagerValidContext) -> garde::Result {
args.page_size_range
.contains(&v)
.then_some(())
.ok_or_else(|| garde::Error::new("page_size is out of range"))
}
fn validate_page_no(v: &usize, args: &PagerValidContext) -> garde::Result {
args.page_no_range
.contains(&v)
.then_some(())
.ok_or_else(|| garde::Error::new("page_no is out of range"))
}
#[derive(Debug, Clone)]
pub struct PagerValidContext {
page_size_range: RangeInclusive<usize>,
page_no_range: RangeFrom<usize>,
}
pub async fn pager_from_form_garde(Garde(Form(pager)): Garde<Form<Pager>>) {
assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no));
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let router = Router::new()
.route("/form", post(pager_from_form_garde))
.with_state(PagerValidContext {
page_size_range: 1..=50,
page_no_range: 1..,
});
// NOTE: The PagerValidContext can also be stored in a XxxState,
// make sure it implements FromRef<XxxState>.
// Consider using Arc to reduce deep copying costs.
axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
.serve(router.into_make_service()) .serve(router.into_make_service())
.await?; .await?;
@@ -272,14 +410,27 @@ async fn main() -> anyhow::Result<()> {
Current module documentation predominantly showcases `Valid` examples, the usage of `ValidEx` is analogous. Current module documentation predominantly showcases `Valid` examples, the usage of `ValidEx` is analogous.
## Features ## 🗂️ Extractors List
| Extractor | Backend / Feature | Data's trait bound | Functionality | Benefits | Drawbacks |
|-----------------------|-------------------|----------------------------------------------------|----------------------------------------|--------------------------------------------|--------------------------------------------------|
| `Valid<E>` | validator | `validator::Validate` | Validation | | |
| `ValidEx<E, A>` | validator | `validator::ValidateArgs` | Validation with arguments | | More complex arguments coding |
| `Garde<E>` | garde | `garde::Validate` | Validation with or without arguments | | Require empty tuple as the argument if use state | |
| `Validated<E>` | validify | `validify::Validate` | Validation | | |
| `Modified<E>` | validify | `validify::Modify` | Modification / Conversion to response | | |
| `Validified<E>` | validify | `validify::Validify` and `serde::DeserializeOwned` | Construction, modification, validation | Treat missing fields as validation errors | Only works with extractors using `serde` |
| `ValidifiedByRef<E>` | validify | `validify::Validate` and `validify::Modify` | Modification, validation | | |
## ⚙️ Features
| Feature | Description | Module | Default | Example | Tests | | Feature | Description | Module | Default | Example | Tests |
|------------------|------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|---------|---------|-------| |------------------|------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|---------|---------|-------|
| default | Enables `validator` and support for `Query`, `Json` and `Form` | [`validator`], [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ | | default | Enables `validator` and support for `Query`, `Json` and `Form` | [`validator`], [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ |
| validator | Enables `validator` (`Valid`, `ValidEx`) | [`validator`] | ✅ | ✅ | ✅ | | validator | Enables `validator` (`Valid`, `ValidEx`) | [`validator`] | ✅ | ✅ | ✅ |
| garde | Enables `garde` (`Garde`) | [`garde`] | ❌ | ✅ | ✅ | | garde | Enables `garde` (`Garde`) | [`garde`] | ❌ | ✅ | ✅ |
| validify | Enables `validify` (`Validated`, `Modified`, `Validified`) | [`validify`] | ❌ | ✅ | ✅ | | validify | Enables `validify` (`Validated`, `Modified`, `Validified`, `ValidifedByRef`) | [`validify`] | ❌ | ✅ | ✅ |
| basic | Enables support for `Query`, `Json` and `Form` | [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ | | basic | Enables support for `Query`, `Json` and `Form` | [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ |
| json | Enables support for `Json` | [`json`] | ✅ | ✅ | ✅ | | json | Enables support for `Json` | [`json`] | ✅ | ✅ | ✅ |
| query | Enables support for `Query` | [`query`] | ✅ | ✅ | ✅ | | query | Enables support for `Query` | [`query`] | ✅ | ✅ | ✅ |
@@ -297,19 +448,22 @@ Current module documentation predominantly showcases `Valid` examples, the usage
| all_types | Enables support for all extractors above | N/A | ❌ | ✅ | ✅ | | all_types | Enables support for all extractors above | N/A | ❌ | ✅ | ✅ |
| 422 | Use `422 Unprocessable Entity` instead of `400 Bad Request` as the status code when validation fails | [`VALIDATION_ERROR_STATUS`] | ❌ | ✅ | ✅ | | 422 | Use `422 Unprocessable Entity` instead of `400 Bad Request` as the status code when validation fails | [`VALIDATION_ERROR_STATUS`] | ❌ | ✅ | ✅ |
| into_json | Validation errors will be serialized into JSON format and returned as the HTTP body | N/A | ❌ | ✅ | ✅ | | into_json | Validation errors will be serialized into JSON format and returned as the HTTP body | N/A | ❌ | ✅ | ✅ |
| full | Enables `all_types`, `422` and `into_json` | N/A | ❌ | ✅ | ✅ | | full_validator | Enables `validator`, `all_types`, `422` and `into_json` | N/A | ❌ | ✅ | ✅ |
| full_garde | Enables `garde`, `all_types`, `422` and `into_json`. Consider using `default-features = false` to exclude default `validator` support | N/A | ❌ | ✅ | ✅ | | full_garde | Enables `garde`, `all_types`, `422` and `into_json`. Consider using `default-features = false` to exclude default `validator` support | N/A | ❌ | ✅ | ✅ |
| full_garde | Enables `validify`, `all_types`, `422` and `into_json`. Consider using `default-features = false` to exclude default `validator` support | N/A | ❌ | ✅ | ✅ | | full_garde | Enables `validify`, `all_types`, `422` and `into_json`. Consider using `default-features = false` to exclude default `validator` support | N/A | ❌ | ✅ | ✅ |
| full | Enables all features above | N/A | ❌ | ✅ | ✅ |
## Compatibility ## 🔌 Compatibility
To determine the compatible versions of `axum-valid`, `axum-extra`, `axum-yaml` and other dependencies that work together, please refer to the dependencies listed in the `Cargo.toml` file. The version numbers listed there will indicate the compatible versions. To determine the compatible versions of dependencies that work together, please refer to the dependencies listed in the `Cargo.toml` file. The version numbers listed there will indicate the compatible versions.
## License If you encounter code compilation problems, it could be attributed to either missing trait bounds, unmet feature requirements, or incorrect dependency version selections.
## 📜 License
This project is licensed under the MIT License. This project is licensed under the MIT License.
## References ## 📚 References
* [axum](https://crates.io/crates/axum) * [axum](https://crates.io/crates/axum)
* [validator](https://crates.io/crates/validator) * [validator](https://crates.io/crates/validator)

View File

@@ -114,6 +114,24 @@ impl<T: validify::Modify> crate::HasModify for Form<T> {
} }
} }
#[cfg(feature = "validify")]
impl<T> crate::PayloadExtractor for Form<T> {
type Payload = T;
fn get_payload(self) -> Self::Payload {
self.0
}
}
#[cfg(feature = "validify")]
impl<T: validify::Validify> crate::HasValidify for Form<T> {
type Validify = T;
type PayloadExtractor = Form<T::Payload>;
fn from_validified(v: Self::Validify) -> Self {
Form(v)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::{ValidTest, ValidTestParameter}; use crate::tests::{ValidTest, ValidTestParameter};

View File

@@ -114,6 +114,25 @@ impl<T: validify::Modify> crate::HasModify for Query<T> {
} }
} }
#[cfg(feature = "validify")]
impl<T> crate::PayloadExtractor for Query<T> {
type Payload = T;
fn get_payload(self) -> Self::Payload {
self.0
}
}
#[cfg(feature = "validify")]
impl<T: validify::Validify> crate::HasValidify for Query<T> {
type Validify = T;
type PayloadExtractor = Query<T::Payload>;
fn from_validified(v: Self::Validify) -> Self {
Query(v)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::{ValidTest, ValidTestParameter}; use crate::tests::{ValidTest, ValidTestParameter};

View File

@@ -97,10 +97,10 @@
#[cfg(feature = "garde")] #[cfg(feature = "garde")]
use crate::Garde; use crate::Garde;
#[cfg(feature = "validify")]
use crate::{Modified, Validated, ValidifiedByRef};
#[cfg(feature = "validator")] #[cfg(feature = "validator")]
use crate::{Valid, ValidEx}; use crate::{Valid, ValidEx};
#[cfg(feature = "validify")]
use crate::{Validated, Validified};
#[cfg(any(feature = "validator", feature = "garde", feature = "validify"))] #[cfg(any(feature = "validator", feature = "garde", feature = "validify"))]
use axum_extra::routing::TypedPath; use axum_extra::routing::TypedPath;
#[cfg(any(feature = "validator", feature = "garde", feature = "validify"))] #[cfg(any(feature = "validator", feature = "garde", feature = "validify"))]
@@ -127,6 +127,11 @@ impl<T: TypedPath + Display> TypedPath for Validated<T> {
} }
#[cfg(feature = "validify")] #[cfg(feature = "validify")]
impl<T: TypedPath + Display> TypedPath for Validified<T> { impl<T: TypedPath + Display> TypedPath for Modified<T> {
const PATH: &'static str = T::PATH;
}
#[cfg(feature = "validify")]
impl<T: TypedPath + Display> TypedPath for ValidifiedByRef<T> {
const PATH: &'static str = T::PATH; const PATH: &'static str = T::PATH;
} }

View File

@@ -114,6 +114,24 @@ impl<T: validify::Modify> crate::HasModify for Form<T> {
} }
} }
#[cfg(feature = "validify")]
impl<T> crate::PayloadExtractor for Form<T> {
type Payload = T;
fn get_payload(self) -> Self::Payload {
self.0
}
}
#[cfg(feature = "validify")]
impl<T: validify::Validify> crate::HasValidify for Form<T> {
type Validify = T;
type PayloadExtractor = Form<T::Payload>;
fn from_validified(v: Self::Validify) -> Self {
Form(v)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::{ValidTest, ValidTestParameter}; use crate::tests::{ValidTest, ValidTestParameter};

View File

@@ -19,6 +19,12 @@ use std::ops::{Deref, DerefMut};
/// # `Garde` data extractor /// # `Garde` data extractor
/// ///
/// Garde uses garde to validate data, supporting validation with or without arguments.
///
/// If not using arguments, its usage is similar to `Valid`. However, if your axum router uses a state, you need to implement `FromRef<StateType>` for `()`.
///
/// If using arguments, you must pass the arguments to Garde extractor via state, meaning implementing `FromRef<StateType>` for your validation arguments type.
///
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct Garde<E>(pub E); pub struct Garde<E>(pub E);

View File

@@ -56,7 +56,8 @@ pub use crate::garde::{Garde, GardeRejection};
#[cfg(feature = "validify")] #[cfg(feature = "validify")]
pub use crate::validify::{ pub use crate::validify::{
HasModify, HasValidify, Modified, PayloadExtractor, Validated, Validified, ValidifyRejection, HasModify, HasValidify, Modified, PayloadExtractor, Validated, Validified, ValidifiedByRef,
ValidifyRejection,
}; };
/// `ValidationRejection` is returned when the validation extractor fails. /// `ValidationRejection` is returned when the validation extractor fails.

View File

@@ -121,6 +121,24 @@ impl<T: validify::Modify> crate::HasModify for MsgPack<T> {
} }
} }
#[cfg(feature = "validify")]
impl<T> crate::PayloadExtractor for MsgPack<T> {
type Payload = T;
fn get_payload(self) -> Self::Payload {
self.0
}
}
#[cfg(feature = "validify")]
impl<T: validify::Validify> crate::HasValidify for MsgPack<T> {
type Validify = T;
type PayloadExtractor = MsgPack<T::Payload>;
fn from_validified(v: Self::Validify) -> Self {
MsgPack(v)
}
}
impl<T> HasValidate for MsgPackRaw<T> { impl<T> HasValidate for MsgPackRaw<T> {
type Validate = T; type Validate = T;
fn get_validate(&self) -> &T { fn get_validate(&self) -> &T {
@@ -144,6 +162,25 @@ impl<T: validify::Modify> crate::HasModify for MsgPackRaw<T> {
&mut self.0 &mut self.0
} }
} }
#[cfg(feature = "validify")]
impl<T> crate::PayloadExtractor for MsgPackRaw<T> {
type Payload = T;
fn get_payload(self) -> Self::Payload {
self.0
}
}
#[cfg(feature = "validify")]
impl<T: validify::Validify> crate::HasValidify for MsgPackRaw<T> {
type Validify = T;
type PayloadExtractor = MsgPackRaw<T::Payload>;
fn from_validified(v: Self::Validify) -> Self {
MsgPackRaw(v)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::{ValidTest, ValidTestParameter}; use crate::tests::{ValidTest, ValidTestParameter};

View File

@@ -133,31 +133,6 @@ impl<T: validify::Modify, R> crate::HasModify for BaseMultipart<T, R> {
} }
} }
#[cfg(feature = "validify")]
impl<T, R> crate::PayloadExtractor for BaseMultipart<T, R> {
type Payload = T;
fn get_payload(self) -> Self::Payload {
self.data
}
}
#[cfg(feature = "validify")]
impl<T: validify::Validify, R> crate::HasValidify for BaseMultipart<T, R> {
type Validify = T;
type PayloadExtractor = BaseMultipart<T::Payload, R>;
fn from_validified(_v: Self::Validify) -> Self {
// BaseMultipart {
// data,
// rejection: Default::default(), // ❌: need rejection to be pub
// }
// waiting for this issue to be resolved:
// https://github.com/murar8/axum_typed_multipart/issues/55
unimplemented!()
}
}
impl<T> HasValidate for TypedMultipart<T> { impl<T> HasValidate for TypedMultipart<T> {
type Validate = T; type Validate = T;
fn get_validate(&self) -> &T { fn get_validate(&self) -> &T {

View File

@@ -2,7 +2,7 @@
//! //!
//! ## Feature //! ## Feature
//! //!
//! Enable the `validify` feature to use `Validated<E>`, `Modified<E>` and `Validified<E>`. //! Enable the `validify` feature to use `Validated<E>`, `Modified<E>`, `Validified<E>` and `ValidifiedByRef<E>`.
//! //!
#[cfg(test)] #[cfg(test)]
@@ -20,6 +20,10 @@ use validify::{Modify, Validate, ValidationErrors, Validify};
/// # `Validated` data extractor /// # `Validated` data extractor
/// ///
/// `Validated` provides simple data validation based on `validify`.
///
/// It only does validation, usage is similar to `Valid`.
///
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct Validated<E>(pub E); pub struct Validated<E>(pub E);
@@ -44,7 +48,7 @@ impl<T: Display> Display for Validated<T> {
} }
impl<E> Validated<E> { impl<E> Validated<E> {
/// Consumes the `Validified` and returns the validated data within. /// Consumes the `Validated` and returns the validated data within.
/// ///
/// This returns the `E` type which represents the data that has been /// This returns the `E` type which represents the data that has been
/// successfully validated. /// successfully validated.
@@ -55,6 +59,20 @@ impl<E> Validated<E> {
/// # `Modified` data extractor / response /// # `Modified` data extractor / response
/// ///
/// ## Extractor
///
/// `Modified` uses `validify`'s modification capabilities to alter data, without validation.
///
/// Operations like trimming and case modification can be done based on `modify` attributes.
///
/// ## Response
///
/// `Modified` also implements the `IntoResponse` trait. When its inner `IntoResponse` type also implements the `HasModify` trait:
///
/// `Modified` will call `validify`'s modify method to alter the inner data.
/// Then call the inner type's own `into_response` method to convert it into a HTTP response.
///
/// This allows applying modifications during response conversion by leveraging validify.
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct Modified<E>(pub E); pub struct Modified<E>(pub E);
@@ -97,6 +115,12 @@ impl<E: IntoResponse + HasModify> IntoResponse for Modified<E> {
/// # `Validified` data extractor /// # `Validified` data extractor
/// ///
/// `Validified` provides construction, modification and validation abilities based on `validify`.
///
/// It requires a serde-based inner extractor.
///
/// And can treat missing fields as validation errors.
///
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy, Default)]
pub struct Validified<E>(pub E); pub struct Validified<E>(pub E);
@@ -121,7 +145,7 @@ impl<T: Display> Display for Validified<T> {
} }
impl<E> Validified<E> { impl<E> Validified<E> {
/// Consumes the `ValidifiedMut` and returns the modified and validated data within. /// Consumes the `Validified` and returns the modified and validated data within.
/// ///
/// This returns the `E` type which represents the data that has been /// This returns the `E` type which represents the data that has been
/// successfully validated. /// successfully validated.
@@ -130,7 +154,46 @@ impl<E> Validified<E> {
} }
} }
/// `ValidifyRejection` is returned when the `Validified` / `ValidifiedMut` / `Modified` extractor fails. /// # `ValidifiedByRef` data extractor
///
/// `ValidifiedByRef` is similar to `Validified`, but operates via reference.
///
/// Suitable for inner extractors not based on `serde`.
///
#[derive(Debug, Clone, Copy, Default)]
pub struct ValidifiedByRef<E>(pub E);
impl<E> Deref for ValidifiedByRef<E> {
type Target = E;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<E> DerefMut for ValidifiedByRef<E> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T: Display> Display for ValidifiedByRef<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl<E> ValidifiedByRef<E> {
/// Consumes the `ValidifiedByRef` and returns the modified and validated data within.
///
/// This returns the `E` type which represents the data that has been
/// successfully validated.
pub fn into_inner(self) -> E {
self.0
}
}
/// `ValidifyRejection` is returned when the `Validated` / `Modified` / `Validified` / `ValidifiedByRef` extractor fails.
/// ///
pub type ValidifyRejection<E> = ValidationRejection<ValidationErrors, E>; pub type ValidifyRejection<E> = ValidationRejection<ValidationErrors, E>;
@@ -159,16 +222,21 @@ pub trait PayloadExtractor {
fn get_payload(self) -> Self::Payload; fn get_payload(self) -> Self::Payload;
} }
/// Trait for types that can supply a reference that can be modified and validated using `validify`. /// Trait for extractors whose inner data type that can be constructed using some payload,
/// then modified and validated using `validify`.
/// ///
/// Extractor types `T` that implement this trait can be used with `Validified`. /// Extractor types `T` that implement this trait can be used with `Validified`.
/// ///
pub trait HasValidify: Sized { pub trait HasValidify: Sized {
/// Inner type that can be modified and validated using `validify`. /// Inner type that can be modified and validated using `validify`.
type Validify: Validify; type Validify: Validify;
///
/// Extracts payload from the request,
/// which will be used to construct the `Self::Validify` type
/// and perform modification and validation on it.
type PayloadExtractor: PayloadExtractor<Payload = <Self::Validify as Validify>::Payload>; type PayloadExtractor: PayloadExtractor<Payload = <Self::Validify as Validify>::Payload>;
///
/// Re-packages the validified data back into the inner Extractor type.
fn from_validified(v: Self::Validify) -> Self; fn from_validified(v: Self::Validify) -> Self;
} }
@@ -282,3 +350,42 @@ where
))) )))
} }
} }
#[async_trait]
impl<State, Body, Extractor> FromRequest<State, Body> for ValidifiedByRef<Extractor>
where
State: Send + Sync,
Body: Send + Sync + 'static,
Extractor: HasValidate + HasModify + FromRequest<State, Body>,
Extractor::Validate: Validate,
{
type Rejection = ValidifyRejection<<Extractor as FromRequest<State, Body>>::Rejection>;
async fn from_request(req: Request<Body>, state: &State) -> Result<Self, Self::Rejection> {
let mut inner = Extractor::from_request(req, state)
.await
.map_err(ValidifyRejection::Inner)?;
inner.get_modify().modify();
inner.get_validate().validate()?;
Ok(ValidifiedByRef(inner))
}
}
#[async_trait]
impl<State, Extractor> FromRequestParts<State> for ValidifiedByRef<Extractor>
where
State: Send + Sync,
Extractor: HasValidate + HasModify + FromRequestParts<State>,
Extractor::Validate: Validate,
{
type Rejection = ValidifyRejection<<Extractor as FromRequestParts<State>>::Rejection>;
async fn from_request_parts(parts: &mut Parts, state: &State) -> Result<Self, Self::Rejection> {
let mut inner = Extractor::from_request_parts(parts, state)
.await
.map_err(ValidifyRejection::Inner)?;
inner.get_modify().modify();
inner.get_validate().validate()?;
Ok(ValidifiedByRef(inner))
}
}

View File

@@ -1,8 +1,10 @@
#![cfg(feature = "validify")] #![cfg(feature = "validify")]
use crate::tests::{ValidTest, ValidTestParameter}; use crate::tests::{ValidTest, ValidTestParameter};
use crate::{HasValidate, Validated, VALIDATION_ERROR_STATUS}; use crate::{
use axum::extract::{FromRef, Path, Query}; HasValidate, Modified, Validated, Validified, ValidifiedByRef, VALIDATION_ERROR_STATUS,
};
use axum::extract::{Path, Query};
use axum::routing::{get, post}; use axum::routing::{get, post};
use axum::{Form, Json, Router}; use axum::{Form, Json, Router};
use hyper::Method; use hyper::Method;
@@ -12,9 +14,9 @@ use serde::{Deserialize, Serialize};
use std::any::type_name; use std::any::type_name;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::ops::Deref; use std::ops::Deref;
use validify::Validate; use validify::{Modify, Validate, Validify};
#[derive(Clone, Deserialize, Serialize, Validate, Eq, PartialEq)] #[derive(Clone, Deserialize, Serialize, Validify, Eq, PartialEq)]
#[cfg_attr(feature = "extra_protobuf", derive(prost::Message))] #[cfg_attr(feature = "extra_protobuf", derive(prost::Message))]
#[cfg_attr( #[cfg_attr(
feature = "typed_multipart", feature = "typed_multipart",
@@ -24,26 +26,32 @@ pub struct ParametersValidify {
#[validate(range(min = 5.0, max = 10.0))] #[validate(range(min = 5.0, max = 10.0))]
#[cfg_attr(feature = "extra_protobuf", prost(int32, tag = "1"))] #[cfg_attr(feature = "extra_protobuf", prost(int32, tag = "1"))]
v0: i32, v0: i32,
#[modify(lowercase)]
#[validate(length(min = 1, max = 10))] #[validate(length(min = 1, max = 10))]
#[cfg_attr(feature = "extra_protobuf", prost(string, tag = "2"))] #[cfg_attr(feature = "extra_protobuf", prost(string, tag = "2"))]
v1: String, v1: String,
} }
trait IsModified: Modify + Clone + PartialEq + Eq {
fn modified(&self) -> bool {
let mut cloned = self.clone();
cloned.modify();
cloned == *self
}
}
impl<T> IsModified for T where T: Modify + Clone + PartialEq + Eq {}
static VALID_PARAMETERS: Lazy<ParametersValidify> = Lazy::new(|| ParametersValidify { static VALID_PARAMETERS: Lazy<ParametersValidify> = Lazy::new(|| ParametersValidify {
v0: 5, v0: 5,
v1: String::from("0123456789"), v1: String::from("ABCDEFG"),
}); });
static INVALID_PARAMETERS: Lazy<ParametersValidify> = Lazy::new(|| ParametersValidify { static INVALID_PARAMETERS: Lazy<ParametersValidify> = Lazy::new(|| ParametersValidify {
v0: 6, v0: 6,
v1: String::from("01234567890"), v1: String::from("ABCDEFGHIJKLMN"),
}); });
#[derive(Debug, Clone, FromRef, Default)]
struct MyState {
no_argument_context: (),
}
impl ValidTestParameter for ParametersValidify { impl ValidTestParameter for ParametersValidify {
fn valid() -> &'static Self { fn valid() -> &'static Self {
VALID_PARAMETERS.deref() VALID_PARAMETERS.deref()
@@ -70,9 +78,33 @@ impl HasValidate for ParametersValidify {
async fn test_main() -> anyhow::Result<()> { async fn test_main() -> anyhow::Result<()> {
let router = Router::new() let router = Router::new()
.route(route::PATH, get(extract_path)) .route(route::PATH, get(extract_path))
.route(route::PATH_MODIFIED, get(extract_path_modified))
.route(route::PATH_VALIDIFIED, get(extract_path_validified))
.route(
route::PATH_VALIDIFIED_BY_REF,
get(extract_path_validified_by_ref),
)
.route(route::QUERY, get(extract_query)) .route(route::QUERY, get(extract_query))
.route(route::QUERY_MODIFIED, get(extract_query_modified))
.route(route::QUERY_VALIDIFIED, get(extract_query_validified))
.route(
route::QUERY_VALIDIFIED_BY_REF,
get(extract_query_validified_by_ref),
)
.route(route::FORM, post(extract_form)) .route(route::FORM, post(extract_form))
.route(route::JSON, post(extract_json)); .route(route::FORM_MODIFIED, post(extract_form_modified))
.route(route::FORM_VALIDIFIED, post(extract_form_validified))
.route(
route::FORM_VALIDIFIED_BY_REF,
post(extract_form_validified_by_ref),
)
.route(route::JSON, post(extract_json))
.route(route::JSON_MODIFIED, post(extract_json_modified))
.route(route::JSON_VALIDIFIED, post(extract_json_validified))
.route(
route::JSON_VALIDIFIED_BY_REF,
post(extract_json_validified_by_ref),
);
#[cfg(feature = "typed_header")] #[cfg(feature = "typed_header")]
let router = router.route( let router = router.route(
@@ -104,10 +136,19 @@ async fn test_main() -> anyhow::Result<()> {
); );
#[cfg(feature = "extra_typed_path")] #[cfg(feature = "extra_typed_path")]
let router = router.route( let router = router
extra_typed_path::route::EXTRA_TYPED_PATH, .route(
get(extra_typed_path::extract_extra_typed_path), extra_typed_path::route::EXTRA_TYPED_PATH,
); get(extra_typed_path::extract_extra_typed_path),
)
.route(
extra_typed_path::route::EXTRA_TYPED_PATH_MODIFIED,
get(extra_typed_path::extract_extra_typed_path_modified),
)
.route(
extra_typed_path::route::EXTRA_TYPED_PATH_VALIDIFIED_BY_REF,
get(extra_typed_path::extract_extra_typed_path_validified_by_ref),
);
#[cfg(feature = "extra_query")] #[cfg(feature = "extra_query")]
let router = router.route( let router = router.route(
@@ -122,10 +163,19 @@ async fn test_main() -> anyhow::Result<()> {
); );
#[cfg(feature = "extra_protobuf")] #[cfg(feature = "extra_protobuf")]
let router = router.route( let router = router
extra_protobuf::route::EXTRA_PROTOBUF, .route(
post(extra_protobuf::extract_extra_protobuf), extra_protobuf::route::EXTRA_PROTOBUF,
); post(extra_protobuf::extract_extra_protobuf),
)
.route(
extra_protobuf::route::EXTRA_PROTOBUF_MODIFIED,
post(extra_protobuf::extract_extra_protobuf_modified),
)
.route(
extra_protobuf::route::EXTRA_PROTOBUF_VALIDIFIED_BY_REF,
post(extra_protobuf::extract_extra_protobuf_validified_by_ref),
);
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
let router = router.route(yaml::route::YAML, post(yaml::extract_yaml)); let router = router.route(yaml::route::YAML, post(yaml::extract_yaml));
@@ -138,8 +188,6 @@ async fn test_main() -> anyhow::Result<()> {
post(msgpack::extract_msgpack_raw), post(msgpack::extract_msgpack_raw),
); );
let router = router.with_state(MyState::default());
let server = axum::Server::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))) let server = axum::Server::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16)))
.serve(router.into_make_service()); .serve(router.into_make_service());
let server_addr = server.local_addr(); let server_addr = server.local_addr();
@@ -157,6 +205,61 @@ async fn test_main() -> anyhow::Result<()> {
test_executor: &TestExecutor, test_executor: &TestExecutor,
route: &str, route: &str,
server_url: &str, server_url: &str,
) -> anyhow::Result<()> {
do_test_extra_path(
test_executor,
route,
server_url,
StatusCode::OK,
StatusCode::BAD_REQUEST,
VALIDATION_ERROR_STATUS,
true,
)
.await
}
async fn test_extra_path_modified(
test_executor: &TestExecutor,
route: &str,
server_url: &str,
) -> anyhow::Result<()> {
do_test_extra_path(
test_executor,
route,
server_url,
StatusCode::OK,
StatusCode::BAD_REQUEST,
StatusCode::OK,
false,
)
.await
}
async fn test_extra_path_validified(
test_executor: &TestExecutor,
route: &str,
server_url: &str,
) -> anyhow::Result<()> {
do_test_extra_path(
test_executor,
route,
server_url,
StatusCode::OK,
StatusCode::BAD_REQUEST,
VALIDATION_ERROR_STATUS,
true,
)
.await
}
async fn do_test_extra_path(
test_executor: &TestExecutor,
route: &str,
server_url: &str,
expected_valid_status: StatusCode,
expected_error_status: StatusCode,
expected_invalid_status: StatusCode,
should_check_json: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let path_type_name = type_name::<Path<ParametersValidify>>(); let path_type_name = type_name::<Path<ParametersValidify>>();
let valid_path_response = test_executor let valid_path_response = test_executor
@@ -169,7 +272,7 @@ async fn test_main() -> anyhow::Result<()> {
.await?; .await?;
assert_eq!( assert_eq!(
valid_path_response.status(), valid_path_response.status(),
StatusCode::OK, expected_valid_status,
"Valid '{}' test failed.", "Valid '{}' test failed.",
path_type_name path_type_name
); );
@@ -181,9 +284,10 @@ async fn test_main() -> anyhow::Result<()> {
.await?; .await?;
assert_eq!( assert_eq!(
error_path_response.status(), error_path_response.status(),
StatusCode::BAD_REQUEST, expected_error_status,
"Error '{}' test failed.", "Error '{}' test failed: {}",
path_type_name path_type_name,
error_path_response.text().await?
); );
let invalid_path_response = test_executor let invalid_path_response = test_executor
@@ -196,22 +300,39 @@ async fn test_main() -> anyhow::Result<()> {
.await?; .await?;
assert_eq!( assert_eq!(
invalid_path_response.status(), invalid_path_response.status(),
VALIDATION_ERROR_STATUS, expected_invalid_status,
"Invalid '{}' test failed.", "Invalid '{}' test failed.",
path_type_name path_type_name
); );
#[cfg(feature = "into_json")] #[cfg(feature = "into_json")]
check_json(path_type_name, invalid_path_response).await; if should_check_json {
check_json(path_type_name, invalid_path_response).await;
}
println!("All {} tests passed.", path_type_name); println!("All {} tests passed.", path_type_name);
Ok(()) Ok(())
} }
test_extra_path(&test_executor, "path", &server_url).await?; test_extra_path(&test_executor, "path", &server_url).await?;
test_extra_path_modified(&test_executor, "path_modified", &server_url).await?;
test_extra_path_validified(&test_executor, "path_validified", &server_url).await?;
test_extra_path(&test_executor, "path_validified_by_ref", &server_url).await?;
// Validified // Validated
test_executor test_executor
.execute::<Query<ParametersValidify>>(Method::GET, route::QUERY) .execute::<Query<ParametersValidify>>(Method::GET, route::QUERY)
.await?; .await?;
// Modified
test_executor
.execute_modified::<Query<ParametersValidify>>(Method::GET, route::QUERY_MODIFIED)
.await?;
// ValidifiedByRef
test_executor
.execute::<Query<ParametersValidify>>(Method::GET, route::QUERY_VALIDIFIED_BY_REF)
.await?;
// Validified
test_executor
.execute_validified::<Query<ParametersValidify>>(Method::GET, route::QUERY_VALIDIFIED)
.await?;
// Validified // Validified
test_executor test_executor
@@ -285,6 +406,44 @@ async fn test_main() -> anyhow::Result<()> {
test_executor: &TestExecutor, test_executor: &TestExecutor,
route: &str, route: &str,
server_url: &str, server_url: &str,
) -> anyhow::Result<()> {
do_test_extra_typed_path(
test_executor,
route,
server_url,
StatusCode::OK,
StatusCode::BAD_REQUEST,
VALIDATION_ERROR_STATUS,
true,
)
.await
}
async fn test_extra_typed_path_modified(
test_executor: &TestExecutor,
route: &str,
server_url: &str,
) -> anyhow::Result<()> {
do_test_extra_typed_path(
test_executor,
route,
server_url,
StatusCode::OK,
StatusCode::BAD_REQUEST,
StatusCode::OK,
false,
)
.await
}
async fn do_test_extra_typed_path(
test_executor: &TestExecutor,
route: &str,
server_url: &str,
expected_valid_status: StatusCode,
expected_error_status: StatusCode,
expected_invalid_status: StatusCode,
should_check_json: bool,
) -> anyhow::Result<()> { ) -> anyhow::Result<()> {
let extra_typed_path_type_name = "T: TypedPath"; let extra_typed_path_type_name = "T: TypedPath";
let valid_extra_typed_path_response = test_executor let valid_extra_typed_path_response = test_executor
@@ -297,7 +456,7 @@ async fn test_main() -> anyhow::Result<()> {
.await?; .await?;
assert_eq!( assert_eq!(
valid_extra_typed_path_response.status(), valid_extra_typed_path_response.status(),
StatusCode::OK, expected_valid_status,
"Validified '{}' test failed.", "Validified '{}' test failed.",
extra_typed_path_type_name extra_typed_path_type_name
); );
@@ -309,7 +468,7 @@ async fn test_main() -> anyhow::Result<()> {
.await?; .await?;
assert_eq!( assert_eq!(
error_extra_typed_path_response.status(), error_extra_typed_path_response.status(),
StatusCode::BAD_REQUEST, expected_error_status,
"Error '{}' test failed.", "Error '{}' test failed.",
extra_typed_path_type_name extra_typed_path_type_name
); );
@@ -324,21 +483,31 @@ async fn test_main() -> anyhow::Result<()> {
.await?; .await?;
assert_eq!( assert_eq!(
invalid_extra_typed_path_response.status(), invalid_extra_typed_path_response.status(),
VALIDATION_ERROR_STATUS, expected_invalid_status,
"Invalid '{}' test failed.", "Invalid '{}' test failed.",
extra_typed_path_type_name extra_typed_path_type_name
); );
#[cfg(feature = "into_json")] #[cfg(feature = "into_json")]
check_json( if should_check_json {
extra_typed_path_type_name, check_json(
invalid_extra_typed_path_response, extra_typed_path_type_name,
) invalid_extra_typed_path_response,
.await; )
.await;
}
println!("All {} tests passed.", extra_typed_path_type_name); println!("All {} tests passed.", extra_typed_path_type_name);
Ok(()) Ok(())
} }
test_extra_typed_path(&test_executor, "extra_typed_path", &server_url).await?; test_extra_typed_path(&test_executor, "extra_typed_path", &server_url).await?;
test_extra_typed_path_modified(&test_executor, "extra_typed_path_modified", &server_url)
.await?;
test_extra_typed_path(
&test_executor,
"extra_typed_path_validified_by_ref",
&server_url,
)
.await?;
} }
#[cfg(feature = "extra_query")] #[cfg(feature = "extra_query")]
@@ -360,12 +529,27 @@ async fn test_main() -> anyhow::Result<()> {
#[cfg(feature = "extra_protobuf")] #[cfg(feature = "extra_protobuf")]
{ {
use axum_extra::protobuf::Protobuf; use axum_extra::protobuf::Protobuf;
// Validated
test_executor test_executor
.execute::<Protobuf<ParametersValidify>>( .execute::<Protobuf<ParametersValidify>>(
Method::POST, Method::POST,
extra_protobuf::route::EXTRA_PROTOBUF, extra_protobuf::route::EXTRA_PROTOBUF,
) )
.await?; .await?;
// Modified
test_executor
.execute_modified::<Protobuf<ParametersValidify>>(
Method::POST,
extra_protobuf::route::EXTRA_PROTOBUF_MODIFIED,
)
.await?;
// ValidifiedByRef
test_executor
.execute::<Protobuf<ParametersValidify>>(
Method::POST,
extra_protobuf::route::EXTRA_PROTOBUF_VALIDIFIED_BY_REF,
)
.await?;
} }
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
@@ -410,6 +594,60 @@ impl From<Url> for TestExecutor {
impl TestExecutor { impl TestExecutor {
/// Execute all tests /// Execute all tests
pub async fn execute<T: ValidTest>(&self, method: Method, route: &str) -> anyhow::Result<()> { pub async fn execute<T: ValidTest>(&self, method: Method, route: &str) -> anyhow::Result<()> {
self.do_execute::<T>(
method,
route,
StatusCode::OK,
T::ERROR_STATUS_CODE,
T::INVALID_STATUS_CODE,
true,
)
.await
}
/// Execute all tests for `Modified` without validation
pub async fn execute_modified<T: ValidTest>(
&self,
method: Method,
route: &str,
) -> anyhow::Result<()> {
self.do_execute::<T>(
method,
route,
StatusCode::OK,
T::ERROR_STATUS_CODE,
StatusCode::OK,
false,
)
.await
}
/// Execute all tests for `Modified` without validation
pub async fn execute_validified<T: ValidTest>(
&self,
method: Method,
route: &str,
) -> anyhow::Result<()> {
self.do_execute::<T>(
method,
route,
StatusCode::OK,
T::INVALID_STATUS_CODE,
T::INVALID_STATUS_CODE,
false,
)
.await
}
async fn do_execute<T: ValidTest>(
&self,
method: Method,
route: &str,
expected_valid_status: StatusCode,
expected_error_status: StatusCode,
expected_invalid_status: StatusCode,
should_check_json: bool,
) -> anyhow::Result<()> {
let url = { let url = {
let mut url_builder = self.server_url.clone(); let mut url_builder = self.server_url.clone();
url_builder.set_path(route); url_builder.set_path(route);
@@ -422,35 +660,37 @@ impl TestExecutor {
let valid_response = T::set_valid_request(valid_builder).send().await?; let valid_response = T::set_valid_request(valid_builder).send().await?;
assert_eq!( assert_eq!(
valid_response.status(), valid_response.status(),
StatusCode::OK, expected_valid_status,
"Validified '{}' test failed.", "Validified '{}' test failed: {}.",
type_name type_name,
valid_response.text().await?
); );
let error_builder = self.client.request(method.clone(), url.clone()); let error_builder = self.client.request(method.clone(), url.clone());
let error_response = T::set_error_request(error_builder).send().await?; let error_response = T::set_error_request(error_builder).send().await?;
assert_eq!( assert_eq!(
error_response.status(), error_response.status(),
T::ERROR_STATUS_CODE, expected_error_status,
"Error '{}' test failed.", "Error '{}' test failed: {}.",
type_name type_name,
error_response.text().await?
); );
let invalid_builder = self.client.request(method, url); let invalid_builder = self.client.request(method, url);
let invalid_response = T::set_invalid_request(invalid_builder).send().await?; let invalid_response = T::set_invalid_request(invalid_builder).send().await?;
assert_eq!( assert_eq!(
invalid_response.status(), invalid_response.status(),
T::INVALID_STATUS_CODE, expected_invalid_status,
"Invalid '{}' test failed.", "Invalid '{}' test failed: {}.",
type_name type_name,
invalid_response.text().await?
); );
#[cfg(feature = "into_json")] #[cfg(feature = "into_json")]
if T::JSON_SERIALIZABLE { if should_check_json && T::JSON_SERIALIZABLE {
check_json(type_name, invalid_response).await; check_json(type_name, invalid_response).await;
} }
println!("All '{}' tests passed.", type_name); println!("All '{}' tests passed.", type_name);
Ok(()) Ok(())
} }
@@ -473,36 +713,121 @@ pub async fn check_json(type_name: &'static str, response: reqwest::Response) {
mod route { mod route {
pub const PATH: &str = "/path/:v0/:v1"; pub const PATH: &str = "/path/:v0/:v1";
pub const PATH_MODIFIED: &str = "/path_modified/:v0/:v1";
pub const PATH_VALIDIFIED: &str = "/path_validified/:v0/:v1";
pub const PATH_VALIDIFIED_BY_REF: &str = "/path_validified_by_ref/:v0/:v1";
pub const QUERY: &str = "/query"; pub const QUERY: &str = "/query";
pub const QUERY_MODIFIED: &str = "/query_modified/:v0/:v1";
pub const QUERY_VALIDIFIED: &str = "/query_validified/:v0/:v1";
pub const QUERY_VALIDIFIED_BY_REF: &str = "/query_validified_by_ref/:v0/:v1";
pub const FORM: &str = "/form"; pub const FORM: &str = "/form";
pub const FORM_MODIFIED: &str = "/form_modified/:v0/:v1";
pub const FORM_VALIDIFIED: &str = "/form_validified/:v0/:v1";
pub const FORM_VALIDIFIED_BY_REF: &str = "/form_validified_by_ref/:v0/:v1";
pub const JSON: &str = "/json"; pub const JSON: &str = "/json";
pub const JSON_MODIFIED: &str = "/json_modified/:v0/:v1";
pub const JSON_VALIDIFIED: &str = "/json_validified/:v0/:v1";
pub const JSON_VALIDIFIED_BY_REF: &str = "/json_validified_by_ref/:v0/:v1";
} }
async fn extract_path( async fn extract_path(
Validated(Path(parameters)): Validated<Path<ParametersValidify>>, Validated(Path(parameters)): Validated<Path<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
}
async fn extract_path_modified(
Modified(Path(parameters)): Modified<Path<ParametersValidify>>,
) -> StatusCode {
check_modified(&parameters)
}
async fn extract_path_validified(
Validified(Path(parameters)): Validified<Path<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
}
async fn extract_path_validified_by_ref(
ValidifiedByRef(Path(parameters)): ValidifiedByRef<Path<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
} }
async fn extract_query( async fn extract_query(
Validated(Query(parameters)): Validated<Query<ParametersValidify>>, Validated(Query(parameters)): Validated<Query<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
}
async fn extract_query_modified(
Modified(Query(parameters)): Modified<Query<ParametersValidify>>,
) -> StatusCode {
check_modified(&parameters)
}
async fn extract_query_validified(
Validified(Query(parameters)): Validified<Query<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
}
async fn extract_query_validified_by_ref(
ValidifiedByRef(Query(parameters)): ValidifiedByRef<Query<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
} }
async fn extract_form( async fn extract_form(
Validated(Form(parameters)): Validated<Form<ParametersValidify>>, Validated(Form(parameters)): Validated<Form<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
}
async fn extract_form_modified(
Modified(Form(parameters)): Modified<Form<ParametersValidify>>,
) -> StatusCode {
check_modified(&parameters)
}
async fn extract_form_validified(
Validified(Form(parameters)): Validified<Form<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
}
async fn extract_form_validified_by_ref(
ValidifiedByRef(Form(parameters)): ValidifiedByRef<Form<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
} }
async fn extract_json( async fn extract_json(
Validated(Json(parameters)): Validated<Json<ParametersValidify>>, Validated(Json(parameters)): Validated<Json<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
fn validate_again<V: Validate>(validate: V) -> StatusCode { async fn extract_json_modified(
Modified(Json(parameters)): Modified<Json<ParametersValidify>>,
) -> StatusCode {
check_modified(&parameters)
}
async fn extract_json_validified(
Validified(Json(parameters)): Validified<Json<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
}
async fn extract_json_validified_by_ref(
ValidifiedByRef(Json(parameters)): ValidifiedByRef<Json<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
}
fn check_validated<V: Validate>(validate: &V) -> StatusCode {
// The `Validified` extractor has validated the `parameters` once, // The `Validified` extractor has validated the `parameters` once,
// it should have returned `400 BAD REQUEST` if the `parameters` were invalid, // it should have returned `400 BAD REQUEST` if the `parameters` were invalid,
// Let's validate them again to check if the `Validified` extractor works well. // Let's validate them again to check if the `Validified` extractor works well.
@@ -513,6 +838,23 @@ fn validate_again<V: Validate>(validate: V) -> StatusCode {
} }
} }
fn check_modified<M: IsModified>(modify: &M) -> StatusCode {
if modify.modified() {
StatusCode::OK
} else {
StatusCode::INTERNAL_SERVER_ERROR
}
}
fn check_validified<D: IsModified + Validate>(data: &D) -> StatusCode {
let status = check_modified(data);
if status != StatusCode::OK {
return status;
}
check_validated(data)
}
#[cfg(feature = "typed_header")] #[cfg(feature = "typed_header")]
mod typed_header { mod typed_header {
@@ -520,7 +862,7 @@ mod typed_header {
pub const TYPED_HEADER: &str = "/typed_header"; pub const TYPED_HEADER: &str = "/typed_header";
} }
use super::{validate_again, ParametersValidify}; use super::{check_validated, ParametersValidify};
use crate::Validated; use crate::Validated;
use axum::headers::{Error, Header, HeaderName, HeaderValue}; use axum::headers::{Error, Header, HeaderName, HeaderValue};
use axum::http::StatusCode; use axum::http::StatusCode;
@@ -531,7 +873,7 @@ mod typed_header {
pub(super) async fn extract_typed_header( pub(super) async fn extract_typed_header(
Validated(TypedHeader(parameters)): Validated<TypedHeader<ParametersValidify>>, Validated(TypedHeader(parameters)): Validated<TypedHeader<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
impl Header for ParametersValidify { impl Header for ParametersValidify {
@@ -583,7 +925,7 @@ mod typed_header {
#[cfg(feature = "typed_multipart")] #[cfg(feature = "typed_multipart")]
mod typed_multipart { mod typed_multipart {
use super::{validate_again, ParametersValidify}; use super::{check_validated, ParametersValidify};
use crate::Validated; use crate::Validated;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_typed_multipart::{BaseMultipart, TypedMultipart, TypedMultipartError}; use axum_typed_multipart::{BaseMultipart, TypedMultipart, TypedMultipartError};
@@ -604,7 +946,7 @@ mod typed_multipart {
pub(super) async fn extract_typed_multipart( pub(super) async fn extract_typed_multipart(
Validated(TypedMultipart(parameters)): Validated<TypedMultipart<ParametersValidify>>, Validated(TypedMultipart(parameters)): Validated<TypedMultipart<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
pub(super) async fn extract_base_multipart( pub(super) async fn extract_base_multipart(
@@ -612,13 +954,13 @@ mod typed_multipart {
BaseMultipart<ParametersValidify, TypedMultipartError>, BaseMultipart<ParametersValidify, TypedMultipartError>,
>, >,
) -> StatusCode { ) -> StatusCode {
validate_again(data) check_validated(&data)
} }
} }
#[cfg(feature = "extra")] #[cfg(feature = "extra")]
mod extra { mod extra {
use super::{validate_again, ParametersValidify}; use super::{check_validated, ParametersValidify};
use crate::tests::{Rejection, ValidTest, ValidTestParameter}; use crate::tests::{Rejection, ValidTest, ValidTestParameter};
use crate::{Validated, ValidifyRejection}; use crate::{Validated, ValidifyRejection};
use axum::extract::FromRequestParts; use axum::extract::FromRequestParts;
@@ -729,7 +1071,7 @@ mod extra {
pub async fn extract_cached( pub async fn extract_cached(
Validated(Cached(parameters)): Validated<Cached<ParametersValidify>>, Validated(Cached(parameters)): Validated<Cached<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
pub async fn extract_with_rejection( pub async fn extract_with_rejection(
@@ -737,7 +1079,7 @@ mod extra {
WithRejection<ParametersValidify, ValidifyWithRejectionRejection>, WithRejection<ParametersValidify, ValidifyWithRejectionRejection>,
>, >,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
pub struct WithRejectionValidifyRejection<E> { pub struct WithRejectionValidifyRejection<E> {
@@ -764,21 +1106,24 @@ mod extra {
WithRejectionValidifyRejection<ParametersRejection>, WithRejectionValidifyRejection<ParametersRejection>,
>, >,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
} }
#[cfg(feature = "extra_typed_path")] #[cfg(feature = "extra_typed_path")]
mod extra_typed_path { mod extra_typed_path {
use super::validate_again; use super::{check_modified, check_validated, check_validified};
use crate::{HasValidate, Validated}; use crate::{HasModify, HasValidate, Modified, Validated, ValidifiedByRef};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_extra::routing::TypedPath; use axum_extra::routing::TypedPath;
use serde::Deserialize; use serde::Deserialize;
use validify::Validate; use validify::{Validate, Validify};
pub mod route { pub mod route {
pub const EXTRA_TYPED_PATH: &str = "/extra_typed_path/:v0/:v1"; pub const EXTRA_TYPED_PATH: &str = "/extra_typed_path/:v0/:v1";
pub const EXTRA_TYPED_PATH_MODIFIED: &str = "/extra_typed_path_modified/:v0/:v1";
pub const EXTRA_TYPED_PATH_VALIDIFIED_BY_REF: &str =
"/extra_typed_path_validified_by_ref/:v0/:v1";
} }
#[derive(Validate, TypedPath, Deserialize)] #[derive(Validate, TypedPath, Deserialize)]
@@ -798,16 +1143,72 @@ mod extra_typed_path {
} }
} }
#[derive(Validify, TypedPath, Deserialize, Clone, PartialEq, Eq)]
#[typed_path("/extra_typed_path_validified_by_ref/:v0/:v1")]
pub struct TypedPathParamValidifiedByRef {
#[validate(range(min = 5.0, max = 10.0))]
v0: i32,
#[modify(lowercase)]
#[validate(length(min = 1, max = 10))]
v1: String,
}
impl HasValidate for TypedPathParamValidifiedByRef {
type Validate = Self;
fn get_validate(&self) -> &Self::Validate {
self
}
}
impl HasModify for TypedPathParamValidifiedByRef {
type Modify = Self;
fn get_modify(&mut self) -> &mut Self::Modify {
self
}
}
#[derive(Validify, TypedPath, Deserialize, Clone, PartialEq, Eq)]
#[typed_path("/extra_typed_path_modified/:v0/:v1")]
pub struct TypedPathParamModified {
#[validate(range(min = 5.0, max = 10.0))]
v0: i32,
#[modify(lowercase)]
#[validate(length(min = 1, max = 10))]
v1: String,
}
impl HasModify for TypedPathParamModified {
type Modify = Self;
fn get_modify(&mut self) -> &mut Self::Modify {
self
}
}
pub async fn extract_extra_typed_path( pub async fn extract_extra_typed_path(
Validated(param): Validated<TypedPathParam>, Validated(param): Validated<TypedPathParam>,
) -> StatusCode { ) -> StatusCode {
validate_again(param) check_validated(&param)
}
pub async fn extract_extra_typed_path_modified(
Modified(param): Modified<TypedPathParamModified>,
) -> StatusCode {
check_modified(&param)
}
pub async fn extract_extra_typed_path_validified_by_ref(
ValidifiedByRef(param): ValidifiedByRef<TypedPathParamValidifiedByRef>,
) -> StatusCode {
check_validified(&param)
} }
} }
#[cfg(feature = "extra_query")] #[cfg(feature = "extra_query")]
mod extra_query { mod extra_query {
use super::{validate_again, ParametersValidify}; use super::{check_validated, ParametersValidify};
use crate::Validated; use crate::Validated;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_extra::extract::Query; use axum_extra::extract::Query;
@@ -819,13 +1220,13 @@ mod extra_query {
pub async fn extract_extra_query( pub async fn extract_extra_query(
Validated(Query(parameters)): Validated<Query<ParametersValidify>>, Validated(Query(parameters)): Validated<Query<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
} }
#[cfg(feature = "extra_form")] #[cfg(feature = "extra_form")]
mod extra_form { mod extra_form {
use super::{validate_again, ParametersValidify}; use super::{check_validated, ParametersValidify};
use crate::Validated; use crate::Validated;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_extra::extract::Form; use axum_extra::extract::Form;
@@ -837,31 +1238,45 @@ mod extra_form {
pub async fn extract_extra_form( pub async fn extract_extra_form(
Validated(Form(parameters)): Validated<Form<ParametersValidify>>, Validated(Form(parameters)): Validated<Form<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
} }
#[cfg(feature = "extra_protobuf")] #[cfg(feature = "extra_protobuf")]
mod extra_protobuf { mod extra_protobuf {
use super::{validate_again, ParametersValidify}; use super::{check_modified, check_validated, check_validified, ParametersValidify};
use crate::Validated; use crate::{Modified, Validated, ValidifiedByRef};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_extra::protobuf::Protobuf; use axum_extra::protobuf::Protobuf;
pub mod route { pub mod route {
pub const EXTRA_PROTOBUF: &str = "/extra_protobuf"; pub const EXTRA_PROTOBUF: &str = "/extra_protobuf";
pub const EXTRA_PROTOBUF_MODIFIED: &str = "/extra_protobuf_modified";
pub const EXTRA_PROTOBUF_VALIDIFIED_BY_REF: &str = "/extra_protobuf_validified_by_ref";
} }
pub async fn extract_extra_protobuf( pub async fn extract_extra_protobuf(
Validated(Protobuf(parameters)): Validated<Protobuf<ParametersValidify>>, Validated(Protobuf(parameters)): Validated<Protobuf<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
}
pub async fn extract_extra_protobuf_modified(
Modified(Protobuf(parameters)): Modified<Protobuf<ParametersValidify>>,
) -> StatusCode {
check_modified(&parameters)
}
pub async fn extract_extra_protobuf_validified_by_ref(
ValidifiedByRef(Protobuf(parameters)): ValidifiedByRef<Protobuf<ParametersValidify>>,
) -> StatusCode {
check_validified(&parameters)
} }
} }
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
mod yaml { mod yaml {
use super::{validate_again, ParametersValidify}; use super::{check_validated, ParametersValidify};
use crate::Validated; use crate::Validated;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_yaml::Yaml; use axum_yaml::Yaml;
@@ -873,13 +1288,13 @@ mod yaml {
pub async fn extract_yaml( pub async fn extract_yaml(
Validated(Yaml(parameters)): Validated<Yaml<ParametersValidify>>, Validated(Yaml(parameters)): Validated<Yaml<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
} }
#[cfg(feature = "msgpack")] #[cfg(feature = "msgpack")]
mod msgpack { mod msgpack {
use super::{validate_again, ParametersValidify}; use super::{check_validated, ParametersValidify};
use crate::Validated; use crate::Validated;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_msgpack::{MsgPack, MsgPackRaw}; use axum_msgpack::{MsgPack, MsgPackRaw};
@@ -892,11 +1307,11 @@ mod msgpack {
pub async fn extract_msgpack( pub async fn extract_msgpack(
Validated(MsgPack(parameters)): Validated<MsgPack<ParametersValidify>>, Validated(MsgPack(parameters)): Validated<MsgPack<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
pub async fn extract_msgpack_raw( pub async fn extract_msgpack_raw(
Validated(MsgPackRaw(parameters)): Validated<MsgPackRaw<ParametersValidify>>, Validated(MsgPackRaw(parameters)): Validated<MsgPackRaw<ParametersValidify>>,
) -> StatusCode { ) -> StatusCode {
validate_again(parameters) check_validated(&parameters)
} }
} }

View File

@@ -114,6 +114,23 @@ impl<T: validify::Modify> crate::HasModify for Yaml<T> {
} }
} }
#[cfg(feature = "validify")]
impl<T> crate::PayloadExtractor for Yaml<T> {
type Payload = T;
fn get_payload(self) -> Self::Payload {
self.0
}
}
#[cfg(feature = "validify")]
impl<T: validify::Validify> crate::HasValidify for Yaml<T> {
type Validify = T;
type PayloadExtractor = Yaml<T::Payload>;
fn from_validified(v: Self::Validify) -> Self {
Yaml(v)
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::{ValidTest, ValidTestParameter}; use crate::tests::{ValidTest, ValidTestParameter};