Skip to content

Commit

Permalink
Add support for actix-web-lab Query (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
rlebran committed Feb 29, 2024
2 parents 6cdf198 + a057b06 commit 2a250bb
Show file tree
Hide file tree
Showing 6 changed files with 209 additions and 16 deletions.
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description = "Actix-web garde wrapper"
readme = "README.md"
keywords = ["garde", "actix", "actix-web", "validation"]
categories = ["web-programming"]
version = "0.5.1"
version = "0.6.0"

authors = ["Netwo <oss@netwo.com>"]
edition = "2021"
Expand All @@ -27,8 +27,13 @@ serde = "1"
serde_urlencoded = "0.7"
thiserror = "1.0"

actix-web-lab = { version = "0.20", optional = true }

serde_qs = { version = "0.12", optional = true }

[features]
lab_query = ["actix-web-lab"]

[dev-dependencies]
actix-test = "0.1"
tokio = { version = "1", features = ["macros"], default-features = false }
Expand Down
31 changes: 18 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
# Garde-actix-web &emsp; [![Documentation]][docs.rs] [![Latest Version]][crates.io] [![Build Status]][build]


[docs.rs]: https://docs.rs/garde-actix-web/

[crates.io]: https://crates.io/crates/garde-actix-web

[build]: https://github.com/netwo-io/garde-actix-web/actions/workflows/build.yaml?branch=main

[Documentation]: https://img.shields.io/docsrs/garde-actix-web

[Latest Version]: https://img.shields.io/crates/v/garde-actix-web.svg

[Build Status]: https://github.com/netwo-io/garde-actix-web/actions/workflows/build.yaml/badge.svg?branch=main

Actix-web wrapper for [garde](https://github.com/jprochazk/garde), a Rust validation library.
Expand All @@ -32,7 +36,8 @@ Your types must implement `Validate` from `garde`. Validation happens during act

If payload is invalid, a 400 error is returned (404 for Path).

Custom error handling can be implemented with an extractor config (`garde_actix_web::web::QueryConfig` in place of `actix_web::web::QueryConfig` for example).
Custom error handling can be implemented with an extractor config (`garde_actix_web::web::QueryConfig` in place
of `actix_web::web::QueryConfig` for example).

```rust
use actix_web::HttpResponse;
Expand All @@ -42,7 +47,7 @@ use garde::Validate;

#[derive(Validate)]
struct MyStruct<'a> {
#[garde(ascii, length(min=3, max=25))]
#[garde(ascii, length(min = 3, max = 25))]
username: &'a str,
}

Expand All @@ -51,17 +56,16 @@ fn test(id: Path<MyStruct>) -> HttpResponse {
}
```

⚠️ When using `garde` [custom validation](https://github.com/jprochazk/garde#custom-validation), the `Context` type needs to implement `Default` which is not required by `garde`.
⚠️ When using `garde` [custom validation](https://github.com/jprochazk/garde#custom-validation), the `Context` type
needs to implement `Default` which is not required by `garde`.

Context needs to be provided through actix's `data` or `app_data`, if not found default will be used instead.


### Feature flags

| name | description | extra dependencies |
|------------|---------------------------------------------------------------|----------------------------------------------------------------------------------------------|
| `serde_qs` | Enables the usage of `garde` for `serde_qs::actix::QsQuery<T>` | [`serde_qs`](https://crates.io/crates/serde_qs) |

| name | description | extra dependencies |
|------------|----------------------------------------------------------------|-------------------------------------------------|
| `serde_qs` | Enables the usage of `garde` for `serde_qs::actix::QsQuery<T>` | [`serde_qs`](https://crates.io/crates/serde_qs) |

### Compatibility matrix

Expand All @@ -71,15 +75,16 @@ Context needs to be provided through actix's `data` or `app_data`, if not found
| `0.15` | `0.2.x` |
| `0.16` | `0.3.x` |
| `0.17` | `0.4.x` |
| `0.18` | `0.5.x` |

| `0.18` | `0.5.x`, `0.6.x` |

### About us

Garde-actix-web is provided by [Netwo](https://www.netwo.io).

We use this crate for our internal needs and therefore are committed to its maintenance, however we cannot provide any additional guaranty. Use it at your own risks.
We use this crate for our internal needs and therefore are committed to its maintenance, however we cannot provide any
additional guaranty. Use it at your own risks.

While we won't invest in any feature we don't need, we are open to accept any pull request you might propose.

We are a France based full-remote company operating in the telecom sector. If you are interested in learning more, feel free to visit [our career page](https://www.netwo.io/carriere).
We are a France based full-remote company operating in the telecom sector. If you are interested in learning more, feel
free to visit [our career page](https://www.netwo.io/carriere).
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,10 @@
//! | `0.15` | `0.2.x` |
//! | `0.16` | `0.3.x` |
//! | `0.17` | `0.4.x` |
//! | `0.18` | `0.5.x` |
//! | `0.18` | `0.5.x`, `0.6.x` |

#![forbid(unsafe_code)]

use actix_web::web::Data;
use actix_web::HttpRequest;
use garde::Validate;
Expand Down
178 changes: 178 additions & 0 deletions src/web/lab_query.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
use crate::validate_for_request;
use crate::web::QueryConfig;
use actix_web::dev::Payload;
use actix_web::error::QueryPayloadError;
use actix_web::{Error, FromRequest, HttpRequest};
use actix_web_lab::__reexports::futures_util::future::LocalBoxFuture;
use garde::Validate;
use serde::de;
use serde::de::DeserializeOwned;

/// Drop in replacement for [actix_web_lab::extract::Query](https://docs.rs/actix-web-lab/latest/actix_web_lab/extract/struct.Query.html)
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Query<T>(pub T);

impl<T> Query<T> {
pub fn into_inner(self) -> T {
self.0
}
}

impl<T: DeserializeOwned> Query<T> {
pub fn from_query(query_str: &str) -> Result<Self, QueryPayloadError> {
actix_web_lab::extract::Query::from_query(query_str).map(|r: actix_web_lab::extract::Query<T>| Self(r.into_inner()))
}
}

impl<T> FromRequest for Query<T>
where
T: DeserializeOwned + Validate + 'static,
T::Context: Default,
{
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;

#[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let req_copy = req.clone();
let req = req.clone();
let mut payload = payload.take();

let error_handler = req.app_data::<QueryConfig>().and_then(|c| c.err_handler.clone());

Box::pin(async move {
actix_web_lab::extract::Query::from_request(&req, &mut payload)
.await
.map_err(|e| QueryPayloadError::Deserialize(de::Error::custom(format!("{}", e))).into())
.and_then(|data| {
let req = req_copy;
validate_for_request(data.0, &req)
})
.map(|res| Self(res))
.map_err(move |e| {
log::debug!(
"Failed during Query extractor deserialization. \
Request path: {:?}",
req.path()
);

if let Some(error_handler) = error_handler {
(error_handler)(e, &req)
} else {
e.into()
}
})
})
}
}

#[cfg(test)]
mod test {
use crate::web::{Query, QueryConfig};
use actix_http::StatusCode;
use actix_web::error::InternalError;
use actix_web::test::{call_service, init_service, TestRequest};
use actix_web::web::{post, resource};
use actix_web::{App, HttpResponse};
use garde::Validate;
use serde::{Deserialize, Serialize};

#[derive(Debug, PartialEq, Validate, Serialize, Deserialize)]
struct QueryData {
#[garde(range(min = 18, max = 28))]
age: u8,
}

#[derive(Debug, PartialEq, Validate, Serialize, Deserialize)]
#[garde(context(NumberContext))]
struct QueryDataWithContext {
#[garde(custom(is_big_enough))]
age: u8,
}

#[derive(Default, Debug)]
struct NumberContext {
min: u8,
}

fn is_big_enough(value: &u8, context: &NumberContext) -> garde::Result {
if value < &context.min {
return Err(garde::Error::new("Number is too low"));
}
Ok(())
}

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

async fn test_handler_with_context(_query: Query<QueryDataWithContext>) -> HttpResponse {
HttpResponse::Ok().finish()
}

#[tokio::test]
async fn test_simple_lab_query_validation() {
let app = init_service(App::new().service(resource("/").route(post().to(test_handler)))).await;

let req = TestRequest::post().uri("/?age=24").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);

let req = TestRequest::post().uri("/?age=30").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}

#[tokio::test]
async fn test_lab_query_validation_custom_config() {
let app = init_service(
App::new()
.app_data(
QueryConfig::default()
.error_handler(|err, _req| InternalError::from_response(err, HttpResponse::Conflict().finish()).into()),
)
.service(resource("/").route(post().to(test_handler))),
)
.await;

let req = TestRequest::post().uri("/?age=24").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);

let req = TestRequest::post().uri("/?age=30").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::CONFLICT);
}

#[tokio::test]
async fn test_lab_query_validation_with_context() {
let number_context = NumberContext { min: 25 };
let app = init_service(
App::new()
.app_data(number_context)
.service(resource("/").route(post().to(test_handler_with_context))),
)
.await;

let req = TestRequest::post().uri("/?age=24").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

let req = TestRequest::post().uri("/?age=30").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}

#[tokio::test]
async fn test_lab_query_validation_with_missing_context() {
let app = init_service(App::new().service(resource("/").route(post().to(test_handler_with_context)))).await;

let req = TestRequest::post().uri("/?age=24").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);

let req = TestRequest::post().uri("/?age=30").to_request();
let resp = call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
}
4 changes: 4 additions & 0 deletions src/web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ mod either;
mod form;
mod header;
mod json;
#[cfg(feature = "lab_query")]
mod lab_query;
mod path;
#[cfg(feature = "serde_qs")]
mod qs_query;
Expand All @@ -12,6 +14,8 @@ pub use either::Either;
pub use form::{Form, FormConfig};
pub use header::Header;
pub use json::{Json, JsonConfig};
#[cfg(feature = "lab_query")]
pub use lab_query::Query as LabQuery;
pub use path::{Path, PathConfig};
#[cfg(feature = "serde_qs")]
pub use qs_query::{QsQuery, QsQueryConfig};
Expand Down
2 changes: 1 addition & 1 deletion src/web/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ where
#[derive(Clone, Default)]
pub struct QueryConfig {
#[allow(clippy::type_complexity)]
err_handler: Option<Arc<dyn Fn(crate::error::Error, &HttpRequest) -> Error + Send + Sync>>,
pub(crate) err_handler: Option<Arc<dyn Fn(crate::error::Error, &HttpRequest) -> Error + Send + Sync>>,
}

impl QueryConfig {
Expand Down

0 comments on commit 2a250bb

Please sign in to comment.