#axum #validation #extractor #garde #validify #web-apps

axum-valid

为您的Axum应用程序提供验证提取器,允许您使用验证器、garde、validify或全部三者进行数据验证

22个版本 (破坏性更新)

0.19.0 2024年7月12日
0.17.0 2024年3月5日
0.13.0 2023年12月14日
0.12.0-alpha2023年11月28日
0.3.0 2023年7月22日

#67 in Web编程

Download history 290/week @ 2024-05-04 441/week @ 2024-05-11 559/week @ 2024-05-18 302/week @ 2024-05-25 461/week @ 2024-06-01 373/week @ 2024-06-08 381/week @ 2024-06-15 458/week @ 2024-06-22 443/week @ 2024-06-29 577/week @ 2024-07-06 624/week @ 2024-07-13 567/week @ 2024-07-20 1030/week @ 2024-07-27 1235/week @ 2024-08-03 1204/week @ 2024-08-10 999/week @ 2024-08-17

4,562 每月下载量
用于 2 crates

MIT 许可证

325KB
6K SLoC

axum-valid

crates.io crates.io download LICENSE dependency status GitHub Workflow Status Coverage Status

📑 概览

axum-valid 是一个库,为Axum网络框架提供数据验证提取器。它集成了Rust生态系统中的三个流行的验证crate:validatorgardevalidify,为Axum应用程序提供方便的验证和数据处理提取器。

🚀 基本用法

📦 Valid<E>

  • 安装
cargo add validator --features derive
cargo add axum-valid
# validator is enabled by default
  • 示例
use axum::extract::Query;
use axum::routing::{get, post};
use axum::{Json, Router};
use axum_valid::Valid;
use serde::Deserialize;
use std::net::SocketAddr;
use tokio::net::TcpListener;
use validator::Validate;

#[derive(Debug, Validate, Deserialize)]
pub struct Paginator {
    #[validate(range(min = 1, max = 50))]
    pub page_size: usize,
    #[validate(range(min = 1))]
    pub page_no: usize,
}

pub async fn paginator_from_query(Valid(Query(paginator)): Valid<Query<Paginator>>) {
    assert!((1..=50).contains(&paginator.page_size));
    assert!((1..).contains(&paginator.page_no));
}

