use crate::body::Body;
use crate::error::Error;
use bytes::Bytes;
use http::header::{HeaderName, CONTENT_TYPE};
use http::{HeaderMap, HeaderValue, StatusCode};
#[derive(Debug)]
pub struct Response {
pub status: StatusCode,
pub headers: HeaderMap,
pub body: Body,
}
impl Response {
pub fn new(status: StatusCode) -> Self {
Self {
status,
headers: HeaderMap::new(),
body: Body::empty(),
}
}
pub fn text(body: impl Into<String>) -> Self {
let mut r = Self::new(StatusCode::OK);
r.headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("text/plain; charset=utf-8"),
);
r.body = Body::from(body.into());
r
}
pub fn bytes(content_type: &'static str, body: impl Into<Bytes>) -> Self {
let mut r = Self::new(StatusCode::OK);
r.headers
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
r.body = Body::from(body.into());
r
}
pub fn stream(content_type: &'static str, body: Body) -> Self {
let mut r = Self::new(StatusCode::OK);
r.headers
.insert(CONTENT_TYPE, HeaderValue::from_static(content_type));
r.body = body;
r
}
pub fn with_status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
pub fn with_cookie(mut self, cookie: crate::cookie::Cookie) -> Self {
if let Ok(v) = HeaderValue::from_str(&cookie.to_header_value()) {
self.headers.append(http::header::SET_COOKIE, v);
}
self
}
pub fn with_header(mut self, name: HeaderName, value: HeaderValue) -> Self {
self.headers.insert(name, value);
self
}
pub fn vary_on(&mut self, field: &str) {
let field = field.trim().to_ascii_lowercase();
if field.is_empty() {
return;
}
let existing: Vec<String> = self
.headers
.get_all(http::header::VARY)
.iter()
.filter_map(|v| v.to_str().ok())
.flat_map(|v| v.split(','))
.map(|v| v.trim().to_ascii_lowercase())
.filter(|v| !v.is_empty())
.collect();
if existing.iter().any(|v| v == "*" || *v == field) {
return;
}
let mut merged = existing;
merged.push(field);
if let Ok(value) = HeaderValue::from_str(&merged.join(", ")) {
self.headers.insert(http::header::VARY, value);
}
}
}
pub trait IntoResponse {
fn into_response(self) -> Response;
}
impl IntoResponse for Response {
fn into_response(self) -> Response {
self
}
}
impl IntoResponse for () {
fn into_response(self) -> Response {
Response::new(StatusCode::OK)
}
}
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 StatusCode {
fn into_response(self) -> Response {
Response::new(self)
}
}
impl<T: IntoResponse> IntoResponse for (StatusCode, T) {
fn into_response(self) -> Response {
let (status, inner) = self;
inner.into_response().with_status(status)
}
}
impl IntoResponse for Error {
fn into_response(self) -> Response {
let mut res = Response::text(self.message().to_string()).with_status(self.status());
let mut replaced: Vec<&HeaderName> = Vec::new();
for (name, value) in self.response_headers() {
if replaced.contains(&name) {
res.headers.append(name.clone(), value.clone());
} else {
res.headers.insert(name.clone(), value.clone());
replaced.push(name);
}
}
res
}
}
impl<T: IntoResponse> IntoResponse for crate::error::Result<T> {
fn into_response(self) -> Response {
match self {
Ok(v) => v.into_response(),
Err(e) => e.into_response(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn text_sets_content_type_and_body() {
let r = Response::text("hi");
assert_eq!(r.status, StatusCode::OK);
assert_eq!(r.body, Bytes::from("hi"));
assert_eq!(
r.headers.get(CONTENT_TYPE).unwrap(),
"text/plain; charset=utf-8"
);
}
#[test]
fn status_tuple_overrides_status() {
let r = (StatusCode::CREATED, "made").into_response();
assert_eq!(r.status, StatusCode::CREATED);
assert_eq!(r.body, Bytes::from("made"));
}
#[test]
fn error_renders_with_its_status() {
let r = Error::bad_request("x").into_response();
assert_eq!(r.status, StatusCode::BAD_REQUEST);
}
#[test]
fn an_error_carrying_two_of_one_header_renders_both() {
let r = Error::new(StatusCode::UNAUTHORIZED, "no")
.with_response_header(
http::header::WWW_AUTHENTICATE,
HeaderValue::from_static("Basic realm=\"api\""),
)
.with_response_header(
http::header::WWW_AUTHENTICATE,
HeaderValue::from_static("Bearer"),
)
.into_response();
let challenges: Vec<_> = r
.headers
.get_all(http::header::WWW_AUTHENTICATE)
.iter()
.map(|v| v.to_str().unwrap())
.collect();
assert_eq!(
challenges,
vec!["Basic realm=\"api\"", "Bearer"],
"both challenges should reach the client, in order"
);
}
#[test]
fn an_error_header_still_overrides_the_body_content_type() {
let r = Error::bad_request("nope")
.with_response_header(
http::header::CONTENT_TYPE,
HeaderValue::from_static("application/json"),
)
.into_response();
assert_eq!(
r.headers.get_all(http::header::CONTENT_TYPE).iter().count(),
1,
"an overriding header must not be appended beside the one it overrides"
);
assert_eq!(
r.headers.get(http::header::CONTENT_TYPE).unwrap(),
"application/json"
);
}
}