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

The Valid(Path(String)) doesn't work as expected #1

Closed noesis-labs closed 1 year ago

noesis-labs commented 1 year ago

Great library!! It works amazing for Valid(Json(...)), but I couldn't make it to work with Valid(Path(path: String))

The error is

error[E0277]: the trait bound `fn(Extension<SharedState>, axum_valid::Valid<axum::extract::Path<std::string::String>>) -> impl Future<Output = Result<Json<Profile>, StatusCode>> {get_profile_handler}: Handler<_, _, _>` is not satisfied
   --> api\.\src\bin\repro.rs:55:31
    |
55  |     .route("/:profileId", get(get_profile_handler))
    |                           --- ^^^^^^^^^^^^^^^^^^^ the trait `Handler<_, _, _>` is not implemented for fn item `fn(Extension<SharedState>, axum_valid::Valid<axum::extract::Path<std::string::String>>) -> impl Future<Output = Result<Json<Profile>, StatusCode>> {get_profile_handler}`
    |                           |
    |                           required by a bound introduced by this call
    |
    = note: Consider using `#[axum::debug_handler]` to improve the error message
    = help: the following other types implement trait `Handler<T, S, B>`:
              <Layered<L, H, T, S, B, B2> as Handler<T, S, B2>>
              <MethodRouter<S, B> as Handler<(), S, B>>
note: required by a bound in `axum::routing::get`
   --> ~\.cargo\registry\src\index.crates.io-6f17d22bba15001f\axum-0.6.18\src\routing\method_routing.rs:403:1
    |
403 | top_level_handler_fn!(get, GET);
    | ^^^^^^^^^^^^^^^^^^^^^^---^^^^^^
    | |                     |
    | |                     required by a bound in this function
    | required by this bound in `get`
    = note: this error originates in the macro `top_level_handler_fn` (in Nightly builds, run with -Z macro-backtrace for more info)

Single file reproducer

//! ```cargo
//! [dependencies]
//! axum = "0.6.18"
//! regex = "1.9.1"
//! axum-valid = "0.2.2"
//! hyper = "0.14.27"
//! serde = { version = "1.0", features = ["derive"] }
//! serde_json = "1.0"
//! tokio = { version = "1.29", features = ["fs", "net", "sync", "rt", "io-util", "macros", "rt-multi-thread"] }
//! tokio-tower = "0.6.0"
//! tokio-util = { version = "0.7.8", features = ["codec"] }
//! tower = { version = "0.4", features = ["util", "reconnect", "retry", "timeout"] }
//! validator = { version = "0.16.1", features = ["derive"] }
//! ```

use axum::{
  extract::{Extension, Path},
  http::StatusCode,
  Json,
  routing::get,
  Router,
};
use axum_valid::Valid;
use regex::Regex;
use serde::Deserialize;
use validator::{Validate, ValidationError};

#[derive(Debug, Deserialize, Validate)]
struct Profile {
  #[validate(custom = "uuid")]
  id: String,
  profile_name: String,
}

pub fn uuid(id: &str) -> Result<(), ValidationError> {
  // Define the regular expression pattern for UUIDv4
  let pattern =
    Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
      .expect("Failed to compile UUID regex pattern");

  if !pattern.is_match(id) {
    let mut error = ValidationError::new("Invalid UUID format");
    error.add_param(std::borrow::Cow::Borrowed("pattern"), &pattern.to_string());
    return Err(error)
  }

  Ok(())
}

#[tokio::main]
async fn main() {
  let app_state = SharedState {};

  let app = Router::new()
    .route("/:profileId", get(get_profile_handler))
    .layer(Extension(app_state));

  axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
    .serve(app.into_make_service())
    .await
    .unwrap();
}

async fn get_profile_handler(
  Extension(_app_state): Extension<SharedState>,
  Valid(Path(profile_id)): Valid<Path<String>>,
) -> Result<Json<Profile>, StatusCode> {
  println!("profile_id = {}", profile_id);

  let profile = Profile {
    id: profile_id,
    profile_name: "Captain Flint".to_string(),
  };
  Ok(Json(profile))
}

#[derive(Debug)]
struct SharedState {}
iilyak commented 1 year ago

You can run the example using rust-script repro.rs (if you have rust-script installed).

gengteng commented 1 year ago

Thank you for using my library and providing the code to reproduce the issue. I'm glad to hear that it works amazingly well with Valid(Json(...)). However, I understand that you are facing difficulties in making it work with Valid<Path<String>>.

Using basic data types directly with the Valid<Path<...>> approach is not supported. Instead, you will need to define a custom struct that implements the Deserialize and Validate traits (and the crate will implement HasValidate trait for it automatically, that's how it works).

This is the modified code that can be compiled successfully:

// using your dependencies

use axum::{
    extract::{Extension, Path},
    http::StatusCode,
    routing::get,
    Json, Router,
};
use axum_valid::Valid;
use regex::Regex;
use serde::{Deserialize, Serialize};
use validator::{Validate, ValidationError};

#[derive(Debug, Serialize, Validate)]
struct Profile {
    id: String,
    profile_name: String,
}

pub fn uuid(id: &str) -> Result<(), ValidationError> {
    // Define the regular expression pattern for UUIDv4
    let pattern =
        Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$")
            .expect("Failed to compile UUID regex pattern");

    if !pattern.is_match(id) {
        let mut error = ValidationError::new("Invalid UUID format");
        error.add_param(std::borrow::Cow::Borrowed("pattern"), &pattern.to_string());
        return Err(error);
    }

    Ok(())
}

#[tokio::main]
async fn main() {
    let app_state = SharedState {};

    let app = Router::new()
        .route("/:profileId", get(get_profile_handler))
        .layer(Extension(app_state));

    axum::Server::bind(&"127.0.0.1:3000".parse().unwrap())
        .serve(app.into_make_service())
        .await
        .unwrap();
}

#[derive(Debug, Deserialize, Validate)]
pub struct ProfileId {
    #[validate(custom = "uuid")]
    profile_id: String,
}

async fn get_profile_handler(
    Extension(_app_state): Extension<SharedState>,
    Valid(Path(profile_id)): Valid<Path<ProfileId>>,
) -> Result<Json<Profile>, StatusCode> {
    let profile_id = profile_id.profile_id;
    println!("profile_id = {}", profile_id);

    let profile = Profile {
        id: profile_id,
        profile_name: "Captain Flint".to_string(),
    };
    Ok(Json(profile))
}

#[derive(Debug, Clone)]
struct SharedState {}
  1. Implement Serialize for Profile (not Deserialize)
  2. Define a ProfileId struct with named fields, and implement Deserialize and Validate for it
  3. Implement Clone for SharedState

If you have any further questions or need additional assistance, please let me know. And your question also reminded me that I will add some examples and test code later.

btw: Using the Uuid type in uuid crate with serde feature would be better in this scenario.

gengteng commented 1 year ago

Examples & tests added: tests/basic.rs, tests/custom.rs