pub async fn paginator_from_json(paginator: Valid<Json<Paginator>>) {
    assert!((1..=50).contains(&paginator.page_size));
    assert!((1..).contains(&paginator.page_no));
    // NOTE: all extractors provided support automatic dereferencing
    println!("page_no: {}, page_size: {}", paginator.page_no, paginator.page_size);
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/query", get(paginator_from_query))
        .route("/json", post(paginator_from_json));
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

在内提取器错误的情况下,它将首先返回内提取器的Rejection。当发生验证错误时,外提取器将自动返回包含验证错误的400状态码作为HTTP消息正文。

📦 Garde<E>

  • 安装
cargo add garde --features derive
cargo add axum --features macros # for FromRef derive macro
cargo add axum-valid --features garde,basic --no-default-features
# excluding validator
  • 示例
use axum::extract::{FromRef, Query, State};
use axum::routing::{get, post};
use axum::{Json, Router};
use axum_valid::Garde;
use garde::Validate;
use serde::Deserialize;
use std::net::SocketAddr;
use tokio::net::TcpListener;

#[derive(Debug, Validate, Deserialize)]
pub struct Paginator {
    #[garde(range(min = 1, max = 50))]
    pub page_size: usize,
    #[garde(range(min = 1))]
    pub page_no: usize,
}

pub async fn paginator_from_query(Garde(Query(paginator)): Garde<Query<Paginator>>) {
    assert!((1..=50).contains(&paginator.page_size));
    assert!((1..).contains(&paginator.page_no));
}

pub async fn paginator_from_json(paginator: Garde<Json<Paginator>>) {
    assert!((1..=50).contains(&paginator.page_size));
    assert!((1..).contains(&paginator.page_no));
    println!("page_no: {}, page_size: {}", paginator.page_no, paginator.page_size);
}

pub async fn get_state(_state: State<MyState>) {}

#[derive(Debug, Clone, FromRef)]
pub struct MyState {
    state_field: i32,
    without_validation_arguments: (),
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/query", get(paginator_from_query))
        .route("/json", post(paginator_from_json));

    // WARNING: If you are using Garde and also have a state,
    // even if that state is unrelated to Garde,
    // you still need to implement FromRef<StateType> for ().
    // Tip: You can add an () field to your state and derive FromRef for it.
    let router = router.route("/state", get(get_state)).with_state(MyState {
        state_field: 1,
        without_validation_arguments: (),
    });
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

📦 Validated<E>Modified<E>ValidatedByRef<E>

  • 安装
cargo add validify
cargo add axum-valid --features validify,basic --no-default-features
  • 示例

此示例的额外依赖项

cargo add axum_typed_multipart
cargo add axum-valid --features validify,basic,typed_multipart --no-default-features
use axum::extract::Query;
use axum::routing::{get, post};
use axum::{Form, Json, Router};
use axum_typed_multipart::{TryFromMultipart, TypedMultipart};
use axum_valid::{Modified, Validated, Validified, ValidifiedByRef};
use serde::Deserialize;
use std::net::SocketAddr;
use tokio::net::TcpListener;
use validify::{Payload, Validate, Validify};

#[derive(Debug, Validify, Deserialize)]
pub struct Paginator {
    #[validate(range(min = 1.0, max = 50.0))]
    pub page_size: usize,
    #[validate(range(min = 1.0))]
    pub page_no: usize,
}

pub async fn paginator_from_query(Validated(Query(paginator)): Validated<Query<Paginator>>) {
    assert!((1..=50).contains(&paginator.page_size));
    assert!((1..).contains(&paginator.page_no));
}

// Payload is now required for Validified. (Added in validify 1.3.0)
#[derive(Debug, Validify, Deserialize, Payload)]
pub struct Parameters {
    #[modify(lowercase)]
    #[validate(length(min = 1, max = 50))]
    pub v0: String,
    #[modify(trim)]
    #[validate(length(min = 1, max = 100))]
    pub v1: String,
}

pub async fn parameters_from_json(modified_parameters: Modified<Json<Parameters>>) {
    assert_eq!(
        modified_parameters.v0,
        modified_parameters.v0.to_lowercase()
    );
    assert_eq!(modified_parameters.v1, modified_parameters.v1.trim())
    // but modified_parameters may be invalid
}

// NOTE: missing required fields will be treated as validation errors.
pub async fn parameters_from_form(parameters: Validified<Form<Parameters>>) {
    assert_eq!(parameters.v0, parameters.v0.to_lowercase());
    assert_eq!(parameters.v1, parameters.v1.trim());
    assert!(parameters.validate().is_ok());
}

// NOTE: TypedMultipart doesn't using serde::Deserialize to construct data
// we should use ValidifiedByRef instead of Validified
#[derive(Debug, Validify, TryFromMultipart)]
pub struct FormData {
    #[modify(lowercase)]
    #[validate(length(min = 1, max = 50))]
    pub v0: String,
    #[modify(trim)]
    #[validate(length(min = 1, max = 100))]
    pub v1: String,
}

pub async fn parameters_from_typed_multipart(
    ValidifiedByRef(TypedMultipart(data)): ValidifiedByRef<TypedMultipart<FormData>>,
) {
    assert_eq!(data.v0, data.v0.to_lowercase());
    assert_eq!(data.v1, data.v1.trim());
    assert!(data.validate().is_ok());
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/validated", get(paginator_from_query))
        .route("/modified", post(parameters_from_json))
        .route("/validified", post(parameters_from_form))
        .route("/validified_by_ref", post(parameters_from_typed_multipart));
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

有关如何使用验证提取器与每个内提取器结合使用的示例,请参阅相应模块的文档

🚀 基于参数的验证

📦 ValidEx<E>

  • 安装
cargo add validator --features derive
cargo add axum-valid
# validator is enabled by default
  • 示例
use axum::routing::post;
use axum::{Form, Router};
use axum_valid::ValidEx;
use serde::Deserialize;
use std::net::SocketAddr;
use std::ops::{RangeFrom, RangeInclusive};
use tokio::net::TcpListener;
use validator::{Validate, ValidationError};

// NOTE: When some fields use custom validation functions with arguments,
// `#[derive(Validate)]` will implement `ValidateArgs` instead of `Validate` for the type.
#[derive(Debug, Validate, Deserialize)]
#[validate(context = PaginatorValidArgs)] // context is required
pub struct Paginator {
    #[validate(custom(function = "validate_page_size", use_context))]
    pub page_size: usize,
    #[validate(custom(function = "validate_page_no", use_context))]
    pub page_no: usize,
}

fn validate_page_size(v: usize, args: &PaginatorValidArgs) -> Result<(), ValidationError> {
    args.page_size_range
        .contains(&v)
        .then_some(())
        .ok_or_else(|| ValidationError::new("page_size is out of range"))
}

fn validate_page_no(v: usize, args: &PaginatorValidArgs) -> Result<(), ValidationError> {
    args.page_no_range
        .contains(&v)
        .then_some(())
        .ok_or_else(|| ValidationError::new("page_no is out of range"))
}

// NOTE: Clone is required, consider using Arc to reduce deep copying costs.
#[derive(Debug, Clone)]
pub struct PaginatorValidArgs {
    page_size_range: RangeInclusive<usize>,
    page_no_range: RangeFrom<usize>,
}

pub async fn paginator_from_form_ex(ValidEx(Form(paginator)): ValidEx<Form<Paginator>>) {
    assert!((1..=50).contains(&paginator.page_size));
    assert!((1..).contains(&paginator.page_no));
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/form", post(paginator_from_form_ex))
        .with_state(PaginatorValidArgs {
            page_size_range: 1..=50,
            page_no_range: 1..,
        });
    // NOTE: The PaginatorValidArgs can also be stored in a XxxState,
    // make sure it implements FromRef<XxxState>.

    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

📦 Garde<E>

  • 安装
cargo add garde
cargo add axum-valid --features garde,basic --no-default-features
# excluding validator
  • 示例
use axum::routing::post;
use axum::{Form, Router};
use axum_valid::Garde;
use garde::Validate;
use serde::Deserialize;
use std::net::SocketAddr;
use std::ops::{RangeFrom, RangeInclusive};
use tokio::net::TcpListener;

#[derive(Debug, Validate, Deserialize)]
#[garde(context(PaginatorValidContext))]
pub struct Paginator {
    #[garde(custom(validate_page_size))]
    pub page_size: usize,
    #[garde(custom(validate_page_no))]
    pub page_no: usize,
}

fn validate_page_size(v: &usize, args: &PaginatorValidContext) -> garde::Result {
    args.page_size_range
        .contains(&v)
        .then_some(())
        .ok_or_else(|| garde::Error::new("page_size is out of range"))
}

fn validate_page_no(v: &usize, args: &PaginatorValidContext) -> garde::Result {
    args.page_no_range
        .contains(&v)
        .then_some(())
        .ok_or_else(|| garde::Error::new("page_no is out of range"))
}

#[derive(Debug, Clone)]
pub struct PaginatorValidContext {
    page_size_range: RangeInclusive<usize>,
    page_no_range: RangeFrom<usize>,
}

pub async fn paginator_from_form_garde(Garde(Form(paginator)): Garde<Form<Paginator>>) {
    assert!((1..=50).contains(&paginator.page_size));
    assert!((1..).contains(&paginator.page_no));
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    let router = Router::new()
        .route("/form", post(paginator_from_form_garde))
        .with_state(PaginatorValidContext {
            page_size_range: 1..=50,
            page_no_range: 1..,
        });
    // NOTE: The PaginatorValidContext can also be stored in a XxxState,
    // make sure it implements FromRef<XxxState>.
    // Consider using Arc to reduce deep copying costs.
    let listener = TcpListener::bind(&SocketAddr::from(([0u8, 0, 0, 0], 0u16))).await?;
    axum::serve(listener, router.into_make_service()).await?;
    Ok(())
}

当前模块文档主要展示 Valid 示例,ValidEx 的用法类似。

🗂️ 提取器列表

提取器 后端/功能 数据的trait约束 功能 优点 缺点
Valid<E> validator validator::验证 验证
ValidEx<E> validator validator::ValidateArgs 带有参数的验证
Garde<E> garde garde::验证 带有或没有参数的验证 如果使用状态,则要求空元组作为参数
已验证<E> validify validify::验证 验证
已修改<E> validify validify::修改 修改/转换为响应
已验证<E> validify validify::Validifyvalidify::ValidifyPayloadserde::DeserializeOwned 构建、修改、验证 将缺失的字段视为验证错误 仅适用于使用 serde 的提取器
ValidatedByRef<E> validify validify::Validatevalidify::Modify 修改、验证

⚙️ 特性

特性 描述 模块 默认 示例 测试
默认 启用 validator 和对 QueryJsonForm 的支持 validatorqueryjsonform
validator 启用 validator (ValidValidEx) validator
garde 启用 garde (Garde) garde
validify 启用 validify (ValidatedModifiedValidatedValidifedByRef) validify
基本 启用对 QueryJsonForm 的支持 queryjsonform
json 启用对 Json 的支持 json
query 启用对 Query 的支持 query
form 启用对 Form 的支持 form
typed_header 启用对 TypedHeaderaxum-extra 的支持 typed_header
typed_multipart 启用对 TypedMultipartBaseMultipartaxum_typed_multipart 的支持 typed_multipart
msgpack 启用对 MsgPackMsgPackRawaxum-serde 的支持 msgpack
yaml 启用对 Yamlaxum-serde 的支持 yaml
xml 启用对 Xmlaxum-serde 的支持 [xml]
toml 启用对 Tomlaxum-serde 的支持 toml
sonic 启用对 Sonicaxum-serde 的支持 sonic
cbor 启用对 Cboraxum-serde 的支持 cbor
extra 启用对 CachedWithRejectionaxum-extra 的支持 extra
extra_typed_path 启用对 T: TypedPathaxum-extra 的支持 extra::typed_path
extra_query 启用对 Queryaxum-extra 的支持 extra::query
extra_form 启用对 Formaxum-extra 的支持 extra::form
extra_protobuf 启用对 Protobufaxum-extra 的支持 extra::protobuf
all_extra_types 启用对 axum-extra 中所有提取器的支持 N/A
all_types 启用对所有提取器的支持 N/A
422 当验证失败时,使用422 Unprocessable Entity而不是400 Bad Request作为状态码 VALIDATION_ERROR_STATUS
into_json 验证错误将序列化为JSON格式,并作为HTTP体返回 N/A
full_validator 启用validatorall_types422into_json N/A
full_garde 启用gardeall_types422into_json。考虑使用default-features = false来排除默认validator支持 N/A
full_garde 启用validifyall_types422into_json。考虑使用default-features = false来排除默认validator支持 N/A
full 启用上述所有功能 N/A
aide 启用对aide的支持 N/A

🔌 兼容性

要确定一起工作的依赖项的兼容版本,请参阅Cargo.toml文件中列出的依赖项。那里列出的版本号将指示兼容版本。

如果您遇到代码编译问题,可能是由于缺少特征界限未满足的特征要求不正确的依赖项版本选择引起的。

📜 许可证

本项目采用MIT许可证。

📚 参考资料

依赖项

~7–25MB
~350K SLoC