axum_xml_up/
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
11use crate::rejection::XmlRejection;
12use axum_core::body::Body;
13use axum_core::extract::{FromRequest, Request};
14use axum_core::response::{IntoResponse, Response};
15use bytes::Bytes;
16use core::pin::Pin;
17use http::{header, HeaderValue, StatusCode};
18use serde::de::DeserializeOwned;
19use serde::Serialize;
20use std::future::Future;
21use std::ops::{Deref, DerefMut};
22
23mod rejection;
24#[cfg(test)]
25mod tests;
26
27/// XML Extractor / Response.
28///
29/// When used as an extractor, it can deserialize request bodies into some type that
30/// implements [`serde::Deserialize`]. If the request body cannot be parsed, or it does not contain
31/// the `Content-Type: application/xml` header, it will reject the request and return a
32/// `400 Bad Request` response.
33///
34/// # Extractor example
35///
36/// ```rust,no_run
37/// use axum::{
38///     extract,
39///     routing::post,
40///     Router,
41/// };
42/// use serde::Deserialize;
43/// use axum_xml_up::Xml;
44///
45/// #[derive(Deserialize)]
46/// struct CreateUser {
47///     email: String,
48///     password: String,
49/// }
50///
51/// async fn create_user(Xml(payload): Xml<CreateUser>) {
52///     // payload is a `CreateUser`
53/// }
54///
55/// let app = Router::new().route("/users", post(create_user));
56/// # async {
57/// # let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
58/// # axum::serve(listener, app).await.unwrap();
59/// # };
60/// ```
61///
62/// When used as a response, it can serialize any type that implements [`serde::Serialize`] to
63/// `XML`, and will automatically set `Content-Type: application/xml` header.
64///
65/// # Response example
66///
67/// ```
68/// use axum::{
69///     extract::Path,
70///     routing::get,
71///     Router,
72/// };
73/// use serde::Serialize;
74/// use axum_xml_up::Xml;
75///
76/// #[derive(Serialize)]
77/// struct User {
78///     id: u32,
79///     username: String,
80/// }
81///
82/// async fn get_user(Path(user_id) : Path<u32>) -> Xml<User> {
83///     let user = find_user(user_id).await;
84///     Xml(user)
85/// }
86///
87/// async fn find_user(user_id: u32) -> User {
88///     // ...
89///     # unimplemented!()
90/// }
91///
92/// let app = Router::new().route("/users/:id", get(get_user));
93/// # async {
94/// # let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
95/// # axum::serve(listener, app).await.unwrap();
96/// # };
97/// ```
98#[derive(Debug, Clone, Copy, Default)]
99pub struct Xml<T>(pub T);
100
101impl<T, S> FromRequest<S> for Xml<T>
102where
103    T: DeserializeOwned,
104    S: Send + Sync,
105{
106    type Rejection = XmlRejection;
107
108    fn from_request<'state, 'future>(
109        req: Request<Body>,
110        state: &'state S,
111    ) -> Pin<Box<dyn Future<Output = Result<Self, Self::Rejection>> + Send + 'future>>
112    where
113        'state: 'future,
114        Self: 'future,
115    {
116        Box::pin(async move {
117            let content_type = content_type(&req);
118            if !content_type.is_some_and(is_xml_type) {
119                return Err(XmlRejection::MissingXMLContentType);
120            }
121
122            let bytes = Bytes::from_request(req, state).await?;
123
124            println!("{:?}", bytes);
125
126            let value = quick_xml::de::from_reader(&*bytes)?;
127
128            Ok(Self(value))
129        })
130    }
131}
132
133/// Obtains and parses the mime type of the Content-Type header
134fn content_type(req: &Request) -> Option<mime::Mime> {
135    req.headers()
136        // Get content type header
137        .get(header::CONTENT_TYPE)
138        // Get the header string value
139        .and_then(|value| value.to_str().ok())
140        // Parse the mime type
141        .and_then(|value| value.parse::<mime::Mime>().ok())
142}
143
144/// Checks whether the provided mime type can be considered xml
145fn is_xml_type(mime: mime::Mime) -> bool {
146    let type_ = mime.type_();
147    // Ensure the main type is application/ or text/
148    (type_ == "application" || type_ == "text")
149    // Ensure the subtype or suffix is xml
150        && (mime.subtype() == "xml" || mime.suffix().is_some_and(|value| value == "xml"))
151}
152
153impl<T> Deref for Xml<T> {
154    type Target = T;
155
156    fn deref(&self) -> &Self::Target {
157        &self.0
158    }
159}
160
161impl<T> DerefMut for Xml<T> {
162    fn deref_mut(&mut self) -> &mut Self::Target {
163        &mut self.0
164    }
165}
166
167impl<T> From<T> for Xml<T> {
168    fn from(inner: T) -> Self {
169        Self(inner)
170    }
171}
172
173impl<T> IntoResponse for Xml<T>
174where
175    T: Serialize,
176{
177    fn into_response(self) -> Response {
178        match quick_xml::se::to_string(&self.0) {
179            Ok(value) => (
180                [(
181                    header::CONTENT_TYPE,
182                    HeaderValue::from_static("application/xml"),
183                )],
184                value,
185            )
186                .into_response(),
187            Err(err) => (
188                StatusCode::INTERNAL_SERVER_ERROR,
189                [(
190                    header::CONTENT_TYPE,
191                    HeaderValue::from_static(mime::TEXT_PLAIN_UTF_8.as_ref()),
192                )],
193                err.to_string(),
194            )
195                .into_response(),
196        }
197    }
198}