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