use axum::body::Body;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use super::{Template, View};
impl<T> View<T> {
#[must_use]
pub fn status(mut self, status: StatusCode) -> Self {
self.status = status;
self
}
#[must_use]
pub fn content_type(mut self, content_type: HeaderValue) -> Self {
self.content_type = content_type;
self
}
#[cfg(feature = "i18n")]
#[must_use]
pub fn in_locale(mut self, locale: &crate::i18n::Locale) -> Self {
self.content_language = HeaderValue::from_str(locale.id().as_str()).ok();
self
}
}
#[cfg(feature = "i18n")]
fn with_content_language(mut response: Response, language: Option<HeaderValue>) -> Response {
if let Some(language) = language {
response
.headers_mut()
.insert(header::CONTENT_LANGUAGE, language);
}
response
}
impl<T: Template> IntoResponse for View<T> {
fn into_response(self) -> Response {
#[cfg(feature = "i18n")]
let content_language = self.content_language.clone();
match self.template.render() {
Ok(body) => {
let response = (
self.status,
[(header::CONTENT_TYPE, self.content_type)],
Body::from(body),
)
.into_response();
#[cfg(feature = "i18n")]
let response = with_content_language(response, content_language);
response
}
Err(error) => crate::Error::from(super::ViewError::from(error)).into_response(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::view::test_support::Unformattable;
use crate::view::view;
#[derive(Template)]
#[template(source = "<p>{{ value }}</p>", ext = "html")]
struct Page {
value: &'static str,
}
async fn body_of(response: Response) -> String {
let bytes = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.expect("the body is readable");
String::from_utf8(bytes.to_vec()).expect("the body is UTF-8")
}
#[tokio::test]
async fn a_view_answers_200_with_html() {
let response = view(Page { value: "hello" }).into_response();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response.headers()[header::CONTENT_TYPE],
"text/html; charset=utf-8"
);
assert_eq!(body_of(response).await, "<p>hello</p>");
}
#[tokio::test]
async fn the_status_and_content_type_are_overridable() {
let response = view(Page { value: "gone" })
.status(StatusCode::GONE)
.content_type(HeaderValue::from_static("application/xhtml+xml"))
.into_response();
assert_eq!(response.status(), StatusCode::GONE);
assert_eq!(
response.headers()[header::CONTENT_TYPE],
"application/xhtml+xml"
);
}
#[tokio::test]
async fn a_view_declares_no_language_by_default() {
let response = view(Page { value: "hello" }).into_response();
assert!(!response.headers().contains_key(header::CONTENT_LANGUAGE));
}
#[cfg(feature = "i18n")]
#[tokio::test]
async fn a_view_that_declares_a_locale_says_so_in_the_headers() {
use crate::i18n::{Catalog, Catalogs, LocaleId, LocaleNegotiator};
let catalogs =
Catalogs::new(Catalog::parse(LocaleId::parse("pt-BR").unwrap(), "hi = Ola").unwrap());
let locale = LocaleNegotiator::new(catalogs).fallback();
let response = view(Page { value: "ola" })
.in_locale(&locale)
.into_response();
assert_eq!(response.headers()[header::CONTENT_LANGUAGE], "pt-BR");
assert_eq!(
response.headers()[header::CONTENT_TYPE],
"text/html; charset=utf-8"
);
}
#[cfg(feature = "i18n")]
#[tokio::test]
async fn a_failed_render_does_not_claim_a_language() {
use crate::i18n::{Catalog, Catalogs, LocaleId, LocaleNegotiator};
let catalogs =
Catalogs::new(Catalog::parse(LocaleId::parse("fr").unwrap(), "hi = Salut").unwrap());
let locale = LocaleNegotiator::new(catalogs).fallback();
let response = view(Unformattable::default())
.in_locale(&locale)
.into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
assert!(!response.headers().contains_key(header::CONTENT_LANGUAGE));
}
#[tokio::test]
async fn a_render_failure_leaks_nothing() {
let response = view(Unformattable::default()).into_response();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
let body = body_of(response).await;
assert!(
!body.contains("secret-template-text"),
"the template's own text reached the client: {body}"
);
assert!(
!body.to_ascii_lowercase().contains("template"),
"the word `template` reached the client: {body}"
);
assert!(
!body.to_ascii_lowercase().contains("askama"),
"the engine named itself to the client: {body}"
);
assert!(
!body.contains("view.rs") && !body.contains("src\\view") && !body.contains("src/view"),
"a source path reached the client: {body}"
);
}
}