use tokio::io::{AsyncWrite, AsyncWriteExt};
use crate::status::Status;
pub enum ContentType {
Csv,
EventStream,
FormData,
Html,
Json,
MsgPack,
OctetStream,
Pdf,
Text,
Xml,
}
impl ContentType {
fn as_str(&self) -> &'static str {
match self {
Self::Csv => "text/csv",
Self::EventStream => "text/event-stream",
Self::FormData => "application/x-www-form-urlencoded",
Self::Html => "text/html; charset=utf-8",
Self::Json => "application/json",
Self::MsgPack => "application/msgpack",
Self::OctetStream => "application/octet-stream",
Self::Pdf => "application/pdf",
Self::Text => "text/plain; charset=utf-8",
Self::Xml => "application/xml",
}
}
}
pub struct Response {
pub(crate) body: Vec<u8>,
pub(crate) headers: Vec<(String, String)>,
pub(crate) status: u16,
}
impl Response {
pub fn json(body: Vec<u8>) -> Self {
Self::bytes_raw("application/json", body)
}
pub fn text(body: impl Into<String>) -> Self {
Self::bytes_raw("text/plain; charset=utf-8", body.into().into_bytes())
}
pub fn status(code: Status) -> Self {
Self { body: Vec::new(), headers: Vec::new(), status: code.into() }
}
pub fn builder() -> ResponseBuilder {
ResponseBuilder { headers: Vec::new(), status: Status::Ok.into() }
}
fn bytes_raw(content_type: &str, body: Vec<u8>) -> Self {
Self {
body,
headers: vec![("content-type".to_owned(), content_type.to_owned())],
status: Status::Ok.into(),
}
}
pub(crate) async fn write_to<W: AsyncWrite + Unpin>(
self,
writer: &mut W,
) -> std::io::Result<()> {
writer.write_all(
format!("HTTP/1.1 {} {}\r\n", self.status, status_reason(self.status)).as_bytes(),
).await?;
writer.write_all(
format!("content-length: {}\r\n", self.body.len()).as_bytes(),
).await?;
for (name, value) in &self.headers {
writer.write_all(format!("{name}: {value}\r\n").as_bytes()).await?;
}
writer.write_all(b"\r\n").await?;
writer.write_all(&self.body).await?;
writer.flush().await
}
}
pub struct ResponseBuilder {
headers: Vec<(String, String)>,
status: u16,
}
impl ResponseBuilder {
pub fn status(mut self, code: Status) -> Self {
self.status = code.into();
self
}
pub fn header(mut self, name: &str, value: &str) -> Self {
self.headers.push((name.to_owned(), value.to_owned()));
self
}
pub fn json(self, body: Vec<u8>) -> Response {
self.finish("application/json", body)
}
pub fn text(self, body: impl Into<String>) -> Response {
self.finish("text/plain; charset=utf-8", body.into().into_bytes())
}
pub fn bytes(self, content_type: ContentType, body: Vec<u8>) -> Response {
self.finish(content_type.as_str(), body)
}
pub fn no_body(self) -> Response {
Response { body: Vec::new(), headers: self.headers, status: self.status }
}
fn finish(self, content_type: &str, body: Vec<u8>) -> Response {
let mut headers = vec![("content-type".to_owned(), content_type.to_owned())];
headers.extend(self.headers);
Response { body, headers, status: self.status }
}
}
pub trait IntoResponse {
fn into_response(self) -> Response;
}
impl IntoResponse for Response {
fn into_response(self) -> Response { self }
}
impl IntoResponse for &'static str {
fn into_response(self) -> Response { Response::text(self) }
}
impl IntoResponse for String {
fn into_response(self) -> Response { Response::text(self) }
}
impl IntoResponse for Status {
fn into_response(self) -> Response { Response::status(self) }
}
fn status_reason(code: u16) -> &'static str {
match code {
100 => "Continue",
101 => "Switching Protocols",
102 => "Processing",
103 => "Early Hints",
200 => "OK",
201 => "Created",
202 => "Accepted",
203 => "Non-Authoritative Information",
204 => "No Content",
205 => "Reset Content",
206 => "Partial Content",
207 => "Multi-Status",
208 => "Already Reported",
226 => "IM Used",
300 => "Multiple Choices",
301 => "Moved Permanently",
302 => "Found",
303 => "See Other",
304 => "Not Modified",
307 => "Temporary Redirect",
308 => "Permanent Redirect",
400 => "Bad Request",
401 => "Unauthorized",
402 => "Payment Required",
403 => "Forbidden",
404 => "Not Found",
405 => "Method Not Allowed",
406 => "Not Acceptable",
407 => "Proxy Authentication Required",
408 => "Request Timeout",
409 => "Conflict",
410 => "Gone",
411 => "Length Required",
412 => "Precondition Failed",
413 => "Content Too Large",
414 => "URI Too Long",
415 => "Unsupported Media Type",
416 => "Range Not Satisfiable",
417 => "Expectation Failed",
418 => "I'm a Teapot",
421 => "Misdirected Request",
422 => "Unprocessable Content",
423 => "Locked",
424 => "Failed Dependency",
425 => "Too Early",
426 => "Upgrade Required",
428 => "Precondition Required",
429 => "Too Many Requests",
431 => "Request Header Fields Too Large",
451 => "Unavailable For Legal Reasons",
500 => "Internal Server Error",
501 => "Not Implemented",
502 => "Bad Gateway",
503 => "Service Unavailable",
504 => "Gateway Timeout",
505 => "HTTP Version Not Supported",
506 => "Variant Also Negotiates",
507 => "Insufficient Storage",
508 => "Loop Detected",
510 => "Not Extended",
511 => "Network Authentication Required",
_ => "",
}
}