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 {
364 let html = rustapi_openapi::swagger_ui_html(&url);
365 html
366 }
367 };
368
369 self.route(&openapi_path, get(spec_handler))
370 .route(path, get(docs_handler))
371 }
372
373 /// Enable Swagger UI documentation with Basic Auth protection
374 ///
375 /// When username and password are provided, the docs endpoint will require
376 /// Basic Authentication. This is useful for protecting API documentation
377 /// in production environments.
378 ///
379 /// # Example
380 ///
381 /// ```rust,ignore
382 /// RustApi::new()
383 /// .route("/users", get(list_users))
384 /// .docs_with_auth("/docs", "admin", "secret123")
385 /// .run("127.0.0.1:8080")
386 /// .await
387 /// ```
388 #[cfg(feature = "swagger-ui")]
389 pub fn docs_with_auth(self, path: &str, username: &str, password: &str) -> Self {
390 let title = self.openapi_spec.info.title.clone();
391 let version = self.openapi_spec.info.version.clone();
392 let description = self.openapi_spec.info.description.clone();
393
394 self.docs_with_auth_and_info(
395 path,
396 username,
397 password,
398 &title,
399 &version,
400 description.as_deref(),
401 )
402 }
403
404 /// Enable Swagger UI documentation with Basic Auth and custom API info
405 ///
406 /// # Example
407 ///
408 /// ```rust,ignore
409 /// RustApi::new()
410 /// .docs_with_auth_and_info(
411 /// "/docs",
412 /// "admin",
413 /// "secret",
414 /// "My API",
415 /// "2.0.0",
416 /// Some("Protected API documentation")
417 /// )
418 /// ```
419 #[cfg(feature = "swagger-ui")]
420 pub fn docs_with_auth_and_info(
421 mut self,
422 path: &str,
423 username: &str,
424 password: &str,
425 title: &str,
426 version: &str,
427 description: Option<&str>,
428 ) -> Self {
429 use crate::router::MethodRouter;
430 use base64::{engine::general_purpose::STANDARD, Engine};
431 use std::collections::HashMap;
432
433 // Update spec info
434 self.openapi_spec.info.title = title.to_string();
435 self.openapi_spec.info.version = version.to_string();
436 if let Some(desc) = description {
437 self.openapi_spec.info.description = Some(desc.to_string());
438 }
439
440 let path = path.trim_end_matches('/');
441 let openapi_path = format!("{}/openapi.json", path);
442
443 // Create expected auth header value
444 let credentials = format!("{}:{}", username, password);
445 let encoded = STANDARD.encode(credentials.as_bytes());
446 let expected_auth = format!("Basic {}", encoded);
447
448 // Clone values for closures
449 let spec_json =
450 serde_json::to_string_pretty(&self.openapi_spec.to_json()).unwrap_or_default();
451 let openapi_url = openapi_path.clone();
452 let expected_auth_spec = expected_auth.clone();
453 let expected_auth_docs = expected_auth;
454
455 // Create spec handler with auth check
456 let spec_handler: crate::handler::BoxedHandler = std::sync::Arc::new(move |req: crate::Request| {
457 let json = spec_json.clone();
458 let expected = expected_auth_spec.clone();
459 Box::pin(async move {
460 if !check_basic_auth(&req, &expected) {
461 return unauthorized_response();
462 }
463 http::Response::builder()
464 .status(http::StatusCode::OK)
465 .header(http::header::CONTENT_TYPE, "application/json")
466 .body(http_body_util::Full::new(bytes::Bytes::from(json)))
467 .unwrap()
468 }) as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
469 });
470
471 // Create docs handler with auth check
472 let docs_handler: crate::handler::BoxedHandler = 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 }) as std::pin::Pin<Box<dyn std::future::Future<Output = crate::Response> + Send>>
481 });
482
483 // Create method routers with boxed handlers
484 let mut spec_handlers = HashMap::new();
485 spec_handlers.insert(http::Method::GET, spec_handler);
486 let spec_router = MethodRouter::from_boxed(spec_handlers);
487
488 let mut docs_handlers = HashMap::new();
489 docs_handlers.insert(http::Method::GET, docs_handler);
490 let docs_router = MethodRouter::from_boxed(docs_handlers);
491
492 self.route(&openapi_path, spec_router)
493 .route(path, docs_router)
494 }
495
496 /// Run the server
497 ///
498 /// # Example
499 ///
500 /// ```rust,ignore
501 /// RustApi::new()
502 /// .route("/", get(hello))
503 /// .run("127.0.0.1:8080")
504 /// .await
505 /// ```
506 pub async fn run(mut self, addr: &str) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
507 // Apply body limit layer if configured (should be first in the chain)
508 if let Some(limit) = self.body_limit {
509 // Prepend body limit layer so it's the first to process requests
510 self.layers.prepend(Box::new(BodyLimitLayer::new(limit)));
511 }
512
513 let server = Server::new(self.router, self.layers);
514 server.run(addr).await
515 }
516
517 /// Get the inner router (for testing or advanced usage)
518 pub fn into_router(self) -> Router {
519 self.router
520 }
521
522 /// Get the layer stack (for testing)
523 pub fn layers(&self) -> &LayerStack {
524 &self.layers
525 }
526}
527
528impl Default for RustApi {
529 fn default() -> Self {
530 Self::new()
531 }
532}
533
534/// Check Basic Auth header against expected credentials
535#[cfg(feature = "swagger-ui")]
536fn check_basic_auth(req: &crate::Request, expected: &str) -> bool {
537 req.headers()
538 .get(http::header::AUTHORIZATION)
539 .and_then(|v| v.to_str().ok())
540 .map(|auth| auth == expected)
541 .unwrap_or(false)
542}
543
544/// Create 401 Unauthorized response with WWW-Authenticate header
545#[cfg(feature = "swagger-ui")]
546fn unauthorized_response() -> crate::Response {
547 http::Response::builder()
548 .status(http::StatusCode::UNAUTHORIZED)
549 .header(http::header::WWW_AUTHENTICATE, "Basic realm=\"API Documentation\"")
550 .header(http::header::CONTENT_TYPE, "text/plain")
551 .body(http_body_util::Full::new(bytes::Bytes::from("Unauthorized")))
552 .unwrap()
553}