raxb_axum/
lib.rs

1#![doc = include_str!("../README.md")]
2/*
3#![warn(
4    missing_docs,
5    missing_debug_implementations,
6    missing_copy_implementations,
7    trivial_casts,
8    trivial_numeric_casts,
9    unused_extern_crates,
10    unused_import_braces,
11    unused_qualifications,
12    variant_size_differences
13)]
14*/
15
16use axum::{
17    body::Bytes,
18    extract::{FromRequest, Request},
19    response::{IntoResponse, Response},
20};
21use hyper::{HeaderMap, StatusCode};
22use raxb::de::XmlDeserialize;
23use thiserror::Error;
24
25#[derive(Debug, Clone, Default)]
26#[must_use]
27pub struct RaxbXml<T>(pub T);
28
29#[async_trait::async_trait]
30impl<T, S> FromRequest<S> for RaxbXml<T>
31where
32    T: XmlDeserialize,
33    S: Send + Sync,
34{
35    type Rejection = RaxbXmlRejection;
36
37    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
38        if xml_content_type(req.headers()) {
39            let bytes = Bytes::from_request(req, state).await?;
40            RaxbXmlSource::from_bytes(bytes).map(|RaxbXmlSource(xml, _)| Self(xml))
41        } else {
42            Err(MissingXmlContentType.into())
43        }
44    }
45}
46
47#[derive(Debug, Clone, Default)]
48#[must_use]
49pub struct RaxbXmlSource<T>(pub T, pub Bytes);
50
51#[async_trait::async_trait]
52impl<T, S> FromRequest<S> for RaxbXmlSource<T>
53where
54    T: XmlDeserialize,
55    S: Send + Sync,
56{
57    type Rejection = RaxbXmlRejection;
58
59    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
60        if xml_content_type(req.headers()) {
61            let bytes = Bytes::from_request(req, state).await?;
62            Self::from_bytes(bytes)
63        } else {
64            Err(MissingXmlContentType.into())
65        }
66    }
67}
68
69impl<T> RaxbXmlSource<T>
70where
71    T: XmlDeserialize,
72{
73    /// Construct a `RaxbXmlSource<T>` from a byte slice. Most users should prefer to use the `FromRequest` impl
74    /// but special cases may require first extracting a `Request` into `Bytes` then optionally
75    /// constructing a `RaxbXmlSource<T>`.
76    pub fn from_bytes(bytes: Bytes) -> Result<Self, RaxbXmlRejection> {
77        raxb::de::from_reader(&*bytes)
78            .map(|xml| Self(xml, bytes))
79            .map_err(From::from)
80    }
81}
82
83#[derive(Debug, Error)]
84pub enum RaxbXmlRejection {
85    #[error(transparent)]
86    MissingXmlContentType(#[from] MissingXmlContentType),
87    #[error(transparent)]
88    AxumError(#[from] axum::extract::rejection::BytesRejection),
89    #[error(transparent)]
90    RaxbError(#[from] raxb::de::XmlDeserializeError),
91}
92
93impl IntoResponse for RaxbXmlRejection {
94    fn into_response(self) -> Response {
95        StatusCode::BAD_REQUEST.into_response()
96    }
97}
98
99fn xml_content_type(headers: &HeaderMap) -> bool {
100    let content_type = if let Some(content_type) = headers.get(hyper::header::CONTENT_TYPE) {
101        content_type
102    } else {
103        return false;
104    };
105
106    let content_type = if let Ok(content_type) = content_type.to_str() {
107        content_type
108    } else {
109        return false;
110    };
111
112    let mime = if let Ok(mime) = content_type.parse::<mime::Mime>() {
113        mime
114    } else {
115        return false;
116    };
117
118    let is_xml_content_type = mime.type_() == "application"
119        && (mime.subtype() == "xml" || mime.suffix().is_some_and(|name| name == "xml"));
120
121    is_xml_content_type
122}
123
124#[derive(Debug, Clone, Copy, Default, Error)]
125#[error("missing or wrong content-type, must be application/xml")]
126pub struct MissingXmlContentType;
127
128impl IntoResponse for MissingXmlContentType {
129    fn into_response(self) -> Response {
130        StatusCode::BAD_REQUEST.into_response()
131    }
132}