22个版本 (破坏性更新)
0.19.0 | 2024年7月12日 |
---|---|
0.17.0 | 2024年3月5日 |
0.13.0 | 2023年12月14日 |
0.12.0-alpha | 2023年11月28日 |
0.3.0 | 2023年7月22日 |
#67 in Web编程
4,562 每月下载量
用于 2 crates
325KB
6K SLoC
axum-valid
📑 概览
axum-valid 是一个库,为Axum网络框架提供数据验证提取器。它集成了Rust生态系统中的三个流行的验证crate:validator、garde 和 validify,为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::Validify ,validify::ValidifyPayload 和 serde::DeserializeOwned |
构建、修改、验证 | 将缺失的字段视为验证错误 | 仅适用于使用 serde 的提取器 |
ValidatedByRef<E> |
validify | validify::Validate 和 validify::Modify |
修改、验证 |
⚙️ 特性
特性 | 描述 | 模块 | 默认 | 示例 | 测试 |
---|---|---|---|---|---|
默认 | 启用 validator 和对 Query 、Json 和 Form 的支持 |
validator ,query ,json ,form |
✅ | ✅ | ✅ |
validator | 启用 validator (Valid ,ValidEx ) |
validator |
✅ | ✅ | ✅ |
garde | 启用 garde (Garde ) |
garde |
❌ | ✅ | ✅ |
validify | 启用 validify (Validated ,Modified ,Validated ,ValidifedByRef ) |
validify |
❌ | ✅ | ✅ |
基本 | 启用对 Query 、Json 和 Form 的支持 |
query ,json ,form |
✅ | ✅ | ✅ |
json | 启用对 Json 的支持 |
json |
✅ | ✅ | ✅ |
query | 启用对 Query 的支持 |
query |
✅ | ✅ | ✅ |
form | 启用对 Form 的支持 |
form |
✅ | ✅ | ✅ |
typed_header | 启用对 TypedHeader 从 axum-extra 的支持 |
typed_header |
❌ | ✅ | ✅ |
typed_multipart | 启用对 TypedMultipart 和 BaseMultipart 从 axum_typed_multipart 的支持 |
typed_multipart |
❌ | ✅ | ✅ |
msgpack | 启用对 MsgPack 和 MsgPackRaw 从 axum-serde 的支持 |
msgpack |
❌ | ✅ | ✅ |
yaml | 启用对 Yaml 从 axum-serde 的支持 |
yaml |
❌ | ✅ | ✅ |
xml | 启用对 Xml 从 axum-serde 的支持 |
[xml ] |
❌ | ✅ | ✅ |
toml | 启用对 Toml 从 axum-serde 的支持 |
toml |
❌ | ✅ | ✅ |
sonic | 启用对 Sonic 从 axum-serde 的支持 |
sonic |
❌ | ✅ | ✅ |
cbor | 启用对 Cbor 从 axum-serde 的支持 |
cbor |
❌ | ✅ | ✅ |
extra | 启用对 Cached 、WithRejection 从 axum-extra 的支持 |
extra |
❌ | ✅ | ✅ |
extra_typed_path | 启用对 T: TypedPath 从 axum-extra 的支持 |
extra::typed_path |
❌ | ✅ | ✅ |
extra_query | 启用对 Query 从 axum-extra 的支持 |
extra::query |
❌ | ✅ | ✅ |
extra_form | 启用对 Form 从 axum-extra 的支持 |
extra::form |
❌ | ✅ | ✅ |
extra_protobuf | 启用对 Protobuf 从 axum-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 | 启用validator 、all_types 、422 和into_json |
N/A | ❌ | ✅ | ✅ |
full_garde | 启用garde 、all_types 、422 和into_json 。考虑使用default-features = false 来排除默认validator 支持 |
N/A | ❌ | ✅ | ✅ |
full_garde | 启用validify 、all_types 、422 和into_json 。考虑使用default-features = false 来排除默认validator 支持 |
N/A | ❌ | ✅ | ✅ |
full | 启用上述所有功能 | N/A | ❌ | ✅ | ✅ |
aide | 启用对aide 的支持 |
N/A | ❌ | ❌ | ❌ |
🔌 兼容性
要确定一起工作的依赖项的兼容版本,请参阅Cargo.toml
文件中列出的依赖项。那里列出的版本号将指示兼容版本。
如果您遇到代码编译问题,可能是由于缺少特征界限、未满足的特征要求或不正确的依赖项版本选择引起的。
📜 许可证
本项目采用MIT许可证。
📚 参考资料
依赖项
~7–25MB
~350K SLoC