From dd68fe613181d37fa6863dc7ad1abed628ba8c82 Mon Sep 17 00:00:00 2001 From: gengteng Date: Sun, 8 Oct 2023 14:34:26 +0800 Subject: [PATCH] update --- Cargo.toml | 4 +- src/extra.rs | 9 +- src/extra/form.rs | 6 +- src/extra/protobuf.rs | 6 +- src/extra/query.rs | 6 +- src/extra/typed_path.rs | 9 +- src/form.rs | 6 +- src/garde.rs | 18 ++ src/json.rs | 7 +- src/lib.rs | 374 ++-------------------------------------- src/msgpack.rs | 8 +- src/path.rs | 7 +- src/query.rs | 7 +- src/test.rs | 16 +- src/typed_header.rs | 6 +- src/typed_multipart.rs | 7 +- src/validator.rs | 367 +++++++++++++++++++++++++++++++++++++++ src/yaml.rs | 6 +- tests/custom.rs | 2 + 19 files changed, 489 insertions(+), 382 deletions(-) create mode 100644 src/validator.rs diff --git a/Cargo.toml b/Cargo.toml index d0d4764..64f1168 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,8 +25,8 @@ features = ["all_types"] [dependencies] axum = { version = "0.6.20", default-features = false } -garde = "0.15.0" -validator = "0.16.1" +garde = { version = "0.15.0", optional = true } +validator = { version = "0.16.1", optional = true} [dependencies.axum_typed_multipart] version = "0.10.0" diff --git a/src/extra.rs b/src/extra.rs index 640e6f1..f18603d 100644 --- a/src/extra.rs +++ b/src/extra.rs @@ -236,8 +236,11 @@ pub mod query; #[cfg(feature = "extra_typed_path")] pub mod typed_path; -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum_extra::extract::{Cached, WithRejection}; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Cached { @@ -248,6 +251,7 @@ impl HasValidate for Cached { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Cached { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { @@ -262,6 +266,7 @@ impl HasValidate for WithRejection { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>, R> HasValidateArgs<'v> for WithRejection { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { @@ -272,6 +277,7 @@ impl<'v, T: ValidateArgs<'v>, R> HasValidateArgs<'v> for WithRejection { #[cfg(test)] mod tests { use crate::tests::{Rejection, ValidTest}; + #[cfg(feature = "validator")] use crate::Valid; use axum::http::StatusCode; use axum_extra::extract::{Cached, WithRejection}; @@ -311,6 +317,7 @@ mod tests { } } + #[cfg(feature = "validator")] impl ValidTest for WithRejection, R> { // just use `418 I'm a teapot` to test const ERROR_STATUS_CODE: StatusCode = StatusCode::IM_A_TEAPOT; diff --git a/src/extra/form.rs b/src/extra/form.rs index 57c470f..7787c33 100644 --- a/src/extra/form.rs +++ b/src/extra/form.rs @@ -38,8 +38,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum_extra::extract::Form; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Form { @@ -49,6 +52,7 @@ impl HasValidate for Form { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Form { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/extra/protobuf.rs b/src/extra/protobuf.rs index 90a6d62..bf1aaa4 100644 --- a/src/extra/protobuf.rs +++ b/src/extra/protobuf.rs @@ -40,8 +40,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum_extra::protobuf::Protobuf; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Protobuf { @@ -51,6 +54,7 @@ impl HasValidate for Protobuf { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Protobuf { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/extra/query.rs b/src/extra/query.rs index 99b5376..e233af0 100644 --- a/src/extra/query.rs +++ b/src/extra/query.rs @@ -41,8 +41,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum_extra::extract::Query; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Query { @@ -52,6 +55,7 @@ impl HasValidate for Query { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Query { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/extra/typed_path.rs b/src/extra/typed_path.rs index 2e26d73..3e26674 100644 --- a/src/extra/typed_path.rs +++ b/src/extra/typed_path.rs @@ -49,19 +49,26 @@ //! } //! ``` -use crate::garde::Garde; +#[cfg(feature = "garde")] +use crate::Garde; +#[cfg(feature = "validator")] use crate::{Valid, ValidEx}; +#[cfg(any(feature = "validator", feature = "garde"))] use axum_extra::routing::TypedPath; +#[cfg(any(feature = "validator", feature = "garde"))] use std::fmt::Display; +#[cfg(feature = "validator")] impl TypedPath for Valid { const PATH: &'static str = T::PATH; } +#[cfg(feature = "validator")] impl TypedPath for ValidEx { const PATH: &'static str = T::PATH; } +#[cfg(feature = "garde")] impl TypedPath for Garde { const PATH: &'static str = T::PATH; } diff --git a/src/form.rs b/src/form.rs index 8810c4b..d3e52f0 100644 --- a/src/form.rs +++ b/src/form.rs @@ -38,8 +38,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum::Form; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Form { @@ -49,6 +52,7 @@ impl HasValidate for Form { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Form { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/garde.rs b/src/garde.rs index a801d67..7d66ad8 100644 --- a/src/garde.rs +++ b/src/garde.rs @@ -145,3 +145,21 @@ where Ok(Garde(inner)) } } + +#[cfg(test)] +mod tests { + use super::*; + + const GARDE: &str = "garde"; + + #[test] + fn garde_deref_deref_mut_into_inner() { + let mut inner = String::from(GARDE); + let mut v = Garde(inner.clone()); + assert_eq!(&inner, v.deref()); + inner.push_str(GARDE); + v.deref_mut().push_str(GARDE); + assert_eq!(&inner, v.deref()); + assert_eq!(inner, v.into_inner()); + } +} diff --git a/src/json.rs b/src/json.rs index 0640cf9..83dcb4f 100644 --- a/src/json.rs +++ b/src/json.rs @@ -12,6 +12,7 @@ //! ## Example //! //! ```no_run +//! //! use axum::routing::post; //! use axum::Json; //! use axum::Router; @@ -38,8 +39,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum::Json; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Json { @@ -49,6 +53,7 @@ impl HasValidate for Json { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Json { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/lib.rs b/src/lib.rs index b7174f0..62b426c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,7 @@ pub mod extra; #[cfg(feature = "form")] pub mod form; +#[cfg(feature = "garde")] pub mod garde; #[cfg(feature = "json")] pub mod json; @@ -14,23 +15,18 @@ 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")] pub mod typed_multipart; +#[cfg(feature = "validator")] +mod validator; #[cfg(feature = "yaml")] pub mod yaml; -use axum::async_trait; -use axum::extract::{FromRef, FromRequest, FromRequestParts}; -use axum::http::request::Parts; -use axum::http::{Request, StatusCode}; -use axum::response::{IntoResponse, Response}; -use std::error::Error; -use std::fmt::{Debug, Display, Formatter}; -use std::ops::{Deref, DerefMut}; -use validator::{Validate, ValidateArgs, ValidationErrors}; +use axum::http::StatusCode; /// Http status code returned when there are validation errors. #[cfg(feature = "422")] @@ -39,180 +35,6 @@ pub const VALIDATION_ERROR_STATUS: StatusCode = StatusCode::UNPROCESSABLE_ENTITY #[cfg(not(feature = "422"))] pub const VALIDATION_ERROR_STATUS: StatusCode = StatusCode::BAD_REQUEST; -/// # `Valid` data extractor -/// -/// This extractor can be used in combination with axum's extractors like -/// Json, Form, Query, Path, etc to validate their inner data automatically. -/// It can also work with custom extractors that implement the `HasValidate` trait. -/// -/// See the docs for each integration module to find examples of using -/// `Valid` with that extractor. -/// -/// For examples with custom extractors, check out the `tests/custom.rs` file. -/// -#[derive(Debug, Clone, Copy, Default)] -pub struct Valid(pub E); - -impl Deref for Valid { - type Target = E; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Valid { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Display for ValidEx { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl Valid { - /// Consume the `Valid` extractor and returns the inner type. - pub fn into_inner(self) -> E { - self.0 - } -} - -/// # `ValidEx` data extractor -/// -/// `ValidEx` can be incorporated with extractors from various modules, similar to `Valid`. -/// Two differences exist between `ValidEx` and `Valid`: -/// -/// - The inner data type in `ValidEx` implements `ValidateArgs` instead of `Validate`. -/// - `ValidEx` includes a second field that represents arguments used during validation of the first field. -/// -/// The implementation of `ValidateArgs` is often automatically handled by validator's derive macros -/// (for more details, please refer to the validator's documentation). -/// -/// Although current module documentation predominantly showcases `Valid` examples, the usage of `ValidEx` is analogous. -/// -#[derive(Debug, Clone, Copy, Default)] -pub struct ValidEx(pub E, pub A); - -impl Deref for ValidEx { - type Target = E; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for ValidEx { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl Display for Valid { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - self.0.fmt(f) - } -} - -impl ValidEx { - /// Consumes the `ValidEx` and returns the validated data within. - /// - /// This returns the `E` type which represents the data that has been - /// successfully validated. - pub fn into_inner(self) -> E { - self.0 - } - - /// Returns a reference to the validation arguments. - /// - /// This provides access to the `A` type which contains the arguments used - /// to validate the data. These arguments were passed to the validation - /// function. - pub fn arguments<'a>(&'a self) -> <>::T as ValidateArgs<'a>>::Args - where - A: Arguments<'a>, - { - self.1.get() - } -} - -fn response_builder(ve: ValidationErrors) -> Response { - #[cfg(feature = "into_json")] - { - (VALIDATION_ERROR_STATUS, axum::Json(ve)).into_response() - } - #[cfg(not(feature = "into_json"))] - { - (VALIDATION_ERROR_STATUS, ve.to_string()).into_response() - } -} - -/// `Arguments` provides the validation arguments for the data type `T`. -/// -/// This trait has an associated type `T` which represents the data type to -/// validate. `T` must implement the `ValidateArgs` trait which defines the -/// validation logic. -/// -/// It's important to mention that types implementing `Arguments` should be a part of the router's state -/// (either through implementing `FromRef` or by directly becoming the state) -/// to enable automatic arguments retrieval during validation. -/// -pub trait Arguments<'a> { - /// The data type to validate using this arguments - type T: ValidateArgs<'a>; - /// This method gets the arguments required by `ValidateArgs::validate_args` - fn get(&'a self) -> <>::T as ValidateArgs<'a>>::Args; -} - -/// `ValidRejection` is returned when the `Valid` extractor fails. -/// -/// This enumeration captures two types of errors that can occur when using `Valid`: errors related to the validation -/// logic itself (encapsulated in `Valid`), and errors that may arise within the inner extractor (represented by `Inner`). -/// -#[derive(Debug)] -pub enum ValidRejection { - /// `Valid` variant captures errors related to the validation logic. It contains `ValidationErrors` - /// which is a collection of validation failures for each field. - Valid(ValidationErrors), - /// `Inner` variant represents potential errors that might occur within the inner extractor. - Inner(E), -} - -impl Display for ValidRejection { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - match self { - ValidRejection::Valid(errors) => write!(f, "{errors}"), - ValidRejection::Inner(error) => write!(f, "{error}"), - } - } -} - -impl Error for ValidRejection { - fn source(&self) -> Option<&(dyn Error + 'static)> { - match self { - ValidRejection::Valid(ve) => Some(ve), - ValidRejection::Inner(e) => Some(e), - } - } -} - -impl From for ValidRejection { - fn from(value: ValidationErrors) -> Self { - Self::Valid(value) - } -} - -impl IntoResponse for ValidRejection { - fn into_response(self) -> Response { - match self { - ValidRejection::Valid(ve) => response_builder(ve), - ValidRejection::Inner(e) => e.into_response(), - } - } -} - /// Trait for types that can supply a reference that can be validated. /// /// Extractor types `T` that implement this trait can be used with `Valid` or `Garde`. @@ -224,111 +46,16 @@ pub trait HasValidate { fn get_validate(&self) -> &Self::Validate; } -/// Trait for types that can supply a reference that can be validated using arguments. -/// -/// Extractor types `T` that implement this trait can be used with `ValidEx`. -/// -pub trait HasValidateArgs<'v> { - /// Inner type that can be validated using arguments - type ValidateArgs: ValidateArgs<'v>; - /// Get the inner value - fn get_validate_args(&self) -> &Self::ValidateArgs; -} +#[cfg(feature = "validator")] +pub use crate::validator::{Arguments, HasValidateArgs, Valid, ValidEx, ValidRejection}; -#[async_trait] -impl FromRequest for Valid -where - State: Send + Sync, - Body: Send + Sync + 'static, - Extractor: HasValidate + FromRequest, - Extractor::Validate: Validate, -{ - type Rejection = ValidRejection<>::Rejection>; - - async fn from_request(req: Request, state: &State) -> Result { - let inner = Extractor::from_request(req, state) - .await - .map_err(ValidRejection::Inner)?; - inner.get_validate().validate()?; - Ok(Valid(inner)) - } -} - -#[async_trait] -impl FromRequestParts for Valid -where - State: Send + Sync, - Extractor: HasValidate + FromRequestParts, - Extractor::Validate: Validate, -{ - type Rejection = ValidRejection<>::Rejection>; - - async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { - let inner = Extractor::from_request_parts(parts, state) - .await - .map_err(ValidRejection::Inner)?; - inner.get_validate().validate()?; - Ok(Valid(inner)) - } -} - -#[async_trait] -impl FromRequest for ValidEx -where - State: Send + Sync, - Body: Send + Sync + 'static, - Args: Send - + Sync - + FromRef - + for<'a> Arguments<'a, T = >::ValidateArgs>, - Extractor: for<'v> HasValidateArgs<'v> + FromRequest, - for<'v> >::ValidateArgs: ValidateArgs<'v>, -{ - type Rejection = ValidRejection<>::Rejection>; - - async fn from_request(req: Request, state: &State) -> Result { - let arguments: Args = FromRef::from_ref(state); - let inner = Extractor::from_request(req, state) - .await - .map_err(ValidRejection::Inner)?; - - inner.get_validate_args().validate_args(arguments.get())?; - Ok(ValidEx(inner, arguments)) - } -} - -#[async_trait] -impl FromRequestParts for ValidEx -where - State: Send + Sync, - Args: Send - + Sync - + FromRef - + for<'a> Arguments<'a, T = >::ValidateArgs>, - Extractor: for<'v> HasValidateArgs<'v> + FromRequestParts, - for<'v> >::ValidateArgs: ValidateArgs<'v>, -{ - type Rejection = ValidRejection<>::Rejection>; - - async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { - let arguments: Args = FromRef::from_ref(state); - let inner = Extractor::from_request_parts(parts, state) - .await - .map_err(ValidRejection::Inner)?; - inner.get_validate_args().validate_args(arguments.get())?; - Ok(ValidEx(inner, arguments)) - } -} +#[cfg(feature = "garde")] +pub use crate::garde::{Garde, GardeRejection}; #[cfg(test)] -pub mod tests { - use crate::{Arguments, Valid, ValidEx, ValidRejection}; +mod tests { use reqwest::{RequestBuilder, StatusCode}; use serde::Serialize; - use std::error::Error; - use std::io; - use std::ops::{Deref, DerefMut}; - use validator::{Validate, ValidateArgs, ValidationError, ValidationErrors}; /// # Valid test parameter pub trait ValidTestParameter: Serialize + 'static { @@ -369,85 +96,4 @@ pub mod tests { pub trait Rejection { const STATUS_CODE: StatusCode; } - - const TEST: &str = "test"; - - #[test] - fn valid_deref_deref_mut_into_inner() { - let mut inner = String::from(TEST); - let mut v = Valid(inner.clone()); - assert_eq!(&inner, v.deref()); - inner.push_str(TEST); - v.deref_mut().push_str(TEST); - assert_eq!(&inner, v.deref()); - assert_eq!(inner, v.into_inner()); - } - - #[test] - fn valid_ex_deref_deref_mut_into_inner_arguments() { - let mut inner = String::from(TEST); - let mut v = ValidEx(inner.clone(), ()); - assert_eq!(&inner, v.deref()); - inner.push_str(TEST); - v.deref_mut().push_str(TEST); - assert_eq!(&inner, v.deref()); - assert_eq!(inner, v.into_inner()); - - fn validate(_v: i32, _args: i32) -> Result<(), ValidationError> { - Ok(()) - } - - #[derive(Validate)] - struct Data { - #[validate(custom(function = "validate", arg = "i32"))] - v: i32, - } - - struct DataVA { - a: i32, - } - - impl<'a> Arguments<'a> for DataVA { - type T = Data; - - fn get(&'a self) -> <>::T as ValidateArgs<'a>>::Args { - self.a - } - } - - let data = Data { v: 12 }; - let args = DataVA { a: 123 }; - let ve = ValidEx(data, args); - assert_eq!(ve.v, 12); - let a = ve.arguments(); - assert_eq!(a, 123); - } - - #[test] - fn display_error() { - // ValidRejection::Valid Display - let mut ve = ValidationErrors::new(); - ve.add(TEST, ValidationError::new(TEST)); - let vr = ValidRejection::::Valid(ve.clone()); - assert_eq!(vr.to_string(), ve.to_string()); - - // ValidRejection::Inner Display - let inner = String::from(TEST); - let vr = ValidRejection::::Inner(inner.clone()); - assert_eq!(inner.to_string(), vr.to_string()); - - // ValidRejection::Valid Error - let mut ve = ValidationErrors::new(); - ve.add(TEST, ValidationError::new(TEST)); - let vr = ValidRejection::::Valid(ve.clone()); - assert!( - matches!(vr.source(), Some(source) if source.downcast_ref::().is_some()) - ); - - // ValidRejection::Valid Error - let vr = ValidRejection::::Inner(io::Error::new(io::ErrorKind::Other, TEST)); - assert!( - matches!(vr.source(), Some(source) if source.downcast_ref::().is_some()) - ); - } } diff --git a/src/msgpack.rs b/src/msgpack.rs index fa94252..2feb911 100644 --- a/src/msgpack.rs +++ b/src/msgpack.rs @@ -12,6 +12,7 @@ //! ## Example //! //! ```no_run +//! //! use axum::routing::post; //! use axum::Router; //! use axum_msgpack::{MsgPack, MsgPackRaw}; @@ -47,8 +48,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum_msgpack::{MsgPack, MsgPackRaw}; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for MsgPack { @@ -58,6 +62,7 @@ impl HasValidate for MsgPack { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for MsgPack { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { @@ -72,6 +77,7 @@ impl HasValidate for MsgPackRaw { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for MsgPackRaw { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/path.rs b/src/path.rs index 931731c..75a7fa0 100644 --- a/src/path.rs +++ b/src/path.rs @@ -8,6 +8,7 @@ //! ## Example //! //! ```no_run +//! //! use axum::extract::Path; //! use axum::routing::post; //! use axum::Router; @@ -37,8 +38,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum::extract::Path; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Path { @@ -48,6 +52,7 @@ impl HasValidate for Path { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Path { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/query.rs b/src/query.rs index 3e386b6..80c9318 100644 --- a/src/query.rs +++ b/src/query.rs @@ -12,6 +12,7 @@ //! ## Example //! //! ```no_run +//! //! use axum::extract::Query; //! use axum::routing::post; //! use axum::Router; @@ -41,8 +42,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum::extract::Query; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Query { @@ -52,6 +56,7 @@ impl HasValidate for Query { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Query { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/test.rs b/src/test.rs index e1b9856..89dc1bc 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,5 +1,5 @@ -use crate::garde::Garde; 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}; @@ -164,9 +164,12 @@ async fn test_main() -> anyhow::Result<()> { .route(route::JSON, post(extract_json)) .route(route::PATH_EX, get(extract_path_ex)) .route(route::QUERY_EX, get(extract_query_ex)) - .route(route::QUERY_GARDE, get(extract_query_garde)) .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")] @@ -367,6 +370,7 @@ async fn test_main() -> anyhow::Result<()> { .await?; // Garde + #[cfg(feature = "garde")] test_executor .execute::>(Method::GET, route::QUERY_GARDE) .await?; @@ -392,6 +396,7 @@ async fn test_main() -> anyhow::Result<()> { .await?; // Garde + #[cfg(feature = "garde")] test_executor .execute::>(Method::POST, route::JSON_GARDE) .await?; @@ -698,11 +703,13 @@ 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"; } @@ -726,6 +733,7 @@ async fn extract_query_ex( validate_again_ex(parameters, args.get()) } +#[cfg(feature = "garde")] async fn extract_query_garde( Garde(Query(parameters)): Garde>, ) -> StatusCode { @@ -752,6 +760,7 @@ 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, ()) } @@ -781,6 +790,7 @@ 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, diff --git a/src/typed_header.rs b/src/typed_header.rs index c3b8f63..a20dcb9 100644 --- a/src/typed_header.rs +++ b/src/typed_header.rs @@ -61,8 +61,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum::TypedHeader; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for TypedHeader { @@ -72,6 +75,7 @@ impl HasValidate for TypedHeader { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for TypedHeader { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/typed_multipart.rs b/src/typed_multipart.rs index 47f4663..e733e7b 100644 --- a/src/typed_multipart.rs +++ b/src/typed_multipart.rs @@ -50,8 +50,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum_typed_multipart::{BaseMultipart, TypedMultipart}; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for BaseMultipart { @@ -61,6 +64,7 @@ impl HasValidate for BaseMultipart { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>, R> HasValidateArgs<'v> for BaseMultipart { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { @@ -75,6 +79,7 @@ impl HasValidate for TypedMultipart { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for TypedMultipart { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/src/validator.rs b/src/validator.rs new file mode 100644 index 0000000..fa90cd3 --- /dev/null +++ b/src/validator.rs @@ -0,0 +1,367 @@ +use crate::{HasValidate, VALIDATION_ERROR_STATUS}; +use axum::async_trait; +use axum::extract::{FromRef, FromRequest, FromRequestParts}; +use axum::http::request::Parts; +use axum::http::Request; +use axum::response::{IntoResponse, Response}; +use std::error::Error; +use std::fmt::Display; +use std::ops::{Deref, DerefMut}; +use validator::{Validate, ValidateArgs, ValidationErrors}; + +/// # `Valid` data extractor +/// +/// This extractor can be used in combination with axum's extractors like +/// Json, Form, Query, Path, etc to validate their inner data automatically. +/// It can also work with custom extractors that implement the `HasValidate` trait. +/// +/// See the docs for each integration module to find examples of using +/// `Valid` with that extractor. +/// +/// For examples with custom extractors, check out the `tests/custom.rs` file. +/// +#[derive(Debug, Clone, Copy, Default)] +pub struct Valid(pub E); + +impl Deref for Valid { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Valid { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for ValidEx { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Valid { + /// Consume the `Valid` extractor and returns the inner type. + pub fn into_inner(self) -> E { + self.0 + } +} + +/// # `ValidEx` data extractor +/// +/// `ValidEx` can be incorporated with extractors from various modules, similar to `Valid`. +/// Two differences exist between `ValidEx` and `Valid`: +/// +/// - The inner data type in `ValidEx` implements `ValidateArgs` instead of `Validate`. +/// - `ValidEx` includes a second field that represents arguments used during validation of the first field. +/// +/// The implementation of `ValidateArgs` is often automatically handled by validator's derive macros +/// (for more details, please refer to the validator's documentation). +/// +/// Although current module documentation predominantly showcases `Valid` examples, the usage of `ValidEx` is analogous. +/// +#[derive(Debug, Clone, Copy, Default)] +pub struct ValidEx(pub E, pub A); + +impl Deref for ValidEx { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ValidEx { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for Valid { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl ValidEx { + /// Consumes the `ValidEx` and returns the validated data within. + /// + /// This returns the `E` type which represents the data that has been + /// successfully validated. + pub fn into_inner(self) -> E { + self.0 + } + + /// Returns a reference to the validation arguments. + /// + /// This provides access to the `A` type which contains the arguments used + /// to validate the data. These arguments were passed to the validation + /// function. + pub fn arguments<'a>(&'a self) -> <>::T as ValidateArgs<'a>>::Args + where + A: Arguments<'a>, + { + self.1.get() + } +} + +fn response_builder(ve: ValidationErrors) -> Response { + #[cfg(feature = "into_json")] + { + (VALIDATION_ERROR_STATUS, axum::Json(ve)).into_response() + } + #[cfg(not(feature = "into_json"))] + { + (VALIDATION_ERROR_STATUS, ve.to_string()).into_response() + } +} + +/// `Arguments` provides the validation arguments for the data type `T`. +/// +/// This trait has an associated type `T` which represents the data type to +/// validate. `T` must implement the `ValidateArgs` trait which defines the +/// validation logic. +/// +/// It's important to mention that types implementing `Arguments` should be a part of the router's state +/// (either through implementing `FromRef` or by directly becoming the state) +/// to enable automatic arguments retrieval during validation. +/// +pub trait Arguments<'a> { + /// The data type to validate using this arguments + type T: ValidateArgs<'a>; + /// This method gets the arguments required by `ValidateArgs::validate_args` + fn get(&'a self) -> <>::T as ValidateArgs<'a>>::Args; +} + +/// `ValidRejection` is returned when the `Valid` extractor fails. +/// +/// This enumeration captures two types of errors that can occur when using `Valid`: errors related to the validation +/// logic itself (encapsulated in `Valid`), and errors that may arise within the inner extractor (represented by `Inner`). +/// +#[derive(Debug)] +pub enum ValidRejection { + /// `Valid` variant captures errors related to the validation logic. It contains `ValidationErrors` + /// which is a collection of validation failures for each field. + Valid(ValidationErrors), + /// `Inner` variant represents potential errors that might occur within the inner extractor. + Inner(E), +} + +impl Display for ValidRejection { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValidRejection::Valid(errors) => write!(f, "{errors}"), + ValidRejection::Inner(error) => write!(f, "{error}"), + } + } +} + +impl Error for ValidRejection { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + ValidRejection::Valid(ve) => Some(ve), + ValidRejection::Inner(e) => Some(e), + } + } +} + +impl From for ValidRejection { + fn from(value: ValidationErrors) -> Self { + Self::Valid(value) + } +} + +impl IntoResponse for ValidRejection { + fn into_response(self) -> Response { + match self { + ValidRejection::Valid(ve) => response_builder(ve), + ValidRejection::Inner(e) => e.into_response(), + } + } +} + +/// Trait for types that can supply a reference that can be validated using arguments. +/// +/// Extractor types `T` that implement this trait can be used with `ValidEx`. +/// +pub trait HasValidateArgs<'v> { + /// Inner type that can be validated using arguments + type ValidateArgs: ValidateArgs<'v>; + /// Get the inner value + fn get_validate_args(&self) -> &Self::ValidateArgs; +} + +#[async_trait] +impl FromRequest for Valid +where + State: Send + Sync, + Body: Send + Sync + 'static, + Extractor: HasValidate + FromRequest, + Extractor::Validate: Validate, +{ + type Rejection = ValidRejection<>::Rejection>; + + async fn from_request(req: Request, state: &State) -> Result { + let inner = Extractor::from_request(req, state) + .await + .map_err(ValidRejection::Inner)?; + inner.get_validate().validate()?; + Ok(Valid(inner)) + } +} + +#[async_trait] +impl FromRequestParts for Valid +where + State: Send + Sync, + Extractor: HasValidate + FromRequestParts, + Extractor::Validate: Validate, +{ + type Rejection = ValidRejection<>::Rejection>; + + async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { + let inner = Extractor::from_request_parts(parts, state) + .await + .map_err(ValidRejection::Inner)?; + inner.get_validate().validate()?; + Ok(Valid(inner)) + } +} + +#[async_trait] +impl FromRequest for ValidEx +where + State: Send + Sync, + Body: Send + Sync + 'static, + Args: Send + + Sync + + FromRef + + for<'a> Arguments<'a, T = >::ValidateArgs>, + Extractor: for<'v> HasValidateArgs<'v> + FromRequest, + for<'v> >::ValidateArgs: ValidateArgs<'v>, +{ + type Rejection = ValidRejection<>::Rejection>; + + async fn from_request(req: Request, state: &State) -> Result { + let arguments: Args = FromRef::from_ref(state); + let inner = Extractor::from_request(req, state) + .await + .map_err(ValidRejection::Inner)?; + + inner.get_validate_args().validate_args(arguments.get())?; + Ok(ValidEx(inner, arguments)) + } +} + +#[async_trait] +impl FromRequestParts for ValidEx +where + State: Send + Sync, + Args: Send + + Sync + + FromRef + + for<'a> Arguments<'a, T = >::ValidateArgs>, + Extractor: for<'v> HasValidateArgs<'v> + FromRequestParts, + for<'v> >::ValidateArgs: ValidateArgs<'v>, +{ + type Rejection = ValidRejection<>::Rejection>; + + async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { + let arguments: Args = FromRef::from_ref(state); + let inner = Extractor::from_request_parts(parts, state) + .await + .map_err(ValidRejection::Inner)?; + inner.get_validate_args().validate_args(arguments.get())?; + Ok(ValidEx(inner, arguments)) + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use std::io; + use validator::ValidationError; + const TEST: &str = "test"; + + #[test] + fn valid_deref_deref_mut_into_inner() { + let mut inner = String::from(TEST); + let mut v = Valid(inner.clone()); + assert_eq!(&inner, v.deref()); + inner.push_str(TEST); + v.deref_mut().push_str(TEST); + assert_eq!(&inner, v.deref()); + assert_eq!(inner, v.into_inner()); + } + + #[test] + fn valid_ex_deref_deref_mut_into_inner_arguments() { + let mut inner = String::from(TEST); + let mut v = ValidEx(inner.clone(), ()); + assert_eq!(&inner, v.deref()); + inner.push_str(TEST); + v.deref_mut().push_str(TEST); + assert_eq!(&inner, v.deref()); + assert_eq!(inner, v.into_inner()); + + fn validate(_v: i32, _args: i32) -> Result<(), ValidationError> { + Ok(()) + } + + #[derive(Validate)] + struct Data { + #[validate(custom(function = "validate", arg = "i32"))] + v: i32, + } + + struct DataVA { + a: i32, + } + + impl<'a> Arguments<'a> for DataVA { + type T = Data; + + fn get(&'a self) -> <>::T as ValidateArgs<'a>>::Args { + self.a + } + } + + let data = Data { v: 12 }; + let args = DataVA { a: 123 }; + let ve = ValidEx(data, args); + assert_eq!(ve.v, 12); + let a = ve.arguments(); + assert_eq!(a, 123); + } + + #[test] + fn display_error() { + // ValidRejection::Valid Display + let mut ve = ValidationErrors::new(); + ve.add(TEST, ValidationError::new(TEST)); + let vr = ValidRejection::::Valid(ve.clone()); + assert_eq!(vr.to_string(), ve.to_string()); + + // ValidRejection::Inner Display + let inner = String::from(TEST); + let vr = ValidRejection::::Inner(inner.clone()); + assert_eq!(inner.to_string(), vr.to_string()); + + // ValidRejection::Valid Error + let mut ve = ValidationErrors::new(); + ve.add(TEST, ValidationError::new(TEST)); + let vr = ValidRejection::::Valid(ve.clone()); + assert!( + matches!(vr.source(), Some(source) if source.downcast_ref::().is_some()) + ); + + // ValidRejection::Valid Error + let vr = ValidRejection::::Inner(io::Error::new(io::ErrorKind::Other, TEST)); + assert!( + matches!(vr.source(), Some(source) if source.downcast_ref::().is_some()) + ); + } +} diff --git a/src/yaml.rs b/src/yaml.rs index 817be96..dea49ed 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -42,8 +42,11 @@ //! } //! ``` -use crate::{HasValidate, HasValidateArgs}; +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; use axum_yaml::Yaml; +#[cfg(feature = "validator")] use validator::ValidateArgs; impl HasValidate for Yaml { @@ -53,6 +56,7 @@ impl HasValidate for Yaml { } } +#[cfg(feature = "validator")] impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Yaml { type ValidateArgs = T; fn get_validate_args(&self) -> &Self::ValidateArgs { diff --git a/tests/custom.rs b/tests/custom.rs index ec59e8d..106fc42 100644 --- a/tests/custom.rs +++ b/tests/custom.rs @@ -1,6 +1,8 @@ //! # Custom extractor validation //! +#![cfg(feature = "validator")] + use axum::extract::FromRequestParts; use axum::http::request::Parts; use axum::response::{IntoResponse, Response};