use crate::prelude::*;
use bytes::Bytes;
use std::convert::Infallible;
#[derive(Debug, Component)]
#[require(ResponseMarker = ResponseMarker{_sealed:()})]
pub struct Response {
parts: ResponseParts,
pub body: Body,
}
#[derive(Debug, Component)]
pub struct ResponseMarker {
_sealed: (),
}
impl std::error::Error for Response {}
impl std::fmt::Display for Response {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"Response - Status: {}, Message: '{}'",
self.parts.status(),
match &self.body {
Body::Stream(_) => "<stream>".into(),
Body::Bytes(bytes) =>
String::from_utf8_lossy(bytes).to_string(),
}
)
}
}
impl PartialEq for Response {
fn eq(&self, other: &Self) -> bool {
self.body.bytes_eq(&other.body)
&& self.parts.status() == other.parts.status()
}
}
impl std::ops::Deref for Response {
type Target = ResponseParts;
fn deref(&self) -> &Self::Target { &self.parts }
}
impl std::ops::DerefMut for Response {
fn deref_mut(&mut self) -> &mut Self::Target { &mut self.parts }
}
impl Response {
pub fn new(parts: ResponseParts, body: Body) -> Self {
Self { parts, body }
}
pub fn ok() -> Self { Self::from_status(StatusCode::Ok) }
pub fn not_found() -> Self { Self::from_status(StatusCode::NotFound) }
pub fn temporary_redirect(location: impl Into<String>) -> Self {
#[cfg(feature = "http")]
let status = StatusCode::Http(http::StatusCode::TEMPORARY_REDIRECT);
#[cfg(not(feature = "http"))]
let status = StatusCode::Ok;
let mut parts = ResponseParts::new(status);
parts.parts_mut().insert_header("location", location.into());
Self {
parts,
body: Default::default(),
}
}
pub fn permanent_redirect(location: impl Into<String>) -> Self {
#[cfg(feature = "http")]
let status = StatusCode::Http(http::StatusCode::MOVED_PERMANENTLY);
#[cfg(not(feature = "http"))]
let status = StatusCode::Ok;
let mut parts = ResponseParts::new(status);
parts.parts_mut().insert_header("location", location.into());
Self {
parts,
body: Default::default(),
}
}
pub fn status(&self) -> StatusCode { self.parts.status() }
pub fn from_status(status: StatusCode) -> Self {
Self {
parts: ResponseParts::new(status),
body: Default::default(),
}
}
pub fn with_body(mut self, body: impl Into<Body>) -> Self {
self.body = body.into();
self
}
pub fn from_status_body(
status: StatusCode,
body: impl AsRef<[u8]>,
content_type: &str,
) -> Self {
let mut parts = ResponseParts::new(status);
parts
.parts_mut()
.insert_header("content-type", content_type);
Self {
parts,
body: Bytes::copy_from_slice(body.as_ref()).into(),
}
}
#[cfg(feature = "http")]
pub fn header(
&self,
header: http::header::HeaderName,
) -> Result<Option<&str>> {
match self.parts.get_header(header.as_str()) {
Some(value) => Ok(Some(value.as_str())),
None => Ok(None),
}
}
#[cfg(feature = "http")]
pub fn header_matches(
&self,
header: http::header::HeaderName,
value: &str,
) -> bool {
self.parts
.get_header(header.as_str())
.map_or(false, |val| val == value)
}
#[cfg(feature = "http")]
pub fn header_contains(
&self,
header: http::header::HeaderName,
value: &str,
) -> bool {
self.parts
.get_header(header.as_str())
.map_or(false, |val| val.contains(value))
}
pub fn from_parts(parts: ResponseParts, body: Bytes) -> Self {
Self {
parts,
body: body.into(),
}
}
#[cfg(feature = "http")]
pub fn from_http_parts(parts: http::response::Parts, body: Bytes) -> Self {
Self {
parts: ResponseParts::from(parts),
body: body.into(),
}
}
pub fn ok_body(body: impl Into<Body>, content_type: &str) -> Self {
let mut parts = ResponseParts::ok();
parts
.parts_mut()
.insert_header("content-type", content_type);
Self {
parts,
body: body.into(),
}
}
#[cfg(feature = "http")]
pub fn ok_mime_guess(
body: impl Into<Body>,
path: impl AsRef<std::path::Path>,
) -> Self {
let mime_type = mime_guess::from_path(path).first_or_octet_stream();
Self::ok_body(body, mime_type.as_ref())
}
pub fn parts(&self) -> &ResponseParts { &self.parts }
pub fn parts_mut(&mut self) -> &mut ResponseParts { &mut self.parts }
pub fn into_parts(self) -> (ResponseParts, Body) { (self.parts, self.body) }
pub async fn bytes(self) -> Result<Bytes> { self.body.into_bytes().await }
pub async fn text(self) -> Result<String> { self.body.into_string().await }
#[cfg(feature = "serde")]
pub async fn json<T: serde::de::DeserializeOwned>(self) -> Result<T> {
self.body.into_json().await
}
#[cfg(feature = "http")]
pub async fn into_http(self) -> Result<http::Response<Bytes>> {
let bytes = self.body.into_bytes().await?;
let http_parts: http::response::Parts = self.parts.try_into()?;
http::Response::from_parts(http_parts, bytes).xok()
}
pub async fn into_result(self) -> Result<Self, HttpError> {
if self.parts.status().is_ok() {
Ok(self)
} else {
Err(self.into_error().await)
}
}
pub async fn into_error(self) -> HttpError {
let status = self.status();
let Ok(bytes) = self.body.into_bytes().await else {
return HttpError::internal_error("Failed to read response body");
};
let message = if !bytes.is_empty() {
String::from_utf8_lossy(&bytes).to_string()
} else {
Default::default()
};
HttpError {
status_code: status,
message,
}
}
pub fn with_header(mut self, key: &str, value: &str) -> Self {
self.parts.parts_mut().insert_header(key, value);
self
}
pub fn with_content_type(self, content_type: &str) -> Self {
self.with_header("content-type", content_type)
}
}
#[cfg(feature = "http")]
impl From<http::Response<Body>> for Response {
fn from(res: http::Response<Body>) -> Self {
let (parts, body) = res.into_parts();
Response {
parts: ResponseParts::from(parts),
body,
}
}
}
#[cfg(feature = "http")]
impl From<http::Response<Bytes>> for Response {
fn from(res: http::Response<Bytes>) -> Self {
let (parts, body) = res.into_parts();
Response {
parts: ResponseParts::from(parts),
body: body.into(),
}
}
}
#[cfg(feature = "http")]
impl From<http::StatusCode> for Response {
fn from(status: http::StatusCode) -> Self {
Response::from_status(StatusCode::from(status))
}
}
impl From<StatusCode> for Response {
fn from(status: StatusCode) -> Self { Response::from_status(status) }
}
pub trait IntoResponse<M> {
fn into_response(self) -> Response;
}
impl<T: IntoResponse<M1>, M1, E: IntoResponse<M2>, M2>
IntoResponse<(Self, M1, M2)> for Result<T, E>
{
fn into_response(self) -> Response {
match self {
Ok(val) => val.into_response(),
Err(err) => err.into_response(),
}
}
}
impl From<BevyError> for Response {
fn from(value: BevyError) -> Response {
HttpError::from_opaque(value).into()
}
}
impl IntoResponse<Self> for Bytes {
fn into_response(self) -> Response {
Response::ok_body(self, "application/octet-stream")
}
}
impl IntoResponse<Self> for &[u8] {
fn into_response(self) -> Response {
Response::ok_body(
Bytes::copy_from_slice(self),
"application/octet-stream",
)
}
}
impl IntoResponse<Self> for Infallible {
fn into_response(self) -> Response {
unreachable!("Infallible cannot be converted to a response");
}
}
impl IntoResponse<Self> for () {
fn into_response(self) -> Response { Response::ok() }
}
impl<T: TryInto<Response>, M1> IntoResponse<(Self, M1)> for T
where
T::Error: IntoResponse<M1>,
{
fn into_response(self) -> Response {
match self.try_into() {
Ok(response) => response,
Err(err) => err.into_response(),
}
}
}
impl<T: IntoResponse<M>, M> IntoResponse<(Self, M)> for Option<T> {
fn into_response(self) -> Response {
match self {
Some(val) => val.into_response(),
None => Response::not_found(),
}
}
}
impl<'a> From<&'a str> for Response {
fn from(value: &'a str) -> Response {
Response::ok_body(value, "text/plain; charset=utf-8")
}
}
impl From<String> for Response {
fn from(value: String) -> Response {
Response::ok_body(value, "text/plain; charset=utf-8")
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn response_ok() {
let response = Response::ok();
response.status().xpect_eq(StatusCode::Ok);
}
#[test]
fn response_not_found() {
let response = Response::not_found();
response.status().xpect_eq(StatusCode::NotFound);
}
#[test]
#[cfg(feature = "http")]
fn response_from_status() {
let response =
Response::from_status(StatusCode::Http(http::StatusCode::CREATED));
response
.status()
.xpect_eq(StatusCode::Http(http::StatusCode::CREATED));
}
#[test]
fn response_with_body() {
let response = Response::ok().with_body("hello");
response
.body
.bytes_eq(&Body::Bytes(Bytes::from("hello")))
.xpect_true();
}
#[test]
#[cfg(feature = "http")]
fn response_from_status_body() {
let response =
Response::from_status_body(StatusCode::Ok, b"data", "text/plain");
response.status().xpect_eq(StatusCode::Ok);
response
.header_contains(http::header::CONTENT_TYPE, "text/plain")
.xpect_true();
}
#[test]
fn response_deref_to_parts() {
let response = Response::ok();
response.status().xpect_eq(StatusCode::Ok);
}
#[test]
#[cfg(feature = "http")]
fn response_from_str() {
let response: Response = "hello".into();
response.status().xpect_eq(StatusCode::Ok);
response
.header_contains(http::header::CONTENT_TYPE, "text/plain")
.xpect_true();
}
#[test]
#[cfg(feature = "http")]
fn response_temporary_redirect() {
let response = Response::temporary_redirect("/new-location");
response
.status()
.xpect_eq(StatusCode::Http(http::StatusCode::TEMPORARY_REDIRECT));
response
.get_header("location")
.unwrap()
.xpect_eq("/new-location");
}
#[test]
#[cfg(feature = "http")]
fn response_permanent_redirect() {
let response = Response::permanent_redirect("/new-location");
response
.status()
.xpect_eq(StatusCode::Http(http::StatusCode::MOVED_PERMANENTLY));
response
.get_header("location")
.unwrap()
.xpect_eq("/new-location");
}
#[test]
fn response_with_header() {
let response = Response::ok().with_header("x-custom", "value");
response.get_header("x-custom").unwrap().xpect_eq("value");
}
#[test]
fn response_into_parts() {
let response = Response::ok().with_body("data");
let (parts, body) = response.into_parts();
parts.status().xpect_eq(StatusCode::Ok);
body.bytes_eq(&Body::Bytes(Bytes::from("data")))
.xpect_true();
}
#[test]
fn response_partial_eq() {
let response1 = Response::ok().with_body("hello");
let response2 = Response::ok().with_body("hello");
let response3 = Response::ok().with_body("world");
(response1 == response2).xpect_true();
(response2 == response3).xpect_false();
}
#[test]
fn response_display() {
let response = Response::ok().with_body("hello");
let display = format!("{}", response);
display.clone().xpect_contains("OK");
display.xpect_contains("hello");
}
#[test]
fn into_response_unit() {
let response = ().into_response();
response.status().xpect_eq(StatusCode::Ok);
}
#[test]
#[cfg(feature = "http")]
fn into_response_status_code() {
let response =
StatusCode::Http(http::StatusCode::CREATED).into_response();
response
.status()
.xpect_eq(StatusCode::Http(http::StatusCode::CREATED));
}
#[test]
#[cfg(feature = "http")]
fn into_response_option_some() {
let response =
Some(StatusCode::Http(http::StatusCode::CREATED)).into_response();
response
.status()
.xpect_eq(StatusCode::Http(http::StatusCode::CREATED));
}
#[test]
fn into_response_option_none() {
let response: Response = None::<StatusCode>.into_response();
response.status().xpect_eq(StatusCode::NotFound);
}
}