use crate::{
error::Error,
headers::{HeaderMap, HeaderName, HeaderValue, TryIntoHeaderPair},
http::{HttpBody, HttpResponse, Response, StatusCode},
};
use std::fmt::{Debug, Formatter};
pub(crate) const SERVER_NAME: &str = "Volga";
pub(crate) const RESPONSE_ERROR: &str = "HTTP Response: Unable to create a response";
pub struct HttpResponseBuilder {
inner: Result<InnerBuilder, Error>,
}
struct InnerBuilder {
status: StatusCode,
headers: HeaderMap,
}
impl Debug for HttpResponseBuilder {
#[inline]
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HttpResponseBuilder(...)").finish()
}
}
impl HttpResponseBuilder {
#[inline]
pub(super) fn new() -> Self {
Self {
inner: Ok(InnerBuilder {
status: StatusCode::OK,
headers: HeaderMap::new(),
}),
}
}
#[inline]
pub fn status<T>(self, status: T) -> Self
where
StatusCode: TryFrom<T>,
Error: From<<StatusCode as TryFrom<T>>::Error>,
{
self.and_then(|mut inner| {
inner.status = status.try_into().map_err(Error::from)?;
Ok(inner)
})
}
#[inline]
pub fn header(self, header: impl TryIntoHeaderPair) -> Self {
self.and_then(move |mut inner| {
let (name, value) = header.try_into_pair()?;
inner.headers.try_append(name, value).map_err(Error::from)?;
Ok(inner)
})
}
#[inline]
pub fn header_raw<K, V>(self, key: K, value: V) -> Self
where
HeaderName: TryFrom<K>,
HeaderValue: TryFrom<V>,
Error: From<<HeaderName as TryFrom<K>>::Error>,
Error: From<<HeaderValue as TryFrom<V>>::Error>,
{
self.and_then(|mut inner| {
let name = HeaderName::try_from(key).map_err(Error::from)?;
let value = HeaderValue::try_from(value).map_err(Error::from)?;
inner.headers.try_append(name, value).map_err(Error::from)?;
Ok(inner)
})
}
#[inline]
pub fn header_static(self, key: &'static str, value: &'static str) -> Self {
self.and_then(|mut inner| {
let name = HeaderName::from_static(key);
let value = HeaderValue::from_static(value);
inner.headers.append(name, value);
Ok(inner)
})
}
#[inline]
pub fn body(self, body: HttpBody) -> Result<HttpResponse, Error> {
self.inner.and_then(|inner| {
let mut response = Response::builder()
.status(inner.status)
.body(body)
.map_err(|_| Error::server_error(RESPONSE_ERROR))?;
*response.headers_mut() = inner.headers;
Ok(HttpResponse::from_inner(response))
})
}
#[inline]
fn and_then<F>(self, func: F) -> Self
where
F: FnOnce(InnerBuilder) -> Result<InnerBuilder, Error>,
{
Self {
inner: self.inner.and_then(func),
}
}
}
#[inline]
#[cfg(debug_assertions)]
pub fn make_builder() -> HttpResponseBuilder {
HttpResponse::builder().header_raw(crate::headers::SERVER, SERVER_NAME)
}
#[inline]
#[cfg(not(debug_assertions))]
pub fn make_builder() -> HttpResponseBuilder {
HttpResponse::builder()
}
#[macro_export]
macro_rules! builder {
() => {
$crate::http::response::builder::make_builder()
};
($status:expr) => {
$crate::builder!().status($status)
};
}
#[macro_export]
macro_rules! response {
($status:expr, $body:expr) => {
$crate::response!($status, $body; [])
};
($status:expr, $body:expr; [ $( $header:expr ),* $(,)? ]) => {
$crate::builder!($status)
$(
.header($header)
)*
.body($body)
};
}
#[cfg(test)]
mod tests {
use super::RESPONSE_ERROR;
use crate::HttpBody;
use crate::headers::{ContentType, Header};
use http_body_util::BodyExt;
#[tokio::test]
async fn builder_sets_status_headers_and_body() {
let response = builder!(200)
.header(ContentType::from_static("text/plain"))
.body(HttpBody::from("hello"))
.expect("response should build");
let response = response.into_inner();
assert_eq!(response.status(), 200);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/plain"
);
let body = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(body, "hello");
}
#[tokio::test]
async fn it_creates_response_with_headers_and_body() {
let header = Header::<ContentType>::from_static("text/plain");
let response = response!(
200,
HttpBody::from("hello");
[header]
);
let response = response.expect("response should build").into_inner();
assert_eq!(response.status(), 200);
assert_eq!(
response.headers().get("content-type").unwrap(),
"text/plain"
);
let body = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(body, "hello");
}
#[tokio::test]
async fn builder_sets_status_headers_raw_and_body() {
let response = builder!(201)
.header_raw("x-test", "1")
.body(HttpBody::from("hello"))
.expect("response should build");
let response = response.into_inner();
assert_eq!(response.status(), 201);
assert_eq!(response.headers().get("x-test").unwrap(), "1");
let body = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(body, "hello");
}
#[tokio::test]
async fn response_macro_builds_with_body() {
let response = response!(200, HttpBody::from("ok")).expect("response should build");
let response = response.into_inner();
assert_eq!(response.status(), 200);
let body = response.into_body().collect().await.unwrap().to_bytes();
assert_eq!(body, "ok");
}
#[test]
fn builder_returns_error_for_invalid_header() {
let result = builder!()
.header_raw("invalid header", "value")
.body(HttpBody::from("ignored"));
let err = result.expect_err("expected invalid header error");
assert!(err.to_string().contains(RESPONSE_ERROR) || err.to_string().contains("header"));
}
}