use std::{error::Error, fmt};
use serde::{
de::{value::Error as SerdeError, DeserializeOwned},
Deserialize,
};
use crate::Body;
#[derive(Debug)]
pub enum PayloadError {
Json(serde_json::Error),
WwwFormUrlEncoded(SerdeError),
}
impl fmt::Display for PayloadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
PayloadError::Json(json) => writeln!(f, "failed to parse payload from application/json {json}"),
PayloadError::WwwFormUrlEncoded(form) => writeln!(
f,
"failed to parse payload from application/x-www-form-urlencoded {form}"
),
}
}
}
impl Error for PayloadError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
PayloadError::Json(json) => Some(json),
PayloadError::WwwFormUrlEncoded(form) => Some(form),
}
}
}
pub trait RequestPayloadExt {
fn payload<D>(&self) -> Result<Option<D>, PayloadError>
where
D: DeserializeOwned;
}
impl RequestPayloadExt for http::Request<Body> {
fn payload<D>(&self) -> Result<Option<D>, PayloadError>
where
for<'de> D: Deserialize<'de>,
{
self.headers()
.get(http::header::CONTENT_TYPE)
.map(|ct| match ct.to_str() {
Ok(content_type) => {
if content_type.starts_with("application/x-www-form-urlencoded") {
return serde_urlencoded::from_bytes::<D>(self.body().as_ref())
.map_err(PayloadError::WwwFormUrlEncoded)
.map(Some);
} else if content_type.starts_with("application/json") {
return serde_json::from_slice::<D>(self.body().as_ref())
.map_err(PayloadError::Json)
.map(Some);
}
Ok(None)
}
_ => Ok(None),
})
.unwrap_or_else(|| Ok(None))
}
}
#[cfg(test)]
mod tests {
use serde::Deserialize;
use super::RequestPayloadExt;
use crate::Body;
#[derive(Deserialize, Eq, PartialEq, Debug)]
struct Payload {
foo: String,
baz: usize,
}
#[test]
fn requests_have_form_post_parsable_payloads() {
let request = http::Request::builder()
.header("Content-Type", "application/x-www-form-urlencoded")
.body(Body::from("foo=bar&baz=2"))
.expect("failed to build request");
let payload: Option<Payload> = request.payload().unwrap_or_default();
assert_eq!(
payload,
Some(Payload {
foo: "bar".into(),
baz: 2
})
);
}
#[test]
fn requests_have_json_parsable_payloads() {
let request = http::Request::builder()
.header("Content-Type", "application/json")
.body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
.expect("failed to build request");
let payload: Option<Payload> = request.payload().unwrap_or_default();
assert_eq!(
payload,
Some(Payload {
foo: "bar".into(),
baz: 2
})
);
}
#[test]
fn requests_match_form_post_content_type_with_charset() {
let request = http::Request::builder()
.header("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8")
.body(Body::from("foo=bar&baz=2"))
.expect("failed to build request");
let payload: Option<Payload> = request.payload().unwrap_or_default();
assert_eq!(
payload,
Some(Payload {
foo: "bar".into(),
baz: 2
})
);
}
#[test]
fn requests_match_json_content_type_with_charset() {
let request = http::Request::builder()
.header("Content-Type", "application/json; charset=UTF-8")
.body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
.expect("failed to build request");
let payload: Option<Payload> = request.payload().unwrap_or_default();
assert_eq!(
payload,
Some(Payload {
foo: "bar".into(),
baz: 2
})
);
}
#[test]
fn requests_omitting_content_types_do_not_support_parsable_payloads() {
let request = http::Request::builder()
.body(Body::from(r#"{"foo":"bar", "baz": 2}"#))
.expect("failed to build request");
let payload: Option<Payload> = request.payload().unwrap_or_default();
assert_eq!(payload, None);
}
}