mod content_type;
mod help;
use bytes::{Bytes, BytesMut};
use content_type::{Content, ContentType};
use futures::{AsyncRead, StreamExt};
use roa_core::{async_trait, http, Context, Result, State};
#[cfg(feature = "json")]
mod decode;
#[cfg(feature = "json")]
mod json;
#[cfg(feature = "urlencoded")]
mod urlencoded;
#[cfg(feature = "template")]
use askama::Template;
#[cfg(feature = "file")]
mod file;
#[cfg(feature = "file")]
pub use file::DispositionType;
#[cfg(feature = "file")]
use file::{write_file, Path};
#[cfg(any(feature = "json", feature = "urlencoded"))]
use serde::de::DeserializeOwned;
#[cfg(feature = "json")]
use serde::Serialize;
#[async_trait(?Send)]
pub trait PowerBody: Content {
async fn body_bytes(&mut self) -> Result<Bytes>;
#[cfg(feature = "json")]
async fn read_json<B: DeserializeOwned>(&mut self) -> Result<B>;
#[cfg(feature = "urlencoded")]
async fn read_form<B: DeserializeOwned>(&mut self) -> Result<B>;
#[cfg(feature = "json")]
fn write_json<B: Serialize>(&mut self, data: &B) -> Result;
#[cfg(feature = "template")]
fn render<B: Template>(&mut self, data: &B) -> Result;
fn write_text<S: ToString>(&mut self, string: S) -> Result;
fn write_octet<B: 'static + AsyncRead + Unpin + Sync + Send>(
&mut self,
reader: B,
) -> Result;
#[cfg(feature = "file")]
async fn write_file<P: 'static + AsRef<Path>>(
&mut self,
path: P,
typ: DispositionType,
) -> Result;
}
#[async_trait(?Send)]
impl<S: State> PowerBody for Context<S> {
async fn body_bytes(&mut self) -> Result<Bytes> {
let mut bytes = BytesMut::new();
let mut stream = self.req_mut().stream();
while let Some(item) = stream.next().await {
bytes.extend(item?)
}
Ok(bytes.freeze())
}
#[cfg(feature = "json")]
async fn read_json<B: DeserializeOwned>(&mut self) -> Result<B> {
let content_type = self.content_type()?;
content_type.expect(mime::APPLICATION_JSON)?;
let data = self.body_bytes().await?;
match content_type.charset() {
None | Some(mime::UTF_8) => json::from_bytes(&data),
Some(charset) => json::from_str(&decode::decode(&data, charset.as_str())?),
}
}
#[cfg(feature = "urlencoded")]
async fn read_form<B: DeserializeOwned>(&mut self) -> Result<B> {
self.content_type()?
.expect(mime::APPLICATION_WWW_FORM_URLENCODED)?;
urlencoded::from_bytes(&self.body_bytes().await?)
}
#[cfg(feature = "json")]
fn write_json<B: Serialize>(&mut self, data: &B) -> Result {
self.resp_mut().write(json::to_bytes(data)?);
let content_type: ContentType = "application/json; charset=utf-8".parse()?;
self.resp_mut()
.headers
.insert(http::header::CONTENT_TYPE, content_type.to_value()?);
Ok(())
}
#[cfg(feature = "template")]
fn render<B: Template>(&mut self, data: &B) -> Result {
self.resp_mut()
.write(data.render().map_err(help::bug_report)?);
let content_type: ContentType = "text/html; charset=utf-8".parse()?;
self.resp_mut()
.headers
.insert(http::header::CONTENT_TYPE, content_type.to_value()?);
Ok(())
}
fn write_text<Str: ToString>(&mut self, string: Str) -> Result {
self.resp_mut().write(string.to_string());
let content_type: ContentType = "text/plain; charset=utf-8".parse()?;
self.resp_mut()
.headers
.insert(http::header::CONTENT_TYPE, content_type.to_value()?);
Ok(())
}
fn write_octet<B: 'static + AsyncRead + Unpin + Sync + Send>(
&mut self,
reader: B,
) -> Result {
self.resp_mut().write_reader(reader);
let content_type: ContentType = "application/octet-stream".parse()?;
self.resp_mut()
.headers
.insert(http::header::CONTENT_TYPE, content_type.to_value()?);
Ok(())
}
#[cfg(feature = "file")]
async fn write_file<P: 'static + AsRef<Path>>(
&mut self,
path: P,
typ: DispositionType,
) -> Result {
write_file(self, path, typ).await
}
}
#[cfg(test)]
mod tests {
use super::PowerBody;
use askama::Template;
use async_std::fs::File;
use async_std::task::spawn;
use encoding::EncoderTrap;
use futures::io::BufReader;
use http::header::CONTENT_TYPE;
use http::StatusCode;
use roa_core::http;
use roa_core::App;
use roa_tcp::Listener;
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, Hash, Eq, PartialEq, Clone, Template)]
#[template(path = "user.html")]
struct User {
id: u64,
name: String,
}
#[tokio::test]
async fn read_json() -> Result<(), Box<dyn std::error::Error>> {
let (addr, server) = App::new(())
.end(move |mut ctx| async move {
let user: User = ctx.read_json().await?;
assert_eq!(
User {
id: 0,
name: "Hexilee".to_string()
},
user
);
Ok(())
})
.run()?;
spawn(server);
let client = reqwest::Client::new();
let data = User {
id: 0,
name: "Hexilee".to_string(),
};
let resp = client
.get(&format!("http://{}", addr))
.header(CONTENT_TYPE, "text/plain/html")
.send()
.await?;
assert_eq!(StatusCode::BAD_REQUEST, resp.status());
assert!(resp
.text()
.await?
.ends_with("Content-Type value is invalid"));
let resp = client
.get(&format!("http://{}", addr))
.json(&data)
.send()
.await?;
assert_eq!(StatusCode::OK, resp.status());
let resp = client
.get(&format!("http://{}", addr))
.body(serde_json::to_vec(&data)?)
.header(CONTENT_TYPE, "text/xml")
.send()
.await?;
assert_eq!(StatusCode::BAD_REQUEST, resp.status());
assert_eq!(
"content type unmatched. application/json != text/xml",
resp.text().await?
);
let resp = client
.get(&format!("http://{}", addr))
.body(
encoding::label::encoding_from_whatwg_label("gbk")
.unwrap()
.encode(&serde_json::to_string(&data)?, EncoderTrap::Strict)
.unwrap(),
)
.header(CONTENT_TYPE, "application/json; charset=gbk")
.send()
.await?;
assert_eq!(StatusCode::OK, resp.status());
Ok(())
}
#[tokio::test]
async fn read_form() -> Result<(), Box<dyn std::error::Error>> {
let (addr, server) = App::new(())
.end(move |mut ctx| async move {
let user: User = ctx.read_form().await?;
assert_eq!(
User {
id: 0,
name: "Hexilee".to_string()
},
user
);
Ok(())
})
.run()?;
spawn(server);
let client = reqwest::Client::new();
let data = User {
id: 0,
name: "Hexilee".to_string(),
};
let resp = client
.get(&format!("http://{}", addr))
.header(CONTENT_TYPE, "text/plain/html")
.send()
.await?;
assert_eq!(StatusCode::BAD_REQUEST, resp.status());
assert!(resp
.text()
.await?
.ends_with("Content-Type value is invalid"));
let resp = client
.get(&format!("http://{}", addr))
.form(&data)
.send()
.await?;
assert_eq!(StatusCode::OK, resp.status());
let resp = client
.get(&format!("http://{}", addr))
.body(serde_json::to_vec(&data)?)
.header(CONTENT_TYPE, "text/xml")
.send()
.await?;
assert_eq!(StatusCode::BAD_REQUEST, resp.status());
assert_eq!(
"content type unmatched. application/x-www-form-urlencoded != text/xml",
resp.text().await?
);
Ok(())
}
#[tokio::test]
async fn render() -> Result<(), Box<dyn std::error::Error>> {
let (addr, server) = App::new(())
.end(move |mut ctx| async move {
let user = User {
id: 0,
name: "Hexilee".to_string(),
};
ctx.render(&user)
})
.run()?;
spawn(server);
let resp = reqwest::get(&format!("http://{}", addr)).await?;
assert_eq!(StatusCode::OK, resp.status());
assert_eq!("text/html; charset=utf-8", resp.headers()[CONTENT_TYPE]);
Ok(())
}
#[tokio::test]
async fn write_text() -> Result<(), Box<dyn std::error::Error>> {
let (addr, server) = App::new(())
.end(move |mut ctx| async move { ctx.write_text("Hello, World!") })
.run()?;
spawn(server);
let resp = reqwest::get(&format!("http://{}", addr)).await?;
assert_eq!(StatusCode::OK, resp.status());
assert_eq!("text/plain; charset=utf-8", resp.headers()[CONTENT_TYPE]);
assert_eq!("Hello, World!", resp.text().await?);
Ok(())
}
#[tokio::test]
async fn write_octet() -> Result<(), Box<dyn std::error::Error>> {
let (addr, server) = App::new(())
.end(move |mut ctx| async move {
ctx.write_octet(BufReader::new(
File::open("../assets/author.txt").await?,
))
})
.run()?;
spawn(server);
let resp = reqwest::get(&format!("http://{}", addr)).await?;
assert_eq!(StatusCode::OK, resp.status());
assert_eq!(
mime::APPLICATION_OCTET_STREAM.as_ref(),
resp.headers()[CONTENT_TYPE]
);
assert_eq!("Hexilee", resp.text().await?);
Ok(())
}
}