Skip to main content

anvil_core/
app.rs

1//! Application builder. Mirrors Laravel 11's `bootstrap/app.rs`.
2
3use std::net::SocketAddr;
4
5use axum::Router as AxumRouter;
6use tower_http::trace::TraceLayer;
7
8use crate::container::{Container, ContainerBuilder};
9use crate::middleware::{install_defaults, MiddlewareRegistry};
10use crate::route::{RouteInfo, Router};
11use crate::server_config::ServerConfig;
12use crate::shutdown::ShutdownHandle;
13
14pub struct Application {
15    pub container: Container,
16    pub registry: MiddlewareRegistry,
17    pub web: AxumRouter<Container>,
18    pub api: AxumRouter<Container>,
19    pub shutdown: ShutdownHandle,
20    pub server_config: ServerConfig,
21    routes: Vec<RouteInfo>,
22}
23
24pub struct ApplicationBuilder {
25    container_builder: ContainerBuilder,
26    registry: MiddlewareRegistry,
27    web_routes: Option<Box<dyn FnOnce(Router) -> Router>>,
28    api_routes: Option<Box<dyn FnOnce(Router) -> Router>>,
29    server_config: ServerConfig,
30}
31
32impl Default for ApplicationBuilder {
33    fn default() -> Self {
34        Self::new()
35    }
36}
37
38impl ApplicationBuilder {
39    pub fn new() -> Self {
40        let registry = MiddlewareRegistry::new();
41        install_defaults(&registry);
42        Self {
43            container_builder: ContainerBuilder::from_env(),
44            registry,
45            web_routes: None,
46            api_routes: None,
47            server_config: ServerConfig::default().apply_env_overrides(),
48        }
49    }
50
51    pub fn container<F>(mut self, configure: F) -> Self
52    where
53        F: FnOnce(ContainerBuilder) -> ContainerBuilder,
54    {
55        self.container_builder = configure(self.container_builder);
56        self
57    }
58
59    pub fn middleware<F>(self, configure: F) -> Self
60    where
61        F: FnOnce(&MiddlewareRegistry),
62    {
63        configure(&self.registry);
64        self
65    }
66
67    pub fn web<F>(mut self, build: F) -> Self
68    where
69        F: FnOnce(Router) -> Router + 'static,
70    {
71        self.web_routes = Some(Box::new(build));
72        self
73    }
74
75    pub fn api<F>(mut self, build: F) -> Self
76    where
77        F: FnOnce(Router) -> Router + 'static,
78    {
79        self.api_routes = Some(Box::new(build));
80        self
81    }
82
83    /// Set the production HTTP serving config (TLS, body limits, compression,
84    /// rate limits, static file mounts, access logs).
85    pub fn server_config(mut self, cfg: ServerConfig) -> Self {
86        self.server_config = cfg;
87        self
88    }
89
90    /// Load `config/anvil.toml` (or the given path) into the builder. Missing
91    /// files are silently ignored — env-derived defaults still apply.
92    pub fn server_config_file(mut self, path: impl AsRef<std::path::Path>) -> Self {
93        self.server_config = ServerConfig::from_file_or_default(path);
94        self
95    }
96
97    pub fn build(self) -> Application {
98        let container = self.container_builder.build();
99        let registry = self.registry;
100        let server_config = self.server_config;
101
102        let mut all_routes: Vec<RouteInfo> = Vec::new();
103
104        let web_router = self.web_routes.map(|f| {
105            let router = Router::new(registry.clone());
106            let built = f(router);
107            let (axum_router, routes) = built.finish();
108            all_routes.extend(routes);
109            axum_router
110        });
111
112        let api_router = self.api_routes.map(|f| {
113            let router = Router::new(registry.clone()).prefix("/api");
114            let built = f(router);
115            let (axum_router, routes) = built.finish();
116            all_routes.extend(routes);
117            axum_router
118        });
119
120        Application {
121            container,
122            registry,
123            web: web_router.unwrap_or_default(),
124            api: api_router.unwrap_or_default(),
125            shutdown: ShutdownHandle::new(),
126            server_config,
127            routes: all_routes,
128        }
129    }
130}
131
132impl Application {
133    pub fn builder() -> ApplicationBuilder {
134        ApplicationBuilder::new()
135    }
136
137    /// Every route registered against the app's web + api routers, in
138    /// declaration order. Used by `anvil routes` to print a table.
139    pub fn routes(&self) -> &[RouteInfo] {
140        &self.routes
141    }
142
143    /// Combine web + api into a single state-applied router. Production layers
144    /// (compression, body limits, rate limits, static files, access logs) are
145    /// applied via `into_router_with_config`.
146    pub fn into_router(self) -> AxumRouter {
147        let cfg = self.server_config.clone();
148        let container_for_mw = self.container.clone();
149        let combined = self.web.merge(self.api);
150        let combined = crate::server::apply_layers(combined, &cfg);
151        combined
152            // Install the container into a task-local for the duration of each
153            // request so the facade helpers (`db()`, `cache()`, `queue()`,
154            // `current()`) work without `State<Container>` in handler signatures.
155            .layer(axum::middleware::from_fn(
156                move |req: axum::http::Request<axum::body::Body>, next: axum::middleware::Next| {
157                    let c = container_for_mw.clone();
158                    async move { crate::middleware::inject_container_mw(c, req, next).await }
159                },
160            ))
161            .layer(TraceLayer::new_for_http())
162            .with_state(self.container.clone())
163    }
164
165    /// Run the app on the address taken from `server_config.bind`, honoring
166    /// TLS, limits, compression, static files, and rate limits.
167    ///
168    /// This is the preferred entry point — `serve(addr)` is retained for
169    /// backward compatibility but always serves plain HTTP.
170    pub async fn run(self) -> Result<(), crate::Error> {
171        let shutdown_handle = self.shutdown.clone().install();
172        let cfg = self.server_config.clone();
173        let container = self.container.clone();
174        let container_for_mw = container.clone();
175        let combined = self.web.merge(self.api);
176        let layered = crate::server::apply_layers(combined, &cfg)
177            // Install the container into a task-local for the duration of each
178            // request — see the corresponding block in `into_router()`.
179            .layer(axum::middleware::from_fn(
180                move |req: axum::http::Request<axum::body::Body>, next: axum::middleware::Next| {
181                    let c = container_for_mw.clone();
182                    async move { crate::middleware::inject_container_mw(c, req, next).await }
183                },
184            ))
185            .layer(TraceLayer::new_for_http())
186            .with_state(container);
187
188        let (tx, rx) = tokio::sync::oneshot::channel::<()>();
189        tokio::spawn(async move {
190            shutdown_handle.wait().await;
191            let _ = tx.send(());
192        });
193
194        crate::server::serve(layered, &cfg, rx).await
195    }
196
197    /// Backward-compatible entry point: serve plain HTTP on `addr`, ignoring
198    /// the server_config's bind address.
199    pub async fn serve(self, addr: SocketAddr) -> Result<(), crate::Error> {
200        let mut cfg = self.server_config.clone();
201        cfg.bind = addr.to_string();
202        cfg.tls = None;
203        let app_with_cfg = Application {
204            server_config: cfg,
205            ..self
206        };
207        app_with_cfg.run().await
208    }
209}