axum_xml/
lib.rs

1//! XML extractor for axum
2//!
3//! This crate provides struct `Xml` that can be used to extract typed information from request's body.
4//!
5//! Under the hood, [quick-xml](https://github.com/tafia/quick-xml) is used to parse payloads.
6//!
7//! ## Features
8//!
9//! - `encoding`: support non utf-8 payload
10#![allow(clippy::module_name_repetitions)]
11
12use std::ops::{Deref, DerefMut};
13
14use async_trait::async_trait;
15use axum_core::extract::{FromRequest, RequestParts};
16use axum_core::response::{IntoResponse, Response};
17use axum_core::BoxError;
18use bytes::Bytes;
19use http::{header, HeaderValue, StatusCode};
20use http_body::Body as HttpBody;
21use serde::de::DeserializeOwned;
22use serde::Serialize;
23
24use crate::rejection::XmlRejection;
25
26mod rejection;
27#[cfg(test)]
28mod tests;
29
30/// XML Extractor / Response.
31///
32/// When used as an extractor, it can deserialize request bodies into some type that
33/// implements [`serde::Deserialize`]. If the request body cannot be parsed, or it does not contain
34/// the `Content-Type: application/xml` header, it will reject the request and return a
35/// `400 Bad Request` response.
36///
37/// # Extractor example
38///
39/// ```rust,no_run
40/// use axum::{
41///     extract,
42///     routing::post,
43///     Router,
44/// };
45/// use serde::Deserialize;
46/// use axum_xml::Xml;
47///
48/// #[derive(Deserialize)]
49/// struct CreateUser {
50///     email: String,
51///     password: String,
52/// }
53///
54/// async fn create_user(Xml(payload): Xml<CreateUser>) {
55///     // payload is a `CreateUser`
56/// }
57///
58/// let app = Router::new().route("/users", post(create_user));
59/// # async {
60/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
61/// # };
62/// ```
63///
64/// When used as a response, it can serialize any type that implements [`serde::Serialize`] to
65/// `XML`, and will automatically set `Content-Type: application/xml` header.
66///
67/// # Response example
68///
69/// ```
70/// use axum::{
71///     extract::Path,
72///     routing::get,
73///     Router,
74/// };
75/// use serde::Serialize;
76/// use uuid::Uuid;
77/// use axum_xml::Xml;
78///
79/// #[derive(Serialize)]
80/// struct User {
81///     id: Uuid,
82///     username: String,
83/// }
84///
85/// async fn get_user(Path(user_id) : Path<Uuid>) -> Xml<User> {
86///     let user = find_user(user_id).await;
87///     Xml(user)
88/// }
89///
90/// async fn find_user(user_id: Uuid) -> User {
91///     // ...
92///     # unimplemented!()
93/// }
94///
95/// let app = Router::new().route("/users/:id", get(get_user));
96/// # async {
97/// # axum::Server::bind(&"".parse().unwrap()).serve(app.into_make_service()).await.unwrap();
98/// # };
99/// ```
100#[derive(Debug, Clone, Copy, Default)]
101pub struct Xml<T>(pub T);
102
103#[async_trait]
104impl<T, B> FromRequest<B> for Xml<T>
105where
106    T: DeserializeOwned,
107    B: HttpBody + Send,
108    B::Data: Send,
109    B::Error: Into<BoxError>,
110{
111    type Rejection = XmlRejection;
112
113    async fn from_request(req: &mut RequestParts<B>) -> Result<Self, Self::Rejection> {
114        if xml_content_type(req) {
115            let bytes = Bytes::from_request(req).await?;
116
117            let value = quick_xml::de::from_reader(&*bytes)?;
118
119            Ok(Self(value))
120        } else {
121            Err(XmlRejection::MissingXMLContentType)
122        }
123    }
124}
125
126fn xml_content_type<B>(req: &RequestParts<B>) -> bool {
127    let content_type = if let Some(content_type) = req.headers().get(header::CONTENT_TYPE) {
128        content_type
129    } else {
130        return false;
131    };
132
133    let content_type = if let Ok(content_type) = content_type.to_str() {
134        content_type
135    } else {
136        return false;
137    };
138
139    let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
140        mime
141    } else {
142        return false;
143    };
144
145    let is_xml_content_type = (mime.type_() == "application" || mime.type_() == "text")
146        && (mime.subtype() == "xml" || mime.suffix().map_or(false, |name| name == "xml"));
147
148    is_xml_content_type
149}
150
151impl<T> Deref for Xml<T> {
152    type Target = T;
153
154    fn deref(&self) -> &Self::Target {
155        &self.0
156    }
157}
158
159impl<T> DerefMut for Xml<T> {
160    fn deref_mut(&mut self) -> &mut Self::Target {
161        &mut self.0
162    }
163}
164
165impl<T> From<T> for Xml<T> {
166    fn from(inner: T) -> Self {
167        Self(inner)
168    }
169}
170
171impl<T> IntoResponse for Xml<T>
172where
173    T: Serialize,
174{
175    fn into_response(self) -> Response {
176        let mut bytes = Vec::new();
177        match quick_xml::se::to_writer(&mut bytes, &self.0) {
178            Ok(_) => (
179                [(
180                    header::CONTENT_TYPE,
181                    HeaderValue::from_static("application/xml"),
182                )],
183                bytes,
184            )
185                .into_response(),
186            Err(err) => (
187                StatusCode::INTERNAL_SERVER_ERROR,
188                [(
189                    header::CONTENT_TYPE,
190                    HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
191                )],
192                err.to_string(),
193            )
194                .into_response(),
195        }
196    }
197}