commit 984a46d0bfa5ed77abeb49e15af10d7db6d2e0dc Author: gengteng Date: Tue Jun 6 09:45:14 2023 +0800 init diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8eb581d --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +/Cargo.lock +/.idea diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..1886159 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "axum-valid" +version = "0.1.0" +description = "Validator for axum" +authors = ["GengTeng "] +license = "MIT" +keywords = [ + "http", + "web", + "framework", + "validator", +] +categories = [ + "asynchronous", + "network-programming", + "web-programming", +] +edition = "2021" + + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +axum = "0.6.18" +serde = "1.0.163" +validator = "0.16.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..640d081 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# axum-valid + diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4e15204 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,124 @@ +use axum::body::HttpBody; +use axum::extract::rejection::{FormRejection, JsonRejection, PathRejection, QueryRejection}; +use axum::extract::{FromRequest, FromRequestParts, Path, Query}; +use axum::http::request::Parts; +use axum::http::{Request, StatusCode}; +use axum::response::{IntoResponse, Response}; +use axum::{async_trait, BoxError, Form, Json}; +use serde::de::DeserializeOwned; +use validator::{Validate, ValidationErrors}; + +#[derive(Debug, Clone, Copy, Default)] +pub struct Valid(pub T); + +pub enum ValidRejection { + Valid(ValidationErrors), + Inner(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(validate_error) => { + (StatusCode::BAD_REQUEST, validate_error.to_string()).into_response() + } + ValidRejection::Inner(json_error) => json_error.into_response(), + } + } +} + +impl From for ValidRejection { + fn from(value: JsonRejection) -> Self { + Self::Inner(value) + } +} + +#[async_trait] +impl FromRequest for Valid> +where + T: DeserializeOwned + Validate, + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, + S: Send + Sync, +{ + type Rejection = ValidRejection; + + async fn from_request(req: Request, state: &S) -> Result { + let json = Json::::from_request(req, state).await?; + json.0.validate()?; + Ok(Valid(json)) + } +} + +impl From for ValidRejection { + fn from(value: QueryRejection) -> Self { + Self::Inner(value) + } +} + +#[async_trait] +impl FromRequestParts for Valid> +where + T: DeserializeOwned + Validate, + S: Send + Sync, +{ + type Rejection = ValidRejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let query = Query::::from_request_parts(parts, state).await?; + query.validate()?; + Ok(Valid(query)) + } +} + +impl From for ValidRejection { + fn from(value: PathRejection) -> Self { + Self::Inner(value) + } +} + +#[async_trait] +impl FromRequestParts for Valid> +where + T: DeserializeOwned + Validate + Send, + S: Send + Sync, +{ + type Rejection = ValidRejection; + + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let path = Path::::from_request_parts(parts, state).await?; + path.validate()?; + Ok(Valid(path)) + } +} + +impl From for ValidRejection { + fn from(value: FormRejection) -> Self { + Self::Inner(value) + } +} + +#[async_trait] +impl FromRequest for Valid> +where + T: DeserializeOwned + Validate, + B: HttpBody + Send + 'static, + B::Data: Send, + B::Error: Into, + S: Send + Sync, +{ + type Rejection = ValidRejection; + + async fn from_request(req: Request, state: &S) -> Result { + let form = Form::::from_request(req, state).await?; + form.validate()?; + Ok(Valid(form)) + } +}