use crate::blueprint::constructor::{Constructor, RegisteredConstructor};
use crate::blueprint::Blueprint;
use crate::f;
use crate::request::body::errors::{
ExtractUrlEncodedBodyError, MissingUrlEncodedContentType, UrlEncodedBodyDeserializationError,
UrlEncodedContentTypeMismatch,
};
use crate::request::body::BufferedBody;
use crate::request::RequestHead;
use http::HeaderMap;
use serde::Deserialize;
#[doc(alias = "UrlEncoded")]
#[doc(alias = "Form")]
#[doc(alias = "FormBody")]
#[doc(alias = "PercentEncoded")]
#[doc(alias = "PercentEncodedBody")]
#[derive(Debug)]
pub struct UrlEncodedBody<T>(pub T);
impl<T> UrlEncodedBody<T> {
pub fn extract<'head, 'body>(
request_head: &'head RequestHead,
buffered_body: &'body BufferedBody,
) -> Result<Self, ExtractUrlEncodedBodyError>
where
T: Deserialize<'body>,
{
check_urlencoded_content_type(&request_head.headers)?;
parse(buffered_body.bytes.as_ref()).map(UrlEncodedBody)
}
}
impl UrlEncodedBody<()> {
pub fn register(bp: &mut Blueprint) -> RegisteredConstructor {
Self::default_constructor().register(bp)
}
pub fn default_constructor() -> Constructor {
Constructor::request_scoped(f!(super::UrlEncodedBody::extract))
.error_handler(f!(super::errors::ExtractUrlEncodedBodyError::into_response))
}
}
fn check_urlencoded_content_type(headers: &HeaderMap) -> Result<(), ExtractUrlEncodedBodyError> {
let Some(content_type) = headers.get(http::header::CONTENT_TYPE) else {
return Err(MissingUrlEncodedContentType.into());
};
let Ok(content_type) = content_type.to_str() else {
return Err(MissingUrlEncodedContentType.into());
};
let Ok(mime) = content_type.parse::<mime::Mime>() else {
return Err(UrlEncodedContentTypeMismatch {
actual: content_type.to_string(),
}
.into());
};
let is_urlencoded_content_type =
mime.type_() == mime::APPLICATION && mime.subtype() == mime::WWW_FORM_URLENCODED;
if !is_urlencoded_content_type {
return Err(UrlEncodedContentTypeMismatch {
actual: content_type.to_string(),
}
.into());
};
Ok(())
}
fn parse<'a, T>(bytes: &'a [u8]) -> Result<T, ExtractUrlEncodedBodyError>
where
T: Deserialize<'a>,
{
serde_html_form::from_bytes(bytes)
.map_err(|e| UrlEncodedBodyDeserializationError { source: e })
.map_err(|e| ExtractUrlEncodedBodyError::DeserializationError(e))
}
#[cfg(test)]
mod tests {
use crate::request::body::UrlEncodedBody;
use std::borrow::Cow;
#[test]
fn test_parse() {
#[derive(serde::Deserialize, Debug, PartialEq)]
struct Home<'a> {
home_id: u32,
home_price: f64,
home_name: Cow<'a, str>,
}
let query = "home_id=1&home_price=0.1&home_name=Hi%20there";
let expected = Home {
home_id: 1,
home_price: 0.1,
home_name: Cow::Borrowed("Hi there"),
};
let actual: Home = crate::request::body::url_encoded::parse(query.as_bytes()).unwrap();
assert_eq!(expected, actual);
}
#[test]
fn missing_content_type() {
let headers = http::HeaderMap::new();
let err = super::check_urlencoded_content_type(&headers).unwrap_err();
insta::assert_snapshot!(err, @"The `Content-Type` header is missing. This endpoint expects requests with a `Content-Type` header set to `application/x-www-form-urlencoded`");
insta::assert_debug_snapshot!(err, @r###"
MissingContentType(
MissingUrlEncodedContentType,
)
"###);
}
#[test]
fn content_type_is_not_valid_mime() {
let mut headers = http::HeaderMap::new();
headers.insert(http::header::CONTENT_TYPE, "hello world".parse().unwrap());
let err = super::check_urlencoded_content_type(&headers).unwrap_err();
insta::assert_snapshot!(err, @"The `Content-Type` header was set to `hello world`. This endpoint expects requests with a `Content-Type` header set to `application/x-www-form-urlencoded`");
insta::assert_debug_snapshot!(err, @r###"
ContentTypeMismatch(
UrlEncodedContentTypeMismatch {
actual: "hello world",
},
)
"###);
}
#[test]
fn content_type_is_not_form() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/json".parse().unwrap(),
);
let err = super::check_urlencoded_content_type(&headers).unwrap_err();
insta::assert_snapshot!(err, @"The `Content-Type` header was set to `application/json`. This endpoint expects requests with a `Content-Type` header set to `application/x-www-form-urlencoded`");
insta::assert_debug_snapshot!(err, @r###"
ContentTypeMismatch(
UrlEncodedContentTypeMismatch {
actual: "application/json",
},
)
"###);
}
#[test]
fn content_type_is_form() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/x-www-form-urlencoded".parse().unwrap(),
);
let outcome = super::check_urlencoded_content_type(&headers);
assert!(outcome.is_ok());
}
#[test]
fn form_content_type_with_charset() {
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/x-www-form-urlencoded; charset=utf-8"
.parse()
.unwrap(),
);
let outcome = super::check_urlencoded_content_type(&headers);
assert!(outcome.is_ok());
}
#[test]
fn missing_form_field() {
#[derive(serde::Deserialize, Debug)]
#[allow(dead_code)]
struct BodySchema {
name: String,
surname: String,
age: u8,
}
let mut headers = http::HeaderMap::new();
headers.insert(
http::header::CONTENT_TYPE,
"application/x-www-form-urlencoded".parse().unwrap(),
);
let request_head = crate::request::RequestHead {
headers,
method: http::Method::POST,
version: http::Version::HTTP_11,
target: "/".parse().unwrap(),
};
let body = "name=John%20Doe&age=43".to_string();
let buffered_body = crate::request::body::BufferedBody { bytes: body.into() };
let outcome: Result<UrlEncodedBody<BodySchema>, _> =
UrlEncodedBody::extract(&request_head, &buffered_body);
let err = outcome.unwrap_err();
insta::assert_snapshot!(err, @r###"
Failed to deserialize the body as a urlencoded form.
missing field `surname`
"###);
insta::assert_debug_snapshot!(err, @r###"
DeserializationError(
UrlEncodedBodyDeserializationError {
source: Error(
"missing field `surname`",
),
},
)
"###);
}
}