refactor doc tests

This commit is contained in:
gengteng
2023-10-09 11:37:27 +08:00
parent 928578a840
commit 6cf325b19d
14 changed files with 1510 additions and 332 deletions

View File

@@ -1,7 +1,7 @@
[package] [package]
name = "axum-valid" name = "axum-valid"
version = "0.9.0" version = "0.9.0"
description = "Provide validator extractor for your axum application." description = "Provides validation extractors for your Axum application to validate data using validator, garde, or both."
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"

View File

@@ -7,11 +7,15 @@
[![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 a `Valid` type for use with `Json`, `Path`, `Query`, and `Form` extractors to validate entities implementing the `Validate` trait from the `validator` crate. This crate provides data validation capabilities for Axum based on the `validator` and `garde` crates.
A `ValidEx` type is also available. Similar to `Valid`, `ValidEx` can execute validations requiring extra arguments with types that implement the `ValidateArgs` trait from the `validator` crate. `validator` support is included by default. To use `garde`, enable it via the `garde` feature. `garde` alone can also be enabled by using `default-features = false`.
Additional extractors such as `TypedHeader`, `MsgPack`, `Yaml`, and others are supported through optional features. The complete list of supported extractors can be found in the Features section below. The `Valid` type enables validation using `validator` for extractors like `Json`, `Path`, `Query` and `Form`. For validations requiring extra arguments, the `ValidEx` type is offered.
The `Garde` type supports both argument and non-argument validations using `garde` in a unified way.
Additional extractors like `TypedHeader`, `MsgPack` and `Yaml` are also supported through optional features. Refer to `Features` for details.
## Basic usage ## Basic usage
@@ -20,6 +24,8 @@ cargo add axum-valid
``` ```
```rust,no_run ```rust,no_run
#[cfg(feature = "validator")]
mod validator_example {
use validator::Validate; use validator::Validate;
use serde::Deserialize; use serde::Deserialize;
use axum_valid::Valid; use axum_valid::Valid;
@@ -43,14 +49,16 @@ pub async fn pager_from_query(
} }
pub async fn pager_from_json( pub async fn pager_from_json(
Valid(Json(pager)): Valid<Json<Pager>>, pager: Valid<Json<Pager>>,
) { ) {
assert!((1..=50).contains(&pager.page_size)); assert!((1..=50).contains(&pager.page_size));
assert!((1..).contains(&pager.page_no)); assert!((1..).contains(&pager.page_no));
// NOTE: support automatic dereferencing
println!("page_no: {}, page_size: {}", pager.page_no, pager.page_size);
} }
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { pub async fn launch() -> anyhow::Result<()> {
let router = Router::new() let router = Router::new()
.route("/query", get(pager_from_query)) .route("/query", get(pager_from_query))
.route("/json", post(pager_from_json)); .route("/json", post(pager_from_json));
@@ -59,6 +67,60 @@ async fn main() -> anyhow::Result<()> {
.await?; .await?;
Ok(()) Ok(())
} }
}
#[cfg(feature = "garde")]
mod garde_example {
use garde::Validate;
use serde::Deserialize;
use axum_valid::Garde;
use axum::extract::Query;
use axum::{Json, Router};
use axum::routing::{get, post};
#[derive(Debug, Validate, Deserialize)]
pub struct Pager {
#[garde(range(min = 1, max = 50))]
pub page_size: usize,
#[garde(range(min = 1))]
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);
}
#[tokio::main]
pub async fn launch() -> anyhow::Result<()> {
let router = Router::new()
.route("/query", get(pager_from_query))
.route("/json", post(pager_from_json));
axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
.serve(router.into_make_service())
.await?;
Ok(())
}
}
fn main() -> anyhow::Result<()> {
#[cfg(feature = "validator")]
validator_example::launch()?;
#[cfg(feature = "garde")]
garde_example::launch()?;
Ok(())
}
``` ```
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.
@@ -70,6 +132,8 @@ To see how each extractor can be used with `Valid`, please refer to the example
Here's a basic example of using the `ValidEx` extractor to validate data in a `Form` using arguments: Here's a basic example of using the `ValidEx` extractor to validate data in a `Form` using arguments:
```rust,no_run ```rust,no_run
#[cfg(feature = "validator")]
mod validator_example {
use axum::routing::post; use axum::routing::post;
use axum::{Form, Router}; use axum::{Form, Router};
use axum_valid::{Arguments, ValidEx}; use axum_valid::{Arguments, ValidEx};
@@ -128,7 +192,7 @@ pub async fn pager_from_form_ex(ValidEx(Form(pager), _): ValidEx<Form<Pager>, Pa
} }
#[tokio::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { pub async fn launch() -> anyhow::Result<()> {
let router = Router::new() let router = Router::new()
.route("/form", post(pager_from_form_ex)) .route("/form", post(pager_from_form_ex))
.with_state(PagerValidArgs { .with_state(PagerValidArgs {
@@ -143,6 +207,23 @@ async fn main() -> anyhow::Result<()> {
.await?; .await?;
Ok(()) Ok(())
} }
}
#[cfg(feature = "garde")]
mod garde_example {
#[tokio::main]
pub async fn launch() -> anyhow::Result<()> {
Ok(())
}
}
fn main() -> anyhow::Result<()> {
#[cfg(feature = "validator")]
validator_example::launch()?;
#[cfg(feature = "garde")]
garde_example::launch()?;
Ok(())
}
``` ```
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.

View File

@@ -283,6 +283,8 @@ impl<'v, T: ValidateArgs<'v>, R> HasValidateArgs<'v> for WithRejection<T, R> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::tests::{Rejection, ValidTest}; use crate::tests::{Rejection, ValidTest};
#[cfg(feature = "garde")]
use crate::Garde;
#[cfg(feature = "validator")] #[cfg(feature = "validator")]
use crate::Valid; use crate::Valid;
use axum::http::StatusCode; use axum::http::StatusCode;
@@ -347,4 +349,29 @@ mod tests {
T::set_invalid_request(builder) T::set_invalid_request(builder)
} }
} }
#[cfg(feature = "garde")]
impl<T: ValidTest, R> ValidTest for WithRejection<Garde<T>, R> {
// just use `418 I'm a teapot` to test
const ERROR_STATUS_CODE: StatusCode = StatusCode::IM_A_TEAPOT;
// If `WithRejection` is the outermost extractor,
// the error code returned will always be the one provided by WithRejection.
const INVALID_STATUS_CODE: StatusCode = StatusCode::IM_A_TEAPOT;
// If `WithRejection` is the outermost extractor,
// the returned body may not be in JSON format.
const JSON_SERIALIZABLE: bool = false;
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
T::set_valid_request(builder)
}
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
// invalid requests will cause the Valid extractor to fail.
T::set_invalid_request(builder)
}
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
T::set_invalid_request(builder)
}
}
} }

View File

