Skip to main content

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}