diff --git a/Cargo.toml b/Cargo.toml index f17aa13..da46e39 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ features = ["all_types", "garde"] axum = { version = "0.6.20", default-features = false } garde = { version = "0.15.0", optional = true } validator = { version = "0.16.1", optional = true} +validify = { version = "1.0.12", optional = true } [dependencies.axum_typed_multipart] version = "0.10.0" @@ -72,6 +73,7 @@ default = ["basic", "validator"] basic = ["json", "form", "query"] garde = ["dep:garde"] validator = ["dep:validator"] +validify = ["dep:validify"] json = ["axum/json"] form = ["axum/form"] query = ["axum/query"] @@ -90,3 +92,4 @@ all_extra_types = ["extra", "extra_typed_path", "extra_query", "extra_form", "ex all_types = ["json", "form", "query", "typed_header", "typed_multipart", "msgpack", "yaml", "all_extra_types"] full = ["validator", "all_types", "422", "into_json"] full_garde = ["garde", "all_types", "422", "into_json"] +full_validify = ["validify", "all_types", "422", "into_json"] diff --git a/README.md b/README.md index 6a6025c..14ced76 100644 --- a/README.md +++ b/README.md @@ -274,30 +274,32 @@ Current module documentation predominantly showcases `Valid` examples, the usage ## Features -| Feature | Description | Module | Default | Example | Tests | -|------------------|---------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|---------|---------|-------| -| default | Enables `validator` and support for `Query`, `Json` and `Form` | [`validator`], [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ | -| validator | Enables `validator` | [`validator`] | ✅ | ✅ | ✅ | -| garde | Enables `garde` | [`garde`] | ❌ | ✅ | ✅ | -| basic | Enables support for `Query`, `Json` and `Form` | [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ | -| json | Enables support for `Json` | [`json`] | ✅ | ✅ | ✅ | -| query | Enables support for `Query` | [`query`] | ✅ | ✅ | ✅ | -| form | Enables support for `Form` | [`form`] | ✅ | ✅ | ✅ | -| typed_header | Enables support for `TypedHeader` | [`typed_header`] | ❌ | ✅ | ✅ | -| typed_multipart | Enables support for `TypedMultipart` and `BaseMultipart` from `axum_typed_multipart` | [`typed_multipart`] | ❌ | ✅ | ✅ | -| msgpack | Enables support for `MsgPack` and `MsgPackRaw` from `axum-msgpack` | [`msgpack`] | ❌ | ✅ | ✅ | -| yaml | Enables support for `Yaml` from `axum-yaml` | [`yaml`] | ❌ | ✅ | ✅ | -| extra | Enables support for `Cached`, `WithRejection` from `axum-extra` | [`extra`] | ❌ | ✅ | ✅ | -| extra_typed_path | Enables support for `T: TypedPath` from `axum-extra` | [`extra::typed_path`] | ❌ | ✅ | ✅ | -| extra_query | Enables support for `Query` from `axum-extra` | [`extra::query`] | ❌ | ✅ | ✅ | -| extra_form | Enables support for `Form` from `axum-extra` | [`extra::form`] | ❌ | ✅ | ✅ | -| extra_protobuf | Enables support for `Protobuf` from `axum-extra` | [`extra::protobuf`] | ❌ | ✅ | ✅ | -| all_extra_types | Enables support for all extractors above from `axum-extra` | N/A | ❌ | ✅ | ✅ | -| all_types | Enables support for all extractors above | N/A | ❌ | ✅ | ✅ | -| 422 | Use `422 Unprocessable Entity` instead of `400 Bad Request` as the status code when validation fails | [`VALIDATION_ERROR_STATUS`] | ❌ | ✅ | ✅ | -| into_json | Validation errors will be serialized into JSON format and returned as the HTTP body | N/A | ❌ | ✅ | ✅ | -| full | Enables `all_types`, `422` and `into_json` | N/A | ❌ | ✅ | ✅ | -| full_garde | Enables `garde`, `all_types`, `422` and `into_json`. Consider using `default-features = false` to exclude default `validator` support | N/A | ❌ | ✅ | ✅ | +| Feature | Description | Module | Default | Example | Tests | +|------------------|------------------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------|---------|---------|-------| +| default | Enables `validator` and support for `Query`, `Json` and `Form` | [`validator`], [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ | +| validator | Enables `validator` (`Valid`, `ValidEx`) | [`validator`] | ✅ | ✅ | ✅ | +| garde | Enables `garde` (`Garde`) | [`garde`] | ❌ | ✅ | ✅ | +| validify | Enables `validify` (`Validated`, `Modified`, `Validified`) | [`validify`] | ❌ | ✅ | ✅ | +| basic | Enables support for `Query`, `Json` and `Form` | [`query`], [`json`], [`form`] | ✅ | ✅ | ✅ | +| json | Enables support for `Json` | [`json`] | ✅ | ✅ | ✅ | +| query | Enables support for `Query` | [`query`] | ✅ | ✅ | ✅ | +| form | Enables support for `Form` | [`form`] | ✅ | ✅ | ✅ | +| typed_header | Enables support for `TypedHeader` | [`typed_header`] | ❌ | ✅ | ✅ | +| typed_multipart | Enables support for `TypedMultipart` and `BaseMultipart` from `axum_typed_multipart` | [`typed_multipart`] | ❌ | ✅ | ✅ | +| msgpack | Enables support for `MsgPack` and `MsgPackRaw` from `axum-msgpack` | [`msgpack`] | ❌ | ✅ | ✅ | +| yaml | Enables support for `Yaml` from `axum-yaml` | [`yaml`] | ❌ | ✅ | ✅ | +| extra | Enables support for `Cached`, `WithRejection` from `axum-extra` | [`extra`] | ❌ | ✅ | ✅ | +| extra_typed_path | Enables support for `T: TypedPath` from `axum-extra` | [`extra::typed_path`] | ❌ | ✅ | ✅ | +| extra_query | Enables support for `Query` from `axum-extra` | [`extra::query`] | ❌ | ✅ | ✅ | +| extra_form | Enables support for `Form` from `axum-extra` | [`extra::form`] | ❌ | ✅ | ✅ | +| extra_protobuf | Enables support for `Protobuf` from `axum-extra` | [`extra::protobuf`] | ❌ | ✅ | ✅ | +| all_extra_types | Enables support for all extractors above from `axum-extra` | N/A | ❌ | ✅ | ✅ | +| all_types | Enables support for all extractors above | N/A | ❌ | ✅ | ✅ | +| 422 | Use `422 Unprocessable Entity` instead of `400 Bad Request` as the status code when validation fails | [`VALIDATION_ERROR_STATUS`] | ❌ | ✅ | ✅ | +| into_json | Validation errors will be serialized into JSON format and returned as the HTTP body | N/A | ❌ | ✅ | ✅ | +| full | Enables `all_types`, `422` and `into_json` | N/A | ❌ | ✅ | ✅ | +| full_garde | Enables `garde`, `all_types`, `422` and `into_json`. Consider using `default-features = false` to exclude default `validator` support | N/A | ❌ | ✅ | ✅ | +| full_garde | Enables `validify`, `all_types`, `422` and `into_json`. Consider using `default-features = false` to exclude default `validator` support | N/A | ❌ | ✅ | ✅ | ## Compatibility diff --git a/src/extra.rs b/src/extra.rs index 47f1d21..2dd4498 100644 --- a/src/extra.rs +++ b/src/extra.rs @@ -463,6 +463,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Cached { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Cached { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + impl HasValidate for WithRejection { type Validate = T; fn get_validate(&self) -> &T { @@ -478,6 +487,15 @@ impl<'v, T: ValidateArgs<'v>, R> HasValidateArgs<'v> for WithRejection { } } +#[cfg(feature = "validify")] +impl crate::HasModify for WithRejection { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{Rejection, ValidTest}; @@ -485,6 +503,8 @@ mod tests { use crate::Garde; #[cfg(feature = "validator")] use crate::Valid; + #[cfg(feature = "validify")] + use crate::Validated; use axum::http::StatusCode; use axum_extra::extract::{Cached, WithRejection}; use reqwest::RequestBuilder; @@ -572,4 +592,29 @@ mod tests { T::set_invalid_request(builder) } } + + #[cfg(feature = "validify")] + 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/extra/form.rs b/src/extra/form.rs index ec26c19..2c5d0a0 100644 --- a/src/extra/form.rs +++ b/src/extra/form.rs @@ -105,6 +105,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Form { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Form { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/extra/protobuf.rs b/src/extra/protobuf.rs index 570f173..f1403d7 100644 --- a/src/extra/protobuf.rs +++ b/src/extra/protobuf.rs @@ -109,6 +109,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Protobuf { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Protobuf { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/extra/query.rs b/src/extra/query.rs index b1f6f1a..19dc9c5 100644 --- a/src/extra/query.rs +++ b/src/extra/query.rs @@ -105,6 +105,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Query { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Query { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/extra/typed_path.rs b/src/extra/typed_path.rs index 7a35af2..e8ba81f 100644 --- a/src/extra/typed_path.rs +++ b/src/extra/typed_path.rs @@ -99,9 +99,11 @@ use crate::Garde; #[cfg(feature = "validator")] use crate::{Valid, ValidEx}; -#[cfg(any(feature = "validator", feature = "garde"))] +#[cfg(feature = "validify")] +use crate::{Validated, Validified}; +#[cfg(any(feature = "validator", feature = "garde", feature = "validify"))] use axum_extra::routing::TypedPath; -#[cfg(any(feature = "validator", feature = "garde"))] +#[cfg(any(feature = "validator", feature = "garde", feature = "validify"))] use std::fmt::Display; #[cfg(feature = "validator")] @@ -118,3 +120,13 @@ impl TypedPath for ValidEx { impl TypedPath for Garde { const PATH: &'static str = T::PATH; } + +#[cfg(feature = "validify")] +impl TypedPath for Validated { + const PATH: &'static str = T::PATH; +} + +#[cfg(feature = "validify")] +impl TypedPath for Validified { + const PATH: &'static str = T::PATH; +} diff --git a/src/form.rs b/src/form.rs index 899892c..2387b77 100644 --- a/src/form.rs +++ b/src/form.rs @@ -105,6 +105,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Form { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Form { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/json.rs b/src/json.rs index 8a95cad..84ec709 100644 --- a/src/json.rs +++ b/src/json.rs @@ -105,6 +105,33 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Json { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Json { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::PayloadExtractor for Json { + type Payload = T; + + fn get_payload(self) -> Self::Payload { + self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::HasValidify for Json { + type Validify = T; + type PayloadExtractor = Json; + fn from_validified(v: Self::Validify) -> Self { + Json(v) + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/lib.rs b/src/lib.rs index 984d8ec..6167123 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -20,6 +20,8 @@ pub mod typed_header; pub mod typed_multipart; #[cfg(feature = "validator")] pub mod validator; +#[cfg(feature = "validify")] +pub mod validify; #[cfg(feature = "yaml")] pub mod yaml; @@ -37,7 +39,7 @@ pub const VALIDATION_ERROR_STATUS: StatusCode = StatusCode::BAD_REQUEST; /// 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`. +/// Extractor types `T` that implement this trait can be used with `Valid`, `Garde` or `Validated`. /// pub trait HasValidate { /// Inner type that can be validated for correctness @@ -52,6 +54,11 @@ pub use crate::validator::{Arguments, HasValidateArgs, Valid, ValidEx, ValidReje #[cfg(feature = "garde")] pub use crate::garde::{Garde, GardeRejection}; +#[cfg(feature = "validify")] +pub use crate::validify::{ + HasModify, HasValidify, Modified, PayloadExtractor, Validated, Validified, ValidifyRejection, +}; + /// `ValidationRejection` is returned when the validation extractor fails. /// /// This enumeration captures two types of errors that can occur when using `Valid`: errors related to the validation diff --git a/src/msgpack.rs b/src/msgpack.rs index 4ce8b9d..17e3983 100644 --- a/src/msgpack.rs +++ b/src/msgpack.rs @@ -112,6 +112,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for MsgPack { } } +#[cfg(feature = "validify")] +impl crate::HasModify for MsgPack { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + impl HasValidate for MsgPackRaw { type Validate = T; fn get_validate(&self) -> &T { @@ -127,6 +136,14 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for MsgPackRaw { } } +#[cfg(feature = "validify")] +impl crate::HasModify for MsgPackRaw { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/path.rs b/src/path.rs index 6c105a0..bc937e6 100644 --- a/src/path.rs +++ b/src/path.rs @@ -100,3 +100,31 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Path { &self.0 } } + +#[cfg(feature = "validify")] +impl crate::HasModify for Path { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::PayloadExtractor for Path { + type Payload = T; + + fn get_payload(self) -> Self::Payload { + self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::HasValidify for Path { + type Validify = T; + type PayloadExtractor = Path; + + fn from_validified(v: Self::Validify) -> Self { + Path(v) + } +} diff --git a/src/query.rs b/src/query.rs index 8a936ad..9e9458d 100644 --- a/src/query.rs +++ b/src/query.rs @@ -105,6 +105,34 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Query { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Query { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::PayloadExtractor for Query { + type Payload = T; + + fn get_payload(self) -> Self::Payload { + self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::HasValidify for Query { + type Validify = T; + type PayloadExtractor = Query; + + fn from_validified(v: Self::Validify) -> Self { + Query(v) + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/typed_header.rs b/src/typed_header.rs index 4dfd05e..1802db6 100644 --- a/src/typed_header.rs +++ b/src/typed_header.rs @@ -141,6 +141,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for TypedHeader { } } +#[cfg(feature = "validify")] +impl crate::HasModify for TypedHeader { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/typed_multipart.rs b/src/typed_multipart.rs index 86a43e9..d859650 100644 --- a/src/typed_multipart.rs +++ b/src/typed_multipart.rs @@ -124,6 +124,40 @@ impl<'v, T: ValidateArgs<'v>, R> HasValidateArgs<'v> for BaseMultipart { } } +#[cfg(feature = "validify")] +impl crate::HasModify for BaseMultipart { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.data + } +} + +#[cfg(feature = "validify")] +impl crate::PayloadExtractor for BaseMultipart { + type Payload = T; + + fn get_payload(self) -> Self::Payload { + self.data + } +} + +#[cfg(feature = "validify")] +impl crate::HasValidify for BaseMultipart { + type Validify = T; + type PayloadExtractor = BaseMultipart; + + fn from_validified(_v: Self::Validify) -> Self { + // BaseMultipart { + // data, + // rejection: Default::default(), // ❌: need rejection to be pub + // } + // waiting for this issue to be resolved: + // https://github.com/murar8/axum_typed_multipart/issues/55 + unimplemented!() + } +} + impl HasValidate for TypedMultipart { type Validate = T; fn get_validate(&self) -> &T { @@ -139,6 +173,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for TypedMultipart { } } +#[cfg(feature = "validify")] +impl crate::HasModify for TypedMultipart { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/src/validator.rs b/src/validator.rs index 7974858..889c8e6 100644 --- a/src/validator.rs +++ b/src/validator.rs @@ -201,7 +201,6 @@ where + FromRef + for<'a> Arguments<'a, T = >::ValidateArgs>, Extractor: for<'v> HasValidateArgs<'v> + FromRequest, - for<'v> >::ValidateArgs: ValidateArgs<'v>, { type Rejection = ValidRejection<>::Rejection>; @@ -225,7 +224,6 @@ where + FromRef + for<'a> Arguments<'a, T = >::ValidateArgs>, Extractor: for<'v> HasValidateArgs<'v> + FromRequestParts, - for<'v> >::ValidateArgs: ValidateArgs<'v>, { type Rejection = ValidRejection<>::Rejection>; diff --git a/src/validify.rs b/src/validify.rs new file mode 100644 index 0000000..de82ea2 --- /dev/null +++ b/src/validify.rs @@ -0,0 +1,284 @@ +//! # Validify support +//! +//! ## Feature +//! +//! Enable the `validify` feature to use `Validated`, `Modified` and `Validified`. +//! + +#[cfg(test)] +pub mod test; + +use crate::{HasValidate, ValidationRejection}; +use axum::async_trait; +use axum::extract::{FromRequest, FromRequestParts}; +use axum::http::request::Parts; +use axum::http::Request; +use axum::response::{IntoResponse, Response}; +use std::fmt::{Display, Formatter}; +use std::ops::{Deref, DerefMut}; +use validify::{Modify, Validate, ValidationErrors, Validify}; + +/// # `Validated` data extractor +/// +#[derive(Debug, Clone, Copy, Default)] +pub struct Validated(pub E); + +impl Deref for Validated { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Validated { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for Validated { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Validated { + /// Consumes the `Validified` 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 + } +} + +/// # `Modified` data extractor / response +/// +#[derive(Debug, Clone, Copy, Default)] +pub struct Modified(pub E); + +impl Deref for Modified { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Modified { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for Modified { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Modified { + /// Consumes the `Modified` and returns the modified data within. + /// + /// This returns the `E` type which represents the data that has been + /// modified. + pub fn into_inner(self) -> E { + self.0 + } +} + +impl IntoResponse for Modified { + fn into_response(mut self) -> Response { + self.get_modify().modify(); + self.0.into_response() + } +} + +/// # `Validified` data extractor +/// +#[derive(Debug, Clone, Copy, Default)] +pub struct Validified(pub E); + +impl Deref for Validified { + type Target = E; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Validified { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl Display for Validified { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl Validified { + /// Consumes the `ValidifiedMut` and returns the modified and validated data within. + /// + /// This returns the `E` type which represents the data that has been + /// successfully validated. + pub fn into_inner(self) -> E { + self.0 + } +} + +/// `ValidifyRejection` is returned when the `Validified` / `ValidifiedMut` / `Modified` extractor fails. +/// +pub type ValidifyRejection = ValidationRejection; + +impl From for ValidifyRejection { + fn from(value: ValidationErrors) -> Self { + Self::Valid(value) + } +} + +/// Trait for types that can supply a reference that can be modified. +/// +/// Extractor types `T` that implement this trait can be used with `Modified`. +/// +pub trait HasModify { + /// Inner type that can be modified + type Modify: Modify; + /// Get the inner value + fn get_modify(&mut self) -> &mut Self::Modify; +} + +/// Extractor to extract payload for constructing data +pub trait PayloadExtractor { + /// Type of payload for constructing data + type Payload; + /// Get payload from the extractor + fn get_payload(self) -> Self::Payload; +} + +/// Trait for types that can supply a reference that can be modified and validated using `validify`. +/// +/// Extractor types `T` that implement this trait can be used with `Validified`. +/// +pub trait HasValidify: Sized { + /// Inner type that can be modified and validated using `validify`. + type Validify: Validify; + /// + type PayloadExtractor: PayloadExtractor::Payload>; + /// + fn from_validified(v: Self::Validify) -> Self; +} + +#[async_trait] +impl FromRequest for Validated +where + State: Send + Sync, + Body: Send + Sync + 'static, + Extractor: HasValidate + FromRequest, + Extractor::Validate: validify::Validate, +{ + type Rejection = ValidifyRejection<>::Rejection>; + + async fn from_request(req: Request, state: &State) -> Result { + let inner = Extractor::from_request(req, state) + .await + .map_err(ValidifyRejection::Inner)?; + inner.get_validate().validate()?; + Ok(Validated(inner)) + } +} + +#[async_trait] +impl FromRequestParts for Validated +where + State: Send + Sync, + Extractor: HasValidate + FromRequestParts, + Extractor::Validate: Validate, +{ + type Rejection = ValidifyRejection<>::Rejection>; + + async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { + let inner = Extractor::from_request_parts(parts, state) + .await + .map_err(ValidifyRejection::Inner)?; + inner.get_validate().validate()?; + Ok(Validated(inner)) + } +} + +#[async_trait] +impl FromRequest for Modified +where + State: Send + Sync, + Body: Send + Sync + 'static, + Extractor: HasModify + FromRequest, +{ + type Rejection = >::Rejection; + + async fn from_request(req: Request, state: &State) -> Result { + let mut inner = Extractor::from_request(req, state).await?; + inner.get_modify().modify(); + Ok(Modified(inner)) + } +} + +#[async_trait] +impl FromRequestParts for Modified +where + State: Send + Sync, + Extractor: HasModify + FromRequestParts, +{ + type Rejection = >::Rejection; + + async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { + let mut inner = Extractor::from_request_parts(parts, state).await?; + inner.get_modify().modify(); + Ok(Modified(inner)) + } +} + +#[async_trait] +impl FromRequest for Validified +where + State: Send + Sync, + Body: Send + Sync + 'static, + Extractor: HasValidify, + Extractor::Validify: Validify, + Extractor::PayloadExtractor: FromRequest, +{ + type Rejection = + ValidifyRejection<>::Rejection>; + + async fn from_request(req: Request, state: &State) -> Result { + let payload = Extractor::PayloadExtractor::from_request(req, state) + .await + .map_err(ValidifyRejection::Inner)?; + Ok(Validified(Extractor::from_validified( + Extractor::Validify::validify(payload.get_payload())?, + ))) + } +} + +#[async_trait] +impl FromRequestParts for Validified +where + State: Send + Sync, + Extractor: HasValidify, + Extractor::Validify: Validify, + Extractor::PayloadExtractor: FromRequestParts, +{ + type Rejection = + ValidifyRejection<>::Rejection>; + + async fn from_request_parts(parts: &mut Parts, state: &State) -> Result { + let payload = Extractor::PayloadExtractor::from_request_parts(parts, state) + .await + .map_err(ValidifyRejection::Inner)?; + Ok(Validified(Extractor::from_validified( + Extractor::Validify::validify(payload.get_payload())?, + ))) + } +} diff --git a/src/validify/test.rs b/src/validify/test.rs new file mode 100644 index 0000000..aca079d --- /dev/null +++ b/src/validify/test.rs @@ -0,0 +1,902 @@ +#![cfg(feature = "validify")] + +use crate::tests::{ValidTest, ValidTestParameter}; +use crate::{HasValidate, Validated, VALIDATION_ERROR_STATUS}; +use axum::extract::{FromRef, Path, Query}; +use axum::routing::{get, post}; +use axum::{Form, Json, Router}; +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; +use validify::Validate; + +#[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 ParametersValidify { + #[validate(range(min = 5.0, max = 10.0))] + #[cfg_attr(feature = "extra_protobuf", prost(int32, tag = "1"))] + v0: i32, + #[validate(length(min = 1, max = 10))] + #[cfg_attr(feature = "extra_protobuf", prost(string, tag = "2"))] + v1: String, +} + +static VALID_PARAMETERS: Lazy = Lazy::new(|| ParametersValidify { + v0: 5, + v1: String::from("0123456789"), +}); + +static INVALID_PARAMETERS: Lazy = Lazy::new(|| ParametersValidify { + v0: 6, + v1: String::from("01234567890"), +}); + +#[derive(Debug, Clone, FromRef, Default)] +struct MyState { + no_argument_context: (), +} + +impl ValidTestParameter for ParametersValidify { + 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 ParametersValidify { + type Validate = ParametersValidify; + + 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_VALIDIFY, + 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?; + + // Validified + test_executor + .execute::>(Method::GET, route::QUERY) + .await?; + + // Validified + test_executor + .execute::>(Method::POST, route::FORM) + .await?; + + // Validified + test_executor + .execute::>(Method::POST, route::JSON) + .await?; + + #[cfg(feature = "typed_header")] + { + use axum::TypedHeader; + // Validified + test_executor + .execute::>( + Method::POST, + typed_header::route::TYPED_HEADER, + ) + .await?; + } + + #[cfg(feature = "typed_multipart")] + { + use axum_typed_multipart::{BaseMultipart, TypedMultipart, TypedMultipartError}; + + // Validified + test_executor + .execute::>( + Method::POST, + typed_multipart::route::BASE_MULTIPART, + ) + .await?; + + // Validified + test_executor + .execute::>( + Method::POST, + typed_multipart::route::TYPED_MULTIPART, + ) + .await?; + } + + #[cfg(feature = "extra")] + { + use axum_extra::extract::{Cached, WithRejection}; + use extra::{ + ParametersRejection, ValidifyWithRejectionRejection, WithRejectionValidifyRejection, + }; + test_executor + .execute::>(Method::POST, extra::route::CACHED) + .await?; + test_executor + .execute::>( + Method::POST, + extra::route::WITH_REJECTION, + ) + .await?; + test_executor + .execute::, + WithRejectionValidifyRejection, + >>(Method::POST, extra::route::WITH_REJECTION_VALIDIFY) + .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, + "Validified '{}' 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, + "Validified '{}' 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( + Validated(Path(parameters)): Validated>, +) -> StatusCode { + validate_again(parameters) +} + +async fn extract_query( + Validated(Query(parameters)): Validated>, +) -> StatusCode { + validate_again(parameters) +} + +async fn extract_form( + Validated(Form(parameters)): Validated>, +) -> StatusCode { + validate_again(parameters) +} + +async fn extract_json( + Validated(Json(parameters)): Validated>, +) -> StatusCode { + validate_again(parameters) +} + +fn validate_again(validate: V) -> StatusCode { + // The `Validified` 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 `Validified` extractor works well. + // If it works properly, this function will never return `500 INTERNAL SERVER ERROR` + match validate.validate() { + Ok(_) => StatusCode::OK, + Err(_) => StatusCode::INTERNAL_SERVER_ERROR, + } +} + +#[cfg(feature = "typed_header")] +mod typed_header { + + pub(crate) mod route { + pub const TYPED_HEADER: &str = "/typed_header"; + } + + use super::{validate_again, ParametersValidify}; + use crate::Validated; + 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( + Validated(TypedHeader(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } + + impl Header for ParametersValidify { + 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(ParametersValidify { + 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 = ParametersValidify { + v0: 123456, + v1: "111111".to_string(), + }; + let mut vec = Vec::new(); + parameter.encode(&mut vec); + let mut iter = vec.iter(); + assert_eq!(parameter, ParametersValidify::decode(&mut iter)?); + Ok(()) + } +} + +#[cfg(feature = "typed_multipart")] +mod typed_multipart { + use super::{validate_again, ParametersValidify}; + use crate::Validated; + 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<&ParametersValidify> for reqwest::multipart::Form { + fn from(value: &ParametersValidify) -> Self { + reqwest::multipart::Form::new() + .text("v0", value.v0.to_string()) + .text("v1", value.v1.clone()) + } + } + + pub(super) async fn extract_typed_multipart( + Validated(TypedMultipart(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } + + pub(super) async fn extract_base_multipart( + Validated(BaseMultipart { data, .. }): Validated< + BaseMultipart, + >, + ) -> StatusCode { + validate_again(data) + } +} + +#[cfg(feature = "extra")] +mod extra { + use super::{validate_again, ParametersValidify}; + use crate::tests::{Rejection, ValidTest, ValidTestParameter}; + use crate::{Validated, ValidifyRejection}; + 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_VALIDIFY: &str = "/with_rejection_validify"; + } + 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 ParametersValidify + 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 ParametersValidify { + const ERROR_STATUS_CODE: StatusCode = CACHED_REJECTION_STATUS; + + fn set_valid_request(builder: RequestBuilder) -> RequestBuilder { + builder.header( + PARAMETERS_HEADER, + serde_json::to_string(ParametersValidify::valid()) + .expect("Failed to serialize parameters"), + ) + } + + fn set_error_request(builder: RequestBuilder) -> RequestBuilder { + builder.header( + PARAMETERS_HEADER, + serde_json::to_string(ParametersValidify::error()) + .expect("Failed to serialize parameters"), + ) + } + + fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder { + builder.header( + PARAMETERS_HEADER, + serde_json::to_string(ParametersValidify::invalid()) + .expect("Failed to serialize parameters"), + ) + } + } + + pub struct ValidifyWithRejectionRejection { + inner: ParametersRejection, + } + + impl Rejection for ValidifyWithRejectionRejection { + const STATUS_CODE: StatusCode = StatusCode::CONFLICT; + } + + impl IntoResponse for ValidifyWithRejectionRejection { + 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 ValidifyWithRejectionRejection { + fn from(inner: ParametersRejection) -> Self { + Self { inner } + } + } + + pub async fn extract_cached( + Validated(Cached(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } + + pub async fn extract_with_rejection( + Validated(WithRejection(parameters, _)): Validated< + WithRejection, + >, + ) -> StatusCode { + validate_again(parameters) + } + + pub struct WithRejectionValidifyRejection { + inner: ValidifyRejection, + } + + impl From> for WithRejectionValidifyRejection { + fn from(inner: ValidifyRejection) -> Self { + Self { inner } + } + } + + impl IntoResponse for WithRejectionValidifyRejection { + 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(Validated(parameters), _): WithRejection< + Validated, + WithRejectionValidifyRejection, + >, + ) -> StatusCode { + validate_again(parameters) + } +} + +#[cfg(feature = "extra_typed_path")] +mod extra_typed_path { + use super::validate_again; + use crate::{HasValidate, Validated}; + use axum::http::StatusCode; + use axum_extra::routing::TypedPath; + use serde::Deserialize; + use validify::Validate; + + 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 { + #[validate(range(min = 5.0, max = 10.0))] + v0: i32, + #[validate(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( + Validated(param): Validated, + ) -> StatusCode { + validate_again(param) + } +} + +#[cfg(feature = "extra_query")] +mod extra_query { + use super::{validate_again, ParametersValidify}; + use crate::Validated; + 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( + Validated(Query(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } +} + +#[cfg(feature = "extra_form")] +mod extra_form { + use super::{validate_again, ParametersValidify}; + use crate::Validated; + 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( + Validated(Form(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } +} + +#[cfg(feature = "extra_protobuf")] +mod extra_protobuf { + use super::{validate_again, ParametersValidify}; + use crate::Validated; + 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( + Validated(Protobuf(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } +} + +#[cfg(feature = "yaml")] +mod yaml { + use super::{validate_again, ParametersValidify}; + use crate::Validated; + use axum::http::StatusCode; + use axum_yaml::Yaml; + + pub mod route { + pub const YAML: &str = "/yaml"; + } + + pub async fn extract_yaml( + Validated(Yaml(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } +} + +#[cfg(feature = "msgpack")] +mod msgpack { + use super::{validate_again, ParametersValidify}; + use crate::Validated; + 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( + Validated(MsgPack(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } + pub async fn extract_msgpack_raw( + Validated(MsgPackRaw(parameters)): Validated>, + ) -> StatusCode { + validate_again(parameters) + } +} diff --git a/src/yaml.rs b/src/yaml.rs index 2bfbf25..90dc473 100644 --- a/src/yaml.rs +++ b/src/yaml.rs @@ -105,6 +105,15 @@ impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Yaml { } } +#[cfg(feature = "validify")] +impl crate::HasModify for Yaml { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + #[cfg(test)] mod tests { use crate::tests::{ValidTest, ValidTestParameter}; diff --git a/tarpaulin.toml b/tarpaulin.toml index b7fd571..1bd0051 100644 --- a/tarpaulin.toml +++ b/tarpaulin.toml @@ -1,10 +1,10 @@ [feature_default] [feature_all_types_into_json] -features = "validator garde all_types into_json" +features = "validator garde validify all_types into_json" [feature_all_types_422] -features = "validator garde all_types 422" +features = "validator garde validify all_types 422" [feature_all_types_422_into_json] -features = "validator garde all_types 422 into_json" \ No newline at end of file +features = "validator garde validify all_types 422 into_json" \ No newline at end of file