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
//! Typed field-shape metadata for request inputs and resource outputs
//! (AP2.1-5, ADR-0007).
//!
//! The `#[request]` macro generates [`FieldShape`] values and exposes them
//! through [`RequestMetadata`]; the `#[resource]` macro exposes them through
//! [`ResourceMetadata`]. Both are plain `&'static` data — no allocation, no
//! runtime reflection, no `TypeId`/`Any` container. The `routes!` macro
//! resolves `<T as RequestMetadata>::FIELDS` / `<T as ResourceMetadata>::FIELDS`
//! at compile time when a route declares `action: T` / `query: T`, baking
//! the full field shape into the [`super::route_metadata::RouteDescriptor`]
//! const. The UAG then carries the field shapes so `arcature-build` codegen
//! can emit the typed `@arcature/actions` / `@arcature/queries` virtual
//! modules.
//!
//! # Layering
//!
//! The macro captures the Rust type of each field *faithfully* as a
//! `&'static str` (e.g. `"String"`, `"Option<String>"`, `"Vec<i64>"`).
//! The Rust→TypeScript type mapping lives in `arcature-build` codegen —
//! the single code generator (ADR-0006 §7 "arcature-build owns code
//! generation"). `FieldShape` carries no pre-rendered TypeScript: deriving
//! the TS type (and the `optional` flag) from the Rust type string is the
//! codegen's responsibility, so one source of truth owns the cross-stack
//! type mapping.
//!
//! The role split into two traits is deliberate compile-time enforcement:
//! `action: T` requires `T: RequestMetadata` (a `#[request]` type — a
//! mutation input), and `query: T` requires `T: ResourceMetadata` (a
//! `#[resource]` type — a read output). A route that accidentally wires a
//! resource as an action input, or a request as a query output, fails to
//! compile — the mistake is caught at the `routes!` invocation site.

/// The shape of one field of a `#[request]` input or `#[resource]` output.
///
/// Every field is `&'static str` or `&'static [&'static str]` — no `Vec`,
/// no `String`, no allocation. A `const` slice of `FieldShape` is the
/// inspection artifact consumed by the application graph, the UAG, and
/// `arcature-build` codegen.
///
/// `ty` is the field's Rust type rendered as a clean string (e.g.
/// `"String"`, `"Option<String>"`, `"Vec<i64>"`). The codegen derives the
/// TypeScript type and the `optional` flag from `ty`; `FieldShape` does not
/// pre-render TypeScript. `validates` carries the `#[validate(...)]` rule
/// strings (e.g. `["url"]`, `["length(min=1,max=120)"]`) for dev-time
/// introspection — empty for resource fields, which carry no validation
/// attributes.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct FieldShape {
    /// The field name (e.g. `"url"`, `"title"`).
    pub name: &'static str,
    /// The Rust type as a clean string (e.g. `"String"`, `"Option<String>"`,
    /// `"Vec<i64>"`). The codegen maps this to TypeScript and derives
    /// optionality; it is the single input to the cross-stack type mapping.
    pub ty: &'static str,
    /// The `#[validate(...)]` rule strings, in source order. Empty for
    /// resource fields (resources carry no validation attributes). Used for
    /// dev-time form hints; validation itself stays server-side
    /// (`Validated<T>`) — the frontend never re-implements validation.
    #[cfg_attr(feature = "serde", serde(default))]
    pub validates: &'static [&'static str],
}

/// The per-request-type metadata trait: the `#[request]` macro emits
/// `impl RequestMetadata for Type` carrying the input's field descriptors as
/// a `&'static [FieldShape]` associated const. The `routes!` macro resolves
/// `<T as RequestMetadata>::FIELDS` when a route declares `action: T`,
/// baking the action's typed input shape into the `RouteDescriptor` const —
/// so the UAG carries it without a runtime type registry, global mutable
/// state, or `TypeId`/`Any` container (AGENTS.md §17/§20).
///
/// This is the join point the action codegen uses: a route's `action_fields`
/// → the typed `@arcature/actions` input interface.
pub trait RequestMetadata {
    /// The input's field descriptors, as a `&'static` slice. Every entry is
    /// plain `&'static` data — no allocation, no `TypeId`, no runtime
    /// reflection. Generated by the `#[request]` macro.
    const FIELDS: &'static [FieldShape];
}

