axum_serde/
macros.rs

1//! Extractor macro
2//!
3
4pub use axum_core::{
5    extract::{FromRequest, Request},
6    response::{IntoResponse, Response},
7};
8pub use bytes::Bytes;
9pub use http;
10pub use mime;
11pub use serde::{de::DeserializeOwned, Deserialize, Serialize};
12
13/// This macro is designed to create an extractor type.
14/// It uses `serde` for extracting data from requests and serializing data into response body.
15///
16/// # Arguments
17///
18/// * `$name` - The name of the data format.
19/// * `$ext` - The actual type name of the HTTP extractor/response.
20/// * `$content_type` - The Content-Type that this extractor supports.
21/// * `$de` - A function identifier for deserializing data from the HTTP request body.
22/// * `$de_err` - The type of error that can occur when deserializing from the request body.
23/// * `$ser` - A function identifier for serializing the HTTP response body to bytes.
24/// * `$test` - The test module name.
25///
26#[macro_export]
27macro_rules! extractor {
28    (
29        $name:tt,
30        $ext:tt,
31        $content_type: tt,
32        $de:ident,
33        $de_err:ident,
34        $ser:ident,
35        $test:ident
36    ) => {
37        #[doc = stringify!($name)]
38        #[doc = " Extractor / Response."]
39        #[doc = ""]
40        #[doc = "When used as an extractor, it can deserialize request bodies into some type that"]
41        #[doc = "implements [`serde::Deserialize`]. The request will be rejected (and a [`crate::Rejection`] will"]
42        #[doc = "be returned) if:"]
43        #[doc = "- The request doesn't have a `Content-Type:"]
44        #[doc = $content_type]
45        #[doc = "` (or similar) header."]
46        #[doc = "- The body doesn't contain syntactically valid "]
47        #[doc = stringify!($name)]
48        #[doc = "."]
49        #[doc = "- The body contains syntactically valid "]
50        #[doc = stringify!($name)]
51        #[doc = " but it couldn't be deserialized into the target"]
52        #[doc = "type."]
53        #[doc = "- Buffering the request body fails."]
54        #[doc = ""]
55        #[doc = "⚠️ Since parsing "]
56        #[doc = stringify!($name)]
57        #[doc = " requires consuming the request body, the "]
58        #[doc = stringify!($ext)]
59        #[doc = " extractor must be last if there are multiple extractors in a handler."]
60        #[doc = ""]
61        #[doc = "See [`crate::Rejection`] for more details."]
62        #[doc = "# Extractor example"]
63        #[doc = " ```rust,ignore"]
64        #[doc = " use axum::{"]
65        #[doc = "     routing::post,"]
66        #[doc = "     Router,"]
67        #[doc = " };"]
68        #[doc = concat!(" use axum_serde::", stringify!($ext), ";")]
69        #[doc = " use serde::Deserialize;"]
70        #[doc = ""]
71        #[doc = " #[derive(Deserialize)]"]
72        #[doc = " struct CreateUser {"]
73        #[doc = "     email: String,"]
74        #[doc = "     password: String,"]
75        #[doc = " }"]
76        #[doc = ""]
77        #[doc = concat!(" async fn create_user(", stringify!($ext), "(payload): ", stringify!($ext), "<CreateUser>) {")]
78        #[doc = "     // payload is a `CreateUser`"]
79        #[doc = " }"]
80        #[doc = ""]
81        #[doc = " let app = Router::new().route(\"/users\", post(create_user));"]
82        #[doc = " # let _: Router = app;"]
83        #[doc = " ```"]
84        #[doc = " When used as a response, it can serialize any type that implements [`serde::Serialize`] to"]
85        #[doc = " `"]
86        #[doc = stringify!($name)]
87        #[doc = "`, and will automatically set `Content-Type:"]
88        #[doc = $content_type]
89        #[doc = "` header."]
90        #[doc = ""]
91        #[doc = " # Response example"]
92        #[doc = ""]
93        #[doc = " ```rust,ignore"]
94        #[doc = " use axum::{"]
95        #[doc = "     extract::Path,"]
96        #[doc = "     routing::get,"]
97        #[doc = "     Router,"]
98        #[doc = " };"]
99        #[doc = concat!(" use axum_serde::", stringify!($ext), ";")]
100        #[doc = " use serde::Serialize;"]
101        #[doc = " use uuid::Uuid;"]
102        #[doc = ""]
103        #[doc = " #[derive(Serialize)]"]
104        #[doc = " struct User {"]
105        #[doc = "     id: Uuid,"]
106        #[doc = "     username: String,"]
107        #[doc = " }"]
108        #[doc = ""]
109        #[doc = concat!(" async fn get_user(Path(user_id) : Path<Uuid>) -> ", stringify!($ext), "<User> {")]
110        #[doc = "     let user = find_user(user_id).await;"]
111        #[doc = concat!("     ", stringify!($ext), "(user)")]
112        #[doc = " }"]
113        #[doc = ""]
114        #[doc = " async fn find_user(user_id: Uuid) -> User {"]
115        #[doc = "     // ..."]
116        #[doc = "     # unimplemented!()"]
117        #[doc = " }"]
118        #[doc = ""]
119        #[doc = " let app = Router::new().route(\"/users/:id\", get(get_user));"]
120        #[doc = " # let _: Router = app;"]
121        #[doc = " ```"]
122        #[derive(Debug, Clone, Copy, Default)]
123        #[must_use]
124        pub struct $ext<T>(pub T);
125
126        impl<T> From<T> for $ext<T> {
127            fn from(inner: T) -> Self {
128                Self(inner)
129            }
130        }
131
132        impl<T> std::ops::Deref for $ext<T> {
133            type Target = T;
134
135            #[inline]
136            fn deref(&self) -> &Self::Target {
137                &self.0
138            }
139        }
140
141        impl<T> std::ops::DerefMut for $ext<T> {
142            #[inline]
143            fn deref_mut(&mut self) -> &mut Self::Target {
144                &mut self.0
145            }
146        }
147
148        impl<T> $ext<T> {
149            #[doc = "Consumes the `"]
150            #[doc = stringify!($ext)]
151            #[doc = "` extractor and returns the inner data."]
152            pub fn into_inner(self) -> T {
153                self.0
154            }
155
156            #[doc = "Content type of "]
157            #[doc = stringify!($name)]
158            #[doc = " format."]
159            pub const CONTENT_TYPE: &'static str = $content_type;
160
161            #[doc = concat!("Construct a `", stringify!($ext), "<T>` from a byte slice.")]
162            #[doc = concat!("Most users should prefer to use the FromRequest impl but special cases may require first extracting a Request into Bytes then optionally constructing a `", stringify!($ext), "<T>`.")]
163            pub fn from_bytes(bytes: &[u8]) -> Result<Self, $crate::Rejection<$de_err>>
164                where T: $crate::macros::DeserializeOwned
165            {
166                Ok($ext($de(&bytes).map_err($crate::Rejection::InvalidContentFormat)?))
167            }
168        }
169
170        impl<T, S> $crate::macros::FromRequest<S> for $ext<T>
171        where
172            T: $crate::macros::DeserializeOwned,
173            S: Send + Sync,
174        {
175            type Rejection = $crate::Rejection<$de_err>;
176
177            async fn from_request(
178                req: $crate::macros::Request,
179                state: &S,
180            ) -> Result<Self, Self::Rejection> {
181                if $crate::check_content_type(req.headers(), Self::CONTENT_TYPE) {
182                    let src = $crate::macros::Bytes::from_request(req, state).await?;
183                    Self::from_bytes(&src)
184                } else {
185                    Err($crate::Rejection::UnsupportedMediaType(Self::CONTENT_TYPE))
186                }
187            }
188        }
189
190        impl<T> $crate::macros::IntoResponse for $ext<T>
191        where
192            T: $crate::macros::Serialize,
193        {
194            fn into_response(self) -> $crate::macros::Response {
195                match $ser(&self.0) {
196                    Ok(vec) => (
197                        [(
198                            $crate::macros::http::header::CONTENT_TYPE,
199                            $crate::macros::http::HeaderValue::from_static(Self::CONTENT_TYPE),
200                        )],
201                        vec,
202                    )
203                        .into_response(),
204                    Err(err) => (
205                        $crate::macros::http::StatusCode::INTERNAL_SERVER_ERROR,
206                        [(
207                            $crate::macros::http::header::CONTENT_TYPE,
208                            $crate::macros::http::HeaderValue::from_static($crate::macros::mime::TEXT_PLAIN_UTF_8.as_ref()),
209                        )],
210                        err.to_string(),
211                    )
212                        .into_response(),
213                }
214            }
215        }
216
217        #[cfg(test)]
218        mod $test {
219            use super::*;
220            use axum::Router;
221            use axum::routing::{get, post};
222            use $crate::macros::{
223                Bytes,
224                {Deserialize, Serialize},
225                http::{
226                    StatusCode,
227                    header::{HeaderValue, CONTENT_TYPE}
228                }
229            };
230
231            #[derive(Deserialize, Serialize, Default)]
232            struct Value {
233                v0: String,
234                v1: i32,
235            }
236
237            const TEST_ROUTE: &'static str = "/value";
238            const EXT_CONTENT_TYPE: &'static str = $content_type;
239
240            #[tokio::test]
241            async fn extractor() {
242                use axum_test::TestServer;
243
244                async fn handler($ext(_user): $ext<Value>) {
245                }
246
247                let my_app = Router::new()
248                    .route(TEST_ROUTE, post(handler));
249
250                let server = TestServer::new(my_app).expect("Failed to create test server");
251
252                let value = Value::default();
253                let bytes = Bytes::from($ser(&value).expect("Failed to serialize value"));
254
255                let response = server.post(TEST_ROUTE)
256                    .bytes(bytes.clone())
257                    .add_header(CONTENT_TYPE, HeaderValue::from_static(EXT_CONTENT_TYPE))
258                    .await;
259
260                assert_eq!(response.status_code(), StatusCode::OK);
261
262                let response = server.post(TEST_ROUTE)
263                    .bytes(bytes.clone())
264                    .await;
265                assert_eq!(response.status_code(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
266
267                let response = server.post(TEST_ROUTE)
268                    .bytes(bytes)
269                    .add_header(CONTENT_TYPE, HeaderValue::from_static("invalid/type"))
270                    .await;
271                assert_eq!(response.status_code(), StatusCode::UNSUPPORTED_MEDIA_TYPE);
272
273                let response = server.post(TEST_ROUTE)
274                    .bytes($crate::macros::Bytes::from_static(b"invalid data"))
275                    .add_header(CONTENT_TYPE, HeaderValue::from_static(EXT_CONTENT_TYPE))
276                    .await;
277                assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY);
278            }
279
280            #[tokio::test]
281            async fn response() {
282                use axum_test::TestServer;
283
284                async fn handler() -> $ext<Value> {
285                    $ext(Value::default())
286                }
287
288                let my_app = Router::new()
289                    .route(TEST_ROUTE, get(handler));
290
291                let server = TestServer::new(my_app).expect("Failed to create test server");
292
293                let response = server.get(TEST_ROUTE).await;
294                assert!($crate::check_content_type(response.headers(), EXT_CONTENT_TYPE));
295                let body = response.as_bytes();
296                let _value: Value = $de(&body).expect("Failed to deserialize");
297            }
298        }
299    };
300}