gengteng / axum-valid

axum-valid is a library that provides data validation extractors for the Axum web framework. It integrates validator, garde and validify, three popular validation crates in the Rust ecosystem, to offer convenient validation and data handling extractors for Axum applications.
MIT License
100 stars 4 forks source link

Support with `aide` #24

Closed Rolv-Apneseth closed 2 months ago

Rolv-Apneseth commented 2 months ago

Hi, I'm currently trying to use this library in a project where I have aide set up, and using the aide feature for axum-valid, I still get an error complaining about trait bounds not being met. Any idea why that might be?

I never understand quite how to track these errors down:

error[E0277]: the trait bound `fn(axum_valid::Valid<axum::extract::Query<WonderParamsFiltering>>, axum_valid::Valid<axum::extract::Query<WonderParamsSorting>>) -> impl Future<Output = impl IntoApiResponse> {get_all_wonders}: Handler<_, _>` is not satisfied
   --> src/web/handlers_wonders.rs:48:34
    |
48  |         .api_route("/", get_with(get_all_wonders, get_all_wonders_docs))
    |                         -------- ^^^^^^^^^^^^^^^ the trait `Handler<_, _>` is not implemented for fn item `fn(Valid<Query<...>>, ...) -> ... {get_all_wonders}`
    |                         |
    |                         required by a bound introduced by this call
    |
    = help: the following other types implement trait `Handler<T, S>`:
              <axum_extra::handler::or::Or<L, R, Lt, Rt, S> as Handler<(M, Lt, Rt), S>>
              <axum_extra::handler::IntoHandler<H, T, S> as Handler<T, S>>
              <Layered<L, H, T, S> as Handler<T, S>>
              <MethodRouter<S> as Handler<(), S>>

Here is the relevant code:

#[derive(Debug, Deserialize, JsonSchema)]
pub struct WonderParamsFiltering {
    ...
}

#[derive(Debug, Deserialize, JsonSchema)]
pub struct WonderParamsSorting {
    ...
}
...
ApiRouter::new()
    .api_route("/", get_with(get_all_wonders, get_all_wonders_docs))
