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 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 Body(actix_web::Error),
52 Deserialize(facet_urlencoded::UrlEncodedError),
54 MissingContentType,
56 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 .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}