use axum::http::HeaderValue;
use axum::http::header::{CONTENT_LENGTH, CONTENT_TYPE};
use axum::response::{IntoResponse, Response};
pub struct Json<T>(pub T);
impl<T> IntoResponse for Json<T>
where
T: serde::Serialize,
{
fn into_response(self) -> Response {
let body = serde_json::to_vec(&self.0).unwrap_or_else(|_| {
br#"{"type":"urn:arcature:problem:internal","title":"Internal Server Error","status":500}"#
.to_vec()
});
let len = body.len();
let mut response = body.into_response();
response
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
response
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from(len));
response
}
}
pub struct Empty;
impl IntoResponse for Empty {
fn into_response(self) -> Response {
(
axum::http::StatusCode::NO_CONTENT,
axum::body::Body::empty(),
)
.into_response()
}
}
pub struct Page<T>(pub T);
#[cfg(not(feature = "inertia"))]
impl<T> IntoResponse for Page<T>
where
T: serde::Serialize,
{
fn into_response(self) -> Response {
Json(self.0).into_response()
}
}
#[cfg(feature = "inertia")]
impl<T> IntoResponse for Page<T>
where
T: crate::inertia::PageType,
{
fn into_response(self) -> Response {
let component = <T as crate::inertia::PageType>::CONTRACT.name();
match serde_json::to_value(&self.0) {
Ok(props) => crate::inertia::PendingPage::new(component, props).into_response(),
Err(error) => crate::api::Problem::of(crate::api::ProblemKind::Internal)
.with_detail(format!(
"The page `{component}` could not be serialized: {error}"
))
.into_response(),
}
}
}
#[must_use]
pub fn page<T>(props: T) -> Page<T> {
Page(props)
}
#[cfg(test)]
mod tests {
use super::*;
use axum::http::StatusCode;
#[test]
fn empty_returns_204() {
let response = Empty.into_response();
assert_eq!(response.status(), StatusCode::NO_CONTENT);
}
#[test]
fn json_serializes_body() {
let response = Json(serde_json::json!({"hello": "world"})).into_response();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(CONTENT_TYPE)
.map(|v| v.to_str().unwrap_or("")),
Some("application/json")
);
}
#[cfg(not(feature = "inertia"))]
#[test]
fn page_serializes_as_json_without_the_inertia_feature() {
#[derive(serde::Serialize)]
struct TestData {
value: u32,
}
let response = Page(TestData { value: 42 }).into_response();
assert_eq!(response.status(), StatusCode::OK);
assert_eq!(
response
.headers()
.get(CONTENT_TYPE)
.map(|v| v.to_str().unwrap_or("")),
Some("application/json")
);
}
#[cfg(feature = "inertia")]
#[test]
fn a_page_defers_its_render_to_the_middleware() {
use crate::inertia::{ClientData, ContractType, PageContract, PageType, PropsSchema};
#[derive(serde::Serialize)]
struct TestPage {
value: u32,
}
impl ClientData for TestPage {
fn exposure_schema() -> PropsSchema {
PropsSchema::new().required("value", ContractType::number())
}
}
impl PageType for TestPage {
const CONTRACT: PageContract<Self> = PageContract::new("Test");
}
let response = Page(TestPage { value: 42 }).into_response();
let pending = response
.extensions()
.get::<crate::inertia::PendingPage>()
.expect("the render is recorded for the middleware");
assert_eq!(pending.component(), "Test");
assert_eq!(pending.props(), &serde_json::json!({"value": 42}));
}
}