arcature 2026.0.0

Arcature application framework: a high-level Application facade over the certified Arcature subsystems, with the low-level Axum/Tower escape hatch preserved.
Documentation
//! The [`ApplicationBuilder`] — the typed, consuming builder for
//! [`Application`].
//!
//! The builder is the normal configuration seam (engine spec §22): small
//! surface, typed configuration, deterministic construction, no globals, no
//! runtime reflection. Each method returns a builder so an application reads
//! as a fluent chain.
//!
//! State flows through the builder at the type level, mirroring
//! [`axum::Router`]:
//!
//! ```text
//! ApplicationBuilder<()>  --routes(Routes<S>)-->  ApplicationBuilder<S>
//! ApplicationBuilder<S>    --state(S)----------->  ApplicationBuilder<()>
//! ApplicationBuilder<()>  --build()------------->  Application<()>  (servable)
//! ```
//!
//! A stateless app skips `.state()`: `.routes(Routes<()>)` keeps `S = ()`.
//! For lifecycle-managed apps, use `.run_with_lifecycle(state_fn)` instead of
//! `.state()` + `.run()` — the engine starts subsystems, builds state from
//! [`Resources`](super::resources::Resources), serves, and shuts down.

use crate::Routes;
use crate::application::ty::{Application, DEFAULT_BIND_ADDR, DEFAULT_PORT, ProxyFn};
use crate::proxy::{ProxyAction, ProxyRequest};

#[cfg(feature = "inertia")]
use crate::inertia::InertiaConfig;
#[cfg(feature = "inertia")]
use crate::inertia::PageContracts;

#[cfg(feature = "pages")]
use crate::pages::{MaintenanceGuard, Pages};

#[cfg(feature = "db")]
use crate::db::DbConfig;

#[cfg(feature = "cache")]
use crate::cache::CacheConfig;

#[cfg(feature = "storage")]
use crate::storage::StorageConfig;

#[cfg(feature = "mail")]
use crate::mail::SmtpConfig;

#[cfg(feature = "jobs")]
use crate::jobs::{Registry, WorkerConfig};

/// A consuming builder for [`Application`].
///
/// Constructed via [`Application::new`](crate::Application::new). State `S` is
/// the Axum router state type tracked at compile time.
pub struct ApplicationBuilder<S = ()> {
    pub(crate) routes: Routes<S>,
    pub(crate) proxy: Option<ProxyFn>,
    pub(crate) bind_address: String,
    pub(crate) port: u16,

    #[cfg(feature = "inertia")]
    pub(crate) inertia_config: Option<InertiaConfig>,
    #[cfg(feature = "inertia")]
    pub(crate) page_contracts: Option<PageContracts>,
    #[cfg(feature = "pages")]
    pub(crate) pages: Option<Pages>,
    #[cfg(feature = "pages")]
    pub(crate) maintenance_guard: Option<MaintenanceGuard>,

    // Lifecycle config (consumed by `startup`).
    #[cfg(feature = "db")]
    pub(crate) database: Option<DbConfig>,
    #[cfg(feature = "cache")]
    pub(crate) cache_config: Option<CacheConfig>,
    #[cfg(feature = "storage")]
    pub(crate) storage_config: Option<StorageConfig>,
    #[cfg(feature = "mail")]
    pub(crate) mail_config: Option<SmtpConfig>,
    #[cfg(feature = "jobs")]
    pub(crate) jobs_registry: Option<Registry>,
    #[cfg(feature = "jobs")]
    pub(crate) worker_config: Option<WorkerConfig>,
}

