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