arcature 2026.2.1

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! The `Validated<T>` Axum extractor (A5).
//!
//! `Validated<T>` is the high-level request DX type: it combines JSON body
//! extraction, deserialization, and validation into a single Axum
//! [`FromRequest`] extractor. When a controller takes
//! `input: Validated<StoreLinkRequest>`, the payload is extracted and
//! validated before the handler runs — the handler may trust that
//! validation succeeded.
//!
//! ## Validation is the trust boundary
//!
//! At the point a handler receives `Validated<T>`, the `T` has passed
//! [`validator::Validate::validate`]. The handler does not re-validate.
//! This is the "validation is the trust boundary" principle (PROGRAM.md
//! "Request validation").
//!
//! **Validation must not imply authorization.** A validated request is not
//! an authorized request. Authorization is a separate, explicit step (A9
//! Policies). This invariant is permanent (PROGRAM.md "Architecture rules").
//!
//! ## Error responses
//!
//! Extraction and validation failures produce RFC 9457 `Problem` responses
//! (422 for validation failures, 400/415 for extraction failures). This
//! reuses the existing `arcature-api` validation infrastructure — no new
//! API error format (PROGRAM.md "Validation errors").
//!
//! The browser/Inertia validation path (redirect-back-with-errors,
//! preserve field errors and safe old input) is a hook for A6; A5
//! delivers the API 422 Problem path.
//!
//! ## Implementation
//!
//! `Validated<T>` delegates to [`arcature_api::ValidatedJson<T>`], the
//! existing extractor that wraps `axum::Json<T>` + `validator::Validate`
//! and maps rejections/validation failures to `Problem` responses. This
//! satisfies "Do NOT build a homemade validation engine" — the
//! `validator` crate is the engine, `arcature-api` is the integration.
//!
//! ## Example
//!
//! ```ignore
//! use arcature::{Deserialize, Validated, Validate};
//! use arcature::Json;
//!
//! #[request]
//! pub struct StoreLinkRequest {
//!     #[validate(url)]
//!     pub url: String,
//!     #[validate(length(min = 1, max = 120))]
//!     pub title: String,
//! }
//!
//! async fn store(input: Validated<StoreLinkRequest>) -> Result<Json<Link>> {
//!     let data = input.into_inner();
//!     // ... create link from `data` ...
//!     Ok(Json(link))
//! }
//! ```

use axum::extract::FromRequest;
use axum::response::Response;
use serde::de::DeserializeOwned;

/// A validated JSON request body extractor.
///
/// Wraps [`arcature_api::ValidatedJson<T>`]: extracts and deserializes the
/// JSON body, validates `T` with [`validator::Validate`], and maps
/// rejections/validation failures to RFC 9457 [`Problem`](arcature_api::Problem)
/// responses (`application/problem+json`).
///
/// `T` must implement [`serde::de::DeserializeOwned`] (for the JSON body)
/// and [`validator::Validate`] (for the rules). The payload is validated
/// exactly once.
///
/// Use [`Validated::into_inner`] to extract the validated value in the
/// handler.
pub struct Validated<T>(pub T);

impl<T> Validated<T> {
    /// Consumes the wrapper and returns the validated inner value.
    ///
    /// After this call, the handler owns the validated `T` and may trust
    /// that validation succeeded.
    #[must_use]
    pub fn into_inner(self) -> T {
        self.0
    }
}

impl<T, S> FromRequest<S> for Validated<T>
where
    T: DeserializeOwned + validator::Validate,
    S: Send + Sync,
{
    type Rejection = Response;

    async fn from_request(req: axum::extract::Request, state: &S) -> Result<Self, Self::Rejection> {
        let arcature_api::ValidatedJson(value) =
            arcature_api::ValidatedJson::<T>::from_request(req, state).await?;
        Ok(Validated(value))
    }
}