arcature 2026.2.1

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! High-level response types for the Arcature application DX layer (A4).
//!
//! These types let a controller return `Result<Json<T>>`, `Result<Empty>`,
//! or `Result<Page<T>>` without manually implementing Axum response plumbing.
//! Each type implements [`axum::response::IntoResponse`] using the
//! established `(status, headers, body).into_response()` pattern.
//!
//! ## Serialize does NOT imply browser-safe
//!
//! `Json<T>` and `Page<T>` require `T: Serialize` for the wire format, but
//! serialization is not a security boundary. A `SeaORM` model that
//! `Serialize`s must not automatically become `ClientData`. Page/Resource
//! declarations are explicit exposure boundaries (PROGRAM.md "Client exposure
//! firewall"). These response types are the server-side rendering seam;
//! browser exposure is governed by the Inertia `PageContract` / `ClientData`
//! system, not by `Serialize` alone.

use axum::response::{IntoResponse, Response};

#[cfg(feature = "serde")]
use axum::http::HeaderValue;
#[cfg(feature = "serde")]
use axum::http::header::{CONTENT_LENGTH, CONTENT_TYPE};

/// A JSON response body. Serializes `T` to JSON and sets
/// `Content-Type: application/json`.
///
/// This is Arcature's `Json<T>` — a replacement for `axum::Json` so normal
/// application code never names `axum::` directly. It implements
/// [`IntoResponse`] directly, so a controller can return
/// `Result<Json<UserResource>>` with no manual response plumbing.
///
/// # Example
///
/// ```ignore
/// use arcature::Json;
///
/// async fn show() -> Result<Json<UserResource>> {
///     Ok(Json(UserResource { id: 42, name: "Alice".into() }))
/// }
/// ```
///
/// `Serialize` does NOT imply browser-safe. The exposure boundary for
/// Inertia `ClientData` is the `PageContract` system, not `Serialize`.
#[cfg(feature = "serde")]
pub struct Json<T>(pub T);

#[cfg(feature = "serde")]
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(|_| {
            // Fallback: a minimal error JSON. Never panics.
            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
    }
}

/// An empty response (204 No Content).
///
/// Use when a handler has nothing to return — e.g. a `DELETE` that succeeded.
///
/// # Example
///
/// ```ignore
/// use arcature::Empty;
///
/// async fn destroy() -> Result<Empty> {
///     // ... delete the resource ...
///     Ok(Empty)
/// }
/// ```
pub struct Empty;

impl IntoResponse for Empty {
    fn into_response(self) -> Response {
        (
            axum::http::StatusCode::NO_CONTENT,
            axum::body::Body::empty(),
        )
            .into_response()
    }
}

/// A generic page response shell (A4).
///
/// `Page<T>` is the high-level response type for Inertia-rendered pages.
/// In A4, it is a thin shell that serializes `T` as JSON. The full
/// `#[page]` / `page!` declaration machinery, `PageContract` integration,
/// `ClientData` exposure, and Cross-Stack Linker wiring arrive in A6.
///
/// `Page<T>` is also the **golden-path return type** for the `#[controller]`
/// macro's page-response derivation (AP2.1-4): a handler returning
/// `Result<Page<T>, E>` or `Page<T>` has its page identity
/// (`T::PAGE_CONTRACT.name()`) inferred into the controller metadata, so the
/// route needs no `page:` / `pages:` declaration. The `T` must be a `#[page]`
/// type — `T::PAGE_CONTRACT` exists only then, so a non-page type fails to
/// compile (the Client Exposure Firewall applied to the return type).
///
/// # Example
///
/// ```ignore
/// use arcature::{Page, page};
///
/// async fn show() -> Result<Page<ShowLinkPage>> {
///     Ok(page(ShowLinkPage { link: LinkResource::from(link) }))
/// }
/// ```
///
/// `Serialize` does NOT imply browser-safe. The `T` in `Page<T>` must be a
/// declared page/resource with explicit exposure boundaries (PROGRAM.md
/// "Client exposure firewall").
#[cfg(feature = "serde")]
pub struct Page<T>(pub T);

#[cfg(feature = "serde")]
impl<T> IntoResponse for Page<T>
where
    T: serde::Serialize,
{
    fn into_response(self) -> Response {
        // A4: render page data as JSON. A6 will connect this to the Inertia
        // PageContract / ClientData system for proper browser rendering.
        Json(self.0).into_response()
    }
}

/// Construct a [`Page<T>`] from its props — the ergonomic golden-path
/// constructor for handlers returning `Result<Page<T>, E>` / `Page<T>`.
///
/// The `#[controller]` macro derives the route→page edge from the return
/// type's `Page<T>` (reading the signature, not the body), so a handler
/// returning `Page<HomePage>` needs no `page:` route declaration — the page
/// identity is inferred from `HomePage::PAGE_CONTRACT.name()` at compile
/// time.
///
/// # Example
///
/// ```ignore
/// use arcature::{Page, page};
///
/// async fn home() -> Result<Page<HomePage>, AppError> {
///     Ok(page(HomePage { name: "Arcature".to_string() }))
/// }
/// ```
#[cfg(feature = "serde")]
#[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);
    }

    #[cfg(feature = "serde")]
    #[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(feature = "serde")]
    #[test]
    fn page_serializes_as_json() {
        #[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")
        );
    }
}