arcature 2026.1.0

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! Route metadata for the Arcature application DX layer.
//!
//! The `routes!` macro generates [`RouteDescriptor`] values as `const` items
//! — plain data with `&'static` slices, no allocation, no runtime reflection.
//! These descriptors feed `arc routes` inspection, `arc check` validation, and
//! the `ApplicationGraph` route metadata.
//!
//! All types are const-constructible (unit enum variants, `&'static str`
//! fields) so the macro can emit them as `const` items.

/// An HTTP method supported by the `routes!` DSL.
///
/// The variants map 1:1 to the axum method-routing constructors
/// (`get`, `post`, `put`, `patch`, `delete`, `head`, `options`).
/// Const-constructible (unit variants).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum RouteMethod {
    /// `GET` — map to `axum::routing::get`.
    Get,
    /// `POST` — map to `axum::routing::post`.
    Post,
    /// `PUT` — map to `axum::routing::put`.
    Put,
    /// `PATCH` — map to `axum::routing::patch`.
    Patch,
    /// `DELETE` — map to `axum::routing::delete`.
    Delete,
    /// `HEAD` — map to `axum::routing::head`.
    Head,
    /// `OPTIONS` — map to `axum::routing::options`.
    Options,
}

impl RouteMethod {
    /// Returns the uppercase HTTP method string (e.g. `"GET"`, `"POST"`).
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Get => "GET",
            Self::Post => "POST",
            Self::Put => "PUT",
            Self::Patch => "PATCH",
            Self::Delete => "DELETE",
            Self::Head => "HEAD",
            Self::Options => "OPTIONS",
        }
    }

    /// Returns the lowercase axum routing function name (e.g. `"get"`,
    /// `"post"`).
    #[must_use]
    pub const fn as_routing_fn(self) -> &'static str {
        match self {
            Self::Get => "get",
            Self::Post => "post",
            Self::Put => "put",
            Self::Patch => "patch",
            Self::Delete => "delete",
            Self::Head => "head",
            Self::Options => "options",
        }
    }
}

impl std::fmt::Display for RouteMethod {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Metadata for a single route, generated by the `routes!` macro.
///
/// Every field is `&'static str` or a unit enum — no `Vec`, no `String`, no
/// allocation. A `const` array of `RouteDescriptor` is the inspection
/// artifact consumed by `arc routes` and `arc check`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct RouteDescriptor {
    /// The HTTP method.
    pub method: RouteMethod,
    /// The full path pattern (e.g. `"/links/{link}"`). Group prefixes are
    /// already prepended by the macro.
    pub path: &'static str,
    /// The dotted route name (e.g. `"auth.login"`, `"links.index"`). Empty
    /// string `""` for unnamed routes.
    pub name: &'static str,
    /// The handler path as a string (e.g. `"SessionsController::create"`).
    pub handler: &'static str,
}

/// A canonical resource action, used by `resource` route expansion and
/// `only`/`except` filtering.
///
/// This enum and its methods are consumed by the `arc routes` and `arc check`
/// CLI commands in later A-phases. They are part of the public inspection
/// API surface.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ResourceAction {
    /// `GET /resource` — list.
    Index,
    /// `GET /resource/new` — new form.
    Create,
    /// `POST /resource` — create.
    Store,
    /// `GET /resource/{id}` — show.
    Show,
    /// `GET /resource/{id}/edit` — edit form.
    Edit,
    /// `PUT /resource/{id}` — update.
    Update,
    /// `DELETE /resource/{id}` — destroy.
    Destroy,
}

#[allow(dead_code)] // Consumed by `arc routes` / `arc check` in later A-phases.
impl ResourceAction {
    /// Returns the action name as a lowercase string (e.g. `"index"`,
    /// `"store"`).
    #[must_use]
    pub const fn as_str(self) -> &'static str {
        match self {
            Self::Index => "index",
            Self::Create => "create",
            Self::Store => "store",
            Self::Show => "show",
            Self::Edit => "edit",
            Self::Update => "update",
            Self::Destroy => "destroy",
        }
    }

    /// Returns the HTTP method for this action.
    #[must_use]
    pub const fn method(self) -> RouteMethod {
        match self {
            Self::Index | Self::Create | Self::Show | Self::Edit => RouteMethod::Get,
            Self::Store => RouteMethod::Post,
            Self::Update => RouteMethod::Put,
            Self::Destroy => RouteMethod::Delete,
        }
    }

    /// Returns the path suffix for this action, relative to the resource
    /// base path. `{param}` is the route parameter placeholder. The
    /// returned string is allocated via `format!`.
    #[must_use]
    pub fn path_suffix(self, param: &str) -> String {
        match self {
            Self::Index | Self::Store => String::new(),
            Self::Create => "/new".to_string(),
            Self::Show | Self::Update | Self::Destroy => format!("/{{{param}}}"),
            Self::Edit => format!("/{{{param}}}/edit"),
        }
    }
}

impl std::fmt::Display for ResourceAction {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.as_str())
    }
}

/// Parses a resource action name string into a [`ResourceAction`].
///
/// Returns `None` for unknown action names. Used by `only`/`except`
/// validation in the `routes!` macro.
#[must_use]
#[allow(dead_code)] // Consumed by `arc routes` / `arc check` in later A-phases.
pub fn parse_resource_action(name: &str) -> Option<ResourceAction> {
    match name {
        "index" => Some(ResourceAction::Index),
        "create" => Some(ResourceAction::Create),
        "store" => Some(ResourceAction::Store),
        "show" => Some(ResourceAction::Show),
        "edit" => Some(ResourceAction::Edit),
        "update" => Some(ResourceAction::Update),
        "destroy" => Some(ResourceAction::Destroy),
        _ => None,
    }
}

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

    #[test]
    fn route_method_display() {
        assert_eq!(RouteMethod::Get.to_string(), "GET");
        assert_eq!(RouteMethod::Post.to_string(), "POST");
        assert_eq!(RouteMethod::Delete.to_string(), "DELETE");
    }

    #[test]
    #[cfg(feature = "serde")]
    fn route_descriptor_serializes_to_json() {
        let descriptor = RouteDescriptor {
            method: RouteMethod::Get,
            path: "/links/{link}",
            name: "links.show",
            handler: "LinksController::show",
        };
        let json = serde_json::to_string(&descriptor).unwrap();
        assert!(
            json.contains("\"method\":\"get\""),
            "method lowercase: {json}"
        );
        assert!(json.contains("\"path\":\"/links/{link}\""), "path: {json}");
        assert!(json.contains("\"name\":\"links.show\""), "name: {json}");
        assert!(
            json.contains("\"handler\":\"LinksController::show\""),
            "handler: {json}"
        );
    }

    #[test]
    fn route_method_routing_fn() {
        assert_eq!(RouteMethod::Get.as_routing_fn(), "get");
        assert_eq!(RouteMethod::Patch.as_routing_fn(), "patch");
    }

    #[test]
    fn resource_action_method_mapping() {
        assert_eq!(ResourceAction::Index.method(), RouteMethod::Get);
        assert_eq!(ResourceAction::Store.method(), RouteMethod::Post);
        assert_eq!(ResourceAction::Update.method(), RouteMethod::Put);
        assert_eq!(ResourceAction::Destroy.method(), RouteMethod::Delete);
    }

    #[test]
    fn parse_known_actions() {
        assert_eq!(parse_resource_action("index"), Some(ResourceAction::Index));
        assert_eq!(
            parse_resource_action("destroy"),
            Some(ResourceAction::Destroy)
        );
        assert_eq!(parse_resource_action("unknown"), None);
    }
}