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
//! `ModuleDescriptor` — the metadata for a single Arcature feature module.
//!
//! A module descriptor is the compile-time metadata that the `module!` macro
//! generates. It is a plain data struct — no behavior, no runtime
//! connection — suitable for side-effect-free inspection (`arc modules`,
//! `arc check`). The `application!` macro collects descriptors into an
//! [`ApplicationGraph`](super::application_graph::ApplicationGraph).
//!
//! All fields use `&'static [&'static str]` (not `Vec`) so a descriptor can
//! be constructed as a `const` by the `module!` macro — no allocation, no
//! runtime cost (PERFORMANCE.md §1: unnecessary allocation is an
//! anti-pattern).

/// A compile-time event → listener binding (A11).
///
/// Records that a listener function is bound to an event type, for `arc
/// check` inspection and `arc modules` display. The `module!` macro
/// generates `&'static [ListenerBinding]` from the `listeners:` section.
///
/// This is metadata only — it does NOT register the listener at runtime.
/// The application registers listeners explicitly via `Dispatcher::register`
/// at startup (PROGRAM.md: "Registration is explicit through module
/// metadata. No runtime discovery.").
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ListenerBinding {
    /// The event type name (e.g. `"UserRegistered"`).
    pub event: &'static str,
    /// The listener function name (e.g. `"send_welcome"`).
    pub listener: &'static str,
}

/// A compile-time job handler binding (A12).
///
/// Records that a handler function is bound to a job kind+version, for
/// `arc check` inspection and `arc modules` display. The `module!` macro
/// generates `&'static [JobBinding]` from the `jobs:` section.
///
/// This is metadata only — it does NOT register the handler at runtime.
/// The application registers handlers explicitly via
/// `arcature_jobs::Registry::add` at startup.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct JobBinding {
    /// The job kind string (e.g. `"send_verification_email"`).
    pub kind: &'static str,
    /// The job version (incremented on payload schema changes).
    pub version: i16,
    /// The handler function name (e.g. `"handle_send_verification_email"`).
    pub handler: &'static str,
}

/// A compile-time application command binding (A12).
///
/// Records that a function is bound to a command name, for `arc check`
/// inspection and `arc modules` display. The `module!` macro generates
/// `&'static [CommandBinding]` from the `commands:` section.
///
/// This is metadata only — it does NOT register the command at runtime.
/// The application registers commands explicitly via
/// `CommandRegistry::register` at startup.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct CommandBinding {
    /// The command name (e.g. `"users:prune"`).
    pub name: &'static str,
    /// The command function name (e.g. `"prune_users"`).
    pub function: &'static str,
}

/// The cadence at which a scheduled job fires (A12).
///
/// Const-constructible (no `Vec`, no `Box`, no `String`) so the `schedule!`
/// macro can generate `&'static [ScheduleBinding]` as a `const`. The
/// scheduler converts this to a `DateTime<Utc>` next-fire time at runtime.
///
/// Time zone is UTC for A12. The `daily` cadence fires at the specified
/// hour:minute in UTC.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
#[cfg_attr(feature = "serde", serde(tag = "kind", rename_all = "snake_case"))]
pub enum ScheduleCadence {
    /// Fire every `seconds` seconds (e.g. `every "5m"` → 300 seconds).
    Every { seconds: u64 },
    /// Fire daily at `hour:minute` UTC (e.g. `daily "03:00"` → hour=3, minute=0).
    Daily { hour: u8, minute: u8 },
}

/// A compile-time schedule binding (A12).
///
/// Records that a job kind+version is scheduled on a cadence, for `arc
/// check` inspection and `arc schedule` display. The `module!` macro
/// generates `&'static [ScheduleBinding]` from the `schedules:` section.
///
/// This is metadata only — it does NOT start the scheduler at runtime.
/// The application builds the scheduler explicitly via the `schedule!`
/// macro's `build_scheduler` function at startup.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ScheduleBinding {
    /// The job kind string (e.g. `"cleanup_sessions"`).
    pub job: &'static str,
    /// The job version.
    pub version: i16,
    /// The cadence (interval or daily time).
    pub cadence: ScheduleCadence,
}

