From 6cf325b19dc615c8ed4ab589b018b46defff8c68 Mon Sep 17 00:00:00 2001 From: gengteng Date: Mon, 9 Oct 2023 11:37:27 +0800 Subject: [PATCH] refactor doc tests --- Cargo.toml | 2 +- README.md | 285 +++++++----- src/extra.rs | 27 ++ src/form.rs | 89 +++- src/garde.rs | 3 + src/garde/test.rs | 890 ++++++++++++++++++++++++++++++++++++ src/json.rs | 89 +++- src/lib.rs | 3 - src/msgpack.rs | 103 +++-- src/path.rs | 88 +++- src/query.rs | 88 +++- src/validator.rs | 3 + src/{ => validator}/test.rs | 84 +--- src/yaml.rs | 88 +++- 14 files changed, 1510 insertions(+), 332 deletions(-) create mode 100644 src/garde/test.rs rename src/{ => validator}/test.rs (94%) diff --git a/Cargo.toml b/Cargo.toml index adff9c8..ef16b2b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "axum-valid" 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 "] license = "MIT" homepage = "https://github.com/gengteng/axum-valid" diff --git a/README.md b/README.md index 6eb9592..f1eec44 100644 --- a/README.md +++ b/README.md @@ -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) [![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 @@ -20,43 +24,101 @@ cargo add axum-valid ``` ```rust,no_run -use validator::Validate; -use serde::Deserialize; -use axum_valid::Valid; -use axum::extract::Query; -use axum::{Json, Router}; -use axum::routing::{get, post}; - -#[derive(Debug, Validate, Deserialize)] -pub struct Pager { - #[validate(range(min = 1, max = 50))] - pub page_size: usize, - #[validate(range(min = 1))] - pub page_no: usize, +#[cfg(feature = "validator")] +mod validator_example { + use validator::Validate; + use serde::Deserialize; + use axum_valid::Valid; + use axum::extract::Query; + use axum::{Json, Router}; + use axum::routing::{get, post}; + + #[derive(Debug, Validate, Deserialize)] + pub struct Pager { + #[validate(range(min = 1, max = 50))] + pub page_size: usize, + #[validate(range(min = 1))] + pub page_no: usize, + } + + pub async fn pager_from_query( + Valid(Query(pager)): Valid>, + ) { + assert!((1..=50).contains(&pager.page_size)); + assert!((1..).contains(&pager.page_no)); + } + + pub async fn pager_from_json( + pager: Valid>, + ) { + 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(()) + } } -pub async fn pager_from_query( - Valid(Query(pager)): Valid>, -) { - assert!((1..=50).contains(&pager.page_size)); - assert!((1..).contains(&pager.page_no)); +#[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>, + ) { + assert!((1..=50).contains(&pager.page_size)); + assert!((1..).contains(&pager.page_no)); + } + + pub async fn pager_from_json( + pager: Garde>, + ) { + 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(()) + } } -pub async fn pager_from_json( - Valid(Json(pager)): Valid>, -) { - 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("/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?; +fn main() -> anyhow::Result<()> { + #[cfg(feature = "validator")] + validator_example::launch()?; + #[cfg(feature = "garde")] + garde_example::launch()?; Ok(()) } ``` @@ -70,77 +132,96 @@ 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: ```rust,no_run -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, &RangeFrom). -// 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"))] - pub page_size: usize, - #[validate(custom(function = "validate_page_no", arg = "&'v_a RangeFrom"))] - pub page_no: usize, -} - -fn validate_page_size(v: usize, args: &RangeInclusive) -> 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) -> 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, - page_no_range: RangeFrom, -} - -// NOTE: This implementation allows PagerValidArgs to be the second member of ValidEx, and provides arguments for actual validation. -// The type mapping >::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: >::Args == (&RangeInclusive, &RangeFrom) - fn get(&'a self) -> >::Args { - (&self.page_size_range, &self.page_no_range) +#[cfg(feature = "validator")] +mod validator_example { + 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, &RangeFrom). + // 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"))] + pub page_size: usize, + #[validate(custom(function = "validate_page_no", arg = "&'v_a RangeFrom"))] + pub page_no: usize, + } + + fn validate_page_size(v: usize, args: &RangeInclusive) -> 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) -> 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, + page_no_range: RangeFrom, + } + + // NOTE: This implementation allows PagerValidArgs to be the second member of ValidEx, and provides arguments for actual validation. + // The type mapping >::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: >::Args == (&RangeInclusive, &RangeFrom) + fn get(&'a self) -> >::Args { + (&self.page_size_range, &self.page_no_range) + } + } + + pub async fn pager_from_form_ex(ValidEx(Form(pager), _): ValidEx, PagerValidArgs>) { + assert!((1..=50).contains(&pager.page_size)); + assert!((1..).contains(&pager.page_no)); + } + + #[tokio::main] + pub async fn launch() -> 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. + + axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) + .serve(router.into_make_service()) + .await?; + Ok(()) } } -pub async fn pager_from_form_ex(ValidEx(Form(pager), _): ValidEx, PagerValidArgs>) { - assert!((1..=50).contains(&pager.page_size)); - assert!((1..).contains(&pager.page_no)); +#[cfg(feature = "garde")] +mod garde_example { + #[tokio::main] + pub async fn launch() -> anyhow::Result<()> { + Ok(()) + } } -#[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. - - axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) - .serve(router.into_make_service()) - .await?; +fn main() -> anyhow::Result<()> { + #[cfg(feature = "validator")] + validator_example::launch()?; + #[cfg(feature = "garde")] + garde_example::launch()?; Ok(()) } ``` diff --git a/src/extra.rs b/src/extra.rs index b61e80a..ce83172 100644 --- a/src/extra.rs +++ b/src/extra.rs @@ -283,6 +283,8 @@ impl<'v, T: ValidateArgs<'v>, R> HasValidateArgs<'v> for WithRejection { #[cfg(test)] mod tests { use crate::tests::{Rejection, ValidTest}; + #[cfg(feature = "garde")] + use crate::Garde; #[cfg(feature = "validator")] use crate::Valid; use axum::http::StatusCode; @@ -347,4 +349,29 @@ mod tests { T::set_invalid_request(builder) } } + + #[cfg(feature = "garde")] + impl ValidTest for WithRejection, 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) + } + } } diff --git a/src/form.rs b/src/form.rs index 8cc3711..ab2e133 100644 --- a/src/form.rs +++ b/src/form.rs @@ -12,32 +12,73 @@ //! ## Example //! //! ```no_run -//! #![cfg(feature = "validator")] +//! #[cfg(feature = "validator")] +//! mod validator_example { +//! use axum::routing::post; +//! use axum::Form; +//! use axum::Router; +//! use axum_valid::Valid; +//! use serde::Deserialize; +//! use validator::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(Valid(Form(parameter)): Valid>) { +//! 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, +//! } +//! } //! -//! use axum::routing::post; -//! use axum::Form; -//! use axum::Router; -//! use axum_valid::Valid; -//! use serde::Deserialize; -//! use validator::Validate; -//! #[tokio::main] -//! async fn main() -> anyhow::Result<()> { -//! let router = Router::new().route("/form", post(handler)); -//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) -//! .serve(router.into_make_service()) -//! .await?; -//! Ok(()) -//! } -//! async fn handler(Valid(Form(parameter)): Valid>) { -//! 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::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>) { +//! 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; diff --git a/src/garde.rs b/src/garde.rs index 7d66ad8..7c3c3ff 100644 --- a/src/garde.rs +++ b/src/garde.rs @@ -1,5 +1,8 @@ //! # Garde support +#[cfg(test)] +pub mod test; + use crate::{HasValidate, VALIDATION_ERROR_STATUS}; use axum::async_trait; use axum::extract::{FromRef, FromRequest, FromRequestParts}; diff --git a/src/garde/test.rs b/src/garde/test.rs new file mode 100644 index 0000000..43c0198 --- /dev/null +++ b/src/garde/test.rs @@ -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 = Lazy::new(|| ParametersGarde { + v0: 5, + v1: String::from("0123456789"), +}); + +static INVALID_PARAMETERS: Lazy = 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::>(); + 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::>(Method::GET, route::QUERY) + .await?; + + // Garde + test_executor + .execute::>(Method::POST, route::FORM) + .await?; + + // Garde + test_executor + .execute::>(Method::POST, route::JSON) + .await?; + + #[cfg(feature = "typed_header")] + { + use axum::TypedHeader; + // Garde + test_executor + .execute::>( + Method::POST, + typed_header::route::TYPED_HEADER, + ) + .await?; + } + + #[cfg(feature = "typed_multipart")] + { + use axum_typed_multipart::{BaseMultipart, TypedMultipart, TypedMultipartError}; + + // Garde + test_executor + .execute::>( + Method::POST, + typed_multipart::route::BASE_MULTIPART, + ) + .await?; + + // Garde + test_executor + .execute::>( + 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::>(Method::POST, extra::route::CACHED) + .await?; + test_executor + .execute::>( + Method::POST, + extra::route::WITH_REJECTION, + ) + .await?; + test_executor + .execute::, WithRejectionGardeRejection>>( + 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::>(Method::POST, extra_query::route::EXTRA_QUERY) + .await?; + } + + #[cfg(feature = "extra_form")] + { + use axum_extra::extract::Form; + test_executor + .execute::>(Method::POST, extra_form::route::EXTRA_FORM) + .await?; + } + + #[cfg(feature = "extra_protobuf")] + { + use axum_extra::protobuf::Protobuf; + test_executor + .execute::>( + Method::POST, + extra_protobuf::route::EXTRA_PROTOBUF, + ) + .await?; + } + + #[cfg(feature = "yaml")] + { + use axum_yaml::Yaml; + test_executor + .execute::>(Method::POST, yaml::route::YAML) + .await?; + } + + #[cfg(feature = "msgpack")] + { + use axum_msgpack::{MsgPack, MsgPackRaw}; + test_executor + .execute::>(Method::POST, msgpack::route::MSGPACK) + .await?; + test_executor + .execute::>(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 for TestExecutor { + fn from(server_url: Url) -> Self { + Self { + client: Default::default(), + server_url, + } + } +} + +impl TestExecutor { + /// Execute all tests + pub async fn execute(&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::(); + + 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::().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>) -> StatusCode { + validate_again(parameters, ()) +} + +async fn extract_query(Garde(Query(parameters)): Garde>) -> StatusCode { + validate_again(parameters, ()) +} + +async fn extract_form(Garde(Form(parameters)): Garde>) -> StatusCode { + validate_again(parameters, ()) +} + +async fn extract_json(Garde(Json(parameters)): Garde>) -> StatusCode { + validate_again(parameters, ()) +} + +fn validate_again(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>, + ) -> StatusCode { + validate_again(parameters, ()) + } + + impl Header for ParametersGarde { + fn name() -> &'static HeaderName { + &AXUM_VALID_PARAMETERS + } + + fn decode<'i, I>(values: &mut I) -> Result + where + Self: Sized, + I: Iterator, + { + 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::>(); + match split.as_slice() { + [v0, v1] => Ok(ParametersGarde { + v0: v0.parse().map_err(|_| Error::invalid())?, + v1: v1.to_string(), + }), + _ => Err(Error::invalid()), + } + } + + fn encode>(&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>, + ) -> StatusCode { + validate_again(parameters, ()) + } + + pub(super) async fn extract_base_multipart( + Garde(BaseMultipart { data, .. }): Garde< + BaseMultipart, + >, + ) -> 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 FromRequestParts for ParametersGarde + where + S: Send + Sync, + { + type Rejection = ParametersRejection; + + async fn from_request_parts(parts: &mut Parts, _: &S) -> Result { + 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 + IntoResponse + impl From for GardeWithRejectionRejection { + fn from(inner: ParametersRejection) -> Self { + Self { inner } + } + } + + pub async fn extract_cached( + Garde(Cached(parameters)): Garde>, + ) -> StatusCode { + validate_again(parameters, ()) + } + + pub async fn extract_with_rejection( + Garde(WithRejection(parameters, _)): Garde< + WithRejection, + >, + ) -> StatusCode { + validate_again(parameters, ()) + } + + pub struct WithRejectionGardeRejection { + inner: GardeRejection, + } + + impl From> for WithRejectionGardeRejection { + fn from(inner: GardeRejection) -> Self { + Self { inner } + } + } + + impl IntoResponse for WithRejectionGardeRejection { + 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, + WithRejectionGardeRejection, + >, + ) -> 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) -> 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>, + ) -> 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>, + ) -> 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>, + ) -> 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>) -> 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>, + ) -> StatusCode { + validate_again(parameters, ()) + } + pub async fn extract_msgpack_raw( + Garde(MsgPackRaw(parameters)): Garde>, + ) -> StatusCode { + validate_again(parameters, ()) + } +} diff --git a/src/json.rs b/src/json.rs index 1e674b5..f1a5823 100644 --- a/src/json.rs +++ b/src/json.rs @@ -12,32 +12,73 @@ //! ## Example //! //! ```no_run -//! #![cfg(feature = "validator")] +//! #[cfg(feature = "validator")] +//! mod validator_example { +//! use axum::routing::post; +//! use axum::Json; +//! use axum::Router; +//! use axum_valid::Valid; +//! use serde::Deserialize; +//! use validator::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(Valid(Json(parameter)): Valid>) { +//! 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, +//! } +//! } //! -//! use axum::routing::post; -//! use axum::Json; -//! use axum::Router; -//! use axum_valid::Valid; -//! use serde::Deserialize; -//! use validator::Validate; -//! #[tokio::main] -//! async fn main() -> 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(Valid(Json(parameter)): Valid>) { -//! 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::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>) { +//! 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; diff --git a/src/lib.rs b/src/lib.rs index ffef128..22f75cd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,9 +14,6 @@ pub mod msgpack; pub mod path; #[cfg(feature = "query")] pub mod query; -#[cfg(test)] -#[cfg(all(feature = "garde", feature = "validator"))] -pub mod test; #[cfg(feature = "typed_header")] pub mod typed_header; #[cfg(feature = "typed_multipart")] diff --git a/src/msgpack.rs b/src/msgpack.rs index 8c6a065..ef09b5c 100644 --- a/src/msgpack.rs +++ b/src/msgpack.rs @@ -12,42 +12,85 @@ //! ## Example //! //! ```no_run -//!#![cfg(feature = "validator")] +//! #[cfg(feature = "validator")] +//! mod validator_example { +//! use axum::routing::post; +//! use axum::Json; +//! use axum::Router; +//! use axum_msgpack::{MsgPack, MsgPackRaw}; +//! use axum_valid::Valid; +//! use serde::Deserialize; +//! use validator::Validate; +//! #[tokio::main] +//! 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>) { +//! assert!(parameter.validate().is_ok()); +//! } //! -//! use axum::routing::post; -//! use axum::Router; -//! use axum_msgpack::{MsgPack, MsgPackRaw}; -//! use axum_valid::Valid; -//! use serde::Deserialize; -//! use validator::Validate; -//! -//! #[tokio::main] -//! async fn main() -> 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 raw_handler(Valid(MsgPackRaw(parameter)): Valid>) { +//! 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, +//! } //! } //! -//! async fn handler(Valid(MsgPack(parameter)): Valid>) { -//! assert!(parameter.validate().is_ok()); +//! #[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() +//! .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(Garde(MsgPack(parameter)): Garde>) { +//! assert!(parameter.validate(&()).is_ok()); +//! } +//! +//! async fn raw_handler(Garde(MsgPackRaw(parameter)): Garde>) { +//! assert!(parameter.validate(&()).is_ok()); +//! } +//! #[derive(Validate, Deserialize)] +//! pub struct Parameter { +//! #[garde(range(min = 5, max = 10))] +//! pub v0: i32, +//! #[garde(length(min = 1, max = 10))] +//! pub v1: String, +//! } //! } //! -//! async fn raw_handler(Valid(MsgPackRaw(parameter)): Valid>) { -//! 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, -//! } +//! # fn main() -> anyhow::Result<()> { +//! # #[cfg(feature = "validator")] +//! # validator_example::launch()?; +//! # #[cfg(feature = "garde")] +//! # garde_example::launch()?; +//! # Ok(()) +//! # } //! ``` +//! use crate::HasValidate; #[cfg(feature = "validator")] diff --git a/src/path.rs b/src/path.rs index 6e86619..a685a62 100644 --- a/src/path.rs +++ b/src/path.rs @@ -8,35 +8,73 @@ //! ## Example //! //! ```no_run -//! #![cfg(feature = "validator")] -//! -//! use axum::extract::Path; -//! use axum::routing::post; -//! use axum::Router; -//! use axum_valid::Valid; -//! use serde::Deserialize; -//! use validator::Validate; -//! -//! #[tokio::main] -//! async fn main() -> anyhow::Result<()> { -//! let router = Router::new().route("/path/:v0/:v1", post(handler)); -//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) -//! .serve(router.into_make_service()) -//! .await?; -//! Ok(()) +//! #[cfg(feature = "validator")] +//! mod validator_example { +//! use axum::extract::Path; +//! use axum::routing::post; +//! use axum::Router; +//! use axum_valid::Valid; +//! use serde::Deserialize; +//! use validator::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(Valid(Path(parameter)): Valid>) { +//! 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, +//! } //! } //! -//! async fn handler(Valid(Path(parameter)): Valid>) { -//! assert!(parameter.validate().is_ok()); +//! #[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>) { +//! 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, +//! } //! } //! -//! #[derive(Validate, Deserialize)] -//! pub struct Parameter { -//! #[validate(range(min = 5, max = 10))] -//! pub v0: i32, -//! #[validate(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; diff --git a/src/query.rs b/src/query.rs index 1556a7d..70b28a2 100644 --- a/src/query.rs +++ b/src/query.rs @@ -12,35 +12,73 @@ //! ## Example //! //! ```no_run -//! #![cfg(feature = "validator")] -//! -//! use axum::extract::Query; -//! use axum::routing::post; -//! use axum::Router; -//! use axum_valid::Valid; -//! use serde::Deserialize; -//! use validator::Validate; -//! -//! #[tokio::main] -//! async fn main() -> anyhow::Result<()> { -//! let router = Router::new().route("/query", post(handler)); -//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) -//! .serve(router.into_make_service()) -//! .await?; -//! Ok(()) +//! #[cfg(feature = "validator")] +//! mod validator_example { +//! use axum::extract::Query; +//! use axum::routing::post; +//! use axum::Router; +//! use axum_valid::Valid; +//! use serde::Deserialize; +//! use validator::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(Valid(Query(parameter)): Valid>) { +//! 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, +//! } //! } //! -//! async fn handler(Valid(Query(parameter)): Valid>) { -//! assert!(parameter.validate().is_ok()); +//! #[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>) { +//! 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, +//! } //! } //! -//! #[derive(Validate, Deserialize)] -//! pub struct Parameter { -//! #[validate(range(min = 5, max = 10))] -//! pub v0: i32, -//! #[validate(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; diff --git a/src/validator.rs b/src/validator.rs index bb533b1..c7818fb 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -1,5 +1,8 @@ //! # Validator support +#[cfg(test)] +pub mod test; + use crate::{HasValidate, VALIDATION_ERROR_STATUS}; use axum::async_trait; use axum::extract::{FromRef, FromRequest, FromRequestParts}; diff --git a/src/test.rs b/src/validator/test.rs similarity index 94% rename from src/test.rs rename to src/validator/test.rs index 89dc1bc..855efc7 100644 --- a/src/test.rs +++ b/src/validator/test.rs @@ -1,5 +1,6 @@ +#![cfg(feature = "validator")] + use crate::tests::{ValidTest, ValidTestParameter}; -use crate::Garde; use crate::{Arguments, HasValidate, HasValidateArgs, Valid, ValidEx, VALIDATION_ERROR_STATUS}; use axum::extract::{FromRef, Path, Query}; 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 = Lazy::new(|| Parameters { v0: 5, v1: String::from("0123456789"), @@ -167,11 +153,6 @@ async fn test_main() -> anyhow::Result<()> { .route(route::FORM_EX, post(extract_form_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")] let router = router .route( @@ -369,12 +350,6 @@ async fn test_main() -> anyhow::Result<()> { .execute::>(Method::GET, route::QUERY_EX) .await?; - // Garde - #[cfg(feature = "garde")] - test_executor - .execute::>(Method::GET, route::QUERY_GARDE) - .await?; - // Valid test_executor .execute::>(Method::POST, route::FORM) @@ -395,12 +370,6 @@ async fn test_main() -> anyhow::Result<()> { .execute::>(Method::POST, route::JSON_EX) .await?; - // Garde - #[cfg(feature = "garde")] - test_executor - .execute::>(Method::POST, route::JSON_GARDE) - .await?; - #[cfg(feature = "typed_header")] { use axum::TypedHeader; @@ -703,14 +672,10 @@ mod route { pub const PATH_EX: &str = "/path_ex/:v0/:v1"; pub const QUERY: &str = "/query"; 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_EX: &str = "/form_ex"; pub const JSON: &str = "/json"; 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>) -> StatusCode { @@ -733,13 +698,6 @@ async fn extract_query_ex( validate_again_ex(parameters, args.get()) } -#[cfg(feature = "garde")] -async fn extract_query_garde( - Garde(Query(parameters)): Garde>, -) -> StatusCode { - validate_again_garde(parameters, ()) -} - async fn extract_form(Valid(Form(parameters)): Valid>) -> StatusCode { validate_again(parameters) } @@ -760,11 +718,6 @@ async fn extract_json_ex( validate_again_ex(parameters, args.get()) } -#[cfg(feature = "garde")] -async fn extract_json_garde(Garde(Json(parameters)): Garde>) -> StatusCode { - validate_again_garde(parameters, ()) -} - fn validate_again(validate: V) -> StatusCode { // The `Valid` extractor has validated the `parameters` once, // 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(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")] mod typed_header { @@ -814,7 +752,7 @@ mod typed_header { } 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 axum::headers::{Error, Header, HeaderName, HeaderValue}; use axum::http::StatusCode; @@ -919,7 +857,7 @@ mod typed_header { #[cfg(feature = "typed_multipart")] mod typed_multipart { - use crate::test::{ + use super::{ validate_again, validate_again_ex, Parameters, ParametersEx, ParametersExValidationArguments, }; @@ -975,7 +913,7 @@ mod typed_multipart { #[cfg(feature = "extra")] mod extra { - use crate::test::{ + use super::{ validate_again, validate_again_ex, Parameters, ParametersEx, ParametersExValidationArguments, }; @@ -1174,7 +1112,7 @@ mod extra { #[cfg(feature = "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 axum::http::StatusCode; use axum_extra::routing::TypedPath; @@ -1256,7 +1194,7 @@ mod extra_typed_path { #[cfg(feature = "extra_query")] mod extra_query { - use crate::test::{ + use super::{ validate_again, validate_again_ex, Parameters, ParametersEx, ParametersExValidationArguments, }; @@ -1287,7 +1225,7 @@ mod extra_query { #[cfg(feature = "extra_form")] mod extra_form { - use crate::test::{ + use super::{ validate_again, validate_again_ex, Parameters, ParametersEx, ParametersExValidationArguments, }; @@ -1318,7 +1256,7 @@ mod extra_form { #[cfg(feature = "extra_protobuf")] mod extra_protobuf { - use crate::test::{ + use super::{ validate_again, validate_again_ex, Parameters, ParametersEx, ParametersExValidationArguments, }; @@ -1349,7 +1287,7 @@ mod extra_protobuf { #[cfg(feature = "yaml")] mod yaml { - use crate::test::{ + use super::{ validate_again, validate_again_ex, Parameters, ParametersEx, ParametersExValidationArguments, }; @@ -1378,7 +1316,7 @@ mod yaml { #[cfg(feature = "msgpack")] mod msgpack { - use crate::test::{ + use super::{ validate_again, validate_again_ex, Parameters, ParametersEx, ParametersExValidationArguments, }; diff --git a/src/yaml.rs b/src/yaml.rs index dfa4eb5..831d87f 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -12,35 +12,73 @@ //! ## Example //! //! ```no_run -//! #![cfg(feature = "validator")] -//! -//! use axum::routing::post; -//! use axum::Router; -//! use axum_valid::Valid; -//! use axum_yaml::Yaml; -//! use serde::Deserialize; -//! use validator::Validate; -//! -//! #[tokio::main] -//! async fn main() -> anyhow::Result<()> { -//! let router = Router::new().route("/yaml", post(handler)); -//! axum::Server::bind(&([0u8, 0, 0, 0], 8080).into()) -//! .serve(router.into_make_service()) -//! .await?; -//! Ok(()) +//! #[cfg(feature = "validator")] +//! mod validator_example { +//! use axum::routing::post; +//! use axum_yaml::Yaml; +//! use axum::Router; +//! use axum_valid::Valid; +//! use serde::Deserialize; +//! use validator::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(Valid(Yaml(parameter)): Valid>) { +//! 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, +//! } //! } //! -//! async fn handler(parameter: Valid>) { -//! assert!(parameter.validate().is_ok()); +//! #[cfg(feature = "garde")] +//! mod garde_example { +//! use axum::routing::post; +//! use axum_yaml::Yaml; +//! 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(Yaml(parameter)): Garde>) { +//! 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, +//! } //! } //! -//! #[derive(Deserialize, Validate)] -//! struct Parameter { -//! #[validate(range(min = 5, max = 10))] -//! v0: i32, -//! #[validate(length(min = 1, max = 10))] -//! v1: String, -//! } +//! # fn main() -> anyhow::Result<()> { +//! # #[cfg(feature = "validator")] +//! # validator_example::launch()?; +//! # #[cfg(feature = "garde")] +//! # garde_example::launch()?; +//! # Ok(()) +//! # } //! ``` use crate::HasValidate;