@@ -12,8 +12,8 @@
//! ## Example //! ## Example
//! //!
//! ```no_run //! ```no_run
//! #![cfg(feature = "validator")] //! #[cfg(feature = "validator")]
//! //! mod validator_example {
//! use axum::routing::post; //! use axum::routing::post;
//! use axum::Form; //! use axum::Form;
//! use axum::Router; //! use axum::Router;
@@ -21,8 +21,8 @@
//! use serde::Deserialize; //! use serde::Deserialize;
//! use validator::Validate; //! use validator::Validate;
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> anyhow::Result<()> { //! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/form", post(handler)); //! let router = Router::new().route("/json", post(handler));
//! 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?;
@@ -30,6 +30,8 @@
//! } //! }
//! async fn handler(Valid(Form(parameter)): Valid<Form<Parameter>>) { //! async fn handler(Valid(Form(parameter)): Valid<Form<Parameter>>) {
//! assert!(parameter.validate().is_ok()); //! assert!(parameter.validate().is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! } //! }
//! #[derive(Validate, Deserialize)] //! #[derive(Validate, Deserialize)]
//! pub struct Parameter { //! pub struct Parameter {
@@ -38,6 +40,45 @@
//! #[validate(length(min = 1, max = 10))] //! #[validate(length(min = 1, max = 10))]
//! pub v1: String, //! pub v1: String,
//! } //! }
//! }
//!
//! #[cfg(feature = "garde")]
//! mod garde_example {
//! use axum::routing::post;
//! use axum::Form;
//! use axum::Router;
//! use axum_valid::Garde;
//! use serde::Deserialize;
//! use garde::Validate;
//! #[tokio::main]
//! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/json", post(handler));
//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
//! .serve(router.into_make_service())
//! .await?;
//! Ok(())
//! }
//! async fn handler(Garde(Form(parameter)): Garde<Form<Parameter>>) {
//! assert!(parameter.validate(&()).is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! }
//! #[derive(Validate, Deserialize)]
//! pub struct Parameter {
//! #[garde(range(min = 5, max = 10))]
//! pub v0: i32,
//! #[garde(length(min = 1, max = 10))]
//! pub v1: String,
//! }
//! }
//!
//! # fn main() -> anyhow::Result<()> {
//! # #[cfg(feature = "validator")]
//! # validator_example::launch()?;
//! # #[cfg(feature = "garde")]
//! # garde_example::launch()?;
//! # Ok(())
//! # }
//! ``` //! ```
use crate::HasValidate; use crate::HasValidate;

View File

@@ -1,5 +1,8 @@
//! # Garde support //! # Garde support
#[cfg(test)]
pub mod test;
use crate::{HasValidate, VALIDATION_ERROR_STATUS}; use crate::{HasValidate, VALIDATION_ERROR_STATUS};
use axum::async_trait; use axum::async_trait;
use axum::extract::{FromRef, FromRequest, FromRequestParts}; use axum::extract::{FromRef, FromRequest, FromRequestParts};

890
src/garde/test.rs Normal file
View File

@@ -0,0 +1,890 @@
#![cfg(feature = "garde")]
use crate::tests::{ValidTest, ValidTestParameter};
use crate::{Garde, HasValidate, VALIDATION_ERROR_STATUS};
use axum::extract::{FromRef, Path, Query};
use axum::routing::{get, post};
use axum::{Form, Json, Router};
use garde::Validate;
use hyper::Method;
use once_cell::sync::Lazy;
use reqwest::{StatusCode, Url};
use serde::{Deserialize, Serialize};
use std::any::type_name;
use std::net::SocketAddr;
use std::ops::Deref;
#[derive(Clone, Deserialize, Serialize, Validate, Eq, PartialEq)]
#[cfg_attr(feature = "extra_protobuf", derive(prost::Message))]
#[cfg_attr(
feature = "typed_multipart",
derive(axum_typed_multipart::TryFromMultipart)
)]
pub struct ParametersGarde {
#[garde(range(min = 5, max = 10))]
#[cfg_attr(feature = "extra_protobuf", prost(int32, tag = "1"))]
v0: i32,
#[garde(length(min = 1, max = 10))]
#[cfg_attr(feature = "extra_protobuf", prost(string, tag = "2"))]
v1: String,
}
static VALID_PARAMETERS: Lazy<ParametersGarde> = Lazy::new(|| ParametersGarde {
v0: 5,
v1: String::from("0123456789"),
});
static INVALID_PARAMETERS: Lazy<ParametersGarde> = Lazy::new(|| ParametersGarde {
v0: 6,
v1: String::from("01234567890"),
});
#[derive(Debug, Clone, FromRef, Default)]
struct MyState {
no_argument_context: (),
}
impl ValidTestParameter for ParametersGarde {
fn valid() -> &'static Self {
VALID_PARAMETERS.deref()
}
fn error() -> &'static [(&'static str, &'static str)] {
&[("not_v0_or_v1", "value")]
}
fn invalid() -> &'static Self {
INVALID_PARAMETERS.deref()
}
}
impl HasValidate for ParametersGarde {
type Validate = ParametersGarde;
fn get_validate(&self) -> &Self::Validate {
self
}
}
#[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 = "typed_multipart")]
let router = router
.route(
typed_multipart::route::TYPED_MULTIPART,
post(typed_multipart::extract_typed_multipart),
)
.route(
typed_multipart::route::BASE_MULTIPART,
post(typed_multipart::extract_base_multipart),
);
#[cfg(feature = "extra")]
let router = router
.route(extra::route::CACHED, post(extra::extract_cached))
.route(
extra::route::WITH_REJECTION,
post(extra::extract_with_rejection),
)
.route(
extra::route::WITH_REJECTION_GARDE,
post(extra::extract_with_rejection_valid),
);
#[cfg(feature = "extra_typed_path")]
let router = router.route(
extra_typed_path::route::EXTRA_TYPED_PATH,
get(extra_typed_path::extract_extra_typed_path),
);
#[cfg(feature = "extra_query")]
let router = router.route(
extra_query::route::EXTRA_QUERY,
post(extra_query::extract_extra_query),
);
#[cfg(feature = "extra_form")]
let router = router.route(
extra_form::route::EXTRA_FORM,
post(extra_form::extract_extra_form),
);
#[cfg(feature = "extra_protobuf")]
let router = router.route(
extra_protobuf::route::EXTRA_PROTOBUF,
post(extra_protobuf::extract_extra_protobuf),
);
#[cfg(feature = "yaml")]
let router = router.route(yaml::route::YAML, post(yaml::extract_yaml));
#[cfg(feature = "msgpack")]
let router = router
.route(msgpack::route::MSGPACK, post(msgpack::extract_msgpack))
.route(
msgpack::route::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)))
.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))?);
async fn test_extra_path(
test_executor: &TestExecutor,
route: &str,
server_url: &str,
) -> anyhow::Result<()> {
let path_type_name = type_name::<Path<ParametersGarde>>();
let valid_path_response = test_executor
.client()
.get(format!(
"{}/{route}/{}/{}",
server_url, VALID_PARAMETERS.v0, VALID_PARAMETERS.v1
))
.send()
.await?;
assert_eq!(
valid_path_response.status(),
StatusCode::OK,
"Valid '{}' test failed.",
path_type_name
);
let error_path_response = test_executor
.client()
.get(format!("{}/{route}/not_i32/path", server_url))
.send()
.await?;
assert_eq!(
error_path_response.status(),
StatusCode::BAD_REQUEST,
"Error '{}' test failed.",
path_type_name
);
let invalid_path_response = test_executor
.client()
.get(format!(
"{}/{route}/{}/{}",
server_url, INVALID_PARAMETERS.v0, INVALID_PARAMETERS.v1
))
.send()
.await?;
assert_eq!(
invalid_path_response.status(),
VALIDATION_ERROR_STATUS,
"Invalid '{}' test failed.",
path_type_name
);
#[cfg(feature = "into_json")]
check_json(path_type_name, invalid_path_response).await;
println!("All {} tests passed.", path_type_name);
Ok(())
}
test_extra_path(&test_executor, "path", &server_url).await?;
// Garde
test_executor
.execute::<Query<ParametersGarde>>(Method::GET, route::QUERY)
.await?;
// Garde
test_executor
.execute::<Form<ParametersGarde>>(Method::POST, route::FORM)
.await?;
// Garde
test_executor
.execute::<Json<ParametersGarde>>(Method::POST, route::JSON)
.await?;
#[cfg(feature = "typed_header")]
{
use axum::TypedHeader;
// Garde
test_executor
.execute::<TypedHeader<ParametersGarde>>(
Method::POST,
typed_header::route::TYPED_HEADER,
)
.await?;
}
#[cfg(feature = "typed_multipart")]
{
use axum_typed_multipart::{BaseMultipart, TypedMultipart, TypedMultipartError};
// Garde
test_executor
.execute::<BaseMultipart<ParametersGarde, TypedMultipartError>>(
Method::POST,
typed_multipart::route::BASE_MULTIPART,
)
.await?;
// Garde
test_executor
.execute::<TypedMultipart<ParametersGarde>>(
Method::POST,
typed_multipart::route::TYPED_MULTIPART,
)
.await?;
}
#[cfg(feature = "extra")]
{
use axum_extra::extract::{Cached, WithRejection};
use extra::{
GardeWithRejectionRejection, ParametersRejection, WithRejectionGardeRejection,
};
test_executor
.execute::<Cached<ParametersGarde>>(Method::POST, extra::route::CACHED)
.await?;
test_executor
.execute::<WithRejection<ParametersGarde, GardeWithRejectionRejection>>(
Method::POST,
extra::route::WITH_REJECTION,
)
.await?;
test_executor
.execute::<WithRejection<Garde<ParametersGarde>, WithRejectionGardeRejection<ParametersRejection>>>(
Method::POST,
extra::route::WITH_REJECTION_GARDE,
)
.await?;
}
#[cfg(feature = "extra_typed_path")]
{
async fn test_extra_typed_path(
test_executor: &TestExecutor,
route: &str,
server_url: &str,
) -> anyhow::Result<()> {
let extra_typed_path_type_name = "T: TypedPath";
let valid_extra_typed_path_response = test_executor
.client()
.get(format!(
"{}/{route}/{}/{}",
server_url, VALID_PARAMETERS.v0, VALID_PARAMETERS.v1
))
.send()
.await?;
assert_eq!(
valid_extra_typed_path_response.status(),
StatusCode::OK,
"Garde '{}' test failed.",
extra_typed_path_type_name
);
let error_extra_typed_path_response = test_executor
.client()
.get(format!("{}/{route}/not_i32/path", server_url))
.send()
.await?;
assert_eq!(
error_extra_typed_path_response.status(),
StatusCode::BAD_REQUEST,
"Error '{}' test failed.",
extra_typed_path_type_name
);
let invalid_extra_typed_path_response = test_executor
.client()
.get(format!(
"{}/{route}/{}/{}",
server_url, INVALID_PARAMETERS.v0, INVALID_PARAMETERS.v1
))
.send()
.await?;
assert_eq!(
invalid_extra_typed_path_response.status(),
VALIDATION_ERROR_STATUS,
"Invalid '{}' test failed.",
extra_typed_path_type_name
);
#[cfg(feature = "into_json")]
check_json(
extra_typed_path_type_name,
invalid_extra_typed_path_response,
)
.await;
println!("All {} tests passed.", extra_typed_path_type_name);
Ok(())
}
test_extra_typed_path(&test_executor, "extra_typed_path", &server_url).await?;
}
#[cfg(feature = "extra_query")]
{
use axum_extra::extract::Query;
test_executor
.execute::<Query<ParametersGarde>>(Method::POST, extra_query::route::EXTRA_QUERY)
.await?;
}
#[cfg(feature = "extra_form")]
{
use axum_extra::extract::Form;
test_executor
.execute::<Form<ParametersGarde>>(Method::POST, extra_form::route::EXTRA_FORM)
.await?;
}
#[cfg(feature = "extra_protobuf")]
{
use axum_extra::protobuf::Protobuf;
test_executor
.execute::<Protobuf<ParametersGarde>>(
Method::POST,
extra_protobuf::route::EXTRA_PROTOBUF,
)
.await?;
}
#[cfg(feature = "yaml")]
{
use axum_yaml::Yaml;
test_executor
.execute::<Yaml<ParametersGarde>>(Method::POST, yaml::route::YAML)
.await?;
}
#[cfg(feature = "msgpack")]
{
use axum_msgpack::{MsgPack, MsgPackRaw};
test_executor
.execute::<MsgPack<ParametersGarde>>(Method::POST, msgpack::route::MSGPACK)
.await?;
test_executor
.execute::<MsgPackRaw<ParametersGarde>>(Method::POST, msgpack::route::MSGPACK_RAW)
.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 type_name = type_name::<T>();
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,
"Garde '{}' test failed.",
type_name
);
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,
"Error '{}' test failed.",
type_name
);
let invalid_builder = self.client.request(method, url);
let invalid_response = T::set_invalid_request(invalid_builder).send().await?;
assert_eq!(
invalid_response.status(),
T::INVALID_STATUS_CODE,
"Invalid '{}' test failed.",
type_name
);
#[cfg(feature = "into_json")]
if T::JSON_SERIALIZABLE {
check_json(type_name, invalid_response).await;
}
println!("All '{}' tests passed.", type_name);
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(type_name: &'static str, response: reqwest::Response) {
assert_eq!(
response.headers()[axum::http::header::CONTENT_TYPE],
axum::http::HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
"'{}' rejection into json test failed",
type_name
);
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";
}
async fn extract_path(Garde(Path(parameters)): Garde<Path<ParametersGarde>>) -> StatusCode {
validate_again(parameters, ())
}
async fn extract_query(Garde(Query(parameters)): Garde<Query<ParametersGarde>>) -> StatusCode {
validate_again(parameters, ())
}
async fn extract_form(Garde(Form(parameters)): Garde<Form<ParametersGarde>>) -> StatusCode {
validate_again(parameters, ())
}
async fn extract_json(Garde(Json(parameters)): Garde<Json<ParametersGarde>>) -> StatusCode {
validate_again(parameters, ())
}
fn validate_again<V: Validate>(validate: V, context: V::Context) -> StatusCode {
// The `Garde` 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 `Garde` extractor works well.
// If it works properly, this function will never return `500 INTERNAL SERVER ERROR`
match validate.validate(&context) {
Ok(_) => StatusCode::OK,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[cfg(feature = "typed_header")]
mod typed_header {
pub(crate) mod route {
pub const TYPED_HEADER: &str = "/typed_header";
}
use super::{validate_again, ParametersGarde};
use crate::Garde;
use axum::headers::{Error, Header, HeaderName, HeaderValue};
use axum::http::StatusCode;
use axum::TypedHeader;
pub static AXUM_VALID_PARAMETERS: HeaderName = HeaderName::from_static("axum-valid-parameters");
pub(super) async fn extract_typed_header(
Garde(TypedHeader(parameters)): Garde<TypedHeader<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
impl Header for ParametersGarde {
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(ParametersGarde {
v0: v0.parse().map_err(|_| Error::invalid())?,
v1: 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 = ParametersGarde {
v0: 123456,
v1: "111111".to_string(),
};
let mut vec = Vec::new();
parameter.encode(&mut vec);
let mut iter = vec.iter();
assert_eq!(parameter, ParametersGarde::decode(&mut iter)?);
Ok(())
}
}
#[cfg(feature = "typed_multipart")]
mod typed_multipart {
use super::{validate_again, ParametersGarde};
use crate::Garde;
use axum::http::StatusCode;
use axum_typed_multipart::{BaseMultipart, TypedMultipart, TypedMultipartError};
pub mod route {
pub const TYPED_MULTIPART: &str = "/typed_multipart";
pub const BASE_MULTIPART: &str = "/base_multipart";
}
impl From<&ParametersGarde> for reqwest::multipart::Form {
fn from(value: &ParametersGarde) -> Self {
reqwest::multipart::Form::new()
.text("v0", value.v0.to_string())
.text("v1", value.v1.clone())
}
}
pub(super) async fn extract_typed_multipart(
Garde(TypedMultipart(parameters)): Garde<TypedMultipart<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
pub(super) async fn extract_base_multipart(
Garde(BaseMultipart { data, .. }): Garde<
BaseMultipart<ParametersGarde, TypedMultipartError>,
>,
) -> StatusCode {
validate_again(data, ())
}
}
#[cfg(feature = "extra")]
mod extra {
use super::{validate_again, ParametersGarde};
use crate::tests::{Rejection, ValidTest, ValidTestParameter};
use crate::{Garde, GardeRejection};
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 mod route {
pub const CACHED: &str = "/cached";
pub const WITH_REJECTION: &str = "/with_rejection";
pub const WITH_REJECTION_GARDE: &str = "/with_rejection_garde";
}
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 ParametersGarde
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 ParametersGarde {
const ERROR_STATUS_CODE: StatusCode = CACHED_REJECTION_STATUS;
fn set_valid_request(builder: RequestBuilder) -> RequestBuilder {
builder.header(
PARAMETERS_HEADER,
serde_json::to_string(ParametersGarde::valid())
.expect("Failed to serialize parameters"),
)
}
fn set_error_request(builder: RequestBuilder) -> RequestBuilder {
builder.header(
PARAMETERS_HEADER,
serde_json::to_string(ParametersGarde::error())
.expect("Failed to serialize parameters"),
)
}
fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder {
builder.header(
PARAMETERS_HEADER,
serde_json::to_string(ParametersGarde::invalid())
.expect("Failed to serialize parameters"),
)
}
}
pub struct GardeWithRejectionRejection {
inner: ParametersRejection,
}
impl Rejection for GardeWithRejectionRejection {
const STATUS_CODE: StatusCode = StatusCode::CONFLICT;
}
impl IntoResponse for GardeWithRejectionRejection {
fn into_response(self) -> Response {
let mut response = self.inner.into_response();
*response.status_mut() = Self::STATUS_CODE;
response
}
}
// satisfy the `WithRejection`'s extractor trait bound
// R: From<E::Rejection> + IntoResponse
impl From<ParametersRejection> for GardeWithRejectionRejection {
fn from(inner: ParametersRejection) -> Self {
Self { inner }
}
}
pub async fn extract_cached(
Garde(Cached(parameters)): Garde<Cached<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
pub async fn extract_with_rejection(
Garde(WithRejection(parameters, _)): Garde<
WithRejection<ParametersGarde, GardeWithRejectionRejection>,
>,
) -> StatusCode {
validate_again(parameters, ())
}
pub struct WithRejectionGardeRejection<E> {
inner: GardeRejection<E>,
}
impl<E> From<GardeRejection<E>> for WithRejectionGardeRejection<E> {
fn from(inner: GardeRejection<E>) -> Self {
Self { inner }
}
}
impl<E: IntoResponse> IntoResponse for WithRejectionGardeRejection<E> {
fn into_response(self) -> Response {
let mut res = self.inner.into_response();
*res.status_mut() = StatusCode::IM_A_TEAPOT;
res
}
}
pub async fn extract_with_rejection_valid(
WithRejection(Garde(parameters), _): WithRejection<
Garde<ParametersGarde>,
WithRejectionGardeRejection<ParametersRejection>,
>,
) -> StatusCode {
validate_again(parameters, ())
}
}
#[cfg(feature = "extra_typed_path")]
mod extra_typed_path {
use super::validate_again;
use crate::{Garde, HasValidate};
use axum::http::StatusCode;
use axum_extra::routing::TypedPath;
use garde::Validate;
use serde::Deserialize;
pub mod route {
pub const EXTRA_TYPED_PATH: &str = "/extra_typed_path/:v0/:v1";
}
#[derive(Validate, TypedPath, Deserialize)]
#[typed_path("/extra_typed_path/:v0/:v1")]
pub struct TypedPathParam {
#[garde(range(min = 5, max = 10))]
v0: i32,
#[garde(length(min = 1, max = 10))]
v1: String,
}
impl HasValidate for TypedPathParam {
type Validate = Self;
fn get_validate(&self) -> &Self::Validate {
self
}
}
pub async fn extract_extra_typed_path(Garde(param): Garde<TypedPathParam>) -> StatusCode {
validate_again(param, ())
}
}
#[cfg(feature = "extra_query")]
mod extra_query {
use super::{validate_again, ParametersGarde};
use crate::Garde;
use axum::http::StatusCode;
use axum_extra::extract::Query;
pub mod route {
pub const EXTRA_QUERY: &str = "/extra_query";
}
pub async fn extract_extra_query(
Garde(Query(parameters)): Garde<Query<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
}
#[cfg(feature = "extra_form")]
mod extra_form {
use super::{validate_again, ParametersGarde};
use crate::Garde;
use axum::http::StatusCode;
use axum_extra::extract::Form;
pub mod route {
pub const EXTRA_FORM: &str = "/extra_form";
}
pub async fn extract_extra_form(
Garde(Form(parameters)): Garde<Form<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
}
#[cfg(feature = "extra_protobuf")]
mod extra_protobuf {
use super::{validate_again, ParametersGarde};
use crate::Garde;
use axum::http::StatusCode;
use axum_extra::protobuf::Protobuf;
pub mod route {
pub const EXTRA_PROTOBUF: &str = "/extra_protobuf";
}
pub async fn extract_extra_protobuf(
Garde(Protobuf(parameters)): Garde<Protobuf<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
}
#[cfg(feature = "yaml")]
mod yaml {
use super::{validate_again, ParametersGarde};
use crate::Garde;
use axum::http::StatusCode;
use axum_yaml::Yaml;
pub mod route {
pub const YAML: &str = "/yaml";
}
pub async fn extract_yaml(Garde(Yaml(parameters)): Garde<Yaml<ParametersGarde>>) -> StatusCode {
validate_again(parameters, ())
}
}
#[cfg(feature = "msgpack")]
mod msgpack {
use super::{validate_again, ParametersGarde};
use crate::Garde;
use axum::http::StatusCode;
use axum_msgpack::{MsgPack, MsgPackRaw};
pub mod route {
pub const MSGPACK: &str = "/msgpack";
pub const MSGPACK_RAW: &str = "/msgpack_raw";
}
pub async fn extract_msgpack(
Garde(MsgPack(parameters)): Garde<MsgPack<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
pub async fn extract_msgpack_raw(
Garde(MsgPackRaw(parameters)): Garde<MsgPackRaw<ParametersGarde>>,
) -> StatusCode {
validate_again(parameters, ())
}
}

View File

@@ -12,8 +12,8 @@
//! ## Example //! ## Example
//! //!
//! ```no_run //! ```no_run
//! #![cfg(feature = "validator")] //! #[cfg(feature = "validator")]
//! //! mod validator_example {
//! use axum::routing::post; //! use axum::routing::post;
//! use axum::Json; //! use axum::Json;
//! use axum::Router; //! use axum::Router;
@@ -21,7 +21,7 @@
//! use serde::Deserialize; //! use serde::Deserialize;
//! use validator::Validate; //! use validator::Validate;
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> anyhow::Result<()> { //! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/json", post(handler)); //! let router = Router::new().route("/json", post(handler));
//! 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())
@@ -30,6 +30,8 @@
//! } //! }
//! async fn handler(Valid(Json(parameter)): Valid<Json<Parameter>>) { //! async fn handler(Valid(Json(parameter)): Valid<Json<Parameter>>) {
//! assert!(parameter.validate().is_ok()); //! assert!(parameter.validate().is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! } //! }
//! #[derive(Validate, Deserialize)] //! #[derive(Validate, Deserialize)]
//! pub struct Parameter { //! pub struct Parameter {
@@ -38,6 +40,45 @@
//! #[validate(length(min = 1, max = 10))] //! #[validate(length(min = 1, max = 10))]
//! pub v1: String, //! pub v1: String,
//! } //! }
//! }
//!
//! #[cfg(feature = "garde")]
//! mod garde_example {
//! use axum::routing::post;
//! use axum::Json;
//! use axum::Router;
//! use axum_valid::Garde;
//! use serde::Deserialize;
//! use garde::Validate;
//! #[tokio::main]
//! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/json", post(handler));
//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
//! .serve(router.into_make_service())
//! .await?;
//! Ok(())
//! }
//! async fn handler(Garde(Json(parameter)): Garde<Json<Parameter>>) {
//! assert!(parameter.validate(&()).is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! }
//! #[derive(Validate, Deserialize)]
//! pub struct Parameter {
//! #[garde(range(min = 5, max = 10))]
//! pub v0: i32,
//! #[garde(length(min = 1, max = 10))]
//! pub v1: String,
//! }
//! }
//!
//! # fn main() -> anyhow::Result<()> {
//! # #[cfg(feature = "validator")]
//! # validator_example::launch()?;
//! # #[cfg(feature = "garde")]
//! # garde_example::launch()?;
//! # Ok(())
//! # }
//! ``` //! ```
use crate::HasValidate; use crate::HasValidate;

View File

@@ -14,9 +14,6 @@ pub mod msgpack;
pub mod path; pub mod path;
#[cfg(feature = "query")] #[cfg(feature = "query")]
pub mod query; pub mod query;
#[cfg(test)]
#[cfg(all(feature = "garde", feature = "validator"))]
pub mod test;
#[cfg(feature = "typed_header")] #[cfg(feature = "typed_header")]
pub mod typed_header; pub mod typed_header;
#[cfg(feature = "typed_multipart")] #[cfg(feature = "typed_multipart")]

View File

@@ -12,17 +12,51 @@
//! ## Example //! ## Example
//! //!
//! ```no_run //! ```no_run
//!#![cfg(feature = "validator")] //! #[cfg(feature = "validator")]
//! //! mod validator_example {
//! use axum::routing::post; //! use axum::routing::post;
//! use axum::Json;
//! use axum::Router; //! use axum::Router;
//! use axum_msgpack::{MsgPack, MsgPackRaw}; //! use axum_msgpack::{MsgPack, MsgPackRaw};
//! use axum_valid::Valid; //! use axum_valid::Valid;
//! use serde::Deserialize; //! use serde::Deserialize;
//! use validator::Validate; //! use validator::Validate;
//!
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> anyhow::Result<()> { //! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new()
//! .route("/msgpack", post(handler))
//! .route("/msgpackraw", post(raw_handler));
//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
//! .serve(router.into_make_service())
//! .await?;
//! Ok(())
//! }
//! async fn handler(Valid(MsgPack(parameter)): Valid<MsgPack<Parameter>>) {
//! assert!(parameter.validate().is_ok());
//! }
//!
//! async fn raw_handler(Valid(MsgPackRaw(parameter)): Valid<MsgPackRaw<Parameter>>) {
//! assert!(parameter.validate().is_ok());
//! }
//! #[derive(Validate, Deserialize)]
//! pub struct Parameter {
//! #[validate(range(min = 5, max = 10))]
//! pub v0: i32,
//! #[validate(length(min = 1, max = 10))]
//! pub v1: String,
//! }
//! }
//!
//! #[cfg(feature = "garde")]
//! mod garde_example {
//! use axum::routing::post;
//! use axum::Router;
//! use axum_msgpack::{MsgPack, MsgPackRaw};
//! use axum_valid::Garde;
//! use serde::Deserialize;
//! use garde::Validate;
//! #[tokio::main]
//! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new() //! let router = Router::new()
//! .route("/msgpack", post(handler)) //! .route("/msgpack", post(handler))
//! .route("/msgpackraw", post(raw_handler)); //! .route("/msgpackraw", post(raw_handler));
@@ -32,22 +66,31 @@
//! Ok(()) //! Ok(())
//! } //! }
//! //!
//! async fn handler(Valid(MsgPack(parameter)): Valid<MsgPack<Parameter>>) { //! async fn handler(Garde(MsgPack(parameter)): Garde<MsgPack<Parameter>>) {
//! assert!(parameter.validate().is_ok()); //! assert!(parameter.validate(&()).is_ok());
//! } //! }
//! //!
//! async fn raw_handler(Valid(MsgPackRaw(parameter)): Valid<MsgPackRaw<Parameter>>) { //! async fn raw_handler(Garde(MsgPackRaw(parameter)): Garde<MsgPackRaw<Parameter>>) {
//! assert!(parameter.validate().is_ok()); //! assert!(parameter.validate(&()).is_ok());
//! } //! }
//!
//! #[derive(Validate, Deserialize)] //! #[derive(Validate, Deserialize)]
//! pub struct Parameter { //! pub struct Parameter {
//! #[validate(range(min = 5, max = 10))] //! #[garde(range(min = 5, max = 10))]
//! pub v0: i32, //! pub v0: i32,
//! #[validate(length(min = 1, max = 10))] //! #[garde(length(min = 1, max = 10))]
//! pub v1: String, //! pub v1: String,
//! } //! }
//! }
//!
//! # fn main() -> anyhow::Result<()> {
//! # #[cfg(feature = "validator")]
//! # validator_example::launch()?;
//! # #[cfg(feature = "garde")]
//! # garde_example::launch()?;
//! # Ok(())
//! # }
//! ``` //! ```
//!
use crate::HasValidate; use crate::HasValidate;
#[cfg(feature = "validator")] #[cfg(feature = "validator")]

View File

@@ -8,28 +8,27 @@
//! ## Example //! ## Example
//! //!
//! ```no_run //! ```no_run
//! #![cfg(feature = "validator")] //! #[cfg(feature = "validator")]
//! //! mod validator_example {
//! use axum::extract::Path; //! use axum::extract::Path;
//! use axum::routing::post; //! use axum::routing::post;
//! use axum::Router; //! use axum::Router;
//! use axum_valid::Valid; //! use axum_valid::Valid;
//! use serde::Deserialize; //! use serde::Deserialize;
//! use validator::Validate; //! use validator::Validate;
//!
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> anyhow::Result<()> { //! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/path/:v0/:v1", post(handler)); //! let router = Router::new().route("/json", post(handler));
//! 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?;
//! Ok(()) //! Ok(())
//! } //! }
//!
//! async fn handler(Valid(Path(parameter)): Valid<Path<Parameter>>) { //! async fn handler(Valid(Path(parameter)): Valid<Path<Parameter>>) {
//! assert!(parameter.validate().is_ok()); //! assert!(parameter.validate().is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! } //! }
//!
//! #[derive(Validate, Deserialize)] //! #[derive(Validate, Deserialize)]
//! pub struct Parameter { //! pub struct Parameter {
//! #[validate(range(min = 5, max = 10))] //! #[validate(range(min = 5, max = 10))]
@@ -37,6 +36,45 @@
//! #[validate(length(min = 1, max = 10))] //! #[validate(length(min = 1, max = 10))]
//! pub v1: String, //! pub v1: String,
//! } //! }
//! }
//!
//! #[cfg(feature = "garde")]
//! mod garde_example {
//! use axum::routing::post;
//! use axum::extract::Path;
//! use axum::Router;
//! use axum_valid::Garde;
//! use serde::Deserialize;
//! use garde::Validate;
//! #[tokio::main]
//! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/json", post(handler));
//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
//! .serve(router.into_make_service())
//! .await?;
//! Ok(())
//! }
//! async fn handler(Garde(Path(parameter)): Garde<Path<Parameter>>) {
//! assert!(parameter.validate(&()).is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! }
//! #[derive(Validate, Deserialize)]
//! pub struct Parameter {
//! #[garde(range(min = 5, max = 10))]
//! pub v0: i32,
//! #[garde(length(min = 1, max = 10))]
//! pub v1: String,
//! }
//! }
//!
//! # fn main() -> anyhow::Result<()> {
//! # #[cfg(feature = "validator")]
//! # validator_example::launch()?;
//! # #[cfg(feature = "garde")]
//! # garde_example::launch()?;
//! # Ok(())
//! # }
//! ``` //! ```
use crate::HasValidate; use crate::HasValidate;

View File

@@ -12,28 +12,27 @@
//! ## Example //! ## Example
//! //!
//! ```no_run //! ```no_run
//! #![cfg(feature = "validator")] //! #[cfg(feature = "validator")]
//! //! mod validator_example {
//! use axum::extract::Query; //! use axum::extract::Query;
//! use axum::routing::post; //! use axum::routing::post;
//! use axum::Router; //! use axum::Router;
//! use axum_valid::Valid; //! use axum_valid::Valid;
//! use serde::Deserialize; //! use serde::Deserialize;
//! use validator::Validate; //! use validator::Validate;
//!
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> anyhow::Result<()> { //! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/query", post(handler)); //! let router = Router::new().route("/json", post(handler));
//! 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?;
//! Ok(()) //! Ok(())
//! } //! }
//!
//! async fn handler(Valid(Query(parameter)): Valid<Query<Parameter>>) { //! async fn handler(Valid(Query(parameter)): Valid<Query<Parameter>>) {
//! assert!(parameter.validate().is_ok()); //! assert!(parameter.validate().is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! } //! }
//!
//! #[derive(Validate, Deserialize)] //! #[derive(Validate, Deserialize)]
//! pub struct Parameter { //! pub struct Parameter {
//! #[validate(range(min = 5, max = 10))] //! #[validate(range(min = 5, max = 10))]
@@ -41,6 +40,45 @@
//! #[validate(length(min = 1, max = 10))] //! #[validate(length(min = 1, max = 10))]
//! pub v1: String, //! pub v1: String,
//! } //! }
//! }
//!
//! #[cfg(feature = "garde")]
//! mod garde_example {
//! use axum::routing::post;
//! use axum::extract::Query;
//! use axum::Router;
//! use axum_valid::Garde;
//! use serde::Deserialize;
//! use garde::Validate;
//! #[tokio::main]
//! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/json", post(handler));
//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
//! .serve(router.into_make_service())
//! .await?;
//! Ok(())
//! }
//! async fn handler(Garde(Query(parameter)): Garde<Query<Parameter>>) {
//! assert!(parameter.validate(&()).is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! }
//! #[derive(Validate, Deserialize)]
//! pub struct Parameter {
//! #[garde(range(min = 5, max = 10))]
//! pub v0: i32,
//! #[garde(length(min = 1, max = 10))]
//! pub v1: String,
//! }
//! }
//!
//! # fn main() -> anyhow::Result<()> {
//! # #[cfg(feature = "validator")]
//! # validator_example::launch()?;
//! # #[cfg(feature = "garde")]
//! # garde_example::launch()?;
//! # Ok(())
//! # }
//! ``` //! ```
use crate::HasValidate; use crate::HasValidate;

View File

@@ -1,5 +1,8 @@
//! # Validator support //! # Validator support
#[cfg(test)]
pub mod test;
use crate::{HasValidate, VALIDATION_ERROR_STATUS}; use crate::{HasValidate, VALIDATION_ERROR_STATUS};
use axum::async_trait; use axum::async_trait;
use axum::extract::{FromRef, FromRequest, FromRequestParts}; use axum::extract::{FromRef, FromRequest, FromRequestParts};

View File

@@ -1,5 +1,6 @@
#![cfg(feature = "validator")]
use crate::tests::{ValidTest, ValidTestParameter}; use crate::tests::{ValidTest, ValidTestParameter};
use crate::Garde;
use crate::{Arguments, HasValidate, HasValidateArgs, Valid, ValidEx, VALIDATION_ERROR_STATUS}; use crate::{Arguments, HasValidate, HasValidateArgs, Valid, ValidEx, VALIDATION_ERROR_STATUS};
use axum::extract::{FromRef, Path, Query}; use axum::extract::{FromRef, Path, Query};
use axum::routing::{get, post}; use axum::routing::{get, post};
@@ -83,21 +84,6 @@ impl Default for ParametersExValidationArgumentsInner {
} }
} }
#[derive(Clone, Deserialize, Serialize, garde::Validate, Eq, PartialEq)]
#[cfg_attr(feature = "extra_protobuf", derive(prost::Message))]
#[cfg_attr(
feature = "typed_multipart",
derive(axum_typed_multipart::TryFromMultipart)
)]
pub struct ParametersGarde {
#[garde(range(min = 5, max = 10))]
#[cfg_attr(feature = "extra_protobuf", prost(int32, tag = "1"))]
v0: i32,
#[garde(length(min = 1, max = 10))]
#[cfg_attr(feature = "extra_protobuf", prost(string, tag = "2"))]
v1: String,
}
static VALID_PARAMETERS: Lazy<Parameters> = Lazy::new(|| Parameters { static VALID_PARAMETERS: Lazy<Parameters> = Lazy::new(|| Parameters {
v0: 5, v0: 5,
v1: String::from("0123456789"), v1: String::from("0123456789"),
@@ -167,11 +153,6 @@ async fn test_main() -> anyhow::Result<()> {
.route(route::FORM_EX, post(extract_form_ex)) .route(route::FORM_EX, post(extract_form_ex))
.route(route::JSON_EX, post(extract_json_ex)); .route(route::JSON_EX, post(extract_json_ex));
#[cfg(feature = "garde")]
let router = router
.route(route::QUERY_GARDE, get(extract_query_garde))
.route(route::JSON_GARDE, post(extract_json_garde));
#[cfg(feature = "typed_header")] #[cfg(feature = "typed_header")]
let router = router let router = router
.route( .route(
@@ -369,12 +350,6 @@ async fn test_main() -> anyhow::Result<()> {
.execute::<Query<Parameters>>(Method::GET, route::QUERY_EX) .execute::<Query<Parameters>>(Method::GET, route::QUERY_EX)
.await?; .await?;
// Garde
#[cfg(feature = "garde")]
test_executor
.execute::<Query<Parameters>>(Method::GET, route::QUERY_GARDE)
.await?;
// Valid // Valid
test_executor test_executor
.execute::<Form<Parameters>>(Method::POST, route::FORM) .execute::<Form<Parameters>>(Method::POST, route::FORM)
@@ -395,12 +370,6 @@ async fn test_main() -> anyhow::Result<()> {
.execute::<Json<Parameters>>(Method::POST, route::JSON_EX) .execute::<Json<Parameters>>(Method::POST, route::JSON_EX)
.await?; .await?;
// Garde
#[cfg(feature = "garde")]
test_executor
.execute::<Json<Parameters>>(Method::POST, route::JSON_GARDE)
.await?;
#[cfg(feature = "typed_header")] #[cfg(feature = "typed_header")]
{ {
use axum::TypedHeader; use axum::TypedHeader;
@@ -703,14 +672,10 @@ mod route {
pub const PATH_EX: &str = "/path_ex/:v0/:v1"; pub const PATH_EX: &str = "/path_ex/:v0/:v1";
pub const QUERY: &str = "/query"; pub const QUERY: &str = "/query";
pub const QUERY_EX: &str = "/query_ex"; pub const QUERY_EX: &str = "/query_ex";
#[cfg(feature = "garde")]
pub const QUERY_GARDE: &str = "/query_garde";
pub const FORM: &str = "/form"; pub const FORM: &str = "/form";
pub const FORM_EX: &str = "/form_ex"; pub const FORM_EX: &str = "/form_ex";
pub const JSON: &str = "/json"; pub const JSON: &str = "/json";
pub const JSON_EX: &str = "/json_ex"; pub const JSON_EX: &str = "/json_ex";
#[cfg(feature = "garde")]
pub const JSON_GARDE: &str = "/json_garde";
} }
async fn extract_path(Valid(Path(parameters)): Valid<Path<Parameters>>) -> StatusCode { async fn extract_path(Valid(Path(parameters)): Valid<Path<Parameters>>) -> StatusCode {
@@ -733,13 +698,6 @@ async fn extract_query_ex(
validate_again_ex(parameters, args.get()) validate_again_ex(parameters, args.get())
} }
#[cfg(feature = "garde")]
async fn extract_query_garde(
Garde(Query(parameters)): Garde<Query<ParametersGarde>>,
) -> StatusCode {
validate_again_garde(parameters, ())
}
async fn extract_form(Valid(Form(parameters)): Valid<Form<Parameters>>) -> StatusCode { async fn extract_form(Valid(Form(parameters)): Valid<Form<Parameters>>) -> StatusCode {
validate_again(parameters) validate_again(parameters)
} }
@@ -760,11 +718,6 @@ async fn extract_json_ex(
validate_again_ex(parameters, args.get()) validate_again_ex(parameters, args.get())
} }
#[cfg(feature = "garde")]
async fn extract_json_garde(Garde(Json(parameters)): Garde<Json<ParametersGarde>>) -> StatusCode {
validate_again_garde(parameters, ())
}
fn validate_again<V: Validate>(validate: V) -> StatusCode { fn validate_again<V: Validate>(validate: V) -> StatusCode {
// The `Valid` extractor has validated the `parameters` once, // The `Valid` 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,
@@ -790,21 +743,6 @@ fn validate_again_ex<'v, V: ValidateArgs<'v>>(
} }
} }
#[cfg(feature = "garde")]
fn validate_again_garde<V>(validate: V, context: V::Context) -> StatusCode
where
V: garde::Validate,
{
// The `Garde` 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 `Garde` extractor works well.
// If it works properly, this function will never return `500 INTERNAL SERVER ERROR`
match validate.validate(&context) {
Ok(_) => StatusCode::OK,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
#[cfg(feature = "typed_header")] #[cfg(feature = "typed_header")]
mod typed_header { mod typed_header {
@@ -814,7 +752,7 @@ mod typed_header {
} }
use super::{validate_again, Parameters}; use super::{validate_again, Parameters};
use crate::test::{validate_again_ex, ParametersEx, ParametersExValidationArguments}; use super::{validate_again_ex, ParametersEx, ParametersExValidationArguments};
use crate::{Arguments, Valid, ValidEx}; use crate::{Arguments, Valid, ValidEx};
use axum::headers::{Error, Header, HeaderName, HeaderValue}; use axum::headers::{Error, Header, HeaderName, HeaderValue};
use axum::http::StatusCode; use axum::http::StatusCode;
@@ -919,7 +857,7 @@ mod typed_header {
#[cfg(feature = "typed_multipart")] #[cfg(feature = "typed_multipart")]
mod typed_multipart { mod typed_multipart {
use crate::test::{ use super::{
validate_again, validate_again_ex, Parameters, ParametersEx, validate_again, validate_again_ex, Parameters, ParametersEx,
ParametersExValidationArguments, ParametersExValidationArguments,
}; };
@@ -975,7 +913,7 @@ mod typed_multipart {
#[cfg(feature = "extra")] #[cfg(feature = "extra")]
mod extra { mod extra {
use crate::test::{ use super::{
validate_again, validate_again_ex, Parameters, ParametersEx, validate_again, validate_again_ex, Parameters, ParametersEx,
ParametersExValidationArguments, ParametersExValidationArguments,
}; };
@@ -1174,7 +1112,7 @@ mod extra {
#[cfg(feature = "extra_typed_path")] #[cfg(feature = "extra_typed_path")]
mod extra_typed_path { mod extra_typed_path {
use crate::test::{validate_again, validate_again_ex}; use super::{validate_again, validate_again_ex};
use crate::{Arguments, HasValidate, HasValidateArgs, Valid, ValidEx}; use crate::{Arguments, HasValidate, HasValidateArgs, Valid, ValidEx};
use axum::http::StatusCode; use axum::http::StatusCode;
use axum_extra::routing::TypedPath; use axum_extra::routing::TypedPath;
@@ -1256,7 +1194,7 @@ mod extra_typed_path {
#[cfg(feature = "extra_query")] #[cfg(feature = "extra_query")]
mod extra_query { mod extra_query {
use crate::test::{ use super::{
validate_again, validate_again_ex, Parameters, ParametersEx, validate_again, validate_again_ex, Parameters, ParametersEx,
ParametersExValidationArguments, ParametersExValidationArguments,
}; };
@@ -1287,7 +1225,7 @@ mod extra_query {
#[cfg(feature = "extra_form")] #[cfg(feature = "extra_form")]
mod extra_form { mod extra_form {
use crate::test::{ use super::{
validate_again, validate_again_ex, Parameters, ParametersEx, validate_again, validate_again_ex, Parameters, ParametersEx,
ParametersExValidationArguments, ParametersExValidationArguments,
}; };
@@ -1318,7 +1256,7 @@ mod extra_form {
#[cfg(feature = "extra_protobuf")] #[cfg(feature = "extra_protobuf")]
mod extra_protobuf { mod extra_protobuf {
use crate::test::{ use super::{
validate_again, validate_again_ex, Parameters, ParametersEx, validate_again, validate_again_ex, Parameters, ParametersEx,
ParametersExValidationArguments, ParametersExValidationArguments,
}; };
@@ -1349,7 +1287,7 @@ mod extra_protobuf {
#[cfg(feature = "yaml")] #[cfg(feature = "yaml")]
mod yaml { mod yaml {
use crate::test::{ use super::{
validate_again, validate_again_ex, Parameters, ParametersEx, validate_again, validate_again_ex, Parameters, ParametersEx,
ParametersExValidationArguments, ParametersExValidationArguments,
}; };
@@ -1378,7 +1316,7 @@ mod yaml {
#[cfg(feature = "msgpack")] #[cfg(feature = "msgpack")]
mod msgpack { mod msgpack {
use crate::test::{ use super::{
validate_again, validate_again_ex, Parameters, ParametersEx, validate_again, validate_again_ex, Parameters, ParametersEx,
ParametersExValidationArguments, ParametersExValidationArguments,
}; };

View File

@@ -12,35 +12,73 @@
//! ## Example //! ## Example
//! //!
//! ```no_run //! ```no_run
//! #![cfg(feature = "validator")] //! #[cfg(feature = "validator")]
//! //! mod validator_example {
//! use axum::routing::post; //! use axum::routing::post;
//! use axum_yaml::Yaml;
//! use axum::Router; //! use axum::Router;
//! use axum_valid::Valid; //! use axum_valid::Valid;
//! use axum_yaml::Yaml;
//! use serde::Deserialize; //! use serde::Deserialize;
//! use validator::Validate; //! use validator::Validate;
//!
//! #[tokio::main] //! #[tokio::main]
//! async fn main() -> anyhow::Result<()> { //! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/yaml", post(handler)); //! let router = Router::new().route("/json", post(handler));
//! 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?;
//! Ok(()) //! Ok(())
//! } //! }
//! //! async fn handler(Valid(Yaml(parameter)): Valid<Yaml<Parameter>>) {
//! async fn handler(parameter: Valid<Yaml<Parameter>>) {
//! assert!(parameter.validate().is_ok()); //! assert!(parameter.validate().is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! }
//! #[derive(Validate, Deserialize)]
//! pub struct Parameter {
//! #[validate(range(min = 5, max = 10))]
//! pub v0: i32,
//! #[validate(length(min = 1, max = 10))]
//! pub v1: String,
//! }
//! } //! }
//! //!
//! #[derive(Deserialize, Validate)] //! #[cfg(feature = "garde")]
//! struct Parameter { //! mod garde_example {
//! #[validate(range(min = 5, max = 10))] //! use axum::routing::post;
//! v0: i32, //! use axum_yaml::Yaml;
//! #[validate(length(min = 1, max = 10))] //! use axum::Router;
//! v1: String, //! use axum_valid::Garde;
//! use serde::Deserialize;
//! use garde::Validate;
//! #[tokio::main]
//! pub async fn launch() -> anyhow::Result<()> {
//! let router = Router::new().route("/json", post(handler));
//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into())
//! .serve(router.into_make_service())
//! .await?;
//! Ok(())
//! } //! }
//! async fn handler(Garde(Yaml(parameter)): Garde<Yaml<Parameter>>) {
//! assert!(parameter.validate(&()).is_ok());
//! // Support automatic dereferencing
//! println!("v0 = {}, v1 = {}", parameter.v0, parameter.v1);
//! }
//! #[derive(Validate, Deserialize)]
//! pub struct Parameter {
//! #[garde(range(min = 5, max = 10))]
//! pub v0: i32,
//! #[garde(length(min = 1, max = 10))]
//! pub v1: String,
//! }
//! }
//!
//! # fn main() -> anyhow::Result<()> {
//! # #[cfg(feature = "validator")]
//! # validator_example::launch()?;
//! # #[cfg(feature = "garde")]
//! # garde_example::launch()?;
//! # Ok(())
//! # }
//! ``` //! ```
use crate::HasValidate; use crate::HasValidate;