Skip to content

Commit

Permalink
Merge pull request #23 from rlebran/master
Browse files Browse the repository at this point in the history
Add support for serde_qs
  • Loading branch information
singulared authored May 18, 2021
2 parents 33fe93f + c531dda commit b8bb1f5
Show file tree
Hide file tree
Showing 6 changed files with 343 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ validator = { version = ">=0.11, <=0.12", features = ["derive"] }
serde = "1"
serde_urlencoded = "0.7"
serde_json = "1"
serde_qs = { version = "0.8", features = ["actix"] }
log = "0.4"
futures = "0.3"
mime = "0.3"
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,11 @@ This crate works with Cargo and can be found on
actix-web-validator = "2.0.3"
```

## Supported `actix_web::web` extractors:
* `web::Json`
* `web::Query`
* `web::Path`
## Supported extractors:
* `actix_web::web::Json`
* `actix_web::web::Query`
* `actix_web::web::Path`
* `serde_qs::actix::QsQuery`

### Supported `actix_web` versions:
* For actix-web-validator `0.*` supported version of actix-web is `1.*`
Expand Down
8 changes: 8 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ pub enum Error {
Deserialize(DeserializeErrors),
#[display(fmt = "Payload error: {}", _0)]
JsonPayloadError(actix_web::error::JsonPayloadError),
#[display(fmt = "Payload error: {}", _0)]
QsError(serde_qs::Error),
}

#[derive(Display, Debug)]
Expand All @@ -30,6 +32,12 @@ impl From<serde_json::error::Error> for Error {
}
}

impl From<serde_qs::Error> for Error {
fn from(error: serde_qs::Error) -> Self {
Error::QsError(error)
}
}

impl From<serde_urlencoded::de::Error> for Error {
fn from(error: serde_urlencoded::de::Error) -> Self {
Error::Deserialize(DeserializeErrors::DeserializeQuery(error))
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@ pub mod error;
mod json;
mod path;
mod query;
mod qsquery;
pub use error::Error;
pub use json::*;
pub use path::*;
pub use query::*;
pub use qsquery::*;
pub use validator::Validate;
248 changes: 248 additions & 0 deletions src/qsquery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,248 @@
//! Query extractor.
use crate::error::Error;
use std::ops::Deref;
use std::sync::Arc;
use std::{fmt, ops};

use actix_web::{FromRequest, HttpRequest};
use futures::future::{err, ok, Ready};
use serde::de;
use validator::Validate;
use serde_qs::Config as QsConfig;

/// Query extractor configuration
///
/// ```rust
/// # #[macro_use] extern crate serde_derive;
/// # #[cfg(feature = "actix")]
/// # use actix_web;
/// # #[cfg(feature = "actix2")]
/// # use actix_web2 as actix_web;
/// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde_qs::actix::QsQuery;
/// use serde_qs::Config as QsConfig;
///
/// #[derive(Deserialize)]
/// struct Info {
/// username: String,
/// }
///
/// /// deserialize `Info` from request's querystring
/// fn index(info: QsQuery<Info>) -> HttpResponse {
/// format!("Welcome {}!", info.username).into()
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/index.html").app_data(
/// // change query extractor configuration
/// QsQuery::<Info>::configure(|cfg| {
/// cfg.error_handler(|err, req| { // <- create custom error response
/// error::InternalError::from_response(
/// err, HttpResponse::Conflict().finish()).into()
/// })
/// .qs_config(QsConfig::default())
/// }))
/// .route(web::post().to(index))
/// );
/// }
/// ```
pub struct QsQueryConfig {
ehandler: Option<Arc<dyn Fn(Error, &HttpRequest) -> actix_web::Error + Send + Sync>>,
qs_config: QsConfig,
}

impl QsQueryConfig {
/// Set custom error handler
pub fn error_handler<F>(mut self, f: F) -> Self
where
F: Fn(Error, &HttpRequest) -> actix_web::Error + Send + Sync + 'static,
{
self.ehandler = Some(Arc::new(f));
self
}

/// Set custom serialization parameters
pub fn qs_config(mut self, config: QsConfig) -> Self {
self.qs_config = config;
self
}
}

impl Default for QsQueryConfig {
fn default() -> Self {
QsQueryConfig {
ehandler: None,
qs_config: QsConfig::default(),
}
}
}

/// Extract and validate typed information from the request's query.
///
/// For query decoding uses *serde_urlencoded* crate
/// [**QueryConfig**](struct.QueryConfig.html) allows to configure extraction process.
///
/// ## Example
///
/// ```rust
/// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// use actix_web_validator::{QsQuery, Validate};
///
/// #[derive(Debug, Deserialize)]
/// pub enum ResponseType {
/// Token,
/// Code
/// }
///
/// #[derive(Deserialize, Validate)]
/// pub struct AuthRequest {
/// #[validate(range(min = 1000, max = 9999))]
/// id: u64,
/// response_type: ResponseType,
/// }
///
/// // Use `Query` extractor for query information (and destructure it within the signature).
/// // This handler gets called only if the request's query string contains a `id` and
/// // `response_type` fields.
/// // The correct request for this handler would be `/index.html?id=1234&response_type=Code"`.
/// async fn index(info: QsQuery<AuthRequest>) -> String {
/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type)
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor
/// }
/// ```
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct QsQuery<T>(pub T);

