use std::ops::{Deref, DerefMut};
use ::serde::{Serialize, de::DeserializeOwned};
use http::{
Method,
header::{CONTENT_TYPE, HeaderValue},
};
use topcoat_core::{context::Cx, error::Result};
use crate::{
Body, Bytes, FromRequest, IntoResponse, OptionalFromRequest, Response, content_type,
error::{bad_request, bad_request_at},
method, to_bytes, uri,
};
#[derive(Debug, Clone, Copy, Default)]
#[must_use]
pub struct Form<T>(pub T);
impl<T> From<T> for Form<T> {
fn from(value: T) -> Self {
Self(value)
}
}
impl<T> Deref for Form<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<T> DerefMut for Form<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl<T> FromRequest for Form<T>
where
T: DeserializeOwned,
{
async fn from_request(cx: &Cx, body: Body) -> Result<Self> {
let RawForm(bytes) = RawForm::from_request(cx, body).await?;
Self::from_bytes(&bytes)
}
}
impl<T> OptionalFromRequest for Form<T>
where
T: DeserializeOwned,
{
async fn from_request(cx: &Cx, body: Body) -> Result<Option<Self>> {
if matches!(method(cx), &Method::GET | &Method::HEAD) {
return match uri(cx).query() {
Some(query) => Ok(Some(Self::from_bytes(query.as_bytes())?)),
None => Ok(None),
};
}
if content_type(cx).is_some() {
Ok(Some(<Self as FromRequest>::from_request(cx, body).await?))
} else {
Ok(None)
}
}
}
impl<T> Form<T>
where
T: DeserializeOwned,
{
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
let deserializer = serde_urlencoded::Deserializer::new(form_urlencoded::parse(bytes));
let value = serde_path_to_error::deserialize(deserializer).map_err(|error| {
bad_request_at(
error.path(),
format!("invalid form value: {}", error.inner()),
)
})?;
Ok(Self(value))
}
}
impl<T> IntoResponse for Form<T>
where
T: Serialize,
{
fn into_response(self, cx: &Cx) -> Result<Response> {
(
[(
CONTENT_TYPE,
HeaderValue::from_static("application/x-www-form-urlencoded"),
)],
serde_urlencoded::to_string(&self.0)?,
)
.into_response(cx)
}
}
#[derive(Debug, Clone, Default)]
#[must_use]
pub struct RawForm(pub Bytes);
impl FromRequest for RawForm {
async fn from_request(cx: &Cx, body: Body) -> Result<Self> {
if matches!(method(cx), &Method::GET | &Method::HEAD) {
let query = uri(cx).query().unwrap_or_default();
return Ok(Self(Bytes::copy_from_slice(query.as_bytes())));
}
if !form_content_type(content_type(cx)) {
return Err(bad_request(
"expected request with `Content-Type: application/x-www-form-urlencoded`",
)
.into());
}
let bytes = to_bytes(body, usize::MAX)
.await
.map_err(|error| bad_request(format!("failed to read request body: {error}")))?;
Ok(Self(bytes))
}
}
fn form_content_type(content_type: Option<&str>) -> bool {
let Some(content_type) = content_type else {
return false;
};
content_type
.split(';')
.next()
.unwrap_or_default()
.trim()
.eq_ignore_ascii_case("application/x-www-form-urlencoded")
}
#[cfg(test)]
mod tests {
use http::{Method, Request, header::CONTENT_TYPE};
use topcoat_core::context::{Cx, CxTestBuilder};
use super::*;
use crate::{Body, FromRequest, OptionalFromRequest, error::BadRequestError, to_bytes};
const FORM_CONTENT_TYPE: &str = "application/x-www-form-urlencoded";
fn cx(method: Method, uri: &str, content_type: Option<&str>) -> Cx {
let mut builder = Request::builder().method(method).uri(uri);
if let Some(content_type) = content_type {
builder = builder.header(CONTENT_TYPE, content_type);
}
let (parts, ()) = builder.body(()).expect("request should build").into_parts();
CxTestBuilder::new().request_context(parts).build()
}
#[tokio::test]
async fn from_request_reads_query_for_get() {
let cx = cx(Method::GET, "/search?a=1&b=two", None);
let Form(pairs) =
<Form<Vec<(String, String)>> as FromRequest>::from_request(&cx, Body::empty())
.await
.expect("a valid query string");
assert_eq!(
pairs,
vec![
("a".to_owned(), "1".to_owned()),
("b".to_owned(), "two".to_owned())
]
);
}
#[tokio::test]
async fn from_request_reads_body_for_post() {
let cx = cx(Method::POST, "/search", Some(FORM_CONTENT_TYPE));
let Form(pairs) = <Form<Vec<(String, String)>> as FromRequest>::from_request(
&cx,
Body::from("a=1&b=two"),
)
.await
.expect("a valid form body");
assert_eq!(
pairs,
vec![
("a".to_owned(), "1".to_owned()),
("b".to_owned(), "two".to_owned())
]
);
}
#[tokio::test]
async fn from_request_post_without_content_type_is_bad_request() {
let cx = cx(Method::POST, "/search", None);
let error =
<Form<Vec<(String, String)>> as FromRequest>::from_request(&cx, Body::from("a=1"))
.await
.expect_err("a missing form content type is rejected");
assert!(error.downcast_ref::<BadRequestError>().is_some());
}
#[tokio::test]
async fn optional_from_request_get_without_query_is_none() {
let cx = cx(Method::GET, "/search", None);
let form =
<Form<Vec<(String, String)>> as OptionalFromRequest>::from_request(&cx, Body::empty())
.await
.expect("an absent query string is not an error");
assert!(form.is_none());
}
#[tokio::test]
async fn optional_from_request_get_with_query_is_some() {
let cx = cx(Method::GET, "/search?a=1", None);
let form =
<Form<Vec<(String, String)>> as OptionalFromRequest>::from_request(&cx, Body::empty())
.await
.expect("a valid query string is not an error");
assert_eq!(
form.expect("a form payload is present").0,
vec![("a".to_owned(), "1".to_owned())]
);
}
#[tokio::test]
async fn optional_from_request_post_without_content_type_is_none() {
let cx = cx(Method::POST, "/search", None);
let form = <Form<Vec<(String, String)>> as OptionalFromRequest>::from_request(
&cx,
Body::from("a=1"),
)
.await
.expect("an absent content type is not an error");
assert!(form.is_none());
}
#[tokio::test]
async fn optional_from_request_post_with_content_type_is_some() {
let cx = cx(Method::POST, "/search", Some(FORM_CONTENT_TYPE));
let form = <Form<Vec<(String, String)>> as OptionalFromRequest>::from_request(
&cx,
Body::from("a=1"),
)
.await
.expect("a valid form body is not an error");
assert_eq!(
form.expect("a form payload is present").0,
vec![("a".to_owned(), "1".to_owned())]
);
}
#[test]
fn from_bytes_deserializes_into_target_type() {
let Form(pairs) =
Form::<Vec<(String, u32)>>::from_bytes(b"a=1&b=2").expect("valid form data");
assert_eq!(pairs, vec![("a".to_owned(), 1), ("b".to_owned(), 2)]);
}
#[test]
fn from_bytes_rejects_values_that_do_not_match_the_target() {
let error = Form::<Vec<(String, u32)>>::from_bytes(b"a=not-a-number")
.expect_err("a type mismatch is rejected");
assert!(error.downcast_ref::<BadRequestError>().is_some());
}
#[tokio::test]
async fn into_response_serializes_form_with_content_type() {
let response = Form(vec![
("a".to_owned(), "1".to_owned()),
("b".to_owned(), "two".to_owned()),
])
.into_response(&Cx::default())
.expect("serialization succeeds");
assert_eq!(
response
.headers()
.get(CONTENT_TYPE)
.map(http::HeaderValue::as_bytes),
Some(FORM_CONTENT_TYPE.as_bytes())
);
let body = to_bytes(response.into_body(), usize::MAX)
.await
.expect("reading the response body");
assert_eq!(&body[..], b"a=1&b=two");
}
#[tokio::test]
async fn raw_form_get_yields_query_bytes() {
let cx = cx(Method::GET, "/search?a=1&b=two", None);
let RawForm(bytes) = RawForm::from_request(&cx, Body::empty())
.await
.expect("a query string");
assert_eq!(&bytes[..], b"a=1&b=two");
}
#[tokio::test]
async fn raw_form_post_yields_body_bytes() {
let cx = cx(Method::POST, "/search", Some(FORM_CONTENT_TYPE));
let RawForm(bytes) = RawForm::from_request(&cx, Body::from("a=1&b=two"))
.await
.expect("a form body");
assert_eq!(&bytes[..], b"a=1&b=two");
}
#[tokio::test]
async fn raw_form_post_without_content_type_is_bad_request() {
let cx = cx(Method::POST, "/search", None);
let error = RawForm::from_request(&cx, Body::from("a=1"))
.await
.expect_err("a missing form content type is rejected");
assert!(error.downcast_ref::<BadRequestError>().is_some());
}
#[test]
fn form_content_type_recognizes_urlencoded_media_types() {
assert!(form_content_type(Some(FORM_CONTENT_TYPE)));
assert!(form_content_type(Some(
"application/x-www-form-urlencoded; charset=utf-8"
)));
assert!(form_content_type(Some("APPLICATION/X-WWW-FORM-URLENCODED")));
assert!(!form_content_type(None));
assert!(!form_content_type(Some("application/json")));
assert!(!form_content_type(Some("text/plain")));
}
}