rustapi_core/app/builder.rs
1use super::config::RustApiConfig;
2use super::dispatcher::RequestDispatcher;
3use super::types::RustApi;
4use crate::events::LifecycleHooks;
5use crate::interceptor::{InterceptorChain, RequestInterceptor, ResponseInterceptor};
6use crate::middleware::{LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT};
7use crate::router::Router;
8use std::future::Future;
9use std::sync::Arc;
10
11impl RustApi {
12 /// Create a new RustAPI application.
13 ///
14 /// Initialize `tracing-subscriber` in `main` when using the `tracing` feature;
15 /// `cargo-rustapi` templates already do this.
16 pub fn new() -> Self {
17 Self {
18 router: Router::new(),
19 openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
20 .register::<rustapi_openapi::ErrorSchema>()
21 .register::<rustapi_openapi::ErrorBodySchema>()
22 .register::<rustapi_openapi::ValidationErrorSchema>()
23 .register::<rustapi_openapi::ValidationErrorBodySchema>()
24 .register::<rustapi_openapi::FieldErrorSchema>(),
25 layers: LayerStack::new(),
26 body_limit: Some(DEFAULT_BODY_LIMIT), // Default 1MB limit
27 interceptors: InterceptorChain::new(),
28 lifecycle_hooks: LifecycleHooks::new(),
29 hot_reload: false,
30 #[cfg(feature = "http3")]
31 http3_config: None,
32 health_check: None,
33 health_endpoint_config: None,
34 status_config: None,
35 #[cfg(feature = "dashboard")]
36 dashboard_config: None,
37 }
38 }
39
40 /// The primary way to build a RustAPI application.
41 ///
42 /// Collects all routes decorated with `#[rustapi_rs::get]`, `#[rustapi_rs::post]`, etc.
43 /// at link time via `linkme` and registers them automatically — no manual `.route()`
44 /// or `.mount_route()` calls needed. This is baked into the core and requires no
45 /// feature flags.
46 ///
47 /// When the `swagger-ui` feature is enabled (included in the default `core` feature),
48 /// Swagger UI is served at `/docs`. Without it, only the auto-discovered routes are
49 /// registered.
50 ///
51 /// Use [`RustApi::new()`] when handlers are plain `async fn` not annotated with
52 /// the route macros, or when you need full manual control over route registration.
53 ///
54 /// # Example
55 ///
56 /// ```rust,ignore
57 /// use rustapi_rs::prelude::*;
58 ///
59 /// #[rustapi_rs::get("/users")]
60 /// async fn list_users() -> Json<Vec<User>> {
61 /// Json(vec![])
62 /// }
63 ///
64 /// #[rustapi_rs::main]
65 /// async fn main() -> Result<()> {
66 /// RustApi::auto().run("0.0.0.0:8080").await
67 /// }
68 /// ```
69 #[cfg(feature = "swagger-ui")]
70 pub fn auto() -> Self {
71 Self::new().mount_auto_routes_grouped().docs("/docs")
72 }
73
74 #[cfg(not(feature = "swagger-ui"))]
75 pub fn auto() -> Self {
76 Self::new().mount_auto_routes_grouped()
77 }
78
79 /// Create a configurable RustAPI application with auto-routes.
80 ///
81 /// Provides builder methods for customization while still
82 /// auto-registering all decorated routes.
83 ///
84 /// # Example
85 ///
86 /// ```rust,ignore
87 /// use rustapi_rs::prelude::*;
88 ///
89 /// RustApi::config()
90 /// .docs_path("/api-docs")
91 /// .body_limit(5 * 1024 * 1024) // 5MB
92 /// .openapi_info("My API", "2.0.0", Some("API Description"))
93 /// .run("0.0.0.0:8080")
94 /// .await?;
95 /// ```
96 pub fn config() -> RustApiConfig {
97 RustApiConfig::new()
98 }
99
100 /// Set the global body size limit for request bodies
101 ///
102 /// This protects against denial-of-service attacks via large payloads.
103 /// The default limit is 1MB (1024 * 1024 bytes).
104 ///
105 /// # Arguments
106 ///
107 /// * `limit` - Maximum body size in bytes
108 ///
109 /// # Example
110 ///
111 /// ```rust,ignore
112 /// use rustapi_rs::prelude::*;
113 ///
114 /// RustApi::new()
115 /// .body_limit(5 * 1024 * 1024) // 5MB limit
116 /// .route("/upload", post(upload_handler))
117 /// .run("127.0.0.1:8080")
118 /// .await
119 /// ```
120 pub fn body_limit(mut self, limit: usize) -> Self {
121 self.body_limit = Some(limit);
122 self
123 }
124
125 /// Disable the body size limit
126 ///
127 /// Warning: This removes protection against large payload attacks.
128 /// Only use this if you have other mechanisms to limit request sizes.
129 ///
130 /// # Example
131 ///
132 /// ```rust,ignore
133 /// RustApi::new()
134 /// .no_body_limit() // Disable body size limit
135 /// .route("/upload", post(upload_handler))
136 /// ```
137 pub fn no_body_limit(mut self) -> Self {
138 self.body_limit = None;
139 self
140 }
141
142 /// Add a middleware layer to the application
143 ///
144 /// Layers are executed in the order they are added (outermost first).
145 /// The first layer added will be the first to process the request and
146 /// the last to process the response.
147 ///
148 /// # Example
149 ///
150 /// ```rust,ignore
151 /// use rustapi_rs::prelude::*;
152 /// use rustapi_core::middleware::{RequestIdLayer, TracingLayer};
153 ///
154 /// RustApi::new()
155 /// .layer(RequestIdLayer::new()) // First to process request
156 /// .layer(TracingLayer::new()) // Second to process request
157 /// .route("/", get(handler))
158 /// .run("127.0.0.1:8080")
159 /// .await
160 /// ```
161 pub fn layer<L>(mut self, layer: L) -> Self
162 where
163 L: MiddlewareLayer,
164 {
165 self.layers.push(Box::new(layer));
166 self
167 }
168
169 /// Add a request interceptor to the application
170 ///
171 /// Request interceptors are executed in registration order before the route handler.
172 /// Each interceptor can modify the request before passing it to the next interceptor
173 /// or handler.
174 ///
175 /// # Example
176 ///
177 /// ```rust,ignore
178 /// use rustapi_core::{RustApi, interceptor::RequestInterceptor, Request};
179 ///
180 /// #[derive(Clone)]
181 /// struct AddRequestId;
182 ///
183 /// impl RequestInterceptor for AddRequestId {
184 /// fn intercept(&self, mut req: Request) -> Request {
185 /// req.extensions_mut().insert(uuid::Uuid::new_v4());
186 /// req
187 /// }
188 ///
189 /// fn clone_box(&self) -> Box<dyn RequestInterceptor> {
190 /// Box::new(self.clone())
191 /// }
192 /// }
193 ///
194 /// RustApi::new()
195 /// .request_interceptor(AddRequestId)
196 /// .route("/", get(handler))
197 /// .run("127.0.0.1:8080")
198 /// .await
199 /// ```
200 pub fn request_interceptor<I>(mut self, interceptor: I) -> Self
201 where
202 I: RequestInterceptor,
203 {
204 self.interceptors.add_request_interceptor(interceptor);
205 self
206 }
207
208 /// Add a response interceptor to the application
209 ///
210 /// Response interceptors are executed in reverse registration order after the route
211 /// handler completes. Each interceptor can modify the response before passing it
212 /// to the previous interceptor or client.
213 ///
214 /// # Example
215 ///
216 /// ```rust,ignore
217 /// use rustapi_core::{RustApi, interceptor::ResponseInterceptor, Response};
218 ///
219 /// #[derive(Clone)]
220 /// struct AddServerHeader;
221 ///
222 /// impl ResponseInterceptor for AddServerHeader {
223 /// fn intercept(&self, mut res: Response) -> Response {
224 /// res.headers_mut().insert("X-Server", "RustAPI".parse().unwrap());
225 /// res
226 /// }
227 ///
228 /// fn clone_box(&self) -> Box<dyn ResponseInterceptor> {
229 /// Box::new(self.clone())
230 /// }
231 /// }
232 ///
233 /// RustApi::new()
234 /// .response_interceptor(AddServerHeader)
235 /// .route("/", get(handler))
236 /// .run("127.0.0.1:8080")
237 /// .await
238 /// ```
239 pub fn response_interceptor<I>(mut self, interceptor: I) -> Self
240 where
241 I: ResponseInterceptor,
242 {
243 self.interceptors.add_response_interceptor(interceptor);
244 self
245 }
246
247 /// Add application state
248 ///
249 /// State is shared across all handlers and can be extracted using `State<T>`.
250 ///
251 /// # Example
252 ///
253 /// ```rust,ignore
254 /// #[derive(Clone)]
255 /// struct AppState {
256 /// db: DbPool,
257 /// }
258 ///
259 /// RustApi::new()
260 /// .state(AppState::new())
261 /// ```
262 pub fn state<S>(self, _state: S) -> Self
263 where
264 S: Clone + Send + Sync + 'static,
265 {
266 // Store state in the router's shared Extensions so `State<T>` extractor can retrieve it.
267 let state = _state;
268 let mut app = self;
269 let r = std::mem::take(&mut app.router);
270 app.router = r.state(state);
271 app
272 }
273
274 /// Register an `on_start` lifecycle hook
275 ///
276 /// The callback runs **after** route registration and **before** the server
277 /// begins accepting connections. Multiple hooks execute in registration order.
278 ///
279 /// # Example
280 ///
281 /// ```rust,ignore
282 /// RustApi::new()
283 /// .on_start(|| async {
284 /// println!("Server starting...");
285 /// // e.g. run DB migrations, warm caches
286 /// })
287 /// .run("127.0.0.1:8080")
288 /// .await
289 /// ```
290 pub fn on_start<F, Fut>(mut self, hook: F) -> Self
291 where
292 F: FnOnce() -> Fut + Send + 'static,
293 Fut: Future<Output = ()> + Send + 'static,
294 {
295 self.lifecycle_hooks
296 .on_start
297 .push(Box::new(move || Box::pin(hook())));
298 self
299 }
300
301 /// Register an `on_shutdown` lifecycle hook
302 ///
303 /// The callback runs **after** the shutdown signal is received and the server
304 /// stops accepting new connections. Multiple hooks execute in registration order.
305 ///
306 /// # Example
307 ///
308 /// ```rust,ignore
309 /// RustApi::new()
310 /// .on_shutdown(|| async {
311 /// println!("Server shutting down...");
312 /// // e.g. flush logs, close DB connections
313 /// })
314 /// .run_with_shutdown("127.0.0.1:8080", ctrl_c())
315 /// .await
316 /// ```
317 pub fn on_shutdown<F, Fut>(mut self, hook: F) -> Self
318 where
319 F: FnOnce() -> Fut + Send + 'static,
320 Fut: Future<Output = ()> + Send + 'static,
321 {
322 self.lifecycle_hooks
323 .on_shutdown
324 .push(Box::new(move || Box::pin(hook())));
325 self
326 }
327
328 /// Enable hot-reload mode for development
329 ///
330 /// When enabled:
331 /// - A dev-mode banner is printed at startup
332 /// - The `RUSTAPI_HOT_RELOAD` env var is set so that `cargo rustapi watch`
333 /// can detect the server is reload-aware
334 /// - If the server is **not** already running under the CLI watcher,
335 /// a helpful hint is printed suggesting `cargo rustapi run --watch`
336 ///
337 /// # Example
338 ///
339 /// ```rust,ignore
340 /// RustApi::new()
341 /// .hot_reload(true)
342 /// .route("/", get(hello))
343 /// .run("127.0.0.1:8080")
344 /// .await
345 /// ```
346 pub fn hot_reload(mut self, enabled: bool) -> Self {
347 self.hot_reload = enabled;
348 self
349 }
350
351 /// Get the inner router (for testing or advanced usage)
352 pub fn into_router(self) -> Router {
353 self.router
354 }
355
356 /// Get a reference to the inner router (for advanced usage, e.g. in-process MCP dispatch).
357 pub fn router(&self) -> &Router {
358 &self.router
359 }
360
361 /// Get the layer stack (for testing)
362 pub fn layers(&self) -> &LayerStack {
363 &self.layers
364 }
365
366 /// Get the interceptor chain (for testing)
367 pub fn interceptors(&self) -> &InterceptorChain {
368 &self.interceptors
369 }
370
371 /// Returns a dispatcher that can execute requests directly through this
372 /// app's router + layers + interceptors, with zero network overhead.
373 ///
374 /// This is intended for in-process protocol integrations (e.g. MCP tool calls
375 /// when running side-by-side with the main HTTP server).
376 pub fn request_dispatcher(&self) -> RequestDispatcher {
377 RequestDispatcher {
378 router: Arc::new(self.router.clone()),
379 layers: self.layers().clone(),
380 interceptors: self.interceptors().clone(),
381 }
382 }
383}
384
385impl Default for RustApi {
386 fn default() -> Self {
387 Self::new()
388 }
389}