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

Feature: Validate extracted data from request #168

Open
chrislearn opened this issue Oct 13, 2022 · 12 comments
Open

Feature: Validate extracted data from request #168

chrislearn opened this issue Oct 13, 2022 · 12 comments
Labels
enhancement New feature or request

Comments

@chrislearn
Copy link
Member

No description provided.

@chrislearn chrislearn added the enhancement New feature or request label Nov 3, 2022
@Voronar
Copy link

Voronar commented Aug 4, 2023

I put it here just as a temporary workaround I found useful. A validated alternative to the existed JsonBody JSON extractor wrapper.

Wrapping the raw validator::ValidationError into the ParseError::Other not an ideal solution, but one can wrap the raw validation error into more useful app specific error container and then catch it with a custom Salvo Catcher before sending an error response.

P.S. In the workaround JsonValidatedBody(T) can't be generic over Option<T> because Validate trait is not implemented for Option<T>. But one can write new JsonValidatedOptionalBody struct with corresponding impl<'de, T: validator::Validate> Extractible<'de> for JsonValidatedOptionalBody<Option<T>> implementation to satisfy Option<T>: Validate constraint.

P.P.S. Tested only with salvo = "0.44"

Implementation

use salvo::{
    async_trait,
    extract::Metadata,
    http::ParseError,
    oapi::{Components, Content, Operation, RequestBody, ToRequestBody},
    prelude::{EndpointArgRegister, ToSchema},
    Extractible, Request,
};
use serde::{Deserialize, Deserializer};
use std::ops::{Deref, DerefMut};

pub struct JsonValidatedBody<T>(pub T);

impl<T> JsonValidatedBody<T> {
    /// Consumes self and returns the value of the parameter.
    pub fn into_inner(self) -> T {
        self.0
    }
}

#[async_trait]
impl<'de, T: validator::Validate> Extractible<'de> for JsonValidatedBody<T>
where
    T: Deserialize<'de> + Send,
{
    fn metadata() -> &'de Metadata {
        static METADATA: Metadata = Metadata::new("");
        &METADATA
    }
    async fn extract(req: &'de mut Request) -> Result<Self, ParseError> {
        let res: Self = req.parse_json().await?;

        res.0.validate().map_err(ParseError::other)?;

        Ok(res)
    }
    async fn extract_with_arg(req: &'de mut Request, _arg: &str) -> Result<Self, ParseError> {
        let res = Self::extract(req).await?;

        res.0.validate().map_err(ParseError::other)?;

        Ok(res)
    }
}

impl<'de, T> Deserialize<'de> for JsonValidatedBody<T>
where
    T: Deserialize<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        T::deserialize(deserializer).map(JsonValidatedBody)
    }
}

impl<'de, T> EndpointArgRegister for JsonValidatedBody<T>
where
    T: Deserialize<'de> + ToSchema,
{
    fn register(components: &mut Components, operation: &mut Operation, _arg: &str) {
        let request_body = Self::to_request_body(components);
        let _ = <T as ToSchema>::to_schema(components);
        operation.request_body = Some(request_body);
    }
}

impl<'de, T> ToRequestBody for JsonValidatedBody<T>
where
    T: Deserialize<'de> + ToSchema,
{
    fn to_request_body(components: &mut Components) -> RequestBody {
        RequestBody::new()
            .description("Extract json format data from request.")
            .add_content("application/json", Content::new(T::to_schema(components)))
    }
}

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

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> DerefMut for JsonValidatedBody<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

Usage

#[derive(Deserialize, Validate)]
struct MyObject;

#[handler]
async fn handle(body: JsonValidatedBody <MyObject>) -> String { format!("OK") }

@z91300
Copy link

z91300 commented Jan 11, 2024

更新至 0.64 版本以上代码报错,错误信息如下:

error[E0195]: lifetime parameters or bounds on method `extract` do not match the trait declaration
  --> rsns-server\src\common\request.rs:51:14
   |
