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
//! Controller method metadata for the Arcature application DX layer (A4).
//!
//! The `#[controller]` macro generates [`ControllerMethod`] values and
//! exposes them through the [`ControllerMetadata`] trait — plain data with
//! `&'static` slices, no allocation, no runtime reflection. The `module!`
//! macro aggregates the trait's `METHODS` associated const into the
//! [`ModuleDescriptor`](super::graph::ModuleDescriptor) so the application
//! graph (and the UAG) carries per-handler response metadata. This is the
//! join point `RouteDescriptor.handler` → controller handler → `Page<T>` →
//! `PageContract` (AP2.1-4 H4 / "derive what can be derived").

/// Metadata for a single controller method, generated by the
/// `#[controller]` attribute macro and exposed via [`ControllerMetadata`].
///
/// Every field is `&'static str`, `&'static [&'static str]`, or
/// `Option<&'static str>` — no `Vec`, no `String`, no allocation. A `const`
/// slice of `ControllerMethod` is the inspection artifact consumed by the
/// application graph and `arc check` validation.
///
/// The `page` field is the page-response metadata derived from the method's
/// **return type** (not its body): when the signature is
/// `Result<Page<T>, E>` or `Page<T>`, the `#[controller]` macro emits
/// `page: Some(T::PAGE_CONTRACT.name())` — the page contract identity
/// (e.g. `"Home"`), resolved at compile time. If `T` is not a `#[page]`
/// type, `T::PAGE_CONTRACT` does not exist and the code fails to compile:
/// that is the Client Exposure Firewall applied to the return type. A
/// handler returning a raw `Response`, `Json<T>`, `Redirect`, or
/// `impl IntoResponse` has `page: None` — no page edge is inferred, and the
/// route must declare `page:`/`pages:` explicitly if it renders a page
/// (the escape hatch).
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ControllerMethod {
    /// The method name (e.g. `"show"`, `"store"`, `"index"`).
    pub name: &'static str,
    /// The parameter names (e.g. `["auth", "input"]`).
    pub params: &'static [&'static str],
    /// The page contract identity this method renders, derived from the
    /// return type's `Page<T>` via `T::PAGE_CONTRACT.name()` (a `&'static str`
    /// const expression). `Some("Home")` for `Result<Page<HomePage>, E>` /
    /// `Page<HomePage>`; `None` for non-page returns (`Response`, `Json<T>`,
    /// `Redirect`, `impl IntoResponse`). The route→page edge is inferred from
    /// this when the route declares no `page:`/`pages:` (the golden path); an
    /// explicit `page:`/`pages:` overrides inference (the escape hatch).
    #[cfg_attr(feature = "serde", serde(default))]
    pub page: Option<&'static str>,
}

/// The per-controller metadata trait: the `#[controller]` macro emits
/// `impl ControllerMetadata for Type` carrying the controller's method
/// descriptors as a `&'static [ControllerMethod]` associated const. The
/// `module!` macro references `<Type as ControllerMetadata>::METHODS` to
/// aggregate handler metadata into the
/// [`ModuleDescriptor`](super::graph::ModuleDescriptor) — so the application
/// graph carries the per-handler page-response edge without runtime
/// reflection, global registration, or function-body parsing.
///
/// This is the join point the route graph uses:
/// `RouteDescriptor.handler` (a `&'static str` like
/// `"HomeController::index"`) → the controller's `METHODS` → the method
/// named `index` → `ControllerMethod.page` → the `PageContract` identity →
/// the frontend page component.
pub trait ControllerMetadata {
    /// The controller's method descriptors, as a `&'static` slice. Every
    /// entry is plain `&'static` data — no allocation, no `TypeId`, no
    /// runtime reflection. Generated by the `#[controller]` macro.
    const METHODS: &'static [ControllerMethod];
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn controller_method_is_const_constructible() {
        const METHOD: ControllerMethod = ControllerMethod {
            name: "show",
            params: &["auth", "link"],
            page: Some("Show"),
        };
        assert_eq!(METHOD.name, "show");
        assert_eq!(METHOD.params, ["auth", "link"]);
        assert_eq!(METHOD.page, Some("Show"));
    }

    #[test]
    fn controller_method_none_page_is_const_constructible() {
        const METHOD: ControllerMethod = ControllerMethod {
            name: "destroy",
            params: &["auth", "link"],
            page: None,
        };
        assert_eq!(METHOD.page, None);
    }

    #[test]
    #[cfg(feature = "serde")]
    fn controller_method_serializes_to_json() {
        let method = ControllerMethod {
            name: "store",
            params: &["auth", "input"],
            page: Some("NewLink"),
        };
        let json = serde_json::to_string(&method).unwrap();
        assert!(json.contains("\"name\":\"store\""), "name: {json}");
        assert!(json.contains("\"params\""), "params field present: {json}");
        assert!(
            json.contains("\"auth\"") && json.contains("\"input\""),
            "params values: {json}"
        );
        assert!(json.contains("\"page\":\"NewLink\""), "page: {json}");
    }

    #[test]
    #[cfg(feature = "serde")]
    fn controller_method_none_page_serializes_as_null() {
        let method = ControllerMethod {
            name: "destroy",
            params: &["auth"],
            page: None,
        };
        let json = serde_json::to_string(&method).unwrap();
        assert!(json.contains("\"page\":null"), "page null: {json}");
    }

    #[test]
    fn controller_metadata_trait_can_be_implemented() {
        struct DummyController;
        impl ControllerMetadata for DummyController {
            const METHODS: &'static [ControllerMethod] = &[
                ControllerMethod {
                    name: "index",
                    params: &[],
                    page: Some("Home"),
                },
                ControllerMethod {
                    name: "destroy",
                    params: &["id"],
                    page: None,
                },
            ];
        }
        assert_eq!(DummyController::METHODS.len(), 2);
        assert_eq!(DummyController::METHODS[0].page, Some("Home"));
        assert_eq!(DummyController::METHODS[1].page, None);
    }
}