impl<T> AsRef<T> for QsQuery<T> {
fn as_ref(&self) -> &T {
&self.0
}
}

impl<T> Deref for QsQuery<T> {
type Target = T;

fn deref(&self) -> &T {
&self.0
}
}

impl<T> ops::DerefMut for QsQuery<T> {
fn deref_mut(&mut self) -> &mut T {
&mut self.0
}
}

impl<T: fmt::Debug> fmt::Debug for QsQuery<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl<T: fmt::Display> fmt::Display for QsQuery<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}

impl<T> QsQuery<T>
where
T: Validate,
{
/// Deconstruct to an inner value.
pub fn into_inner(self) -> T {
self.0
}
}

/// Extract typed information from the request's query.
///
/// ## Example
///
/// ```rust
/// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// use actix_web_validator::{QsQuery, Validate};
///
/// #[derive(Debug, Deserialize)]
/// pub enum ResponseType {
/// Token,
/// Code
/// }
///
/// #[derive(Deserialize, Validate)]
/// pub struct AuthRequest {
/// #[validate(range(min = 1000, max = 9999))]
/// id: u64,
/// response_type: ResponseType,
/// }
///
/// // Use `Query` extractor for query information (and destructure it within the signature).
/// // This handler gets called only if the request's query string contains a `id` and
/// // `response_type` fields.
/// // The correct request for this handler would be `/index.html?id=19&response_type=Code"`.
/// async fn index(QsQuery(info): QsQuery<AuthRequest>) -> String {
/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type)
/// }
///
/// fn main() {
/// let app = App::new().service(
/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor
/// }
/// ```
impl<T> FromRequest for QsQuery<T>
where
T: de::DeserializeOwned + Validate,
{
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
type Config = QsQueryConfig;

/// Builds Query struct from request and provides validation mechanism
#[inline]
fn from_request(
req: &actix_web::web::HttpRequest,
_: &mut actix_web::dev::Payload,
) -> Self::Future {
let query_config = req.app_data::<QsQueryConfig>();

let error_handler = query_config.map(|c| c.ehandler.clone())
.unwrap_or(None);

let default_qsconfig = QsConfig::default();
let qsconfig = query_config
.map(|c| &c.qs_config)
.unwrap_or(&default_qsconfig);

qsconfig
.deserialize_str::<T>(req.query_string())
.map_err(Error::from)
.and_then(|value| {
value
.validate()
.map(move |_| value)
.map_err(Error::Validate)
})
.map_err(move |e| {
log::debug!(
"Failed during Query extractor validation. \
Request path: {:?}",
req.path()
);
if let Some(error_handler) = error_handler {
(error_handler)(e, req)
} else {
e.into()
}
})
.map(|value| ok(QsQuery(value)))
.unwrap_or_else(|e| err(e))
}
}
79 changes: 79 additions & 0 deletions tests/test_qsquery_validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
use actix_web::{error, http::StatusCode, test, test::call_service, web, App, HttpResponse};
use actix_web_validator::{Validate, Error, QsQuery};
use serde_derive::Deserialize;

#[derive(Debug, Validate, Deserialize, PartialEq)]
struct QueryParams {
#[validate(range(min = 8, max = 28))]
id: u8,
}

async fn test_handler(_query: QsQuery<QueryParams>) -> HttpResponse {
HttpResponse::Ok().finish()
}

#[actix_rt::test]
async fn test_qsquery_validation() {
let mut app =
test::init_service(App::new().service(web::resource("/test").to(test_handler))).await;

// Test 400 status
let req = test::TestRequest::with_uri("/test?id=42").to_request();
let resp = call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

// Test 200 status
let req = test::TestRequest::with_uri("/test?id=28").to_request();
let resp = call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}

#[actix_rt::test]
async fn test_custom_qsquery_validation_error() {
let mut app = test::init_service(
App::new()
.app_data(
actix_web_validator::QsQueryConfig::default().error_handler(|err, _req| {
assert!(matches!(err, Error::Validate(_)));
error::InternalError::from_response(err, HttpResponse::Conflict().finish())
.into()
}),
)
.service(web::resource("/test").to(test_handler)),
)
.await;

let req = test::TestRequest::with_uri("/test?id=42").to_request();
let resp = call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::CONFLICT);
}

#[actix_rt::test]
async fn test_deref_validated_qsquery() {
let mut app = test::init_service(App::new().service(web::resource("/test").to(
|query: QsQuery<QueryParams>| {
assert_eq!(query.id, 28);
HttpResponse::Ok().finish()
},
)))
.await;

let req = test::TestRequest::with_uri("/test?id=28").to_request();
call_service(&mut app, req).await;
}

#[actix_rt::test]
async fn test_qsquery_implementation() {
async fn test_handler(query: QsQuery<QueryParams>) -> HttpResponse {
let reference = QueryParams { id: 28 };
assert_eq!(query.as_ref(), &reference);
assert_eq!(query.into_inner(), reference);
HttpResponse::Ok().finish()
}

let mut app =
test::init_service(App::new().service(web::resource("/test").to(test_handler))).await;
let req = test::TestRequest::with_uri("/test?id=28").to_request();
let resp = call_service(&mut app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}

0 comments on commit b8bb1f5

Please sign in to comment.