Skip to main content

serverust_core/
pipeline.rs

1//! Pipeline declarativa do serverust: Guards, Pipes e Interceptors.
2//!
3//! - [`Guard`]: verificação síncrona/async aplicada ANTES do handler. Macros
4//!   `#[guard(...)]` injetam um [`GuardCheck`] como extractor no início da
5//!   assinatura do handler, fazendo o axum rejeitar a requisição (401/403/etc.)
6//!   sem chegar ao corpo do handler.
7//! - [`Pipe<I>`]: transformação tipada de input → output. `ParseUuidPipe` é
8//!   fornecido como exemplo canônico (`String` → `Uuid`). [`PipePath`] é o
9//!   extractor que aplica o pipe sobre um segmento da URL.
10//! - [`Interceptor`]: middleware tower-style aplicado via `App::interceptor()`.
11//!   Envolve toda a execução (guards + pipes + handler), permitindo pré/pós
12//!   processamento de request/response.
13
14// As primitivas devolvem `Result<_, axum::response::Response>` por design —
15// Response é a moeda de erro idiomática do axum. O `result_large_err` aqui é
16// estrutural e não há ganho em boxar.
17#![allow(clippy::result_large_err)]
18
19use std::marker::PhantomData;
20
21use axum::extract::FromRequestParts;
22use axum::extract::Path;
23use axum::extract::Request;
24use axum::middleware::Next;
25use axum::response::{IntoResponse, Response};
26use http::StatusCode;
27use http::request::Parts;
28
29// ---------------------------------------------------------------------------
30// Guard
31// ---------------------------------------------------------------------------
32
33/// Verificação de autorização/permissão executada antes do handler.
34///
35/// Implementadores recebem a parte "leve" da requisição (`Parts` — sem body)
36/// e devolvem `Ok(())` para permitir a execução ou um [`Response`] de
37/// rejeição (tipicamente 401/403) para curto-circuitar.
38pub trait Guard: Send + Sync + 'static {
39    fn check(parts: &Parts) -> impl Future<Output = Result<(), Response>> + Send;
40}
41
42/// Extractor zero-cost que dispara a verificação do guard `G` como parte do
43/// pipeline de extractors do axum. Usado pelas macros `#[guard(G)]`; raramente
44/// instanciado diretamente.
45pub struct GuardCheck<G: Guard>(PhantomData<fn() -> G>);
46
47impl<S, G> FromRequestParts<S> for GuardCheck<G>
48where
49    G: Guard,
50    S: Send + Sync,
51{
52    type Rejection = Response;
53
54    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
55        G::check(parts).await?;
56        Ok(Self(PhantomData))
57    }
58}
59
60// ---------------------------------------------------------------------------
61// Pipe
62// ---------------------------------------------------------------------------
63
64/// Transformação tipada aplicada a um input antes de chegar ao handler.
65///
66/// Pipes substituem código repetitivo de parsing/normalização. Em caso de
67/// falha devolvem um [`Response`] (tipicamente 400) que curto-circuita a
68/// execução do handler.
69pub trait Pipe<I>: Send + Sync + 'static {
70    type Output;
71
72    fn transform(input: I) -> Result<Self::Output, Response>;
73}
74
75/// Extractor que aplica um [`Pipe<String>`] sobre um único segmento da URL
76/// (extraído via `axum::extract::Path<String>`).
77///
78/// Exemplo: `PipePath<ParseUuidPipe>` em `/user/{id}` extrai o segmento como
79/// `String` e o transforma em `Uuid`, devolvendo 400 se inválido.
80pub struct PipePath<P>(pub <P as Pipe<String>>::Output)
81where
82    P: Pipe<String>;
83
84impl<S, P> FromRequestParts<S> for PipePath<P>
85where
86    P: Pipe<String>,
87    S: Send + Sync,
88{
89    type Rejection = Response;
90
91    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
92        let Path(raw): Path<String> = Path::from_request_parts(parts, state)
93            .await
94            .map_err(IntoResponse::into_response)?;
95        let out = P::transform(raw)?;
96        Ok(PipePath(out))
97    }
98}
99
100/// Pipe que converte uma `String` em [`uuid::Uuid`]. Falha → HTTP 400 com
101/// payload `{ "error": "invalid_uuid", "input": "<valor>" }`.
102pub struct ParseUuidPipe;
103
104impl Pipe<String> for ParseUuidPipe {
105    type Output = uuid::Uuid;
106
107    fn transform(input: String) -> Result<Self::Output, Response> {
108        uuid::Uuid::parse_str(&input).map_err(|_| {
109            let body = serde_json::json!({
110                "error": "invalid_uuid",
111                "input": input,
112            });
113            (StatusCode::BAD_REQUEST, axum::Json(body)).into_response()
114        })
115    }
116}
117
118// ---------------------------------------------------------------------------
119// Interceptor
120// ---------------------------------------------------------------------------
121
122/// Middleware com semântica de "wrap" sobre a execução: pode inspecionar a
123/// request antes do handler e/ou modificar a response depois.
124///
125/// Registrado via [`crate::App::interceptor`]. Internamente vira uma camada
126/// `axum::middleware::from_fn`, aplicada apenas às rotas do usuário (não às
127/// rotas de documentação geradas pelo App).
128pub trait Interceptor: Send + Sync + 'static {
129    fn intercept(&self, req: Request, next: Next) -> impl Future<Output = Response> + Send;
130}