From af02a87ef18ddd14fc766da7f823206b274429bf Mon Sep 17 00:00:00 2001 From: gengteng Date: Sun, 14 Apr 2024 21:41:16 +0800 Subject: [PATCH] feat: add support for Cbor --- CHANGELOG.md | 11 +++ Cargo.toml | 14 ++-- src/cbor.rs | 179 ++++++++++++++++++++++++++++++++++++++++++ src/garde/test.rs | 27 +++++++ src/lib.rs | 2 + src/validator/test.rs | 44 +++++++++++ src/validify/test.rs | 80 +++++++++++++++++++ 7 files changed, 351 insertions(+), 6 deletions(-) create mode 100644 src/cbor.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index b732895..6f90967 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ ### Fixed +## axum-valid 0.18.0 (2024-04-14) + +### Added + +### Changed + +* Upgrade validator to 0.18.1. +* Upgrade validify to 1.4.0. +* Upgrade axum-serde to 0.4.1. + * Add support for `Cbor`. + ## axum-valid 0.17.0 (2024-03-05) ### Added diff --git a/Cargo.toml b/Cargo.toml index 178ff42..2f21398 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "axum-valid" -version = "0.17.0" +version = "0.18.0" description = "Provides validation extractors for your Axum application, allowing you to validate data using validator, garde, validify or all of them." authors = ["GengTeng "] license = "MIT" @@ -27,8 +27,8 @@ features = ["full", "aide"] [dependencies] axum = { version = "0.7.3", default-features = false } garde = { version = "0.18.0", optional = true } -validator = { version = "0.18.0", optional = true } -validify = { version = "1.3.0", optional = true } +validator = { version = "0.18.1", optional = true } +validify = { version = "1.4.0", optional = true } [dependencies.axum-extra] version = "0.9.0" @@ -36,7 +36,7 @@ default-features = false optional = true [dependencies.axum-serde] -version = "0.3.0" +version = "0.4.1" optional = true [dependencies.axum_typed_multipart] @@ -55,13 +55,14 @@ optional = true anyhow = "1.0.75" axum = { version = "0.7.1", features = ["macros"] } tokio = { version = "1.34.0", features = ["full"] } -reqwest = { version = "0.11.23", features = ["json", "multipart"] } +reqwest = { version = "0.12.3", features = ["json", "multipart"] } serde = { version = "1.0.195", features = ["derive"] } validator = { version = "0.18.0", features = ["derive"] } garde = { version = "0.18.0", features = ["serde", "derive"] } serde_json = "1.0.108" serde_yaml = "0.9.27" quick-xml = { version = "0.31.0", features = ["serialize"] } +ciborium = { version = "0.2.2" } toml = "0.8.8" mime = "0.3.17" prost = "0.12.3" @@ -83,6 +84,7 @@ yaml = ["dep:axum-serde", "axum-serde/yaml"] xml = ["dep:axum-serde", "axum-serde/xml"] toml = ["dep:axum-serde", "axum-serde/toml"] sonic = ["dep:axum-serde", "axum-serde/sonic"] +cbor = ["dep:axum-serde", "axum-serde/cbor"] typed_multipart = ["dep:axum_typed_multipart"] into_json = ["json", "dep:serde", "garde?/serde"] 422 = [] @@ -92,7 +94,7 @@ extra_query = ["extra", "axum-extra/query"] extra_form = ["extra", "axum-extra/form"] extra_protobuf = ["extra", "axum-extra/protobuf"] all_extra_types = ["extra", "typed_header", "extra_typed_path", "extra_query", "extra_form", "extra_protobuf"] -all_types = ["json", "form", "query", "msgpack", "yaml", "xml", "toml", "sonic", "all_extra_types", "typed_multipart"] +all_types = ["json", "form", "query", "msgpack", "yaml", "xml", "toml", "sonic", "cbor", "all_extra_types", "typed_multipart"] full_validator = ["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/src/cbor.rs b/src/cbor.rs new file mode 100644 index 0000000..61f2132 --- /dev/null +++ b/src/cbor.rs @@ -0,0 +1,179 @@ +//! # Support for `Cbor` +//! +//! ## Feature +//! +//! Enable the `cbor` feature to use `Valid>`. +//! +//! ## Usage +//! +//! 1. Implement `Deserialize` and `Validate` for your data type `T`. +//! 2. In your handler function, use `Valid>` as some parameter's type. +//! +//! ## Example +//! +//! ```no_run +//! #[cfg(feature = "validator")] +//! mod validator_example { +//! use axum::routing::post; +//! use axum_serde::Cbor; +//! use axum::Router; +//! use axum_valid::Valid; +//! use serde::Deserialize; +//! use validator::Validate; +//! +//! pub fn router() -> Router { +//! Router::new().route("/cbor", post(handler)) +//! } +//! +//! async fn handler(Valid(Cbor(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, +//! } +//! } +//! +//! #[cfg(feature = "garde")] +//! mod garde_example { +//! use axum::routing::post; +//! use axum_serde::Cbor; +//! use axum::Router; +//! use axum_valid::Garde; +//! use serde::Deserialize; +//! use garde::Validate; +//! +//! pub fn router() -> Router { +//! Router::new().route("/cbor", post(handler)) +//! } +//! +//! async fn handler(Garde(Cbor(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, +//! } +//! } +//! +//! # #[tokio::main] +//! # async fn main() -> anyhow::Result<()> { +//! # use std::net::SocketAddr; +//! # use axum::Router; +//! # use tokio::net::TcpListener; +//! # let router = Router::new(); +//! # #[cfg(feature = "validator")] +//! # let router = router.nest("/validator", validator_example::router()); +//! # #[cfg(feature = "garde")] +//! # let router = router.nest("/garde", garde_example::router()); +//! # let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?; +//! # axum::serve(listener, router.into_make_service()) +//! # .await?; +//! # Ok(()) +//! # } +//! ``` + +use crate::HasValidate; +#[cfg(feature = "validator")] +use crate::HasValidateArgs; +use axum_serde::Cbor; +#[cfg(feature = "validator")] +use validator::ValidateArgs; + +impl HasValidate for Cbor { + type Validate = T; + fn get_validate(&self) -> &T { + &self.0 + } +} + +#[cfg(feature = "validator")] +impl<'v, T: ValidateArgs<'v>> HasValidateArgs<'v> for Cbor { + type ValidateArgs = T; + fn get_validate_args(&self) -> &Self::ValidateArgs { + &self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::HasModify for Cbor { + type Modify = T; + + fn get_modify(&mut self) -> &mut Self::Modify { + &mut self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::PayloadExtractor for Cbor { + type Payload = T; + + fn get_payload(self) -> Self::Payload { + self.0 + } +} + +#[cfg(feature = "validify")] +impl crate::HasValidify for Cbor { + type Validify = T; + type PayloadExtractor = Cbor; + fn from_validify(v: Self::Validify) -> Self { + Cbor(v) + } +} + +#[cfg(test)] +mod tests { + use crate::tests::{ValidTest, ValidTestParameter}; + use axum::http::StatusCode; + use axum_serde::Cbor; + use reqwest::RequestBuilder; + use serde::Serialize; + + impl ValidTest for Cbor { + const ERROR_STATUS_CODE: StatusCode = StatusCode::UNPROCESSABLE_ENTITY; + + fn set_valid_request(builder: RequestBuilder) -> RequestBuilder { + let mut vec = Vec::new(); + ciborium::ser::into_writer(&T::valid(), &mut vec) + .expect("Failed to serialize parameters to cbor"); + builder + .header(reqwest::header::CONTENT_TYPE, "application/cbor") + .body(vec) + } + + fn set_error_request(builder: RequestBuilder) -> RequestBuilder { + #[derive(Serialize, Default)] + struct ErrorData { + error_field: i32, + } + let mut vec = Vec::new(); + ciborium::ser::into_writer(&ErrorData::default(), &mut vec) + .expect("Failed to serialize parameters to cbor"); + builder + .header(reqwest::header::CONTENT_TYPE, "application/cbor") + .body(vec) + } + + fn set_invalid_request(builder: RequestBuilder) -> RequestBuilder { + let mut vec = Vec::new(); + ciborium::ser::into_writer(&T::invalid(), &mut vec) + .expect("Failed to serialize parameters to cbor"); + builder + .header(reqwest::header::CONTENT_TYPE, "application/cbor") + .body(vec) + } + } +} diff --git a/src/garde/test.rs b/src/garde/test.rs index b06405e..c7540bb 100644 --- a/src/garde/test.rs +++ b/src/garde/test.rs @@ -148,6 +148,9 @@ async fn test_main() -> anyhow::Result<()> { #[cfg(feature = "sonic")] let router = router.route(sonic::route::SONIC, post(sonic::extract_sonic)); + #[cfg(feature = "cbor")] + let router = router.route(cbor::route::CBOR, post(cbor::extract_cbor)); + let router = router.with_state(MyState::default()); let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?; @@ -420,6 +423,14 @@ async fn test_main() -> anyhow::Result<()> { .await?; } + #[cfg(feature = "cbor")] + { + use axum_serde::Cbor; + test_executor + .execute::>(Method::POST, cbor::route::CBOR) + .await?; + } + Ok(()) } @@ -970,3 +981,19 @@ mod sonic { validate_again(parameters, ()) } } + +#[cfg(feature = "cbor")] +mod cbor { + use super::{validate_again, ParametersGarde}; + use crate::Garde; + use axum::http::StatusCode; + use axum_serde::Cbor; + + pub mod route { + pub const CBOR: &str = "/cbor"; + } + + pub async fn extract_cbor(Garde(Cbor(parameters)): Garde>) -> StatusCode { + validate_again(parameters, ()) + } +} diff --git a/src/lib.rs b/src/lib.rs index 0a189fe..d3dea81 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ pub mod validify; #[cfg(feature = "yaml")] pub mod yaml; +#[cfg(feature = "cbor")] +pub mod cbor; #[cfg(feature = "sonic")] pub mod sonic; #[cfg(feature = "toml")] diff --git a/src/validator/test.rs b/src/validator/test.rs index 1bbd93b..aa2e10d 100644 --- a/src/validator/test.rs +++ b/src/validator/test.rs @@ -289,6 +289,11 @@ async fn test_main() -> anyhow::Result<()> { .route(sonic::route::SONIC, post(sonic::extract_sonic)) .route(sonic::route::SONIC_EX, post(sonic::extract_sonic_ex)); + #[cfg(feature = "cbor")] + let router = router + .route(cbor::route::CBOR, post(cbor::extract_cbor)) + .route(cbor::route::CBOR_EX, post(cbor::extract_cbor_ex)); + let router = router.with_state(state); let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?; @@ -644,6 +649,17 @@ async fn test_main() -> anyhow::Result<()> { .await?; } + #[cfg(feature = "cbor")] + { + use axum_serde::Cbor; + test_executor + .execute::>(Method::POST, cbor::route::CBOR) + .await?; + test_executor + .execute::>(Method::POST, cbor::route::CBOR_EX) + .await?; + } + Ok(()) } @@ -1514,3 +1530,31 @@ mod sonic { validate_again_ex(parameters, &arguments) } } + +#[cfg(feature = "cbor")] +mod cbor { + use super::{ + validate_again, validate_again_ex, Parameters, ParametersEx, + ParametersExValidationArguments, + }; + use crate::{Valid, ValidEx}; + use axum::extract::State; + use axum::http::StatusCode; + use axum_serde::Cbor; + + pub mod route { + pub const CBOR: &str = "/cbor"; + pub const CBOR_EX: &str = "/cbor_ex"; + } + + pub async fn extract_cbor(Valid(Cbor(parameters)): Valid>) -> StatusCode { + validate_again(parameters) + } + + pub async fn extract_cbor_ex( + State(arguments): State, + ValidEx(Cbor(parameters)): ValidEx>, + ) -> StatusCode { + validate_again_ex(parameters, &arguments) + } +} diff --git a/src/validify/test.rs b/src/validify/test.rs index 92482c8..2430f16 100644 --- a/src/validify/test.rs +++ b/src/validify/test.rs @@ -383,6 +383,22 @@ async fn test_main() -> anyhow::Result<()> { post(sonic::extract_sonic_validified_by_ref), ); + #[cfg(feature = "cbor")] + let router = router + .route(cbor::route::CBOR, post(cbor::extract_cbor)) + .route( + cbor::route::CBOR_MODIFIED, + post(cbor::extract_cbor_modified), + ) + .route( + cbor::route::CBOR_VALIDIFIED, + post(cbor::extract_cbor_validified), + ) + .route( + cbor::route::CBOR_VALIDIFIED_BY_REF, + post(cbor::extract_cbor_validified_by_ref), + ); + let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?; let server_addr = listener.local_addr()?; let server = axum::serve(listener, router.into_make_service()); @@ -1066,6 +1082,31 @@ async fn test_main() -> anyhow::Result<()> { .await?; } + #[cfg(feature = "cbor")] + { + use axum_serde::Cbor; + + // Validated + test_executor + .execute::>(Method::POST, cbor::route::CBOR) + .await?; + // Modified + test_executor + .execute_modified::>(Method::POST, cbor::route::CBOR_MODIFIED) + .await?; + // Validified + test_executor + .execute_validified::>( + Method::POST, + cbor::route::CBOR_VALIDIFIED, + ) + .await?; + // ValidifiedByRef + test_executor + .execute::>(Method::POST, cbor::route::CBOR_VALIDIFIED_BY_REF) + .await?; + } + Ok(()) } @@ -2157,3 +2198,42 @@ mod sonic { check_validified(¶meters) } } + +#[cfg(feature = "cbor")] +mod cbor { + use super::{check_modified, check_validated, check_validified, ParametersValidify}; + use crate::{Modified, Validated, Validified, ValidifiedByRef}; + use axum::http::StatusCode; + use axum_serde::Cbor; + + pub mod route { + pub const CBOR: &str = "/cbor"; + pub const CBOR_MODIFIED: &str = "/cbor_modified"; + pub const CBOR_VALIDIFIED: &str = "/cbor_validified"; + pub const CBOR_VALIDIFIED_BY_REF: &str = "/cbor_validified_by_ref"; + } + + pub async fn extract_cbor( + Validated(Cbor(parameters)): Validated>, + ) -> StatusCode { + check_validated(¶meters) + } + + pub async fn extract_cbor_modified( + Modified(Cbor(parameters)): Modified>, + ) -> StatusCode { + check_modified(¶meters) + } + + pub async fn extract_cbor_validified( + Validified(Cbor(parameters)): Validified>, + ) -> StatusCode { + check_validified(¶meters) + } + + pub async fn extract_cbor_validified_by_ref( + ValidifiedByRef(Cbor(parameters)): ValidifiedByRef>, + ) -> StatusCode { + check_validified(¶meters) + } +}