impl<S> ApplicationBuilder<S>
where
    S: Clone + Send + Sync + 'static,
{
    /// Install the application proxy function — application-owned global
    /// request policy that runs *before* route selection (engine spec §5).
    ///
    /// The engine owns all Axum/Tower plumbing; the application owns only the
    /// policy, expressed as a pure synchronous function from
    /// [`ProxyRequest`] to [`ProxyAction`]. The proxy is executed by the
    /// pre-routing Tower service (the `proxy::service` module) which rewrites the
    /// request URI before the Axum router sees it (engine spec §3/§4).
    #[must_use]
    pub fn proxy<F>(mut self, proxy: F) -> Self
    where
        F: Fn(ProxyRequest<'_>) -> ProxyAction + Send + Sync + 'static,
    {
        self.proxy = Some(std::sync::Arc::new(proxy));
        self
    }

    /// Set the bind address (host) for [`Application::run`](crate::Application).
    /// Default: `127.0.0.1`.
    #[must_use]
    pub fn bind(mut self, address: impl Into<String>) -> Self {
        self.bind_address = address.into();
        self
    }

    /// Set the bind port for [`Application::run`](crate::Application).
    /// Default: `3000`.
    #[must_use]
    pub fn port(mut self, port: u16) -> Self {
        self.port = port;
        self
    }

    /// Install the Inertia config. When set, the pipeline assembler applies
    /// `InertiaLayer` as a post-routing layer (engine spec §36) so Inertia
    /// protocol responses (page objects, version-mismatch 409, fragment
    /// redirects) are handled by the certified `arcature-inertia` middleware.
    ///
    /// Only available when the `inertia` feature is enabled.
    #[cfg(feature = "inertia")]
    #[must_use]
    pub fn inertia(mut self, config: InertiaConfig) -> Self {
        self.inertia_config = Some(config);
        self
    }

    /// Register typed Inertia page contracts with the application.
    ///
    /// This does not change Axum route selection. It gives the engine and
    /// Cross-Stack Linker one explicit page/props registry.
    #[cfg(feature = "inertia")]
    #[must_use]
    pub fn page_contracts(mut self, contracts: PageContracts) -> Self {
        self.page_contracts = Some(contracts);
        self
    }

    /// Install the special-pages renderer. When set, the pipeline assembler
    /// uses `Pages::not_found_service()` as the router's 404 fallback (engine
    /// spec §9/§37). When `None`, the engine uses `Pages::default()`.
    ///
    /// Only available when the `pages` feature is enabled.
    #[cfg(feature = "pages")]
    #[must_use]
    pub fn pages(mut self, pages: Pages) -> Self {
        self.pages = Some(pages);
        self
    }

    /// Install the maintenance guard. When set, the pipeline assembler applies
    /// `MaintenanceLayer` as a post-routing layer that short-circuits with 503
    /// and `Retry-After` when the application is in maintenance mode (engine
    /// spec §10). The guard is the single shared handle for maintenance state;
    /// clone it for the `arc down` / `arc up` CLI ops.
    ///
    /// Only available when the `pages` feature is enabled (maintenance is a
    /// special page).
    #[cfg(feature = "pages")]
    #[must_use]
    pub fn maintenance(mut self, guard: MaintenanceGuard) -> Self {
        self.maintenance_guard = Some(guard);
        self
    }

    /// Configure the database. When set, the engine builds one `PgPool` via
    /// `Db::connect` on `run_with_lifecycle`, shares it with jobs, and exposes
    /// the `Db` handle via `Resources::db()`.
    ///
    /// Only available when the `db` feature is enabled.
    #[cfg(feature = "db")]
    #[must_use]
    pub fn database(mut self, config: DbConfig) -> Self {
        self.database = Some(config);
        self
    }

    /// Configure the cache. When set, the engine connects via
    /// `Cache::connect` on `run_with_lifecycle`.
    #[cfg(feature = "cache")]
    #[must_use]
    pub fn cache(mut self, config: CacheConfig) -> Self {
        self.cache_config = Some(config);
        self
    }

    /// Configure the storage backend. When set, the engine connects via
    /// `Storage::connect` on `run_with_lifecycle`.
    #[cfg(feature = "storage")]
    #[must_use]
    pub fn storage(mut self, config: StorageConfig) -> Self {
        self.storage_config = Some(config);
        self
    }

    /// Configure the mailer. When set, the engine constructs a `Mailer` via
    /// `Mailer::smtp` on `run_with_lifecycle`.
    #[cfg(feature = "mail")]
    #[must_use]
    pub fn mail(mut self, config: SmtpConfig) -> Self {
        self.mail_config = Some(config);
        self
    }

    /// Configure the job handler registry. When set, the engine spawns a
    /// worker over the shared `PgPool` on `run_with_lifecycle`. The worker runs
    /// until shutdown, then drains in-flight jobs before the pool closes.
    #[cfg(feature = "jobs")]
    #[must_use]
    pub fn jobs(mut self, registry: Registry) -> Self {
        self.jobs_registry = Some(registry);
        self
    }

    /// Override the worker config. Defaults to `WorkerConfig::default()` when
    /// not called. Only used when `.jobs(registry)` is also set.
    #[cfg(feature = "jobs")]
    #[must_use]
    pub fn worker_config(mut self, config: WorkerConfig) -> Self {
        self.worker_config = Some(config);
        self
    }

    /// Resolve the router state to `()` (the engine analogue of
    /// [`axum::Router::with_state`]) so the application becomes servable.
    /// Consumes `state` and the builder; returns a builder with `S = ()`.
    #[must_use]
    pub fn state(self, state: S) -> ApplicationBuilder<()> {
        ApplicationBuilder {
            routes: self.routes.with_state(state),
            proxy: self.proxy,
            bind_address: self.bind_address,
            port: self.port,
            #[cfg(feature = "inertia")]
            inertia_config: self.inertia_config,
            #[cfg(feature = "inertia")]
            page_contracts: self.page_contracts,
            #[cfg(feature = "pages")]
            pages: self.pages,
            #[cfg(feature = "pages")]
            maintenance_guard: self.maintenance_guard,
            #[cfg(feature = "db")]
            database: self.database,
            #[cfg(feature = "cache")]
            cache_config: self.cache_config,
            #[cfg(feature = "storage")]
            storage_config: self.storage_config,
            #[cfg(feature = "mail")]
            mail_config: self.mail_config,
            #[cfg(feature = "jobs")]
            jobs_registry: self.jobs_registry,
            #[cfg(feature = "jobs")]
            worker_config: self.worker_config,
        }
    }

    /// Freeze the builder into an [`Application`]. Construction is
    /// deterministic and allocation-free beyond storing the parts.
    #[must_use]
    pub fn build(self) -> Application<S> {
        Application {
            routes: self.routes,
            proxy: self.proxy,
            bind_address: self.bind_address,
            port: self.port,
            #[cfg(feature = "inertia")]
            inertia_config: self.inertia_config,
            #[cfg(feature = "inertia")]
            page_contracts: self.page_contracts,
            #[cfg(feature = "pages")]
            pages: self.pages,
            #[cfg(feature = "pages")]
            maintenance_guard: self.maintenance_guard,
            #[cfg(feature = "db")]
            database: self.database,
            #[cfg(feature = "cache")]
            cache_config: self.cache_config,
            #[cfg(feature = "storage")]
            storage_config: self.storage_config,
            #[cfg(feature = "mail")]
            mail_config: self.mail_config,
            #[cfg(feature = "jobs")]
            jobs_registry: self.jobs_registry,
            #[cfg(feature = "jobs")]
            worker_config: self.worker_config,
        }
    }
}

impl ApplicationBuilder<()> {
    /// Start a new stateless builder with an empty route table and default
    /// bind address (`127.0.0.1:3000`). Most applications call
    /// [`Application::new`](crate::Application::new) instead.
    #[must_use]
    pub fn new() -> Self {
        Self {
            routes: Routes::new(),
            proxy: None,
            bind_address: DEFAULT_BIND_ADDR.to_owned(),
            port: DEFAULT_PORT,
            #[cfg(feature = "inertia")]
            inertia_config: None,
            #[cfg(feature = "inertia")]
            page_contracts: None,
            #[cfg(feature = "pages")]
            pages: None,
            #[cfg(feature = "pages")]
            maintenance_guard: None,
            #[cfg(feature = "db")]
            database: None,
            #[cfg(feature = "cache")]
            cache_config: None,
            #[cfg(feature = "storage")]
            storage_config: None,
            #[cfg(feature = "mail")]
            mail_config: None,
            #[cfg(feature = "jobs")]
            jobs_registry: None,
            #[cfg(feature = "jobs")]
            worker_config: None,
        }
    }
}

impl Default for ApplicationBuilder<()> {
    fn default() -> Self {
        Self::new()
    }
}

// `routes` changes the builder's state type, so it is a free-standing impl
// block (not bound by `S: Clone + Send + Sync + 'static`) so a stateful
// router can be installed from the initial `ApplicationBuilder<()>`.
impl<S> ApplicationBuilder<S> {
    /// Replace the route table. The previous routes are discarded; the
    /// builder's state type becomes the router's state type `S2`.
    #[must_use]
    pub fn routes<S2>(self, routes: Routes<S2>) -> ApplicationBuilder<S2> {
        ApplicationBuilder {
            routes,
            proxy: self.proxy,
            bind_address: self.bind_address,
            port: self.port,
            #[cfg(feature = "inertia")]
            inertia_config: self.inertia_config,
            #[cfg(feature = "inertia")]
            page_contracts: self.page_contracts,
            #[cfg(feature = "pages")]
            pages: self.pages,
            #[cfg(feature = "pages")]
            maintenance_guard: self.maintenance_guard,
            #[cfg(feature = "db")]
            database: self.database,
            #[cfg(feature = "cache")]
            cache_config: self.cache_config,
            #[cfg(feature = "storage")]
            storage_config: self.storage_config,
            #[cfg(feature = "mail")]
            mail_config: self.mail_config,
            #[cfg(feature = "jobs")]
            jobs_registry: self.jobs_registry,
            #[cfg(feature = "jobs")]
            worker_config: self.worker_config,
        }
    }
}