/// The metadata for a single feature module.
///
/// Generated by the `module!` macro as a `const` and registered into the
/// [`ApplicationGraph`](super::application_graph::ApplicationGraph) by the
/// `application!` macro. Every field is a `&'static` slice — no `Vec`, no
/// trait objects, no `TypeId`, no runtime reflection, no allocation.
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ModuleDescriptor {
    /// The module name (e.g. `"Accounts"`, `"Links"`).
    pub name: &'static str,
    /// Modules this module imports (by name). Only imported modules'
    /// exported capabilities are visible to this module.
    pub imports: &'static [&'static str],
    /// Capabilities this module exports (by name). Only exported
    /// capabilities are visible to importing modules.
    pub exports: &'static [&'static str],
    /// Controller type names registered in this module.
    pub controllers: &'static [&'static str],
    /// Per-controller method metadata, parallel to
    /// [`controllers`](Self::controllers "controllers"): entry `i` is the
    /// `ControllerMetadata::METHODS` slice for controller `controllers[i]`.
    /// Each [`ControllerMethod`](super::controller_metadata::ControllerMethod)
    /// carries the method name and the page-contract identity derived from
    /// the handler's `Page<T>` return type. The UAG joins
    /// `RouteDescriptor.handler` (`"ControllerType::method"`) to these
    /// entries to infer the route→page edge when the route declares no
    /// `page:`/`pages:` (the golden path, AP2.1-4 H4). Empty by default.
    pub controller_methods: &'static [&'static [super::controller_metadata::ControllerMethod]],
    /// Service type names registered in this module.
    pub services: &'static [&'static str],
    /// Policy type names registered in this module.
    pub policies: &'static [&'static str],
    /// Route descriptors for routes declared in this module. Empty by
    /// default; populated when a `routes:` section is added to `module!`
    /// (A3+). See [`RouteDescriptor`](super::route_metadata::RouteDescriptor).
    pub routes: &'static [super::route_metadata::RouteDescriptor],
    /// Event → listener bindings declared in this module (A11). Empty by
    /// default; populated when a `listeners:` section is added to `module!`.
    pub listeners: &'static [ListenerBinding],
    /// Job handler bindings declared in this module (A12). Empty by
    /// default; populated when a `jobs:` section is added to `module!`.
    pub jobs: &'static [JobBinding],
    /// Application command bindings declared in this module (A12). Empty
    /// by default; populated when a `commands:` section is added to
    /// `module!`.
    pub commands: &'static [CommandBinding],
    /// Schedule bindings declared in this module (A12). Empty by
    /// default; populated when a `schedules:` section is added to `module!`.
    pub schedules: &'static [ScheduleBinding],
}

impl ModuleDescriptor {
    /// Creates a new module descriptor with the given name and empty lists.
    pub const fn new(name: &'static str) -> Self {
        Self {
            name,
            imports: &[],
            exports: &[],
            controllers: &[],
            controller_methods: &[],
            services: &[],
            policies: &[],
            routes: &[],
            listeners: &[],
            jobs: &[],
            commands: &[],
            schedules: &[],
        }
    }
}

/// A node in the module dependency graph, used for cycle detection and
/// inspection. Derived from a [`ModuleDescriptor`]'s import edges.
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct ModuleNode {
    /// The module name.
    pub name: &'static str,
    /// The modules this module imports (edges in the dependency graph).
    pub imports: Vec<&'static str>,
}

impl From<&ModuleDescriptor> for ModuleNode {
    fn from(desc: &ModuleDescriptor) -> Self {
        ModuleNode {
            name: desc.name,
            imports: desc.imports.to_vec(),
        }
    }
}

/// Builds a map of module-name → [`ModuleNode`] from a slice of descriptors.
/// Used by [`ApplicationGraph`](super::application_graph::ApplicationGraph)
/// for cycle detection and inspection.
pub fn module_node_map(
    descriptors: &[ModuleDescriptor],
) -> std::collections::BTreeMap<&'static str, ModuleNode> {
    descriptors
        .iter()
        .map(|d| (d.name, ModuleNode::from(d)))
        .collect()
}

#[cfg(all(test, feature = "serde"))]
mod serde_tests {
    use super::*;

    #[test]
    fn schedule_cadence_serializes_with_kind_tag() {
        let every = ScheduleCadence::Every { seconds: 300 };
        assert_eq!(
            serde_json::to_string(&every).unwrap(),
            r#"{"kind":"every","seconds":300}"#
        );
        let daily = ScheduleCadence::Daily { hour: 3, minute: 0 };
        assert_eq!(
            serde_json::to_string(&daily).unwrap(),
            r#"{"kind":"daily","hour":3,"minute":0}"#
        );
    }

    #[test]
    fn module_descriptor_serializes_to_json() {
        const LISTENERS: &[ListenerBinding] = &[ListenerBinding {
            event: "UserRegistered",
            listener: "send_welcome",
        }];
        const SCHEDULES: &[ScheduleBinding] = &[ScheduleBinding {
            job: "cleanup_sessions",
            version: 1,
            cadence: ScheduleCadence::Every { seconds: 60 },
        }];
        const DESCRIPTOR: ModuleDescriptor = ModuleDescriptor {
            name: "Links",
            imports: &["Accounts"],
            exports: &["LinkService"],
            controllers: &["LinksController"],
            controller_methods: &[],
            services: &["LinkService"],
            policies: &["LinkPolicy"],
            routes: &[],
            listeners: LISTENERS,
            jobs: &[],
            commands: &[],
            schedules: SCHEDULES,
        };
        let json = serde_json::to_string(&DESCRIPTOR).unwrap();
        assert!(
            json.contains("\"name\":\"Links\""),
            "name should serialize: {json}"
        );
        assert!(
            json.contains("\"event\":\"UserRegistered\""),
            "listener should serialize: {json}"
        );
        assert!(
            json.contains("\"job\":\"cleanup_sessions\""),
            "schedule should serialize: {json}"
        );
        assert!(
            json.contains("\"kind\":\"every\""),
            "cadence kind should serialize: {json}"
        );
    }
}