rustapi_core/app.rs
1//! RustApi application builder
2
3use crate::error::Result;
4use crate::middleware::{BodyLimitLayer, LayerStack, MiddlewareLayer, DEFAULT_BODY_LIMIT};
5use crate::router::{MethodRouter, Router};
6use crate::server::Server;
7use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
8
9/// Main application builder for RustAPI
10///
11/// # Example
12///
13/// ```rust,ignore
14/// use rustapi_rs::prelude::*;
15///
16/// #[tokio::main]
17/// async fn main() -> Result<()> {
18/// RustApi::new()
19/// .state(AppState::new())
20/// .route("/", get(hello))
21/// .route("/users/{id}", get(get_user))
22/// .run("127.0.0.1:8080")
23/// .await
24/// }
25/// ```
26pub struct RustApi {
27 router: Router,
28 openapi_spec: rustapi_openapi::OpenApiSpec,
29 layers: LayerStack,
30 body_limit: Option<usize>,
31}
32
33impl RustApi {
34 /// Create a new RustAPI application
35 pub fn new() -> Self {
36 // Initialize tracing if not already done
37 let _ = tracing_subscriber::registry()
38 .with(
39 EnvFilter::try_from_default_env()
40 .unwrap_or_else(|_| EnvFilter::new("info,rustapi=debug")),
41 )
42 .with(tracing_subscriber::fmt::layer())
43 .try_init();
44
45 Self {
46 router: Router::new(),
47 openapi_spec: rustapi_openapi::OpenApiSpec::new("RustAPI Application", "1.0.0")
48 .register::<rustapi_openapi::ErrorSchema>()
49 .register::<rustapi_openapi::ValidationErrorSchema>()
50 .register::<rustapi_openapi::FieldErrorSchema>(),
51 layers: LayerStack::new(),
52 body_limit: Some(DEFAULT_BODY_LIMIT), // Default 1MB limit
53 }
54 }
55
56 /// Set the global body size limit for request bodies
57 ///
58 /// This protects against denial-of-service attacks via large payloads.
59 /// The default limit is 1MB (1024 * 1024 bytes).
60 ///
61 /// # Arguments
62 ///
63 /// * `limit` - Maximum body size in bytes
64 ///
65 /// # Example
66 ///
67 /// ```rust,ignore
68 /// use rustapi_rs::prelude::*;
69 ///
70 /// RustApi::new()
71 /// .body_limit(5 * 1024 * 1024) // 5MB limit
72 /// .route("/upload", post(upload_handler))
73 /// .run("127.0.0.1:8080")
74 /// .await
75 /// ```
76 pub fn body_limit(mut self, limit: usize) -> Self {
77 self.body_limit = Some(limit);
78 self
79 }
80
81 /// Disable the body size limit
82 ///
83 /// Warning: This removes protection against large payload attacks.
84 /// Only use this if you have other mechanisms to limit request sizes.
85 ///
86 /// # Example
87 ///
88 /// ```rust,ignore
89 /// RustApi::new()
90 /// .no_body_limit() // Disable body size limit
91 /// .route("/upload", post(upload_handler))
92 /// ```
93 pub fn no_body_limit(mut self) -> Self {
94 self.body_limit = None;
95 self
96 }
97
98 /// Add a middleware layer to the application
99 ///
100 /// Layers are executed in the order they are added (outermost first).
101 /// The first layer added will be the first to process the request and
102 /// the last to process the response.
103 ///
104 /// # Example
105 ///
106 /// ```rust,ignore
107 /// use rustapi_rs::prelude::*;
108 /// use rustapi_core::middleware::{RequestIdLayer, TracingLayer};
109 ///
110 /// RustApi::new()
111 /// .layer(RequestIdLayer::new()) // First to process request
112 /// .layer(TracingLayer::new()) // Second to process request
113 /// .route("/", get(handler))
114 /// .run("127.0.0.1:8080")
115 /// .await
116 /// ```
117 pub fn layer<L>(mut self, layer: L) -> Self
118 where
119 L: MiddlewareLayer,
120 {
121 self.layers.push(Box::new(layer));
122 self
123 }
124
125 /// Add application state
126 ///
127 /// State is shared across all handlers and can be extracted using `State<T>`.
128 ///
129 /// # Example
130 ///
131 /// ```rust,ignore
132 /// #[derive(Clone)]
133 /// struct AppState {
134 /// db: DbPool,
135 /// }
136 ///
137 /// RustApi::new()
138 /// .state(AppState::new())
139 /// ```
140 pub fn state<S>(self, _state: S) -> Self
141 where
142 S: Clone + Send + Sync + 'static,
143 {
144 // For now, state is handled by the router/handlers directly capturing it
145 // or through a middleware. The current router (matchit) implementation
146 // doesn't support state injection directly in the same way axum does.
147 // This is a placeholder for future state management.
148 self
149 }
150
151 /// Register an OpenAPI schema
152 ///
153 /// # Example
154 ///
155 /// ```rust,ignore
156 /// #[derive(Schema)]
157 /// struct User { ... }
158 ///
159 /// RustApi::new()
160 /// .register_schema::<User>()
161 /// ```
162 pub fn register_schema<T: for<'a> rustapi_openapi::Schema<'a>>(mut self) -> Self {
163 self.openapi_spec = self.openapi_spec.register::<T>();
164 self
165 }
166
167 /// Configure OpenAPI info (title, version, description)
168 pub fn openapi_info(mut self, title: &str, version: &str, description: Option<&str>) -> Self {
169 self.openapi_spec = rustapi_openapi::OpenApiSpec::new(title, version);
170 if let Some(desc) = description {
171 self.openapi_spec = self.openapi_spec.description(desc);
172 }
173 self
174 }
175
176 /// Add a route
177 ///
178 /// # Example
179 ///
180 /// ```rust,ignore
181 /// RustApi::new()
182 /// .route("/", get(index))
183 /// .route("/users", get(list_users).post(create_user))
184 /// .route("/users/{id}", get(get_user).delete(delete_user))
185 /// ```
186 pub fn route(mut self, path: &str, method_router: MethodRouter) -> Self {
187 // Register operations in OpenAPI spec
188 for (method, op) in &method_router.operations {
189 self.openapi_spec = self.openapi_spec.path(path, method.as_str(), op.clone());
190 }
191
192 self.router = self.router.route(path, method_router);
193 self
194 }
195
196 /// Mount a handler (convenience method)
197 ///
198 /// Alias for `.route(path, method_router)` for a single handler.
199 #[deprecated(note = "Use route() directly or mount_route() for macro-based routing")]
200 pub fn mount(self, path: &str, method_router: MethodRouter) -> Self {
201 self.route(path, method_router)
202 }
203
204 /// Mount a route created with `#[rustapi::get]`, `#[rustapi::post]`, etc.
205 ///
206 /// # Example
207 ///
208 /// ```rust,ignore
209 /// use rustapi_rs::prelude::*;
210 ///
211 /// #[rustapi::get("/users")]
212 /// async fn list_users() -> Json<Vec<User>> {
213 /// Json(vec![])
214 /// }
215 ///
216 /// RustApi::new()
217 /// .mount_route(route!(list_users))
218 /// .run("127.0.0.1:8080")
219 /// .await
220 /// ```
221 pub fn mount_route(mut self, route: crate::handler::Route) -> Self {
222 let method_enum = match route.method {
223 "GET" => http::Method::GET,
224 "POST" => http::Method::POST,
225 "PUT" => http::Method::PUT,
226 "DELETE" => http::Method::DELETE,
227 "PATCH" => http::Method::PATCH,
228 _ => http::Method::GET,
229 };
230
231 // Register operation in OpenAPI spec
232 self.openapi_spec = self
233 .openapi_spec
234 .path(route.path, route.method, route.operation);
235
236 self.route_with_method(route.path, method_enum, route.handler)
237 }
238
239 /// Helper to mount a single method handler
240 fn route_with_method(
241 self,
242 path: &str,
243 method: http::Method,
244 handler: crate::handler::BoxedHandler,
245 ) -> Self {
246 use crate::router::MethodRouter;
247 // use http::Method; // Removed
248
249 // This is simplified. In a real implementation we'd merge with existing router at this path
250 // For now we assume one handler per path or we simply allow overwriting for this MVP step
251 // (matchit router doesn't allow easy merging/updating existing entries without rebuilding)
252 //
253 // TOOD: Enhance Router to support method merging
254
255 let path = if !path.starts_with('/') {
256 format!("/{}", path)
257 } else {
258 path.to_string()
259 };
260
261 // Check if we already have this path?
262 // For MVP, valid assumption: user calls .route() or .mount() once per path-method-combo
263 // But we need to handle multiple methods on same path.
264 // Our Router wrapper currently just inserts.
265
266 // Since we can't easily query matchit, we'll just insert.
267 // Limitations: strictly sequential mounting for now.
268
269 let mut handlers = std::collections::HashMap::new();
270 handlers.insert(method, handler);
271
272 let method_router = MethodRouter::from_boxed(handlers);
273 self.route(&path, method_router)
274 }
275
276 /// Nest a router under a prefix
277 ///
278 /// # Example
279 ///
280 /// ```rust,ignore
281 /// let api_v1 = Router::new()
282 /// .route("/users", get(list_users));
283 ///
284 /// RustApi::new()
285 /// .nest("/api/v1", api_v1)
286 /// ```
287 pub fn nest(mut self, prefix: &str, router: Router) -> Self {
288 self.router = self.router.nest(prefix, router);
289 self
290 }
291
292 /// Enable Swagger UI documentation
293 ///
294 /// This adds two endpoints:
295 /// - `{path}` - Swagger UI interface
296 /// - `{path}/openapi.json` - OpenAPI JSON specification
297 ///
298 /// # Example
299 ///
300 /// ```text
301 /// RustApi::new()
302 /// .route("/users", get(list_users))
303 /// .docs("/docs") // Swagger UI at /docs, spec at /docs/openapi.json
304 /// .run("127.0.0.1:8080")
305 /// .await
306 /// ```
307 #[cfg(feature = "swagger-ui")]
308 pub fn docs(self, path: &str) -> Self {
309 let title = self.openapi_spec.info.title.clone();
310 let version = self.openapi_spec.info.version.clone();
311 let description = self.openapi_spec.info.description.clone();
312
313 self.docs_with_info(path, &title, &version, description.as_deref())
314 }
315
316 /// Enable Swagger UI documentation with custom API info
317 ///
318 /// # Example
319 ///
320 /// ```rust,ignore
321 /// RustApi::new()
322 /// .docs_with_info("/docs", "My API", "2.0.0", Some("API for managing users"))
323 /// ```
324 #[cfg(feature = "swagger-ui")]
325 pub fn docs_with_info(
326 mut self,
327 path: &str,
328 title: &str,
329 version: &str,
330 description: Option<&str>,
331 ) -> Self {
332 use crate::router::get;
333 // Update spec info
334 self.openapi_spec.info.title = title.to_string();
335 self.openapi_spec.info.version = version.to_string();
336 if let Some(desc) = description {
337 self.openapi_spec.info.description = Some(desc.to_string());
338 }
339
340 let path = path.trim_end_matches('/');
341 let openapi_path = format!("{}/openapi.json", path);
342
343 // Clone values for closures
344 let spec_json =
345 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
346 let openapi_url = openapi_path.clone();
347
348 // Add OpenAPI JSON endpoint
349 let spec_handler = move || {
350 let json = spec_json.clone();
351 async move {
352 http::Response::builder()
353 .status(http::StatusCode::OK)
354 .header(http::header::CONTENT_TYPE, "application/json")
355 .body(http_body_util::Full::new(bytes::Bytes::from(json)))
356 .unwrap()
357 }
358 };
359
360 // Add Swagger UI endpoint
361 let docs_handler = move || {
362 let url = openapi_url.clone();
363 async move { rustapi_openapi::swagger_ui_html(&url) }
364 };
365
366 self.route(&openapi_path, get(spec_handler))
367 .route(path, get(docs_handler))
368 }
369
370 /// Enable Swagger UI documentation with Basic Auth protection
371 ///
372 /// When username and password are provided, the docs endpoint will require
373 /// Basic Authentication. This is useful for protecting API documentation
374 /// in production environments.
375 ///
376 /// # Example
377 ///
378 /// ```rust,ignore
379 /// RustApi::new()
380 /// .route("/users", get(list_users))
381 /// .docs_with_auth("/docs", "admin", "secret123")
382 /// .run("127.0.0.1:8080")
383 /// .await
384 /// ```
385 #[cfg(feature = "swagger-ui")]
386 pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
387 let title = self.openapi_spec.info.title.clone();
388 let version = self.openapi_spec.info.version.clone();
389 let description = self.openapi_spec.info.description.clone();
390
391 self.docs_with_auth_and_info(
392 path,
393 username,
394 password,
395 &title,
396 &version,
397 description.as_deref(),
398 )
399 }
400
401 /// Enable Swagger UI documentation with Basic Auth and custom API info
402 ///
403 /// # Example
404 ///
405 /// ```rust,ignore
406 /// RustApi::new()
407 /// .docs_with_auth_and_info(
408 /// "/docs",
409 /// "admin",
410 /// "secret",
411 /// "My API",
412 /// "2.0.0",
413 /// Some("Protected API documentation")
414 /// )
415 /// ```
416 #[cfg(feature = "swagger-ui")]
417 pub fn docs_with_auth_and_info(
418 mut self,
419 path: &str,
420 username: &str,
421 password: &str,
422 title: &str,
423 version: &str,
424 description: Option<&str>,
425 ) -> Self {
426 use crate::router::MethodRouter;
427 use base64::{engine::general_purpose::STANDARD, Engine};
428 use std::collections::HashMap;
429
430 // Update spec info
431 self.openapi_spec.info.title = title.to_string();
432 self.openapi_spec.info.version = version.to_string();
433 if let Some(desc) = description {
434 self.openapi_spec.info.description = Some(desc.to_string());
435 }
436
437 let path = path.trim_end_matches('/');
438 let openapi_path = format!("{}/openapi.json", path);
439
440 // Create expected auth header value
441 let credentials = format!("{}:{}", username, password);
442 let encoded = STANDARD.encode(credentials.as_bytes());
443 let expected_auth = format!("Basic {}", encoded);
444
445 // Clone values for closures
446 let spec_json =
447 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
448 let openapi_url = openapi_path.clone();
449 let expected_auth_spec = expected_auth.clone();
450 let expected_auth_docs = expected_auth;
451
452 // Create spec handler with auth check
453 let spec_handler: crate::handler::BoxedHandler =
454 std::sync::Arc::new(move |req: crate::Request| {
455 let json = spec_json.clone();
456 let expected = expected_auth_spec.clone();
457 Box::pin(async move {
458 if !check_basic_auth(&req, &expected) {
459 return unauthorized_response();
460 }
461 http::Response::builder()
462 .status(http::StatusCode::OK)
463 .header(http::header::CONTENT_TYPE, "application/json")
464 .body(http_body_util::Full::new(bytes::Bytes::from(json)))
465 .unwrap()
466 })
467 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
468 });
469
470 // Create docs handler with auth check
471 let docs_handler: crate::handler::BoxedHandler =
472 std::sync::Arc::new(move |req: crate::Request| {
473 let url = openapi_url.clone();
474 let expected = expected_auth_docs.clone();
475 Box::pin(async move {
476 if !check_basic_auth(&req, &expected) {
477 return unauthorized_response();
478 }
479 rustapi_openapi::swagger_ui_html(&url)
480 })
481 as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
482 });
483
484 // Create method routers with boxed handlers
485 let mut spec_handlers = HashMap::new();
486 spec_handlers.insert(http::Method::GET, spec_handler);
487 let spec_router = MethodRouter::from_boxed(spec_handlers);
488
489 let mut docs_handlers = HashMap::new();
490 docs_handlers.insert(http::Method::GET, docs_handler);
491 let docs_router = MethodRouter::from_boxed(docs_handlers);
492
493 self.route(&openapi_path, spec_router)
494 .route(path, docs_router)
495 }
496
497 /// Run the server
498 ///
499 /// # Example
500 ///
501 /// ```rust,ignore
502 /// RustApi::new()
503 /// .route("/", get(hello))
504 /// .run("127.0.0.1:8080")
505 /// .await
506 /// ```
507 pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
508 // Apply body limit layer if configured (should be first in the chain)
509 if let Some(limit) = self.body_limit {
510 // Prepend body limit layer so it's the first to process requests
511 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
512 }
513
514 let server = Server::new(self.router, self.layers);
515 server.run(addr).await
516 }
517
518 /// Get the inner router (for testing or advanced usage)
519 pub fn into_router(self) -> Router {
520 self.router
521 }
522
523 /// Get the layer stack (for testing)
524 pub fn layers(&self) -> &LayerStack {
525 &self.layers
526 }
527}
528
529impl Default for RustApi {
530 fn default() -> Self {
531 Self::new()
532 }
533}
534
535/// Check Basic Auth header against expected credentials
536#[cfg(feature = "swagger-ui")]
537fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
538 req.headers()
539 .get(http::header::AUTHORIZATION)
540 .and_then(|v| v.to_str().ok())
541 .map(|auth| auth == expected)
542 .unwrap_or(false)
543}
544
545/// Create 401 Unauthorized response with WWW-Authenticate header
546#[cfg(feature = "swagger-ui")]
547fn unauthorized_response() -> crate::Response {
548 http::Response::builder()
549 .status(http::StatusCode::UNAUTHORIZED)
550 .header(
551 http::header::WWW_AUTHENTICATE,
552 "Basic realm=\"API Documentation\"",
553 )
554 .header(http::header::CONTENT_TYPE, "text/plain")
555 .body(http_body_util::Full::new(bytes::Bytes::from(
556 "Unauthorized",
557 )))
558 .unwrap()
559}