/// The per-resource-type metadata trait: the `#[resource]` macro emits
/// `impl ResourceMetadata for Type` carrying the output's field descriptors
/// as a `&'static [FieldShape]` associated const. The `routes!` macro
/// resolves `<T as ResourceMetadata>::FIELDS` when a route declares
/// `query: T` (or `query: Vec<T>`), baking the query's typed response shape
/// into the `RouteDescriptor` const — so the UAG carries it without a runtime
/// type registry (AGENTS.md §17/§20).
///
/// This is the join point the query codegen uses: a route's `query_fields`
/// → the typed `@arcature/queries` response interface.
///
/// `#[resource]` continues to emit `Serialize` + `ClientData` (the
/// browser-exposure firewall); `ResourceMetadata` is an *additive* static
/// view of the same fields for typed query codegen. A type annotated
/// `#[resource]` gains this impl automatically.
pub trait ResourceMetadata {
    /// The output's field descriptors, as a `&'static` slice. Every entry is
    /// plain `&'static` data — no allocation, no `TypeId`, no runtime
    /// reflection. Generated by the `#[resource]` macro.
    const FIELDS: &'static [FieldShape];
}

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

    #[test]
    fn field_shape_is_const_constructible() {
        const FIELD: FieldShape = FieldShape {
            name: "url",
            ty: "String",
            validates: &["url"],
        };
        assert_eq!(FIELD.name, "url");
        assert_eq!(FIELD.ty, "String");
        assert_eq!(FIELD.validates, ["url"]);
    }

    #[test]
    fn field_shape_optional_type_is_const_constructible() {
        const FIELD: FieldShape = FieldShape {
            name: "description",
            ty: "Option<String>",
            validates: &[],
        };
        assert_eq!(FIELD.ty, "Option<String>");
        assert!(FIELD.validates.is_empty());
    }

    #[test]
    fn request_metadata_trait_can_be_implemented() {
        struct DummyRequest;
        impl RequestMetadata for DummyRequest {
            const FIELDS: &'static [FieldShape] = &[
                FieldShape {
                    name: "url",
                    ty: "String",
                    validates: &["url"],
                },
                FieldShape {
                    name: "description",
                    ty: "Option<String>",
                    validates: &[],
                },
            ];
        }
        assert_eq!(DummyRequest::FIELDS.len(), 2);
        assert_eq!(DummyRequest::FIELDS[0].name, "url");
        assert_eq!(DummyRequest::FIELDS[1].ty, "Option<String>");
    }

    #[test]
    fn resource_metadata_trait_can_be_implemented() {
        struct DummyResource;
        impl ResourceMetadata for DummyResource {
            const FIELDS: &'static [FieldShape] = &[
                FieldShape {
                    name: "id",
                    ty: "String",
                    validates: &[],
                },
                FieldShape {
                    name: "tags",
                    ty: "Vec<String>",
                    validates: &[],
                },
            ];
        }
        assert_eq!(DummyResource::FIELDS.len(), 2);
        assert_eq!(DummyResource::FIELDS[1].ty, "Vec<String>");
    }

    #[test]
    #[cfg(feature = "serde")]
    fn field_shape_serializes_to_json() {
        let field = FieldShape {
            name: "title",
            ty: "String",
            validates: &["length(min=1,max=120)"],
        };
        let json = serde_json::to_string(&field).expect("serialize");
        assert!(json.contains("\"name\":\"title\""), "name: {json}");
        assert!(json.contains("\"ty\":\"String\""), "ty: {json}");
        assert!(
            json.contains("\"length(min=1,max=120)\""),
            "validates: {json}"
        );
    }
}