this/core/validation/
extractor.rs

1//! Axum extractor for validated entities
2//!
3//! This module provides the `Validated<T>` extractor that automatically
4//! validates and filters request payloads before they reach handlers.
5
6use super::config::EntityValidationConfig;
7use axum::{
8    Json,
9    extract::{FromRequest, Request},
10    http::StatusCode,
11    response::{IntoResponse, Response},
12};
13use serde_json::{Value, json};
14
15/// Trait for entities that support validation
16///
17/// This is automatically implemented by the `impl_data_entity_validated!` macro
18pub trait ValidatableEntity {
19    /// Get the validation configuration for a specific operation
20    fn validation_config(operation: &str) -> EntityValidationConfig;
21}
22
23/// Axum extractor that validates and filters entity data
24///
25/// # Usage
26///
27/// ```rust,ignore
28/// pub async fn create_invoice(
29///     Validated::<Invoice>(payload): Validated<Invoice>,
30/// ) -> Result<Json<Invoice>, StatusCode> {
31///     // payload is already validated and filtered!
32/// }
33/// ```
34pub struct Validated<T>(pub Value, std::marker::PhantomData<T>);
35
36impl<T> Validated<T> {
37    /// Create a new validated payload
38    pub fn new(payload: Value) -> Self {
39        Self(payload, std::marker::PhantomData)
40    }
41
42    /// Get the inner payload
43    pub fn into_inner(self) -> Value {
44        self.0
45    }
46}
47
48// Allow dereferencing to Value
49impl<T> std::ops::Deref for Validated<T> {
50    type Target = Value;
51
52    fn deref(&self) -> &Self::Target {
53        &self.0
54    }
55}
56
57impl<S, T> FromRequest<S> for Validated<T>
58where
59    S: Send + Sync,
60    T: ValidatableEntity + Send + Sync,
61{
62    type Rejection = Response;
63
64    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
65        // Extract the HTTP method
66        let method = req.method().clone();
67
68        // Extract JSON payload
69        let Json(payload): Json<Value> = match Json::from_request(req, state).await {
70            Ok(json) => json,
71            Err(e) => {
72                return Err((
73                    StatusCode::BAD_REQUEST,
74                    Json(json!({
75                        "error": "Invalid JSON",
76                        "details": e.to_string()
77                    })),
78                )
79                    .into_response());
80            }
81        };
82
83        // Determine operation from HTTP method
84        let operation = match method.as_str() {
85            "POST" => "create",
86            "PUT" | "PATCH" => "update",
87            _ => "create", // default
88        };
89
90        // Get validation config from entity
91        let config = T::validation_config(operation);
92
93        // Validate and filter
94        match config.validate_and_filter(payload) {
95            Ok(validated_payload) => Ok(Validated::new(validated_payload)),
96            Err(errors) => Err((
97                StatusCode::UNPROCESSABLE_ENTITY,
98                Json(json!({
99                    "error": "Validation failed",
100                    "errors": errors
101                })),
102            )
103                .into_response()),
104        }
105    }
106}