├── .github ├── dependabot.yml └── workflows │ └── rust.yml ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── README.md ├── release.toml ├── src ├── error.rs ├── form.rs ├── json.rs ├── lib.rs ├── path.rs ├── qsquery.rs └── query.rs └── tests ├── test_error_transformation.rs ├── test_form_validation.rs ├── test_json_validation.rs ├── test_path_validation.rs ├── test_qsquery_validation.rs └── test_query_validation.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "cargo" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "daily" 17 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | tests: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Update 20 | run: cargo update 21 | - name: Check 22 | run: cargo check --all 23 | - name: Run tests 24 | run: cargo test --all-features --all 25 | 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | **/*.rs.bk 3 | Cargo.lock 4 | **/target 5 | tarpaulin-report.html 6 | 7 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | 3 | services: 4 | - redis-server 5 | 6 | rust: 7 | - stable 8 | - nightly 9 | 10 | jobs: 11 | allow_failures: 12 | - rust: nightly 13 | fast_finish: true 14 | 15 | script: 16 | - cargo update 17 | - cargo check --all --no-default-features 18 | - cargo test --all-features --all 19 | 20 | after_success: 21 | - | 22 | if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then 23 | cargo install cargo-tarpaulin 24 | echo "Uploaded code coverage" 25 | cargo tarpaulin --ignore-tests --out Xml --ciserver travis-ci --coveralls $TRAVIS_JOB_ID 26 | fi 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [Unreleased] 8 | 9 | ## [6.0.0] 2024-07-11 10 | ### Changed 11 | - [#55](https://github.com/rambler-digital-solutions/actix-web-validator/pull/55): Update validator dependency to 0.18 12 | - [#53](https://github.com/rambler-digital-solutions/actix-web-validator/pull/53): Update serde_qs to 0.13 13 | 14 | ## [5.0.1] 2022-08-25 15 | ### Fixed 16 | - Update README 17 | 18 | ## [5.0.0] 2022-08-25 19 | ### Changed 20 | - [#37](https://github.com/rambler-digital-solutions/actix-web-validator/pull/37): Update validator dependency to 0.16 21 | 22 | ## [4.0.0] 2022-07-18 23 | ### Added 24 | - [#36](https://github.com/rambler-digital-solutions/actix-web-validator/pull/36): Support for nested errors in default output 25 | 26 | ### Changed 27 | - [#34](https://github.com/rambler-digital-solutions/actix-web-validator/pull/34): Update validator dependency to 0.15 28 | 29 | ## [3.0.0] 2022-03-15 30 | ### Added 31 | - [#29](https://github.com/rambler-digital-solutions/actix-web-validator/pull/29): Add actix-web 4.x support. 32 | 33 | ### Changed 34 | - Rust edition changed to 2021. 35 | 36 | ### Removed 37 | - [#29](https://github.com/rambler-digital-solutions/actix-web-validator/pull/29): ValidatedJson ValidatedQuery and ValidatedPath now removed. 38 | 39 | ## [2.2.0] 2021-10-04 40 | ### Added 41 | - [#27](https://github.com/rambler-digital-solutions/actix-web-validator/pull/27): Add validation support for Form extractor. 42 | 43 | ## [2.1.1] 2021-06-07 44 | ### Fixed 45 | - Fix QsQuery and QsQueryConfig documentation. 46 | 47 | ## [2.1.0] 2021-06-07 48 | ### Added 49 | - [#23](https://github.com/rambler-digital-solutions/actix-web-validator/pull/23): Add serde_qs support query deserialization. 50 | - [#20](https://github.com/rambler-digital-solutions/actix-web-validator/issues/20): Adds some additional information about validation errors into default HTTP response. 51 | 52 | ### Deprecated 53 | - Deprecate of reexporting `validator::Validate` trait. 54 | 55 | ## [2.0.3] 2021-01-12 56 | ### Added 57 | - [#17](https://github.com/rambler-digital-solutions/actix-web-validator/issues/17): Add reexport of Validate trait from validator. 58 | 59 | ## [2.0.2] 2020-12-28 60 | ### Fixed 61 | - [#16](https://github.com/rambler-digital-solutions/actix-web-validator/issues/16): Update validator dependency to 0.12. 62 | 63 | ## [2.0.1] 2020-09-27 64 | ### Fixed 65 | - [#15](https://github.com/rambler-digital-solutions/actix-web-validator/issues/15): Disable default features for actix-web dependency. 66 | 67 | ## [2.0.0] 2020-09-18 68 | ### Added 69 | - [#13](https://github.com/rambler-digital-solutions/actix-web-validator/issues/13): Add actix-web 3.x.x support. 70 | 71 | ### Deprecated 72 | - `ValidatedJson`, `ValidatedQuery` and `ValidatedPath` are deprecated in favor of same names from actix (`Json`, `Query` and `Path`). 73 | 74 | ## [1.0.0] 2020-06-06 75 | - [#11](https://github.com/rambler-digital-solutions/actix-web-validator/issues/11): Add actix-web 2.0.0 support. 76 | 77 | ## [0.2.1] 2020-06-05 78 | ### Added 79 | - Add documentation link and Readme into crates.io page. 80 | 81 | ## [0.2.0] 2020-06-05 82 | ### Added 83 | - [#6](https://github.com/rambler-digital-solutions/actix-web-validator/issues/6): Add Deref trait implementation for VaidatedQuery. 84 | - [#5](https://github.com/rambler-digital-solutions/actix-web-validator/issues/5): Add ValidatedJson implementation. 85 | - [#3](https://github.com/rambler-digital-solutions/actix-web-validator/issues/3): Add ValidatedPath implementation. 86 | - [#2](https://github.com/rambler-digital-solutions/actix-web-validator/issues/2): Add tests. 87 | - [#1](https://github.com/rambler-digital-solutions/actix-web-validator/issues/1): Add documentation. 88 | 89 | ## [0.1.2] 2020-02-28 90 | ### Added 91 | - Add implementation of `ValidatedQuery` 92 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-web-validator" 3 | version = "6.0.0" 4 | authors = ["Belousow Makc ", "Andrey Ermilov ", "Kolomatskiy Artem "] 5 | edition = "2021" 6 | description = "Validation mechanism for actix-web" 7 | keywords = ["actix-web", "serialization", "querystring", "validator", "validation"] 8 | categories = ["encoding", "web-programming"] 9 | license = "MIT" 10 | readme = "README.md" 11 | repository = "https://github.com/rambler-digital-solutions/actix-web-validator" 12 | documentation = "https://docs.rs/actix-web-validator/" 13 | 14 | [dependencies] 15 | actix-web = { version = "4", default-features = false } 16 | actix-http = { version = "3" } 17 | validator = { version = "0.18" } 18 | serde = "1" 19 | serde_urlencoded = "0.7" 20 | serde_json = "1" 21 | serde_qs = { version = "0.13", features = ["actix4"] } 22 | log = "0.4" 23 | futures = "0.3" 24 | mime = "0.3" 25 | bytes = "1" 26 | actix-router = "0.5" 27 | futures-util = "0.3" 28 | thiserror = "1.0" 29 | 30 | [dev-dependencies] 31 | actix-web = { version = "4", default-features = false, features = ["macros"] } 32 | validator = { version = "0.18", features = ["derive"]} 33 | serde = { version = "1", features = ["derive"] } 34 | actix-service = "2" 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Artem Kolomatskiy 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # actix-web-validator [![Latest Version]][crates.io] [![Documentation]][docs-rs] [![Coverage]][coveralls] [![Build Status]][travis] 2 | 3 | [Latest Version]: https://img.shields.io/crates/v/actix-web-validator 4 | [Documentation]: https://docs.rs/actix-web-validator/badge.svg 5 | [docs-rs]: https://docs.rs/actix-web-validator/ 6 | [crates.io]: https://crates.io/crates/actix-web-validator 7 | [Coverage]: https://coveralls.io/repos/github/rambler-digital-solutions/actix-web-validator/badge.svg?branch=master 8 | [coveralls]: https://coveralls.io/github/rambler-digital-solutions/actix-web-validator?branch=master 9 | [Build Status]: https://travis-ci.org/rambler-digital-solutions/actix-web-validator.svg?branch=master 10 | [travis]: https://travis-ci.org/rambler-digital-solutions/actix-web-validator 11 | [Validator crate]: https://crates.io/crates/validator 12 | 13 | This crate is a Rust library for providing validation mechanism to actix-web with [Validator crate]. 14 | 15 | Installation 16 | ============ 17 | 18 | This crate works with Cargo and can be found on 19 | [crates.io] with a `Cargo.toml` like: 20 | 21 | ```toml 22 | [dependencies] 23 | actix-web-validator = "6.0.0" 24 | validator = { version = "0.16", features = ["derive"] } 25 | serde = { version = "1", features = ["derive"] } 26 | ``` 27 | 28 | ## Supported extractors: 29 | * `actix_web::web::Json` 30 | * `actix_web::web::Query` 31 | * `actix_web::web::Path` 32 | * `actix_web::web::Form` 33 | * `serde_qs::actix::QsQuery` 34 | 35 | ### Supported `actix_web` versions: 36 | * For actix-web-validator `0.*` supported version of actix-web is `1.*` 37 | * For actix-web-validator `1.* ` supported version of actix-web is `2.*` 38 | * For actix-web-validator `2.* ` supported version of actix-web is `3.*` 39 | * For actix-web-validator `3+ ` supported version of actix-web is `4.*` 40 | 41 | ### Example: 42 | 43 | ```rust 44 | use actix_web::{web, App}; 45 | use serde::Deserialize; 46 | use actix_web_validator::Query; 47 | use validator::Validate; 48 | 49 | #[derive(Debug, Deserialize)] 50 | pub enum ResponseType { 51 | Token, 52 | Code 53 | } 54 | 55 | #[derive(Deserialize, Validate)] 56 | pub struct AuthRequest { 57 | #[validate(range(min = 1000, max = 9999))] 58 | id: u64, 59 | response_type: ResponseType, 60 | } 61 | 62 | // Use `Query` extractor for query information (and destructure it within the signature). 63 | // This handler gets called only if the request's query string contains a `id` and 64 | // `response_type` fields. 65 | // The correct request for this handler would be `/index.html?id=1234&response_type=Code"`. 66 | async fn index(info: Query) -> String { 67 | format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) 68 | } 69 | 70 | fn main() { 71 | let app = App::new().service( 72 | web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor 73 | } 74 | ``` 75 | 76 | ## License 77 | 78 | actix-web-validator is licensed under MIT license ([LICENSE](LICENSE) or http://opensource.org/licenses/MIT) 79 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | sign-commit = true 2 | sign-tag = true 3 | pre-release-commit-message = "Release {{crate_name}} {{version}} 🎉🎉" 4 | tag-message = "Release {{crate_name}} {{prefix}}{{version}}" 5 | tag-prefix = "" 6 | tag-name = "{{prefix}}{{version}}" 7 | 8 | pre-release-replacements = [ 9 | { file = "README.md", search = "actix-web-validator = \"[a-z0-9\\.-]+\"", replace = "actix-web-validator = \"{{version}}\"" }, 10 | { file = "CHANGELOG.md", search = "\\[Unreleased\\]", replace = "[{{version}}] {{date}}" }, 11 | { file = "CHANGELOG.md", search = "\\(https://semver.org/spec/v2.0.0.html\\).", replace = "(https://semver.org/spec/v2.0.0.html).\n\n## [Unreleased]" }, 12 | ] 13 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | //! Error declaration. 2 | use actix_web::http::StatusCode; 3 | use actix_web::{HttpResponse, ResponseError}; 4 | use thiserror::Error; 5 | use validator::{ValidationError, ValidationErrors, ValidationErrorsKind}; 6 | 7 | #[derive(Error, Debug)] 8 | pub enum Error { 9 | #[error("Validation error: {0}")] 10 | Validate(#[from] validator::ValidationErrors), 11 | #[error(transparent)] 12 | Deserialize(#[from] DeserializeErrors), 13 | #[error("Payload error: {0}")] 14 | JsonPayloadError(#[from] actix_web::error::JsonPayloadError), 15 | #[error("Url encoded error: {0}")] 16 | UrlEncodedError(#[from] actix_web::error::UrlencodedError), 17 | #[error("Query error: {0}")] 18 | QsError(#[from] serde_qs::Error), 19 | } 20 | 21 | #[derive(Error, Debug)] 22 | pub enum DeserializeErrors { 23 | #[error("Query deserialize error: {0}")] 24 | DeserializeQuery(serde_urlencoded::de::Error), 25 | #[error("Json deserialize error: {0}")] 26 | DeserializeJson(serde_json::error::Error), 27 | #[error("Path deserialize error: {0}")] 28 | DeserializePath(serde::de::value::Error), 29 | } 30 | 31 | impl From for Error { 32 | fn from(error: serde_json::error::Error) -> Self { 33 | Error::Deserialize(DeserializeErrors::DeserializeJson(error)) 34 | } 35 | } 36 | 37 | impl From for Error { 38 | fn from(error: serde_urlencoded::de::Error) -> Self { 39 | Error::Deserialize(DeserializeErrors::DeserializeQuery(error)) 40 | } 41 | } 42 | 43 | impl ResponseError for Error { 44 | fn error_response(&self) -> HttpResponse { 45 | HttpResponse::build(StatusCode::BAD_REQUEST).body(match self { 46 | Self::Validate(e) => { 47 | format!( 48 | "Validation errors in fields:\n{}", 49 | flatten_errors(e) 50 | .iter() 51 | .map(|(_, field, err)| { format!("\t{}: {}", field, err) }) 52 | .collect::>() 53 | .join("\n") 54 | ) 55 | } 56 | _ => format!("{}", *self), 57 | }) 58 | } 59 | } 60 | 61 | /// Helper function for error extraction and formatting. 62 | /// Return Vec of tuples where first element is full field path (separated by dot) 63 | /// and second is error. 64 | #[inline] 65 | pub fn flatten_errors(errors: &ValidationErrors) -> Vec<(u16, String, &ValidationError)> { 66 | _flatten_errors(errors, None, None) 67 | } 68 | 69 | #[inline] 70 | fn _flatten_errors( 71 | errors: &ValidationErrors, 72 | path: Option, 73 | indent: Option, 74 | ) -> Vec<(u16, String, &ValidationError)> { 75 | errors 76 | .errors() 77 | .iter() 78 | .flat_map(|(&field, err)| { 79 | let indent = indent.unwrap_or(0); 80 | let actual_path = path 81 | .as_ref() 82 | .map(|path| [path.as_str(), field].join(".")) 83 | .unwrap_or_else(|| field.to_owned()); 84 | match err { 85 | ValidationErrorsKind::Field(field_errors) => field_errors 86 | .iter() 87 | .map(|error| (indent, actual_path.clone(), error)) 88 | .collect::>(), 89 | ValidationErrorsKind::List(list_error) => list_error 90 | .iter() 91 | .flat_map(|(index, errors)| { 92 | let actual_path = format!("{}[{}]", actual_path.as_str(), index); 93 | _flatten_errors(errors, Some(actual_path), Some(indent + 1)) 94 | }) 95 | .collect::>(), 96 | ValidationErrorsKind::Struct(struct_errors) => { 97 | _flatten_errors(struct_errors, Some(actual_path), Some(indent + 1)) 98 | } 99 | } 100 | }) 101 | .collect::>() 102 | } 103 | -------------------------------------------------------------------------------- /src/form.rs: -------------------------------------------------------------------------------- 1 | use actix_http::Payload; 2 | use actix_web::{dev::UrlEncoded, FromRequest, HttpRequest}; 3 | use futures::future::LocalBoxFuture; 4 | use futures::FutureExt; 5 | use serde::de::DeserializeOwned; 6 | use std::{ops::Deref, rc::Rc}; 7 | use validator::Validate; 8 | 9 | use crate::Error; 10 | 11 | /// Form can be used for extracting typed information and validation 12 | /// from request's form data. 13 | /// 14 | /// To extract and typed information from request's form data, the type `T` must 15 | /// implement the `Deserialize` trait from *serde* 16 | /// and `Validate` trait from *validator* crate. 17 | /// 18 | /// [**FormConfig**](struct.FormConfig.html) allows to configure extraction 19 | /// process. 20 | /// 21 | /// ## Example 22 | /// 23 | /// ```rust 24 | /// use actix_web::{web, App}; 25 | /// use actix_web_validator::Form; 26 | /// use serde::Deserialize; 27 | /// use validator::Validate; 28 | /// 29 | /// #[derive(Deserialize, Validate)] 30 | /// struct Info { 31 | /// #[validate(length(min = 3))] 32 | /// username: String, 33 | /// } 34 | /// 35 | /// /// deserialize `Info` from request's form data 36 | /// async fn index(info: Form) -> String { 37 | /// format!("Welcome {}!", info.username) 38 | /// } 39 | /// 40 | /// fn main() { 41 | /// let app = App::new().service( 42 | /// web::resource("/index.html").route( 43 | /// web::post().to(index)) 44 | /// ); 45 | /// } 46 | /// ``` 47 | #[derive(Debug)] 48 | pub struct Form(pub T); 49 | 50 | impl Form { 51 | /// Deconstruct to an inner value 52 | pub fn into_inner(self) -> T { 53 | self.0 54 | } 55 | } 56 | 57 | impl AsRef for Form { 58 | fn as_ref(&self) -> &T { 59 | &self.0 60 | } 61 | } 62 | 63 | impl Deref for Form { 64 | type Target = T; 65 | 66 | fn deref(&self) -> &T { 67 | &self.0 68 | } 69 | } 70 | 71 | /// Form data helper (`application/x-www-form-urlencoded`). Allow to extract typed information from request's 72 | /// payload and validate it. 73 | /// 74 | /// To extract typed information from request's body, the type `T` must 75 | /// implement the `Deserialize` trait from *serde*. 76 | /// 77 | /// To validate payload, the type `T` must implement the `Validate` trait 78 | /// from *validator* crate. 79 | /// 80 | /// [**FormConfig**](struct.FormConfig.html) allows to configure extraction 81 | /// process. 82 | /// 83 | /// ## Example 84 | /// 85 | /// ```rust 86 | /// use actix_web::{web, App}; 87 | /// use actix_web_validator::Form; 88 | /// use serde::Deserialize; 89 | /// use validator::Validate; 90 | /// 91 | /// #[derive(Deserialize, Validate)] 92 | /// struct Info { 93 | /// #[validate(length(min = 3))] 94 | /// username: String, 95 | /// } 96 | /// 97 | /// /// deserialize `Info` from request's form data 98 | /// async fn index(info: Form) -> String { 99 | /// format!("Welcome {}!", info.username) 100 | /// } 101 | /// ``` 102 | impl FromRequest for Form 103 | where 104 | T: DeserializeOwned + Validate + 'static, 105 | { 106 | type Error = actix_web::Error; 107 | type Future = LocalBoxFuture<'static, Result>; 108 | 109 | #[inline] 110 | fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { 111 | let req2 = req.clone(); 112 | let (limit, error_handler) = req 113 | .app_data::() 114 | .map(|c| (c.limit, c.ehandler.clone())) 115 | .unwrap_or((16_384, None)); 116 | 117 | UrlEncoded::new(req, payload) 118 | .limit(limit) 119 | .map(|res: Result| match res { 120 | Ok(data) => data.validate().map(|_| Form(data)).map_err(Error::from), 121 | Err(e) => Err(Error::from(e)), 122 | }) 123 | .map(move |res| match res { 124 | Err(e) => { 125 | if let Some(err) = error_handler { 126 | Err((*err)(e, &req2)) 127 | } else { 128 | Err(e.into()) 129 | } 130 | } 131 | Ok(item) => Ok(item), 132 | }) 133 | .boxed_local() 134 | } 135 | } 136 | 137 | type ErrHandler = Rc actix_web::Error>; 138 | 139 | /// Form extractor configuration 140 | /// 141 | /// ```rust 142 | /// use actix_web::{error, web, App, FromRequest, HttpResponse}; 143 | /// use serde::Deserialize; 144 | /// use actix_web_validator::{Form, FormConfig}; 145 | /// use validator::Validate; 146 | /// 147 | /// #[derive(Deserialize, Validate)] 148 | /// struct Info { 149 | /// #[validate(length(min = 3))] 150 | /// username: String, 151 | /// } 152 | /// 153 | /// /// deserialize `Info` from request's form data, max payload size is 4kb 154 | /// async fn index(info: Form) -> String { 155 | /// format!("Welcome {}!", info.username) 156 | /// } 157 | /// 158 | /// fn main() { 159 | /// let form_config = FormConfig::default() 160 | /// .limit(4096) 161 | /// .error_handler(|err, req| { // <- create custom error response 162 | /// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() 163 | /// }); 164 | /// let app = App::new().service( 165 | /// web::resource("/index.html") 166 | /// .app_data(form_config) 167 | /// .route(web::post().to(index)) 168 | /// ); 169 | /// } 170 | /// ``` 171 | #[derive(Clone)] 172 | pub struct FormConfig { 173 | limit: usize, 174 | ehandler: Option, 175 | } 176 | 177 | impl FormConfig { 178 | /// Change max size of payload. By default max size is 16Kb 179 | pub fn limit(mut self, limit: usize) -> Self { 180 | self.limit = limit; 181 | self 182 | } 183 | 184 | /// Set custom error handler 185 | pub fn error_handler(mut self, f: F) -> Self 186 | where 187 | F: Fn(Error, &HttpRequest) -> actix_web::Error + 'static, 188 | { 189 | self.ehandler = Some(Rc::new(f)); 190 | self 191 | } 192 | } 193 | 194 | impl Default for FormConfig { 195 | fn default() -> Self { 196 | Self { 197 | limit: 16_384, 198 | ehandler: None, 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /src/json.rs: -------------------------------------------------------------------------------- 1 | //! Json extractor. 2 | use core::fmt::Debug; 3 | use std::ops::Deref; 4 | use std::sync::Arc; 5 | 6 | use actix_web::dev::{JsonBody, Payload}; 7 | use actix_web::FromRequest; 8 | use actix_web::HttpRequest; 9 | use futures::future::{FutureExt, LocalBoxFuture}; 10 | // use futures_util::future::{LocalBoxFuture, Try}; 11 | use serde::de::DeserializeOwned; 12 | use validator::Validate; 13 | 14 | use crate::error::Error; 15 | 16 | /// Json can be used for exstracting typed information and validation 17 | /// from request's payload. 18 | /// 19 | /// To extract and typed information from request's body, the type `T` must 20 | /// implement the `Deserialize` trait from *serde* 21 | /// and `Validate` trait from *validator* crate. 22 | /// 23 | /// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction 24 | /// process. 25 | /// 26 | /// ## Example 27 | /// 28 | /// ```rust 29 | /// use actix_web::{web, App}; 30 | /// use actix_web_validator::Json; 31 | /// use serde::Deserialize; 32 | /// use validator::Validate; 33 | /// 34 | /// #[derive(Deserialize, Validate)] 35 | /// struct Info { 36 | /// #[validate(length(min = 3))] 37 | /// username: String, 38 | /// } 39 | /// 40 | /// /// deserialize `Info` from request's body 41 | /// async fn index(info: Json) -> String { 42 | /// format!("Welcome {}!", info.username) 43 | /// } 44 | /// 45 | /// fn main() { 46 | /// let app = App::new().service( 47 | /// web::resource("/index.html").route( 48 | /// web::post().to(index)) 49 | /// ); 50 | /// } 51 | /// ``` 52 | #[derive(Debug)] 53 | pub struct Json(pub T); 54 | 55 | impl Json { 56 | /// Deconstruct to an inner value 57 | pub fn into_inner(self) -> T { 58 | self.0 59 | } 60 | } 61 | 62 | impl AsRef for Json { 63 | fn as_ref(&self) -> &T { 64 | &self.0 65 | } 66 | } 67 | 68 | impl Deref for Json { 69 | type Target = T; 70 | 71 | fn deref(&self) -> &T { 72 | &self.0 73 | } 74 | } 75 | 76 | /// Json extractor. Allow to extract typed information from request's 77 | /// payload and validate it. 78 | /// 79 | /// To extract typed information from request's body, the type `T` must 80 | /// implement the `Deserialize` trait from *serde*. 81 | /// 82 | /// To validate payload, the type `T` must implement the `Validate` trait 83 | /// from *validator* crate. 84 | /// 85 | /// [**JsonConfig**](struct.JsonConfig.html) allows to configure extraction 86 | /// process. 87 | /// 88 | /// ## Example 89 | /// 90 | /// ```rust 91 | /// use actix_web::{web, App}; 92 | /// use actix_web_validator::Json; 93 | /// use serde::Deserialize; 94 | /// use validator::Validate; 95 | /// 96 | /// #[derive(Deserialize, Validate)] 97 | /// struct Info { 98 | /// #[validate(length(min = 3))] 99 | /// username: String, 100 | /// } 101 | /// 102 | /// /// deserialize `Info` from request's body 103 | /// async fn index(info: Json) -> String { 104 | /// format!("Welcome {}!", info.username) 105 | /// } 106 | /// 107 | /// fn main() { 108 | /// let app = App::new().service( 109 | /// web::resource("/index.html").route( 110 | /// web::post().to(index)) 111 | /// ); 112 | /// } 113 | /// ``` 114 | impl FromRequest for Json 115 | where 116 | T: DeserializeOwned + Validate + 'static, 117 | { 118 | type Error = actix_web::Error; 119 | type Future = LocalBoxFuture<'static, Result>; 120 | 121 | #[inline] 122 | fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { 123 | let req2 = req.clone(); 124 | let (limit, err, ctype) = req 125 | .app_data::() 126 | .map(|c| (c.limit, c.ehandler.clone(), c.content_type.clone())) 127 | .unwrap_or((32768, None, None)); 128 | 129 | JsonBody::new(req, payload, ctype.as_deref(), false) 130 | .limit(limit) 131 | .map(|res: Result| match res { 132 | Ok(data) => data.validate().map(|_| Json(data)).map_err(Error::from), 133 | Err(e) => Err(Error::from(e)), 134 | }) 135 | .map(move |res| match res { 136 | Ok(data) => Ok(data), 137 | Err(e) => { 138 | log::debug!( 139 | "Failed to deserialize Json from payload. \ 140 | Request path: {}", 141 | req2.path() 142 | ); 143 | if let Some(err) = err { 144 | Err((*err)(e, &req2)) 145 | } else { 146 | Err(e.into()) 147 | } 148 | } 149 | }) 150 | .boxed_local() 151 | } 152 | } 153 | 154 | type ErrHandler = Arc actix_web::Error + Send + Sync>; 155 | 156 | /// Json extractor configuration 157 | /// 158 | /// ```rust 159 | /// use actix_web::{error, web, App, FromRequest, HttpResponse}; 160 | /// use serde::Deserialize; 161 | /// use actix_web_validator::{Json, JsonConfig}; 162 | /// use validator::Validate; 163 | /// 164 | /// #[derive(Deserialize, Validate)] 165 | /// struct Info { 166 | /// #[validate(length(min = 3))] 167 | /// username: String, 168 | /// } 169 | /// 170 | /// /// deserialize `Info` from request's body, max payload size is 4kb 171 | /// async fn index(info: Json) -> String { 172 | /// format!("Welcome {}!", info.username) 173 | /// } 174 | /// 175 | /// fn main() { 176 | /// let json_config = JsonConfig::default().limit(4096) 177 | /// .content_type(|mime| { // <- accept text/plain content type 178 | /// mime.type_() == mime::TEXT && mime.subtype() == mime::PLAIN 179 | /// }) 180 | /// .error_handler(|err, req| { // <- create custom error response 181 | /// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() 182 | /// }); 183 | /// let app = App::new().service( 184 | /// web::resource("/index.html") 185 | /// .app_data(json_config) 186 | /// .route(web::post().to(index)) 187 | /// ); 188 | /// } 189 | /// ``` 190 | #[derive(Clone)] 191 | pub struct JsonConfig { 192 | limit: usize, 193 | ehandler: Option, 194 | content_type: Option bool + Send + Sync>>, 195 | } 196 | 197 | impl JsonConfig { 198 | /// Change max size of payload. By default max size is 32Kb 199 | pub fn limit(mut self, limit: usize) -> Self { 200 | self.limit = limit; 201 | self 202 | } 203 | 204 | /// Set custom error handler 205 | pub fn error_handler(mut self, f: F) -> Self 206 | where 207 | F: Fn(Error, &HttpRequest) -> actix_web::Error + Send + Sync + 'static, 208 | { 209 | self.ehandler = Some(Arc::new(f)); 210 | self 211 | } 212 | 213 | /// Set predicate for allowed content types 214 | pub fn content_type(mut self, predicate: F) -> Self 215 | where 216 | F: Fn(mime::Mime) -> bool + Send + Sync + 'static, 217 | { 218 | self.content_type = Some(Arc::new(predicate)); 219 | self 220 | } 221 | } 222 | 223 | impl Default for JsonConfig { 224 | fn default() -> Self { 225 | JsonConfig { 226 | limit: 32768, 227 | ehandler: None, 228 | content_type: None, 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! *actix-web-validator* is a crate for providing validation mechanism to actix-web with *validator* crate. 2 | //! 3 | //! The main idea of this crate is to add full validation support provided by validator derive traits 4 | //! and provide maximum compatibility with base `actix_web::web::{Query, Json, Path}` structures. 5 | //! 6 | //! ## Example 7 | //! 8 | //! ```rust 9 | //! use actix_web::{web, App}; 10 | //! use serde::Deserialize; 11 | //! use actix_web_validator::Query; 12 | //! use validator::Validate; 13 | //! 14 | //! #[derive(Debug, Deserialize)] 15 | //! pub enum ResponseType { 16 | //! Token, 17 | //! Code 18 | //! } 19 | //! 20 | //! #[derive(Deserialize, Validate)] 21 | //! pub struct AuthRequest { 22 | //! #[validate(range(min = 1000, max = 9999))] 23 | //! id: u64, 24 | //! response_type: ResponseType, 25 | //! } 26 | //! 27 | //! // Use `Query` extractor for query information (and destructure it within the signature). 28 | //! // This handler gets called only if the request's query string contains a `id` and 29 | //! // `response_type` fields. 30 | //! // The correct request for this handler would be `/index.html?id=19&response_type=Code"`. 31 | //! async fn index(info: Query) -> String { 32 | //! assert!(info.id >= 1000); 33 | //! format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) 34 | //! } 35 | //! 36 | //! fn main() { 37 | //! let app = App::new().service( 38 | //! web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor 39 | //! } 40 | //! ``` 41 | pub mod error; 42 | mod form; 43 | mod json; 44 | mod path; 45 | mod qsquery; 46 | mod query; 47 | pub use error::Error; 48 | pub use form::*; 49 | pub use json::*; 50 | pub use path::*; 51 | pub use qsquery::*; 52 | pub use query::*; 53 | -------------------------------------------------------------------------------- /src/path.rs: -------------------------------------------------------------------------------- 1 | //! Path extractor. 2 | use std::fmt; 3 | use std::ops::Deref; 4 | use std::sync::Arc; 5 | 6 | use actix_router::PathDeserializer; 7 | use actix_web::dev::Payload; 8 | use actix_web::{FromRequest, HttpRequest}; 9 | use futures::future::{ready, Ready}; 10 | use serde::de::{Deserialize, DeserializeOwned}; 11 | use validator::Validate; 12 | 13 | use crate::error::{DeserializeErrors, Error}; 14 | 15 | /// Extract typed information from the request's path. 16 | /// 17 | /// ## Example 18 | /// 19 | /// It is possible to extract path information to a specific type that 20 | /// implements `Deserialize` trait from *serde* and `Validate` trait from *validator*. 21 | /// 22 | /// ```rust 23 | /// use actix_web::{web, App, Error}; 24 | /// use serde::Deserialize; 25 | /// use actix_web_validator::Path; 26 | /// use validator::Validate; 27 | /// 28 | /// #[derive(Deserialize, Validate)] 29 | /// struct Info { 30 | /// #[validate(length(min = 1))] 31 | /// username: String, 32 | /// } 33 | /// 34 | /// /// extract `Info` from a path using serde 35 | /// async fn index(info: Path) -> Result { 36 | /// Ok(format!("Welcome {}!", info.username)) 37 | /// } 38 | /// 39 | /// fn main() { 40 | /// let app = App::new().service( 41 | /// web::resource("/{username}/index.html") // <- define path parameters 42 | /// .route(web::get().to(index)) // <- use handler with Path` extractor 43 | /// ); 44 | /// } 45 | /// ``` 46 | #[derive(PartialEq, Eq, PartialOrd, Ord)] 47 | pub struct Path { 48 | inner: T, 49 | } 50 | 51 | impl Path { 52 | /// Deconstruct to an inner value 53 | pub fn into_inner(self) -> T { 54 | self.inner 55 | } 56 | } 57 | 58 | impl AsRef for Path { 59 | fn as_ref(&self) -> &T { 60 | &self.inner 61 | } 62 | } 63 | 64 | impl Deref for Path { 65 | type Target = T; 66 | 67 | fn deref(&self) -> &T { 68 | &self.inner 69 | } 70 | } 71 | 72 | impl fmt::Debug for Path { 73 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 74 | self.inner.fmt(f) 75 | } 76 | } 77 | 78 | impl fmt::Display for Path { 79 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 80 | self.inner.fmt(f) 81 | } 82 | } 83 | 84 | /// Extract typed information from the request's path. 85 | /// 86 | /// ## Example 87 | /// 88 | /// It is possible to extract path information to a specific type that 89 | /// implements `Deserialize` trait from *serde* and `Validate` trait from *validator*. 90 | /// 91 | /// ```rust 92 | /// use actix_web::{web, App, Error}; 93 | /// use serde::Deserialize; 94 | /// use actix_web_validator::Path; 95 | /// use validator::Validate; 96 | /// 97 | /// #[derive(Deserialize, Validate)] 98 | /// struct Info { 99 | /// #[validate(length(min = 1))] 100 | /// username: String, 101 | /// } 102 | /// 103 | /// /// extract `Info` from a path using serde 104 | /// async fn index(info: Path) -> Result { 105 | /// Ok(format!("Welcome {}!", info.username)) 106 | /// } 107 | /// 108 | /// fn main() { 109 | /// let app = App::new().service( 110 | /// web::resource("/{username}/index.html") // <- define path parameters 111 | /// .route(web::get().to(index)) // <- use handler with Path` extractor 112 | /// ); 113 | /// } 114 | /// ``` 115 | impl FromRequest for Path 116 | where 117 | T: DeserializeOwned + Validate, 118 | { 119 | type Error = actix_web::Error; 120 | type Future = Ready>; 121 | 122 | #[inline] 123 | fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { 124 | let error_handler = req 125 | .app_data::() 126 | .map(|c| c.ehandler.clone()) 127 | .unwrap_or(None); 128 | ready( 129 | Deserialize::deserialize(PathDeserializer::new(req.match_info())) 130 | .map_err(|error| Error::Deserialize(DeserializeErrors::DeserializePath(error))) 131 | .and_then(|value: T| { 132 | value 133 | .validate() 134 | .map(move |_| value) 135 | .map_err(Error::Validate) 136 | }) 137 | .map(|inner| Path { inner }) 138 | .map_err(move |e| { 139 | log::debug!( 140 | "Failed during Path extractor deserialization. \ 141 | Request path: {:?}", 142 | req.path() 143 | ); 144 | if let Some(error_handler) = error_handler { 145 | (error_handler)(e, req) 146 | } else { 147 | actix_web::error::ErrorNotFound(e) 148 | } 149 | }), 150 | ) 151 | } 152 | } 153 | 154 | type ErrHandler = Arc actix_web::Error + Send + Sync>; 155 | 156 | /// Path extractor configuration 157 | /// 158 | /// ```rust 159 | /// use actix_web_validator::{PathConfig, Path}; 160 | /// use actix_web::{error, web, App, FromRequest, HttpResponse}; 161 | /// use serde::Deserialize; 162 | /// use validator::Validate; 163 | /// 164 | /// #[derive(Deserialize, Debug)] 165 | /// enum Folder { 166 | /// #[serde(rename = "inbox")] 167 | /// Inbox, 168 | /// #[serde(rename = "outbox")] 169 | /// Outbox, 170 | /// } 171 | /// 172 | /// #[derive(Deserialize, Debug, Validate)] 173 | /// struct Filter { 174 | /// folder: Folder, 175 | /// #[validate(range(min = 1024))] 176 | /// id: u64, 177 | /// } 178 | /// 179 | /// // deserialize `Info` from request's path 180 | /// async fn index(folder: Path) -> String { 181 | /// format!("Selected folder: {:?}!", folder) 182 | /// } 183 | /// 184 | /// fn main() { 185 | /// let app = App::new().service( 186 | /// web::resource("/messages/{folder}") 187 | /// .app_data(PathConfig::default().error_handler(|err, req| { 188 | /// error::InternalError::from_response( 189 | /// err, 190 | /// HttpResponse::Conflict().finish(), 191 | /// ) 192 | /// .into() 193 | /// })) 194 | /// .route(web::post().to(index)), 195 | /// ); 196 | /// } 197 | /// ``` 198 | #[derive(Clone, Default)] 199 | pub struct PathConfig { 200 | ehandler: Option, 201 | } 202 | 203 | impl PathConfig { 204 | /// Set custom error handler 205 | pub fn error_handler(mut self, f: F) -> Self 206 | where 207 | F: Fn(Error, &HttpRequest) -> actix_web::Error + Send + Sync + 'static, 208 | { 209 | self.ehandler = Some(Arc::new(f)); 210 | self 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/qsquery.rs: -------------------------------------------------------------------------------- 1 | //! Query extractor (serde_qs based). 2 | use crate::error::Error; 3 | use std::ops::Deref; 4 | use std::sync::Arc; 5 | use std::{fmt, ops}; 6 | 7 | use actix_web::{FromRequest, HttpRequest}; 8 | use futures::future::{err, ok, Ready}; 9 | use serde::de; 10 | use serde_qs::Config as QsConfig; 11 | use validator::Validate; 12 | 13 | type ErrHandler = Arc actix_web::Error + Send + Sync>; 14 | 15 | /// Query extractor configuration (serde_qs based). 16 | /// 17 | /// ```rust 18 | /// use actix_web::{error, web, App, FromRequest, HttpResponse}; 19 | /// use actix_web_validator::QsQueryConfig; 20 | /// use serde_qs::actix::QsQuery; 21 | /// use serde_qs::Config as QsConfig; 22 | /// use serde::Deserialize; 23 | /// 24 | /// #[derive(Deserialize)] 25 | /// struct Info { 26 | /// username: String, 27 | /// } 28 | /// 29 | /// /// deserialize `Info` from request's querystring 30 | /// async fn index(info: QsQuery) -> String { 31 | /// format!("Welcome {}!", info.username) 32 | /// } 33 | /// 34 | /// fn main() { 35 | /// let qs_query_config = QsQueryConfig::default() 36 | /// .error_handler(|err, req| { // <- create custom error response 37 | /// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() 38 | /// }) 39 | /// .qs_config(QsConfig::default()); 40 | /// let app = App::new().service( 41 | /// web::resource("/index.html").app_data(qs_query_config) 42 | /// .route(web::post().to(index)) 43 | /// ); 44 | /// } 45 | /// ``` 46 | #[derive(Default)] 47 | pub struct QsQueryConfig { 48 | ehandler: Option, 49 | qs_config: QsConfig, 50 | } 51 | 52 | impl QsQueryConfig { 53 | /// Set custom error handler 54 | pub fn error_handler(mut self, f: F) -> Self 55 | where 56 | F: Fn(Error, &HttpRequest) -> actix_web::Error + Send + Sync + 'static, 57 | { 58 | self.ehandler = Some(Arc::new(f)); 59 | self 60 | } 61 | 62 | /// Set custom serialization parameters 63 | pub fn qs_config(mut self, config: QsConfig) -> Self { 64 | self.qs_config = config; 65 | self 66 | } 67 | } 68 | 69 | /// Extract and validate typed information from the request's query (serde_qs based). 70 | /// 71 | /// For query decoding uses [serde_qs](https://docs.rs/serde_qs/latest/serde_qs/) crate 72 | /// [`QsQueryConfig`] allows to configure extraction process. 73 | /// 74 | /// ## Example 75 | /// 76 | /// ```rust 77 | /// use actix_web::{web, App}; 78 | /// use serde::Deserialize; 79 | /// use actix_web_validator::QsQuery; 80 | /// use validator::Validate; 81 | /// 82 | /// #[derive(Debug, Deserialize)] 83 | /// pub enum ResponseType { 84 | /// Token, 85 | /// Code 86 | /// } 87 | /// 88 | /// #[derive(Deserialize, Validate)] 89 | /// pub struct AuthRequest { 90 | /// #[validate(range(min = 1000, max = 9999))] 91 | /// id: u64, 92 | /// response_type: ResponseType, 93 | /// } 94 | /// 95 | /// // Use `Query` extractor for query information (and destructure it within the signature). 96 | /// // This handler gets called only if the request's query string contains a `id` and 97 | /// // `response_type` fields. 98 | /// // The correct request for this handler would be `/index.html?id=1234&response_type=Code"`. 99 | /// async fn index(info: QsQuery) -> String { 100 | /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) 101 | /// } 102 | /// 103 | /// fn main() { 104 | /// let app = App::new().service( 105 | /// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor 106 | /// } 107 | /// ``` 108 | #[derive(PartialEq, Eq, PartialOrd, Ord)] 109 | pub struct QsQuery(pub T); 110 | 111 | impl AsRef for QsQuery { 112 | fn as_ref(&self) -> &T { 113 | &self.0 114 | } 115 | } 116 | 117 | impl Deref for QsQuery { 118 | type Target = T; 119 | 120 | fn deref(&self) -> &T { 121 | &self.0 122 | } 123 | } 124 | 125 | impl ops::DerefMut for QsQuery { 126 | fn deref_mut(&mut self) -> &mut T { 127 | &mut self.0 128 | } 129 | } 130 | 131 | impl fmt::Debug for QsQuery { 132 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 133 | self.0.fmt(f) 134 | } 135 | } 136 | 137 | impl fmt::Display for QsQuery { 138 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 139 | self.0.fmt(f) 140 | } 141 | } 142 | 143 | impl QsQuery 144 | where 145 | T: Validate, 146 | { 147 | /// Deconstruct to an inner value. 148 | pub fn into_inner(self) -> T { 149 | self.0 150 | } 151 | } 152 | 153 | /// Extract typed information from the request's query. 154 | /// 155 | /// ## Example 156 | /// 157 | /// ```rust 158 | /// use actix_web::{web, App}; 159 | /// use serde::Deserialize; 160 | /// use actix_web_validator::QsQuery; 161 | /// use validator::Validate; 162 | /// 163 | /// #[derive(Debug, Deserialize)] 164 | /// pub enum ResponseType { 165 | /// Token, 166 | /// Code 167 | /// } 168 | /// 169 | /// #[derive(Deserialize, Validate)] 170 | /// pub struct AuthRequest { 171 | /// #[validate(range(min = 1000, max = 9999))] 172 | /// id: u64, 173 | /// response_type: ResponseType, 174 | /// } 175 | /// 176 | /// // Use `Query` extractor for query information (and destructure it within the signature). 177 | /// // This handler gets called only if the request's query string contains a `id` and 178 | /// // `response_type` fields. 179 | /// // The correct request for this handler would be `/index.html?id=19&response_type=Code"`. 180 | /// async fn index(QsQuery(info): QsQuery) -> String { 181 | /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) 182 | /// } 183 | /// 184 | /// fn main() { 185 | /// let app = App::new().service( 186 | /// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor 187 | /// } 188 | /// ``` 189 | impl FromRequest for QsQuery 190 | where 191 | T: de::DeserializeOwned + Validate, 192 | { 193 | type Error = actix_web::Error; 194 | type Future = Ready>; 195 | 196 | /// Builds Query struct from request and provides validation mechanism 197 | #[inline] 198 | fn from_request(req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { 199 | let query_config = req.app_data::(); 200 | 201 | let error_handler = query_config.map(|c| c.ehandler.clone()).unwrap_or(None); 202 | 203 | let default_qsconfig = QsConfig::default(); 204 | let qsconfig = query_config 205 | .map(|c| &c.qs_config) 206 | .unwrap_or(&default_qsconfig); 207 | 208 | qsconfig 209 | .deserialize_str::(req.query_string()) 210 | .map_err(Error::from) 211 | .and_then(|value| { 212 | value 213 | .validate() 214 | .map(move |_| value) 215 | .map_err(Error::Validate) 216 | }) 217 | .map_err(move |e| { 218 | log::debug!( 219 | "Failed during Query extractor validation. \ 220 | Request path: {:?}", 221 | req.path() 222 | ); 223 | if let Some(error_handler) = error_handler { 224 | (error_handler)(e, req) 225 | } else { 226 | e.into() 227 | } 228 | }) 229 | .map(|value| ok(QsQuery(value))) 230 | .unwrap_or_else(err) 231 | } 232 | } 233 | -------------------------------------------------------------------------------- /src/query.rs: -------------------------------------------------------------------------------- 1 | //! Query extractor. 2 | use crate::error::Error; 3 | use std::ops::Deref; 4 | use std::sync::Arc; 5 | use std::{fmt, ops}; 6 | 7 | use actix_web::{FromRequest, HttpRequest}; 8 | use futures::future::{err, ok, Ready}; 9 | use serde::de; 10 | use validator::Validate; 11 | 12 | type ErrHandler = Arc actix_web::Error + Send + Sync>; 13 | 14 | /// Query extractor configuration. 15 | /// 16 | /// ## Example 17 | /// 18 | /// ```rust 19 | /// use actix_web::{error, web, App, FromRequest, HttpResponse}; 20 | /// use serde::Deserialize; 21 | /// use actix_web_validator::{Query, QueryConfig}; 22 | /// use validator::Validate; 23 | /// 24 | /// #[derive(Deserialize, Validate)] 25 | /// struct Info { 26 | /// #[validate(length(min = 1))] 27 | /// username: String, 28 | /// } 29 | /// 30 | /// /// deserialize `Info` from request's querystring 31 | /// async fn index(info: Query) -> String { 32 | /// format!("Welcome {}!", info.username) 33 | /// } 34 | /// 35 | /// fn main() { 36 | /// let query_config = QueryConfig::default() 37 | /// .error_handler(|err, req| { // <- create custom error response 38 | /// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() 39 | /// }); 40 | /// let app = App::new().service( 41 | /// web::resource("/index.html") 42 | /// .app_data(query_config) 43 | /// .route(web::post().to(index)) 44 | /// ); 45 | /// } 46 | /// ``` 47 | #[derive(Clone, Default)] 48 | pub struct QueryConfig { 49 | pub ehandler: Option, 50 | } 51 | 52 | impl QueryConfig { 53 | /// Set custom error handler 54 | pub fn error_handler(mut self, f: F) -> Self 55 | where 56 | F: Fn(Error, &HttpRequest) -> actix_web::Error + Send + Sync + 'static, 57 | { 58 | self.ehandler = Some(Arc::new(f)); 59 | self 60 | } 61 | } 62 | 63 | /// Extract and validate typed information from the request's query. 64 | /// 65 | /// For query decoding uses *serde_urlencoded* crate 66 | /// [**QueryConfig**](struct.QueryConfig.html) allows to configure extraction process. 67 | /// 68 | /// ## Example 69 | /// 70 | /// ```rust 71 | /// use actix_web::{web, App}; 72 | /// use serde::Deserialize; 73 | /// use actix_web_validator::Query; 74 | /// use validator::Validate; 75 | /// 76 | /// #[derive(Debug, Deserialize)] 77 | /// pub enum ResponseType { 78 | /// Token, 79 | /// Code 80 | /// } 81 | /// 82 | /// #[derive(Deserialize, Validate)] 83 | /// pub struct AuthRequest { 84 | /// #[validate(range(min = 1000, max = 9999))] 85 | /// id: u64, 86 | /// response_type: ResponseType, 87 | /// } 88 | /// 89 | /// // Use `Query` extractor for query information (and destructure it within the signature). 90 | /// // This handler gets called only if the request's query string contains a `id` and 91 | /// // `response_type` fields. 92 | /// // The correct request for this handler would be `/index.html?id=1234&response_type=Code"`. 93 | /// async fn index(info: Query) -> String { 94 | /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) 95 | /// } 96 | /// 97 | /// fn main() { 98 | /// let app = App::new().service( 99 | /// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor 100 | /// } 101 | /// ``` 102 | #[derive(PartialEq, Eq, PartialOrd, Ord)] 103 | pub struct Query(pub T); 104 | 105 | impl AsRef for Query { 106 | fn as_ref(&self) -> &T { 107 | &self.0 108 | } 109 | } 110 | 111 | impl Deref for Query { 112 | type Target = T; 113 | 114 | fn deref(&self) -> &T { 115 | &self.0 116 | } 117 | } 118 | 119 | impl ops::DerefMut for Query { 120 | fn deref_mut(&mut self) -> &mut T { 121 | &mut self.0 122 | } 123 | } 124 | 125 | impl fmt::Debug for Query { 126 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 127 | self.0.fmt(f) 128 | } 129 | } 130 | 131 | impl fmt::Display for Query { 132 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 133 | self.0.fmt(f) 134 | } 135 | } 136 | 137 | impl Query 138 | where 139 | T: Validate, 140 | { 141 | /// Deconstruct to an inner value. 142 | pub fn into_inner(self) -> T { 143 | self.0 144 | } 145 | } 146 | 147 | /// Extract typed information from the request's query. 148 | /// 149 | /// ## Example 150 | /// 151 | /// ```rust 152 | /// use actix_web::{web, App}; 153 | /// use serde::Deserialize; 154 | /// use actix_web_validator::Query; 155 | /// use validator::Validate; 156 | /// 157 | /// #[derive(Debug, Deserialize)] 158 | /// pub enum ResponseType { 159 | /// Token, 160 | /// Code 161 | /// } 162 | /// 163 | /// #[derive(Deserialize, Validate)] 164 | /// pub struct AuthRequest { 165 | /// #[validate(range(min = 1000, max = 9999))] 166 | /// id: u64, 167 | /// response_type: ResponseType, 168 | /// } 169 | /// 170 | /// // Use `Query` extractor for query information (and destructure it within the signature). 171 | /// // This handler gets called only if the request's query string contains a `id` and 172 | /// // `response_type` fields. 173 | /// // The correct request for this handler would be `/index.html?id=19&response_type=Code"`. 174 | /// async fn index(web::Query(info): web::Query) -> String { 175 | /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) 176 | /// } 177 | /// 178 | /// fn main() { 179 | /// let app = App::new().service( 180 | /// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor 181 | /// } 182 | /// ``` 183 | impl FromRequest for Query 184 | where 185 | T: de::DeserializeOwned + Validate, 186 | { 187 | type Error = actix_web::Error; 188 | type Future = Ready>; 189 | 190 | /// Builds Query struct from request and provides validation mechanism 191 | #[inline] 192 | fn from_request(req: &actix_web::HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { 193 | let error_handler = req 194 | .app_data::() 195 | .map(|c| c.ehandler.clone()) 196 | .unwrap_or(None); 197 | 198 | serde_urlencoded::from_str::(req.query_string()) 199 | .map_err(Error::from) 200 | .and_then(|value| { 201 | value 202 | .validate() 203 | .map(move |_| value) 204 | .map_err(Error::Validate) 205 | }) 206 | .map_err(move |e| { 207 | log::debug!( 208 | "Failed during Query extractor validation. \ 209 | Request path: {:?}", 210 | req.path() 211 | ); 212 | if let Some(error_handler) = error_handler { 213 | (error_handler)(e, req) 214 | } else { 215 | e.into() 216 | } 217 | }) 218 | .map(|value| ok(Query(value))) 219 | .unwrap_or_else(err) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /tests/test_error_transformation.rs: -------------------------------------------------------------------------------- 1 | use actix_web_validator::{error::DeserializeErrors, Error}; 2 | use serde::{Deserialize, Serialize}; 3 | use validator::{Validate, ValidationErrorsKind}; 4 | 5 | #[derive(Deserialize)] 6 | #[allow(dead_code)] 7 | struct Query { 8 | test: i32, 9 | value: i32, 10 | } 11 | 12 | #[test] 13 | fn test_serde_urlencoded_error_transformation() { 14 | let error = serde_urlencoded::from_str::("test=42&value=[").map_err(Error::from); 15 | assert!(matches!( 16 | error, 17 | Err(Error::Deserialize(DeserializeErrors::DeserializeQuery(_))) 18 | )); 19 | } 20 | 21 | #[test] 22 | fn test_serde_json_error_transformation() { 23 | let error = 24 | serde_json::from_str::("{\"test\": 42, \"value\": null}").map_err(Error::from); 25 | assert!(matches!( 26 | error, 27 | Err(Error::Deserialize(DeserializeErrors::DeserializeJson(_))) 28 | )); 29 | } 30 | 31 | #[derive(Serialize, Deserialize, Validate, Debug)] 32 | #[serde(rename_all = "camelCase")] 33 | pub struct SearchParams { 34 | #[validate(nested)] 35 | page_params: PageParams, 36 | #[validate(url)] 37 | redirect_results: String, 38 | } 39 | 40 | #[derive(Serialize, Deserialize, Validate, Debug)] 41 | #[serde(rename_all = "camelCase")] 42 | pub struct PageParams { 43 | #[validate(range(min = 1))] 44 | page: u16, 45 | #[validate(range(min = 1, max = 100))] 46 | page_size: u8, 47 | } 48 | 49 | macro_rules! cast { 50 | ($target: expr, $pat: path) => {{ 51 | if let $pat(a) = $target { 52 | a 53 | } else { 54 | panic!("mismatch variant when cast to {}", stringify!($pat)); 55 | } 56 | }}; 57 | } 58 | 59 | #[test] 60 | fn test_flatten_error() { 61 | let params = serde_json::from_str::( 62 | "{\"pageParams\":{\"page\":0,\"pageSize\":101},\"redirectResults\":\"invalid url\"}", 63 | ) 64 | .map_err(crate::Error::from) 65 | .expect("invalid json"); 66 | let validation = params.validate().unwrap_err(); 67 | let errors = actix_web_validator::error::flatten_errors(&validation); 68 | assert_eq!( 69 | ( 70 | &1u16, 71 | &cast!( 72 | &validation.errors().get("page_params").unwrap(), 73 | ValidationErrorsKind::Struct 74 | ) 75 | .field_errors() 76 | .get("page_size") 77 | .unwrap()[0] 78 | ), 79 | errors 80 | .iter() 81 | .find(|(_, field, _)| field == "page_params.page_size") 82 | .map(|(indent, _, e)| (indent, *e)) 83 | .unwrap() 84 | ); 85 | assert_eq!( 86 | ( 87 | &0u16, 88 | &validation.field_errors().get("redirect_results").unwrap()[0] 89 | ), 90 | errors 91 | .iter() 92 | .find(|(_, field, _)| field == "redirect_results") 93 | .map(|(indent, _, e)| (indent, *e)) 94 | .unwrap() 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /tests/test_form_validation.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | error, 3 | http::StatusCode, 4 | test, 5 | test::call_service, 6 | web::{self}, 7 | App, HttpResponse, 8 | }; 9 | use actix_web_validator::{Form, FormConfig}; 10 | use serde::{Deserialize, Serialize}; 11 | use validator::Validate; 12 | 13 | #[derive(Debug, PartialEq, Validate, Serialize, Deserialize)] 14 | struct FormData { 15 | #[validate(url)] 16 | page_url: String, 17 | #[validate(range(min = 18, max = 28))] 18 | age: u8, 19 | } 20 | 21 | async fn test_handler(query: Form) -> HttpResponse { 22 | dbg!(&query.into_inner()); 23 | HttpResponse::Ok().finish() 24 | } 25 | 26 | #[actix_web::test] 27 | async fn test_form_validation() { 28 | let mut app = test::init_service( 29 | App::new().service(web::resource("/test").route(web::post().to(test_handler))), 30 | ) 31 | .await; 32 | 33 | // Test 200 status 34 | let req = test::TestRequest::post() 35 | .uri("/test") 36 | .set_form(&FormData { 37 | page_url: "https://my_page.com".to_owned(), 38 | age: 24, 39 | }) 40 | .to_request(); 41 | let resp = call_service(&mut app, req).await; 42 | assert_eq!(resp.status(), StatusCode::OK); 43 | 44 | // Test 400 status 45 | let req = test::TestRequest::post() 46 | .uri("/test") 47 | .set_form(&FormData { 48 | page_url: "invalid_url".to_owned(), 49 | age: 24, 50 | }) 51 | .to_request(); 52 | let resp = call_service(&mut app, req).await; 53 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 54 | } 55 | 56 | #[actix_web::test] 57 | async fn test_custom_form_validation_error() { 58 | let form_config = FormConfig::default().error_handler(|err, _req| { 59 | error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() 60 | }); 61 | let mut app = test::init_service( 62 | App::new().service( 63 | web::resource("/test") 64 | .app_data(form_config) 65 | .route(web::post().to(test_handler)), 66 | ), 67 | ) 68 | .await; 69 | 70 | let req = test::TestRequest::post() 71 | .uri("/test") 72 | .set_form(&FormData { 73 | page_url: "invalid".to_owned(), 74 | age: 24, 75 | }) 76 | .to_request(); 77 | let resp = call_service(&mut app, req).await; 78 | dbg!(&resp); 79 | assert_eq!(resp.status(), StatusCode::CONFLICT); 80 | } 81 | 82 | #[actix_web::test] 83 | async fn test_validated_form_asref_deref() { 84 | let mut app = test::init_service(App::new().service(web::resource("/test").to( 85 | |payload: Form| async move { 86 | assert_eq!(payload.age, 24); 87 | let reference = FormData { 88 | page_url: "https://my_page.com".to_owned(), 89 | age: 24, 90 | }; 91 | assert_eq!(payload.as_ref(), &reference); 92 | HttpResponse::Ok().finish() 93 | }, 94 | ))) 95 | .await; 96 | 97 | let req = test::TestRequest::post() 98 | .uri("/test") 99 | .set_form(&FormData { 100 | page_url: "https://my_page.com".to_owned(), 101 | age: 24, 102 | }) 103 | .to_request(); 104 | call_service(&mut app, req).await; 105 | } 106 | 107 | #[actix_web::test] 108 | async fn test_validated_form_into_inner() { 109 | let mut app = test::init_service(App::new().service(web::resource("/test").to( 110 | |payload: Form| async { 111 | let payload = payload.into_inner(); 112 | assert_eq!(payload.age, 24); 113 | assert_eq!(payload.page_url, "https://my_page.com"); 114 | HttpResponse::Ok().finish() 115 | }, 116 | ))) 117 | .await; 118 | 119 | let req = test::TestRequest::post() 120 | .uri("/test") 121 | .set_form(&FormData { 122 | page_url: "https://my_page.com".to_owned(), 123 | age: 24, 124 | }) 125 | .to_request(); 126 | call_service(&mut app, req).await; 127 | } 128 | 129 | #[actix_web::test] 130 | async fn test_validated_form_limit() { 131 | let mut app = test::init_service( 132 | App::new() 133 | .app_data(FormConfig::default().limit(1)) 134 | .service(web::resource("/test").route(web::post().to(test_handler))), 135 | ) 136 | .await; 137 | 138 | let req = test::TestRequest::post() 139 | .uri("/test") 140 | .set_form(&FormData { 141 | page_url: "https://my_page.com".to_owned(), 142 | age: 24, 143 | }) 144 | .to_request(); 145 | let resp = call_service(&mut app, req).await; 146 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 147 | } 148 | -------------------------------------------------------------------------------- /tests/test_json_validation.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{error, http::StatusCode, test, test::call_service, web, App, HttpResponse}; 2 | use actix_web_validator::{Json, JsonConfig}; 3 | use serde::{Deserialize, Serialize}; 4 | use validator::Validate; 5 | 6 | #[derive(Debug, PartialEq, Validate, Serialize, Deserialize)] 7 | struct JsonPayload { 8 | #[validate(url)] 9 | page_url: String, 10 | #[validate(range(min = 18, max = 28))] 11 | age: u8, 12 | } 13 | 14 | async fn test_handler(query: Json) -> HttpResponse { 15 | dbg!(&query.into_inner()); 16 | HttpResponse::Ok().finish() 17 | } 18 | 19 | #[actix_web::test] 20 | async fn test_json_validation() { 21 | let mut app = test::init_service( 22 | App::new().service(web::resource("/test").route(web::post().to(test_handler))), 23 | ) 24 | .await; 25 | 26 | // Test 200 status 27 | let req = test::TestRequest::post() 28 | .uri("/test") 29 | .set_json(&JsonPayload { 30 | page_url: "https://my_page.com".to_owned(), 31 | age: 24, 32 | }) 33 | .to_request(); 34 | let resp = call_service(&mut app, req).await; 35 | assert_eq!(resp.status(), StatusCode::OK); 36 | 37 | // Test 400 status 38 | let req = test::TestRequest::post() 39 | .uri("/test") 40 | .set_json(&JsonPayload { 41 | page_url: "invalid_url".to_owned(), 42 | age: 24, 43 | }) 44 | .to_request(); 45 | let resp = call_service(&mut app, req).await; 46 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 47 | } 48 | 49 | #[actix_web::test] 50 | async fn test_custom_json_validation_error() { 51 | let json_config = JsonConfig::default().error_handler(|err, _req| { 52 | error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() 53 | }); 54 | let mut app = test::init_service( 55 | App::new().service( 56 | web::resource("/test") 57 | .app_data(json_config) 58 | .route(web::post().to(test_handler)), 59 | ), 60 | ) 61 | .await; 62 | 63 | let req = test::TestRequest::post() 64 | .uri("/test") 65 | .set_json(&JsonPayload { 66 | page_url: "invalid".to_owned(), 67 | age: 24, 68 | }) 69 | .to_request(); 70 | let resp = call_service(&mut app, req).await; 71 | dbg!(&resp); 72 | assert_eq!(resp.status(), StatusCode::CONFLICT); 73 | } 74 | 75 | #[actix_web::test] 76 | async fn test_validated_json_asref_deref() { 77 | let mut app = test::init_service(App::new().service(web::resource("/test").to( 78 | |payload: Json| async move { 79 | assert_eq!(payload.age, 24); 80 | let reference = JsonPayload { 81 | page_url: "https://my_page.com".to_owned(), 82 | age: 24, 83 | }; 84 | assert_eq!(payload.as_ref(), &reference); 85 | HttpResponse::Ok().finish() 86 | }, 87 | ))) 88 | .await; 89 | 90 | let req = test::TestRequest::post() 91 | .uri("/test") 92 | .set_json(&JsonPayload { 93 | page_url: "https://my_page.com".to_owned(), 94 | age: 24, 95 | }) 96 | .to_request(); 97 | call_service(&mut app, req).await; 98 | } 99 | 100 | #[actix_web::test] 101 | async fn test_validated_json_into_inner() { 102 | let mut app = test::init_service(App::new().service(web::resource("/test").to( 103 | |payload: Json| async { 104 | let payload = payload.into_inner(); 105 | assert_eq!(payload.age, 24); 106 | assert_eq!(payload.page_url, "https://my_page.com"); 107 | HttpResponse::Ok().finish() 108 | }, 109 | ))) 110 | .await; 111 | 112 | let req = test::TestRequest::post() 113 | .uri("/test") 114 | .set_json(&JsonPayload { 115 | page_url: "https://my_page.com".to_owned(), 116 | age: 24, 117 | }) 118 | .to_request(); 119 | call_service(&mut app, req).await; 120 | } 121 | 122 | #[actix_web::test] 123 | async fn test_validated_json_limit() { 124 | let mut app = test::init_service( 125 | App::new() 126 | .app_data(JsonConfig::default().limit(1)) 127 | .service(web::resource("/test").route(web::post().to(test_handler))), 128 | ) 129 | .await; 130 | 131 | let req = test::TestRequest::post() 132 | .uri("/test") 133 | .set_json(&JsonPayload { 134 | page_url: "https://my_page.com".to_owned(), 135 | age: 24, 136 | }) 137 | .to_request(); 138 | let resp = call_service(&mut app, req).await; 139 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 140 | } 141 | -------------------------------------------------------------------------------- /tests/test_path_validation.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | use actix_web::{error, http::StatusCode, test, test::call_service, web, App, HttpResponse}; 4 | use actix_web_validator::Path; 5 | use serde::Deserialize; 6 | use validator::Validate; 7 | 8 | #[derive(Debug, Validate, Deserialize, PartialEq)] 9 | struct PathParams { 10 | #[validate(range(min = 8, max = 28))] 11 | id: u8, 12 | } 13 | 14 | impl fmt::Display for PathParams { 15 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 16 | write!(f, "{{ id: {} }}", self.id) 17 | } 18 | } 19 | 20 | async fn test_handler(_query: Path) -> HttpResponse { 21 | HttpResponse::Ok().finish() 22 | } 23 | 24 | #[actix_web::test] 25 | async fn test_path_validation() { 26 | let mut app = 27 | test::init_service(App::new().service(web::resource("/test/{id}/").to(test_handler))).await; 28 | 29 | // Test 400 status 30 | let req = test::TestRequest::with_uri("/test/42/").to_request(); 31 | let resp = call_service(&mut app, req).await; 32 | assert_eq!(resp.status(), StatusCode::NOT_FOUND); 33 | 34 | // Test 200 status 35 | let req = test::TestRequest::with_uri("/test/28/").to_request(); 36 | let resp = call_service(&mut app, req).await; 37 | assert_eq!(resp.status(), StatusCode::OK); 38 | } 39 | 40 | #[actix_web::test] 41 | async fn test_custom_path_validation_error() { 42 | let mut app = test::init_service( 43 | App::new() 44 | .app_data( 45 | actix_web_validator::PathConfig::default().error_handler(|err, _req| { 46 | error::InternalError::from_response(err, HttpResponse::Conflict().finish()) 47 | .into() 48 | }), 49 | ) 50 | .service(web::resource("/test/{id}/").to(test_handler)), 51 | ) 52 | .await; 53 | 54 | let req = test::TestRequest::with_uri("/test/42/").to_request(); 55 | let resp = call_service(&mut app, req).await; 56 | assert_eq!(resp.status(), StatusCode::CONFLICT); 57 | } 58 | 59 | #[actix_web::test] 60 | async fn test_deref_validated_path() { 61 | let mut app = test::init_service(App::new().service(web::resource("/test/{id}/").to( 62 | |query: Path| async move { 63 | assert_eq!(query.id, 28); 64 | HttpResponse::Ok().finish() 65 | }, 66 | ))) 67 | .await; 68 | 69 | let req = test::TestRequest::with_uri("/test/28/").to_request(); 70 | call_service(&mut app, req).await; 71 | } 72 | 73 | #[actix_web::test] 74 | async fn test_path_implementation() { 75 | async fn test_handler(query: Path) -> HttpResponse { 76 | let reference = PathParams { id: 28 }; 77 | assert_eq!(format!("{:?}", &reference), format!("{:?}", &query)); 78 | assert_eq!(format!("{}", &reference), format!("{}", &query)); 79 | assert_eq!(query.as_ref(), &reference); 80 | assert_eq!(query.into_inner(), reference); 81 | HttpResponse::Ok().finish() 82 | } 83 | 84 | let mut app = 85 | test::init_service(App::new().service(web::resource("/test/{id}/").to(test_handler))).await; 86 | let req = test::TestRequest::with_uri("/test/28/").to_request(); 87 | let resp = call_service(&mut app, req).await; 88 | assert_eq!(resp.status(), StatusCode::OK); 89 | } 90 | -------------------------------------------------------------------------------- /tests/test_qsquery_validation.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{error, http::StatusCode, test, test::call_service, web, App, HttpResponse}; 2 | use actix_web_validator::{Error, QsQuery}; 3 | use serde::Deserialize; 4 | use validator::Validate; 5 | 6 | #[derive(Debug, Validate, Deserialize, PartialEq)] 7 | struct QueryParams { 8 | #[validate(range(min = 8, max = 28))] 9 | id: u8, 10 | } 11 | 12 | async fn test_handler(_query: QsQuery) -> HttpResponse { 13 | HttpResponse::Ok().finish() 14 | } 15 | 16 | #[actix_web::test] 17 | async fn test_qsquery_validation() { 18 | let mut app = 19 | test::init_service(App::new().service(web::resource("/test").to(test_handler))).await; 20 | 21 | // Test 400 status 22 | let req = test::TestRequest::with_uri("/test?id=42").to_request(); 23 | let resp = call_service(&mut app, req).await; 24 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 25 | 26 | // Test 200 status 27 | let req = test::TestRequest::with_uri("/test?id=28").to_request(); 28 | let resp = call_service(&mut app, req).await; 29 | assert_eq!(resp.status(), StatusCode::OK); 30 | } 31 | 32 | #[actix_web::test] 33 | async fn test_custom_qsquery_validation_error() { 34 | let mut app = test::init_service( 35 | App::new() 36 | .app_data( 37 | actix_web_validator::QsQueryConfig::default().error_handler(|err, _req| { 38 | assert!(matches!(err, Error::Validate(_))); 39 | error::InternalError::from_response(err, HttpResponse::Conflict().finish()) 40 | .into() 41 | }), 42 | ) 43 | .service(web::resource("/test").to(test_handler)), 44 | ) 45 | .await; 46 | 47 | let req = test::TestRequest::with_uri("/test?id=42").to_request(); 48 | let resp = call_service(&mut app, req).await; 49 | assert_eq!(resp.status(), StatusCode::CONFLICT); 50 | } 51 | 52 | #[actix_web::test] 53 | async fn test_deref_validated_qsquery() { 54 | let mut app = test::init_service(App::new().service(web::resource("/test").to( 55 | |query: QsQuery| async move { 56 | assert_eq!(query.id, 28); 57 | HttpResponse::Ok().finish() 58 | }, 59 | ))) 60 | .await; 61 | 62 | let req = test::TestRequest::with_uri("/test?id=28").to_request(); 63 | call_service(&mut app, req).await; 64 | } 65 | 66 | #[actix_web::test] 67 | async fn test_qsquery_implementation() { 68 | async fn test_handler(query: QsQuery) -> HttpResponse { 69 | let reference = QueryParams { id: 28 }; 70 | assert_eq!(query.as_ref(), &reference); 71 | assert_eq!(query.into_inner(), reference); 72 | HttpResponse::Ok().finish() 73 | } 74 | 75 | let mut app = 76 | test::init_service(App::new().service(web::resource("/test").to(test_handler))).await; 77 | let req = test::TestRequest::with_uri("/test?id=28").to_request(); 78 | let resp = call_service(&mut app, req).await; 79 | assert_eq!(resp.status(), StatusCode::OK); 80 | } 81 | -------------------------------------------------------------------------------- /tests/test_query_validation.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{error, http::StatusCode, test, test::call_service, web, App, HttpResponse}; 2 | use actix_web_validator::{Error, Query}; 3 | use serde::Deserialize; 4 | use validator::Validate; 5 | 6 | #[derive(Debug, Validate, Deserialize, PartialEq)] 7 | struct QueryParams { 8 | #[validate(range(min = 8, max = 28))] 9 | id: u8, 10 | } 11 | 12 | async fn test_handler(_query: Query) -> HttpResponse { 13 | HttpResponse::Ok().finish() 14 | } 15 | 16 | #[actix_web::test] 17 | async fn test_query_validation() { 18 | let mut app = 19 | test::init_service(App::new().service(web::resource("/test").to(test_handler))).await; 20 | 21 | // Test 400 status 22 | let req = test::TestRequest::with_uri("/test?id=42").to_request(); 23 | let resp = call_service(&mut app, req).await; 24 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 25 | 26 | // Test 200 status 27 | let req = test::TestRequest::with_uri("/test?id=28").to_request(); 28 | let resp = call_service(&mut app, req).await; 29 | assert_eq!(resp.status(), StatusCode::OK); 30 | } 31 | 32 | #[actix_web::test] 33 | async fn test_custom_query_validation_error() { 34 | let mut app = test::init_service( 35 | App::new() 36 | .app_data( 37 | actix_web_validator::QueryConfig::default().error_handler(|err, _req| { 38 | assert!(matches!(err, Error::Validate(_))); 39 | error::InternalError::from_response(err, HttpResponse::Conflict().finish()) 40 | .into() 41 | }), 42 | ) 43 | .service(web::resource("/test").to(test_handler)), 44 | ) 45 | .await; 46 | 47 | let req = test::TestRequest::with_uri("/test?id=42").to_request(); 48 | let resp = call_service(&mut app, req).await; 49 | assert_eq!(resp.status(), StatusCode::CONFLICT); 50 | } 51 | 52 | #[actix_web::test] 53 | async fn test_deref_validated_query() { 54 | let mut app = test::init_service(App::new().service(web::resource("/test").to( 55 | |query: Query| async move { 56 | assert_eq!(query.id, 28); 57 | HttpResponse::Ok().finish() 58 | }, 59 | ))) 60 | .await; 61 | 62 | let req = test::TestRequest::with_uri("/test?id=28").to_request(); 63 | call_service(&mut app, req).await; 64 | } 65 | 66 | #[actix_web::test] 67 | async fn test_query_implementation() { 68 | async fn test_handler(query: Query) -> HttpResponse { 69 | let reference = QueryParams { id: 28 }; 70 | assert_eq!(query.as_ref(), &reference); 71 | assert_eq!(query.into_inner(), reference); 72 | HttpResponse::Ok().finish() 73 | } 74 | 75 | let mut app = 76 | test::init_service(App::new().service(web::resource("/test").to(test_handler))).await; 77 | let req = test::TestRequest::with_uri("/test?id=28").to_request(); 78 | let resp = call_service(&mut app, req).await; 79 | assert_eq!(resp.status(), StatusCode::OK); 80 | } 81 | --------------------------------------------------------------------------------