Skip to main content

facet_actix/
form.rs

1use core::fmt;
2use std::{
3    marker::PhantomData,
4    ops,
5    pin::Pin,
6    task::{Context, Poll, ready},
7};
8
9use actix_web::{
10    FromRequest, HttpRequest, ResponseError,
11    http::{StatusCode, header::CONTENT_TYPE},
12    mime::{APPLICATION_WWW_FORM_URLENCODED, MULTIPART_FORM_DATA},
13    web::Bytes,
14};
15use facet::Facet;
16
17#[derive(Debug, facet::Facet)]
18#[facet(transparent)]
19pub struct Form<T>(pub T);
20
21impl<T> Form<T> {
22    /// Unwrap into inner `T` value.
23    pub fn into_inner(self) -> T {
24        self.0
25    }
26}
27
28impl<T> ops::Deref for Form<T> {
29    type Target = T;
30
31    fn deref(&self) -> &T {
32        &self.0
33    }
34}
35
36impl<T> ops::DerefMut for Form<T> {
37    fn deref_mut(&mut self) -> &mut T {
38        &mut self.0
39    }
40}
41
42impl<T: fmt::Display> fmt::Display for Form<T> {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        fmt::Display::fmt(&self.0, f)
45    }
46}
47
48#[derive(Debug)]
49pub enum FormRejection {
50    /// Failed to read the request body.
51    Body(actix_web::Error),
52    /// Failed to deserialize the form data.
53    Deserialize(facet_urlencoded::UrlEncodedError),
54    /// Missing `Content-Type: x-www-form-urlencoded` header.
55    MissingContentType,
56    /// Invalid `Content-Type` header (not x-www-form-urlencoded).
57    InvalidContentType,
58}
59
60impl fmt::Display for FormRejection {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            FormRejection::Body(err) => {
64                write!(f, "Failed to read request body: {err}")
65            }
66            FormRejection::Deserialize(err) => {
67                write!(f, "Failed to deserialize form: {err}")
68            }
69            FormRejection::MissingContentType => {
70                write!(f, "Missing `Content-Type: x-www-form-urlencoded` header")
71            }
72            FormRejection::InvalidContentType => {
73                write!(
74                    f,
75                    "Invalid `Content-Type` header: expected `x-www-form-urlencoded`"
76                )
77            }
78        }
79    }
80}
81
82impl ResponseError for FormRejection {
83    fn status_code(&self) -> StatusCode {
84        match self {
85            FormRejection::Body(_error) => StatusCode::BAD_REQUEST,
86            FormRejection::Deserialize(_deserialize_error) => StatusCode::UNPROCESSABLE_ENTITY,
87            FormRejection::MissingContentType | FormRejection::InvalidContentType => {
88                StatusCode::UNSUPPORTED_MEDIA_TYPE
89            }
90        }
91    }
92}
93
94impl<T: Facet<'static>> actix_web::FromRequest for Form<T> {
95    type Error = FormRejection;
96    type Future = FormExtractFut<T>;
97
98    fn from_request(
99        req: &actix_web::HttpRequest,
100        payload: &mut actix_web::dev::Payload,
101    ) -> Self::Future {
102        FormExtractFut {
103            req: Some(req.clone()),
104            bytes: Bytes::from_request(req, payload),
105            marker: PhantomData,
106        }
107    }
108}
109
110pub struct FormExtractFut<T: Facet<'static>> {
111    req: Option<HttpRequest>,
112    bytes: <Bytes as FromRequest>::Future,
113    marker: PhantomData<T>,
114}
115
116impl<T: Facet<'static>> Unpin for FormExtractFut<T> {}
117
118impl<T: Facet<'static>> Future for FormExtractFut<T> {
119    type Output = Result<Form<T>, FormRejection>;
120
121    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
122        let FormExtractFut { req, bytes, .. } = self.get_mut();
123
124        if let Some(req) = req.take() {
125            match req.headers().get(CONTENT_TYPE) {
126                Some(ct)
127                    if !ct
128                        .to_str()
129                        // TODO: remove unwrap
130                        .unwrap()
131                        .starts_with(APPLICATION_WWW_FORM_URLENCODED.as_ref())
132                        && !ct
133                            .to_str()
134                            .unwrap()
135                            .starts_with(MULTIPART_FORM_DATA.as_ref()) =>
136                {
137                    Err(FormRejection::InvalidContentType)?
138                }
139                Some(_) => (),
140                None => Err(FormRejection::MissingContentType)?,
141            }
142        }
143
144        let fut = Pin::new(bytes);
145
146        let res = ready!(fut.poll(cx));
147
148        let res = match res {
149            Err(err) => Err(FormRejection::Body(err)),
150            Ok(data) => {
151                match facet_urlencoded::from_str_owned::<T>(str::from_utf8(data.as_ref()).unwrap())
152                {
153                    Ok(data) => Ok(Form(data)),
154                    Err(e) => Err(FormRejection::Deserialize(e))?,
155                }
156            }
157        };
158
159        Poll::Ready(res)
160    }
161}