Skip to main content

baukit_http/
extract.rs

1use std::ops::{Deref, DerefMut};
2
3use axum::{
4    Json,
5    extract::{FromRequest, FromRequestParts, Path as AxumPath, Request},
6    http::request::Parts,
7};
8use serde::de::DeserializeOwned;
9
10use crate::{ApiError, options::JsonRejectionMode};
11
12/// A JSON body extractor whose rejections use Baukit's standard error envelope.
13///
14/// This delegates parsing to Axum's [`Json`] extractor. The configured JSON
15/// rejection mode maps failures to safe [`ApiError`] values containing the
16/// current request ID.
17#[derive(Clone, Copy, Debug, Default)]
18pub struct ApiJson<T>(
19    /// The successfully deserialized request body.
20    pub T,
21);
22
23impl<T, S> FromRequest<S> for ApiJson<T>
24where
25    T: DeserializeOwned,
26    S: Send + Sync,
27{
28    type Rejection = ApiError;
29
30    async fn from_request(request: Request, state: &S) -> Result<Self, Self::Rejection> {
31        let rejection_mode = request
32            .extensions()
33            .get::<JsonRejectionMode>()
34            .cloned()
35            .unwrap_or_else(|| JsonRejectionMode::Legacy("validation_failed".to_owned()));
36        Json::<T>::from_request(request, state)
37            .await
38            .map(|Json(value)| Self(value))
39            .map_err(|rejection| ApiError::json_rejection(&rejection, &rejection_mode))
40    }
41}
42
43impl<T> Deref for ApiJson<T> {
44    type Target = T;
45
46    fn deref(&self) -> &Self::Target {
47        &self.0
48    }
49}
50
51impl<T> DerefMut for ApiJson<T> {
52    fn deref_mut(&mut self) -> &mut Self::Target {
53        &mut self.0
54    }
55}
56
57/// A route-path extractor whose rejections use Baukit's standard error envelope.
58///
59/// This delegates parsing to Axum's [`Path`] extractor and maps every rejection
60/// to a safe `validation_failed` [`ApiError`] containing the current request ID.
61#[derive(Clone, Copy, Debug, Default)]
62pub struct Path<T>(
63    /// The successfully deserialized route parameters.
64    pub T,
65);
66
67impl<T, S> FromRequestParts<S> for Path<T>
68where
69    T: DeserializeOwned + Send,
70    S: Send + Sync,
71{
72    type Rejection = ApiError;
73
74    async fn from_request_parts(parts: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
75        AxumPath::<T>::from_request_parts(parts, state)
76            .await
77            .map(|AxumPath(value)| Self(value))
78            .map_err(ApiError::from)
79    }
80}
81
82impl<T> Deref for Path<T> {
83    type Target = T;
84
85    fn deref(&self) -> &Self::Target {
86        &self.0
87    }
88}
89
90impl<T> DerefMut for Path<T> {
91    fn deref_mut(&mut self) -> &mut Self::Target {
92        &mut self.0
93    }
94}
95
96/// Backward-compatible name for [`Path`].
97pub use Path as ApiPath;
98
99/// A query-string extractor whose rejections use Baukit's standard error envelope.
100///
101/// Collection fields use HTML-form repeated-parameter encoding, for example
102/// `?tag=rust&tag=typescript` deserializes into `tag: Vec<String>`. This is also
103/// OpenAPI's default `form`/`explode=true` query-array representation.
104///
105/// The extractor intentionally uses the name `Query`, which Utoipa's Axum
106/// integration recognizes when inferring query rather than path parameters.
107#[derive(Clone, Copy, Debug, Default)]
108pub struct Query<T>(
109    /// The successfully deserialized query parameters.
110    pub T,
111);
112
113impl<T, S> FromRequestParts<S> for Query<T>
114where
115    T: DeserializeOwned,
116    S: Send + Sync,
117{
118    type Rejection = ApiError;
119
120    async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> {
121        serde_html_form::from_str(parts.uri.query().unwrap_or_default())
122            .map(Self)
123            .map_err(|_| ApiError::query_rejection())
124    }
125}
126
127impl<T> Deref for Query<T> {
128    type Target = T;
129
130    fn deref(&self) -> &Self::Target {
131        &self.0
132    }
133}
134
135impl<T> DerefMut for Query<T> {
136    fn deref_mut(&mut self) -> &mut Self::Target {
137        &mut self.0
138    }
139}
140
141/// Backward-compatible name for [`Query`].
142pub use Query as ApiQuery;