Skip to main content

alun_web/
extract.rs

1//! 请求提取器:ValidatedJson —— 对标 aifei 的 Argument + Validate
2//!
3//! 设计要点:
4//! - 自动校验 JSON 请求体的字段合法性
5//! - 校验失败返回 422 Unprocessable Entity
6//! - 若目标类型实现 Validate trait,则自动调用 validate()
7//! - 也可使用 alun_utils::valid::Valid 独立校验
8
9use axum::{
10    extract::{FromRequest, Json},
11    response::{IntoResponse, Response},
12};
13use alun_core::api::ApiError;
14use serde::de::DeserializeOwned;
15use validator::ValidationError;
16
17/// 带自动校验的 JSON 提取器
18///
19/// # 示例
20///
21/// ```ignore
22/// #[derive(Debug, Deserialize, Validate)]
23/// pub struct CreateUserReq {
24///     #[validate(length(min = 2, max = 50))]
25///     pub username: String,
26///     #[validate(email)]
27///     pub email: String,
28///     #[validate(custom(function = "validate_uuid"))]
29///     pub parent_id: String,
30/// }
31///
32/// async fn create_user(ValidatedJson(req): ValidatedJson<CreateUserReq>) -> Res<String> {
33///     Res::ok(req.username)
34/// }
35/// ```
36#[derive(Debug, Clone, Copy, Default)]
37pub struct ValidatedJson<T>(pub T);
38
39/// 实现 axum 的 `FromRequest` 提取器,将 JSON 请求体解析为 `T` 类型。
40///
41/// 解析失败时返回 `ValidatedJsonRejection`,该 rejection 会转换为 HTTP 400 响应。
42impl<T, S> FromRequest<S> for ValidatedJson<T>
43where
44    T: DeserializeOwned,
45    S: Send + Sync,
46{
47    type Rejection = ValidatedJsonRejection;
48
49    async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
50        match Json::<T>::from_request(req, state).await {
51            Ok(Json(value)) => Ok(ValidatedJson(value)),
52            Err(rejection) => {
53                let msg = format!("请求体格式错误: {}", rejection.body_text());
54                Err(ValidatedJsonRejection(ApiError::bad_request(msg)))
55            }
56        }
57    }
58}
59
60/// ValidatedJson 的校验便捷方法 —— 对实现了 `validator::Validate` 的类型调用 `validate()`
61impl<T: validator::Validate> ValidatedJson<T> {
62    /// 执行字段级校验
63    ///
64    /// 校验失败返回 `ApiError::unprocessable_entity`,包含格式化的校验错误列表。
65    pub fn validate(self) -> Result<Self, ApiError> {
66        self.0.validate().map_err(|e| {
67            ApiError::unprocessable_entity(
68                alun_utils::valid::Valid::format_validation_errors(&e)
69            )
70        })?;
71        Ok(self)
72    }
73}
74
75impl<T> std::ops::Deref for ValidatedJson<T> {
76    type Target = T;
77
78    fn deref(&self) -> &Self::Target {
79        &self.0
80    }
81}
82
83impl<T> std::ops::DerefMut for ValidatedJson<T> {
84    fn deref_mut(&mut self) -> &mut Self::Target {
85        &mut self.0
86    }
87}
88
89/// ValidatedJson 提取失败时的错误类型
90#[derive(Debug)]
91pub struct ValidatedJsonRejection(pub ApiError);
92
93impl IntoResponse for ValidatedJsonRejection {
94    fn into_response(self) -> Response {
95        self.0.into_response()
96    }
97}
98
99impl From<ValidatedJsonRejection> for ApiError {
100    fn from(rejection: ValidatedJsonRejection) -> Self {
101        rejection.0
102    }
103}
104
105// ---- 自定义 validator 校验函数 ----
106//
107// 以下函数可直接用于 #[validate(custom(function = "..."))] 属性宏
108// 字段为空时自动跳过校验(validate_password_strength 除外)
109
110/// validator 自定义校验:UUID 格式
111///
112/// 字段为空时自动跳过校验;有值时校验 UUID 格式(v1~v7 均支持)。
113///
114/// # 使用示例
115///
116/// ```ignore
117/// #[derive(Debug, Deserialize, Validate)]
118/// pub struct Req {
119///     #[validate(custom(function = "validate_uuid"))]
120///     pub parent_id: String,
121/// }
122/// ```
123pub fn validate_uuid(value: &str) -> Result<(), ValidationError> {
124    if value.is_empty() {
125        return Ok(());
126    }
127    if alun_utils::valid::Valid::is_uuid(value) {
128        Ok(())
129    } else {
130        Err(ValidationError::new("invalid_uuid")
131            .with_message("必须是有效的 UUID 格式".into()))
132    }
133}
134
135/// validator 自定义校验:手机号/固话(中国大陆)
136///
137/// 字段为空时自动跳过校验;有值时校验手机号或固话格式。
138///
139/// ```ignore
140/// #[validate(custom(function = "validate_mobile"))]
141/// pub mobile: String,
142/// ```
143pub fn validate_mobile(value: &str) -> Result<(), ValidationError> {
144    if value.is_empty() {
145        return Ok(());
146    }
147    if alun_utils::valid::Valid::is_mobile(value) {
148        Ok(())
149    } else {
150        Err(ValidationError::new("invalid_mobile")
151            .with_message("必须是有效的手机号".into()))
152    }
153}
154
155/// validator 自定义校验:密码强度
156///
157/// 始终校验,为空时也会报错(密码必须满足强度要求)。
158///
159/// ```ignore
160/// #[validate(custom(function = "validate_password_strength"))]
161/// pub password: String,
162/// ```
163pub fn validate_password_strength(value: &str) -> Result<(), ValidationError> {
164    if alun_utils::valid::Valid::is_strong_password(value) {
165        Ok(())
166    } else {
167        Err(ValidationError::new("weak_password")
168            .with_message("密码必须至少 8 位,包含大小写字母、数字和特殊字符".into()))
169    }
170}
171
172/// validator 自定义校验:身份证号
173///
174/// 字段为空时自动跳过校验;有值时校验中国居民身份证号(含校验位)。
175///
176/// ```ignore
177/// #[validate(custom(function = "validate_id_card"))]
178/// pub id_card: String,
179/// ```
180pub fn validate_id_card(value: &str) -> Result<(), ValidationError> {
181    if value.is_empty() {
182        return Ok(());
183    }
184    if alun_utils::valid::Valid::is_id_card(value) {
185        Ok(())
186    } else {
187        Err(ValidationError::new("invalid_id_card")
188            .with_message("必须是有效的身份证号".into()))
189    }
190}
191
192/// validator 自定义校验:日期格式(YYYY-MM-DD)
193///
194/// 字段为空时自动跳过校验;有值时校验日期格式。
195///
196/// ```ignore
197/// #[validate(custom(function = "validate_date"))]
198/// pub date: String,
199/// ```
200pub fn validate_date(value: &str) -> Result<(), ValidationError> {
201    if value.is_empty() {
202        return Ok(());
203    }
204    if alun_utils::valid::Valid::is_date(value) {
205        Ok(())
206    } else {
207        Err(ValidationError::new("invalid_date")
208            .with_message("日期格式必须为 YYYY-MM-DD".into()))
209    }
210}
211
212/// validator 自定义校验:邮箱
213///
214/// 字段为空时自动跳过校验;有值时校验邮箱格式。
215///
216/// ```ignore
217/// #[validate(custom(function = "validate_email"))]
218/// pub email: String,
219/// ```
220pub fn validate_email(value: &str) -> Result<(), ValidationError> {
221    if value.is_empty() {
222        return Ok(());
223    }
224    if alun_utils::valid::Valid::is_email(value) {
225        Ok(())
226    } else {
227        Err(ValidationError::new("invalid_email")
228            .with_message("必须是有效的邮箱格式".into()))
229    }
230}
231
232/// validator 自定义校验:URL
233///
234/// 字段为空时自动跳过校验;有值时校验 URL 格式。
235///
236/// ```ignore
237/// #[validate(custom(function = "validate_url"))]
238/// pub url: String,
239/// ```
240pub fn validate_url(value: &str) -> Result<(), ValidationError> {
241    if value.is_empty() {
242        return Ok(());
243    }
244    if alun_utils::valid::Valid::is_url(value) {
245        Ok(())
246    } else {
247        Err(ValidationError::new("invalid_url")
248            .with_message("必须是有效的 URL 格式".into()))
249    }
250}
251
252/// validator 自定义校验:日期时间格式(ISO 8601 / RFC 3339)
253///
254/// 字段为空时自动跳过校验;有值时校验日期时间格式。
255///
256/// ```ignore
257/// #[validate(custom(function = "validate_datetime"))]
258/// pub publish_time: String,
259/// ```
260pub fn validate_datetime(value: &str) -> Result<(), ValidationError> {
261    if value.is_empty() {
262        return Ok(());
263    }
264    if alun_utils::valid::Valid::is_datetime(value) {
265        Ok(())
266    } else {
267        Err(ValidationError::new("invalid_datetime")
268            .with_message("日期时间格式必须为 ISO 8601/RFC 3339".into()))
269    }
270}
271
272/// 验证日期(YYYY-MM-DD)或日期时间格式(ISO 8601 / RFC 3339)
273///
274/// 用于允许纯日期(如 "2025-04-01")和完整时间戳(如 "2025-04-01T00:00:00Z")混合的字段校验。
275/// 使用方式:
276///
277/// ```ignore
278/// #[validate(custom(function = "validate_date_or_datetime"))]
279/// pub release_date: Option<String>,
280/// ```
281pub fn validate_date_or_datetime(value: &str) -> Result<(), ValidationError> {
282    if value.is_empty() {
283        return Ok(());
284    }
285    if alun_utils::valid::Valid::is_datetime(value)
286        || alun_utils::valid::Valid::is_date(value)
287    {
288        Ok(())
289    } else {
290        Err(ValidationError::new("invalid_date_or_datetime")
291            .with_message("日期格式必须为 YYYY-MM-DD 或 ISO 8601/RFC 3339".into()))
292    }
293}
294
295/// 为实现了 `Validate` 的类型提供便捷的校验方法
296///
297/// 在 handler 中通过 `req.validate_or_reject()?;` 即可完成校验。
298pub trait ValidateExt {
299    fn validate_or_reject(&self) -> Result<(), ApiError>;
300}
301
302impl<T: validator::Validate> ValidateExt for T {
303    fn validate_or_reject(&self) -> Result<(), ApiError> {
304        self.validate().map_err(|e| {
305            ApiError::unprocessable_entity(
306                alun_utils::valid::Valid::format_validation_errors(&e)
307            )
308        })
309    }
310}