51 |     async fn extract(req: &'de mut Request) -> Result<Self, ParseError> {
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lifetimes do not match method in trait

error[E0195]: lifetime parameters or bounds on method `extract_with_arg` do not match the trait declaration
  --> rsns-server\src\common\request.rs:65:14
   |
65 |     async fn extract_with_arg(req: &'de mut Request, _arg: &str) -> Result<Self, ParseError> {
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lifetimes do not match method in trait

@chrislearn
Copy link
Member Author

更新至 0.64 版本以上代码报错,错误信息如下:

error[E0195]: lifetime parameters or bounds on method `extract` do not match the trait declaration
  --> rsns-server\src\common\request.rs:51:14
   |
51 |     async fn extract(req: &'de mut Request) -> Result<Self, ParseError> {
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lifetimes do not match method in trait

error[E0195]: lifetime parameters or bounds on method `extract_with_arg` do not match the trait declaration
  --> rsns-server\src\common\request.rs:65:14
   |
65 |     async fn extract_with_arg(req: &'de mut Request, _arg: &str) -> Result<Self, ParseError> {
   |              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lifetimes do not match method in trait

请更新至最新版本 rust, 如果还是出错,麻烦给出最小重现的例子,谢谢。

@z91300
Copy link

z91300 commented Jan 25, 2024

@chrislearn 0.65.1 最新版本配合上述代码 示例如下 ↓

salvo_validator_test.zip

0.640.65版本报错信息同上,0.63版本正常

@chrislearn
Copy link
Member Author

@chrislearn 0.65.1 最新版本配合上述代码 示例如下 ↓

salvo_validator_test.zip

0.640.65版本报错信息同上,0.63版本正常

去除掉上面的 #[async_trait]

#[async_trait]
impl<'de, T: validator::Validate> Extractible<'de> for JsonValidatedBody<T>

@ollyde
Copy link

ollyde commented Mar 28, 2024

Did you guys fix lifetime parameters or bounds on method `extract` do not match the trait declaration lifetimes do not match method in trait

I still have it

From the zip

use salvo::{
    async_trait,
    extract::Metadata,
    http::ParseError,
    oapi::{Components, Content, Operation, RequestBody, ToRequestBody},
    prelude::{EndpointArgRegister, ToSchema},
    Extractible, Request,
};
use serde::{Deserialize, Deserializer};
use std::ops::{Deref, DerefMut};

pub struct JsonValidatedBody<T>(pub T);

impl<T> JsonValidatedBody<T> {
    /// Consumes self and returns the value of the parameter.
    pub fn into_inner(self) -> T {
        self.0
    }
}

#[async_trait]
impl<'de, T: validator::Validate> Extractible<'de> for JsonValidatedBody<T>
where
    T: Deserialize<'de> + Send,
{
    fn metadata() -> &'de Metadata {
        static METADATA: Metadata = Metadata::new("");
        &METADATA
    }
    async fn extract(req: &'de mut Request) -> Result<Self, ParseError> {
        let res: Self = req.parse_json().await?;

        res.0.validate().map_err(ParseError::other)?;

        Ok(res)
    }
    async fn extract_with_arg(req: &'de mut Request, _arg: &str) -> Result<Self, ParseError> {
        let res = Self::extract(req).await?;

        res.0.validate().map_err(ParseError::other)?;

        Ok(res)
    }
}

impl<'de, T> Deserialize<'de> for JsonValidatedBody<T>
where
    T: Deserialize<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        T::deserialize(deserializer).map(JsonValidatedBody)
    }
}

impl<'de, T> EndpointArgRegister for JsonValidatedBody<T>
where
    T: Deserialize<'de> + ToSchema,
{
    fn register(components: &mut Components, operation: &mut Operation, _arg: &str) {
        let request_body = Self::to_request_body(components);
        let _ = <T as ToSchema>::to_schema(components);
        operation.request_body = Some(request_body);
    }
}

impl<'de, T> ToRequestBody for JsonValidatedBody<T>
where
    T: Deserialize<'de> + ToSchema,
{
    fn to_request_body(components: &mut Components) -> RequestBody {
        RequestBody::new()
            .description("Extract json format data from request.")
            .add_content("application/json", Content::new(T::to_schema(components)))
    }
}

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

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

impl<T> DerefMut for JsonValidatedBody<T> {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.0
    }
}

@ollyde
Copy link

ollyde commented Mar 28, 2024

This is broken, trying to validate this

#[derive(Serialize, Deserialize, Debug, ToSchema, ToResponse, Validate)]
#[salvo(extract(default_source(from = "body")))]
pub struct Input {
    #[validate(max_length = 50)]
    #[validate(min_length = 2)]
    #[validate(pattern = "[a-z0-9 ]*")]
    #[salvo(schema(max_length = 50, min_length = 2, pattern = "[a-z0-9 ]*"))]
    pub new_name: Option<String>,
    pub group_id: String,
}

/// Fetch message for a group
#[endpoint(tags("Messages"))]
pub async fn edit_message_group(
    req: &mut Request,
    depot: &mut Depot,
    input: JsonValidatedBody<Input>,
) -> Result<Json<MessageGroup>, ApiError> {

I get

the trait bound messaging::routes::edit_message_group::Input: validator::Validate is not satisfied
the trait validator::Validate is implemented for &T

@chrislearn
Copy link
Member Author

I'm using the code below, but I'm not getting compilation errors. If you do encounter a compilation error, please provide a minimal reproducible code base. I cannot run the code you gave above directly.

use salvo::oapi::extract::*;
use salvo::prelude::*;
use serde::{Serialize, Deserialize};
use validator::Validate;


#[derive(Serialize, Deserialize, Debug, ToSchema, ToResponse, Validate)]
#[salvo(extract(default_source(from = "body")))]
pub struct Input {
    #[validate(length(min = 50))]
    #[salvo(schema(max_length = 50, min_length = 2, pattern = "[a-z0-9 ]*"))]
    pub new_name: Option<String>,
    pub group_id: String,
}

/// Fetch message for a group
#[endpoint(tags("Messages"))]
pub async fn hello(
    input: JsonBody<Input>,
) -> String{
"".into()
}

#[tokio::main]
async fn main() {
    tracing_subscriber::fmt().init();

    let router = Router::new().push(Router::with_path("hello").get(hello));

    let doc = OpenApi::new("test api", "0.0.1").merge_router(&router);

    let router = router
        .unshift(doc.into_router("/api-doc/openapi.json"))
        .unshift(SwaggerUi::new("/api-doc/openapi.json").into_router("/swagger-ui"));

    let acceptor = TcpListener::new("0.0.0.0:5800").bind().await;
    Server::new(acceptor).serve(router).await;
}

@ollyde
Copy link

ollyde commented Mar 29, 2024

@chrislearn is it possible to use the salvo macro property instead of labeling it twice?

@chrislearn
Copy link
Member Author

Now you do need to write it twice. Perhaps verification-related functions will be implemented in future versions and there is no need to write it twice.

@ollyde
Copy link

ollyde commented Apr 2, 2024

@chrislearn would be super nice not to write it twice and manually check :-D thanks for reply.

@chrislearn chrislearn mentioned this issue Apr 4, 2024
7 tasks
@brussee
Copy link

brussee commented Jun 10, 2024

Hope you don't mind me posting this drop of knowledge:
"Parse, don't validate" which I agree with fully.

For more info, please see this comment: serde-rs/serde#939 (comment) and the following comment, both referring to: https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/

Hope this helps!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

5 participants