Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for serde_qs #23

Merged
merged 6 commits into from
May 18, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}