...
async fn get_all_wonders(
    Valid(Query(filtering_params)): Valid<Query<WonderParamsFiltering>>,
    Valid(Query(sorting_params)): Valid<Query<WonderParamsSorting>>,
) -> impl IntoApiResponse {

Thank you for making and maintaining this library by the way, it seems really cool.

gengteng commented 2 months ago

Hi,

I attempted to reproduce the issue using a minimal example with axum-valid and aide. The following code compiles successfully, but it may not ensure documentation generation:

use aide::axum::routing::post_with;
use aide::axum::{ApiRouter, IntoApiResponse};
use aide::openapi::OpenApi;
use aide::transform::{TransformOpenApi, TransformOperation};
use axum::extract::Query;
use axum_valid::Valid;
use schemars::JsonSchema;
use serde::Deserialize;
use tokio::net::TcpListener;
use validator::Validate;

fn api_docs(api: TransformOpenApi) -> TransformOpenApi {
    api
}

#[tokio::main]
async fn main() {
    let mut api = OpenApi::default();
    let listener = TcpListener::bind("0.0.0.0:8888").await.unwrap();
    let router = ApiRouter::new()
        .api_route("/", post_with(handler, handler_docs))
        .finish_api_with(&mut api, api_docs);
    axum::serve(listener, router).await.unwrap();
}

#[derive(Deserialize, JsonSchema, Validate)]
struct Param {
    v0: String,
}

async fn handler(Valid(Query(Param { v0 })): Valid<Query<Param>>) -> impl IntoApiResponse {
    println!("v0: {v0}");
}

fn handler_docs(op: TransformOperation) -> TransformOperation {
    op.description("Handler.").response::<200, ()>()
}

Cargo.toml:

[package]
name = "av-test"
version = "0.1.0"
edition = "2021"

[dependencies]
aide = { version = "0.13.4", features = ["axum"] }
axum = "0.7.5"
axum-valid = { version = "0.18.0", features = ["aide"] }
validator = { version = "0.18.1", features = ["derive"] }
schemars = "0.8.21"
serde = { version = "1.0.203", features = ["derive"] }
tokio = { version = "1.38.0", features = ["full"] }

Using a similar approach, I was able to introduce Valid in the aide todo example and successfully generate the documentation. Please check the dependency versions and ensure all necessary traits are derived (e.g., Validate from validator). Let me know if this helps or if you need further assistance!


Rolv-Apneseth commented 2 months ago

Hey, sorry, not had much time. Thank you for the reply. The minimal example does indeed work for me.

Having looked into it further, my Query was actually a custom extractor, and the impl I was missing is this:

impl<T> HasValidate for Query<T> {
    type Validate = T;
    fn get_validate(&self) -> &T {
        &self.0
    }
}

I'll be sure to do this for any other custom extractors and hopefully all will be fine. Thanks again for the reply and I'll come back if I run into any other issues (though this weekend I am occupied).

Rolv-Apneseth commented 2 months ago

Hey again. I think I've run into this again with garde, but the previous fix I mentioned didn't work. Modifying the minimal example you provided also gives the same trait bound error:

Show ```rust use aide::axum::routing::post_with; use aide::axum::{ApiRouter, IntoApiResponse}; use aide::openapi::OpenApi; use aide::transform::{TransformOpenApi, TransformOperation}; use axum::extract::Query; use axum_valid::Garde; use garde::Validate; use schemars::JsonSchema; use serde::Deserialize; use tokio::net::TcpListener; fn api_docs(api: TransformOpenApi) -> TransformOpenApi { api } #[tokio::main] async fn main() { let mut api = OpenApi::default(); let listener = TcpListener::bind("0.0.0.0:8888").await.unwrap(); let router = ApiRouter::new() .api_route("/", post_with(handler, handler_docs)) .finish_api_with(&mut api, api_docs); axum::serve(listener, router).await.unwrap(); } #[derive(Deserialize, JsonSchema, Validate)] struct Param { #[garde(length(min = 1))] v0: String, } async fn handler(Garde(Query(Param { v0 })): Garde>) -> impl IntoApiResponse { println!("v0: {v0}"); } fn handler_docs(op: TransformOperation) -> TransformOperation { op.description("Handler.").response::<200, ()>() } ``` Cargo.toml: ```toml [package] name = "av-test" version = "0.1.0" edition = "2021" [dependencies] axum = "0.7.5" aide = { version = "0.13.4", features = ["axum"] } axum-valid = { version = "0.18.0", features = [ "basic", "aide", "garde", ], default-features = false } schemars = "0.8.21" serde = { version = "1.0.203", features = ["derive"] } tokio = { version = "1.38.0", features = ["full"] } garde = { version = "0.20.0", features = ["full"] } ```

Edit: This appears to be the same for validifiy, hope I'm not doing something silly here but here is the same minimal example:

Show ```rust use aide::axum::routing::post_with; use aide::axum::{ApiRouter, IntoApiResponse}; use aide::openapi::OpenApi; use aide::transform::{TransformOpenApi, TransformOperation}; use axum::extract::Query; use axum_valid::Validified; use schemars::JsonSchema; use serde::Deserialize; use tokio::net::TcpListener; use validify::Validify; fn api_docs(api: TransformOpenApi) -> TransformOpenApi { api } #[tokio::main] async fn main() { let mut api = OpenApi::default(); let listener = TcpListener::bind("0.0.0.0:8888").await.unwrap(); let router = ApiRouter::new() .api_route("/", post_with(handler, handler_docs)) .finish_api_with(&mut api, api_docs); axum::serve(listener, router).await.unwrap(); } #[derive(Deserialize, JsonSchema, Validify)] struct Param { #[validate(length(min = 1, max = 50))] v0: String, } async fn handler( Validified(Query(Param { v0 })): Validified>, ) -> impl IntoApiResponse { println!("v0: {v0}"); } fn handler_docs(op: TransformOperation) -> TransformOperation { op.description("Handler.").response::<200, ()>() } ``` Cargo.toml: ```toml [package] name = "av-test" version = "0.1.0" edition = "2021" [dependencies] axum = "0.7.5" aide = { version = "0.13.4", features = ["axum"] } axum-valid = { version = "0.18.0", features = [ "basic", "aide", "validify", "typed_multipart", ], default-features = false } schemars = "0.8.21" serde = { version = "1.0.203", features = ["derive"] } tokio = { version = "1.38.0", features = ["full"] } validify = "1.4.0" ```
gengteng commented 2 months ago

axum-valid 0.18.0 only support garde 0.18.0.

try this:

// main.rs
use aide::axum::routing::post_with;
use aide::axum::{ApiRouter, IntoApiResponse};
use aide::openapi::OpenApi;
use aide::transform::{TransformOpenApi, TransformOperation};
use axum::extract::Query;
use axum_valid::{Garde, Valid, Validified};
use schemars::JsonSchema;
use serde::Deserialize;
use tokio::net::TcpListener;
use validator::Validate;

fn api_docs(api: TransformOpenApi) -> TransformOpenApi {
    api
}

#[tokio::main]
async fn main() {
    aide::gen::on_error(|error| {
        println!("{error}");
    });

    aide::gen::extract_schemas(true);

    let mut api = OpenApi::default();
    let listener = TcpListener::bind("0.0.0.0:8888").await.unwrap();
    let router = ApiRouter::new()
        .api_route("/validator", post_with(handler, handler_docs))
        .api_route("/garde", post_with(handler_g, handler_docs))
        .api_route("/validify", post_with(handler_v, handler_docs))
        .finish_api_with(&mut api, api_docs);
    axum::serve(listener, router).await.unwrap();
}

#[derive(Deserialize, JsonSchema, Validate)]
struct Param {
    v0: String,
}

#[derive(Deserialize, JsonSchema, garde::Validate)]
struct ParamG {
    #[garde(length(min = 1, max = 10))]
    v0: String,
}

#[derive(Deserialize, JsonSchema, validify::Validify, validify::Payload)]
struct ParamV {
    v0: String,
}

async fn handler(Valid(Query(Param { v0 })): Valid<Query<Param>>) -> impl IntoApiResponse {
    println!("v0: {v0}");
}

async fn handler_g(Garde(Query(ParamG { v0 })): Garde<Query<ParamG>>) -> impl IntoApiResponse {
    println!("v0: {v0}");
}

async fn handler_v(
    Validified(Query(ParamV { v0 })): Validified<Query<ParamV>>,
) -> impl IntoApiResponse {
    println!("v0: {v0}");
}

fn handler_docs(op: TransformOperation) -> TransformOperation {
    op.description("Handler.").response::<201, ()>()
}
# Cargo.toml
[dependencies]
aide = { version = "0.13.4", features = ["axum"] }
axum = "0.7.5"
axum-valid = { version = "0.18.0", features = ["aide", "garde", "validify"] }
validator = { version = "0.18.1", features = ["derive"] }
schemars = "0.8.21"
serde = { version = "1.0.203", features = ["derive"] }
tokio = { version = "1.38.0", features = ["full"] }
garde = { version = "0.18.0", features = ["derive", "serde"] }
validify = { version = "1.4.0" }
Rolv-Apneseth commented 2 months ago

axum-valid 0.18.0 only support garde 0.18.0.

Ah I see, my bad.

Yes that example works great, thank you again for the help, I appreciate it.

gengteng commented 2 months ago

I've been a bit busy recently and haven't yet updated axum-valid to support the latest version of garde (0.20.0). I will work on this update as soon as possible.

Thanks for bringing this to my attention.

Rolv-Apneseth commented 2 months ago

No problem, thanks for working on this. I'm just using axum and it's ecosystem for the first time currently.