use crate::{
header::{HeaderValue, LOCATION},
Request, Response, StatusCode,
};
use skyzen_core::Responder;
#[derive(Debug, Clone)]
pub struct Redirect {
status: StatusCode,
location: String,
}
impl Redirect {
#[must_use]
pub fn to(location: impl Into<String>) -> Self {
Self::with_status(StatusCode::FOUND, location)
}
#[must_use]
pub fn see_other(location: impl Into<String>) -> Self {
Self::with_status(StatusCode::SEE_OTHER, location)
}
#[must_use]
pub fn temporary(location: impl Into<String>) -> Self {
Self::with_status(StatusCode::TEMPORARY_REDIRECT, location)
}
#[must_use]
pub fn permanent(location: impl Into<String>) -> Self {
Self::with_status(StatusCode::PERMANENT_REDIRECT, location)
}
#[must_use]
pub fn with_status(status: StatusCode, location: impl Into<String>) -> Self {
Self {
status,
location: location.into(),
}
}
#[must_use]
pub const fn status(&self) -> StatusCode {
self.status
}
#[must_use]
pub fn location(&self) -> &str {
&self.location
}
}
#[skyzen::error(
message = "Redirect location `{0}` cannot be sent as a header value",
status = StatusCode::INTERNAL_SERVER_ERROR
)]
pub struct InvalidRedirectLocation(String);
impl Responder for Redirect {
type Error = InvalidRedirectLocation;
fn respond_to(self, _request: &Request, response: &mut Response) -> Result<(), Self::Error> {
let value = HeaderValue::from_str(&self.location)
.map_err(|_| InvalidRedirectLocation(self.location.clone()))?;
*response.status_mut() = self.status;
response.headers_mut().insert(LOCATION, value);
Ok(())
}
#[cfg(feature = "openapi")]
fn openapi() -> Option<Vec<crate::openapi::ResponseSchema>> {
Some(vec![crate::openapi::ResponseSchema {
status: None,
description: Some("Redirect to another location"),
schema: None,
content_type: None,
}])
}
}
#[cfg(test)]
mod tests {
use super::Redirect;
use crate::{header::LOCATION, Body, Request, Response, StatusCode};
use http_kit::HttpError;
use skyzen_core::Responder;
fn respond(redirect: Redirect) -> Response {
let mut response = Response::new(Body::empty());
redirect
.respond_to(&Request::new(Body::empty()), &mut response)
.expect("a plain path is a valid header value");
response
}
#[test]
fn each_constructor_sends_its_documented_status() {
for (redirect, expected) in [
(Redirect::to("/a"), StatusCode::FOUND),
(Redirect::see_other("/a"), StatusCode::SEE_OTHER),
(Redirect::temporary("/a"), StatusCode::TEMPORARY_REDIRECT),
(Redirect::permanent("/a"), StatusCode::PERMANENT_REDIRECT),
] {
let response = respond(redirect);
assert_eq!(response.status(), expected);
assert_eq!(response.headers().get(LOCATION).unwrap(), "/a");
}
}
#[test]
fn a_location_that_cannot_be_a_header_value_is_a_server_error() {
let mut response = Response::new(Body::empty());
let error = Redirect::to("/bad\nheader")
.respond_to(&Request::new(Body::empty()), &mut response)
.unwrap_err();
assert_eq!(error.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
}