1pub mod fast_router;
7pub mod grpc_routing;
8pub mod handler;
9pub mod lifecycle_execution;
10pub mod request_extraction;
11
12use crate::handler_trait::{Handler, HandlerResult, RequestData};
13use crate::{CorsConfig, ServerConfig};
14use axum::Router as AxumRouter;
15use axum::body::Body;
16use axum::extract::{DefaultBodyLimit, Path};
17use axum::http::StatusCode;
18use axum::routing::{MethodRouter, get, post};
19use spikard_core::type_hints;
20use std::collections::HashMap;
21use std::net::SocketAddr;
22use std::sync::Arc;
23use std::time::Duration;
24use tokio::net::TcpListener;
25use tower_governor::governor::GovernorConfigBuilder;
26use tower_governor::key_extractor::GlobalKeyExtractor;
27use tower_http::compression::CompressionLayer;
28use tower_http::compression::predicate::{NotForContentType, Predicate, SizeAbove};
29use tower_http::request_id::{MakeRequestId, PropagateRequestIdLayer, RequestId, SetRequestIdLayer};
30use tower_http::sensitive_headers::SetSensitiveRequestHeadersLayer;
31use tower_http::services::ServeDir;
32use tower_http::set_header::SetResponseHeaderLayer;
33use tower_http::timeout::TimeoutLayer;
34use tower_http::trace::TraceLayer;
35use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
36use uuid::Uuid;
37
38type RouteHandlerPair = (crate::Route, Arc<dyn Handler>);
40
41#[derive(Clone)]
42struct GrpcMiddlewareState {
43 registry: Arc<crate::grpc::GrpcRegistry>,
44 config: crate::grpc::GrpcConfig,
45}
46
47#[cfg(feature = "di")]
51fn extract_handler_dependencies(route: &crate::Route) -> Vec<String> {
52 route.handler_dependencies.clone()
53}
54
55fn method_expects_body(method: &crate::Method) -> bool {
57 matches!(method, crate::Method::Post | crate::Method::Put | crate::Method::Patch)
58}
59
60fn looks_like_json(body: &str) -> bool {
61 let trimmed = body.trim_start();
62 trimmed.starts_with('{') || trimmed.starts_with('[')
63}
64
65fn route_to_metadata(route: &crate::Route) -> crate::RouteMetadata {
66 #[cfg(feature = "di")]
67 {
68 crate::RouteMetadata {
69 method: route.method.to_string(),
70 path: route.path.clone(),
71 handler_name: route.handler_name.clone(),
72 request_schema: route
73 .request_validator
74 .as_ref()
75 .map(|validator| validator.schema().clone()),
76 response_schema: route
77 .response_validator
78 .as_ref()
79 .map(|validator| validator.schema().clone()),
80 parameter_schema: route
81 .parameter_validator
82 .as_ref()
83 .map(|validator| validator.schema().clone()),
84 file_params: route.file_params.clone(),
85 is_async: route.is_async,
86 cors: route.cors.clone(),
87 body_param_name: route.expects_json_body.then(|| "body".to_string()),
88 handler_dependencies: Some(route.handler_dependencies.clone()),
89 jsonrpc_method: route
90 .jsonrpc_method
91 .as_ref()
92 .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
93 static_response: None,
94 }
95 }
96 #[cfg(not(feature = "di"))]
97 {
98 crate::RouteMetadata {
99 method: route.method.to_string(),
100 path: route.path.clone(),
101 handler_name: route.handler_name.clone(),
102 request_schema: route
103 .request_validator
104 .as_ref()
105 .map(|validator| validator.schema().clone()),
106 response_schema: route
107 .response_validator
108 .as_ref()
109 .map(|validator| validator.schema().clone()),
110 parameter_schema: route
111 .parameter_validator
112 .as_ref()
113 .map(|validator| validator.schema().clone()),
114 file_params: route.file_params.clone(),
115 is_async: route.is_async,
116 cors: route.cors.clone(),
117 body_param_name: route.expects_json_body.then(|| "body".to_string()),
118 jsonrpc_method: route
119 .jsonrpc_method
120 .as_ref()
121 .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
122 static_response: None,
123 }
124 }
125}
126
127fn error_to_response(status: StatusCode, body: String) -> axum::response::Response {
128 let content_type = if looks_like_json(&body) {
129 "application/json"
130 } else {
131 "text/plain; charset=utf-8"
132 };
133
134 axum::response::Response::builder()
135 .status(status)
136 .header(axum::http::header::CONTENT_TYPE, content_type)
137 .body(Body::from(body))
138 .unwrap_or_else(|_| {
139 axum::response::Response::builder()
140 .status(StatusCode::INTERNAL_SERVER_ERROR)
141 .header(axum::http::header::CONTENT_TYPE, "text/plain; charset=utf-8")
142 .body(Body::from("Failed to build error response"))
143 .unwrap()
144 })
145}
146
147fn handler_result_to_response(result: HandlerResult) -> axum::response::Response {
148 match result {
149 Ok(response) => response,
150 Err((status, body)) => error_to_response(status, body),
151 }
152}
153
154async fn grpc_routing_middleware(
155 axum::extract::State(state): axum::extract::State<GrpcMiddlewareState>,
156 request: axum::extract::Request,
157 next: axum::middleware::Next,
158) -> axum::response::Response {
159 if grpc_routing::is_grpc_request(&request) {
160 return match grpc_routing::route_grpc_request(Arc::clone(&state.registry), &state.config, request).await {
161 Ok(response) => response,
162 Err((status, body)) => error_to_response(status, body),
163 };
164 }
165
166 next.run(request).await
167}
168
169#[inline]
170async fn call_with_optional_hooks(
171 req: axum::http::Request<Body>,
172 request_data: RequestData,
173 handler: Arc<dyn Handler>,
174 hooks: Option<Arc<crate::LifecycleHooks>>,
175) -> HandlerResult {
176 let request_data = if let Some(claims) = req.extensions().get::<crate::auth::Claims>() {
177 let mut request_data = request_data;
178 if let Ok(serialized_claims) = serde_json::to_string(claims) {
179 let mut headers = (*request_data.headers).clone();
180 headers.insert(crate::auth::INTERNAL_JWT_CLAIMS_HEADER.to_string(), serialized_claims);
181 request_data.headers = Arc::new(headers);
182 }
183 request_data
184 } else {
185 request_data
186 };
187
188 if hooks.as_ref().is_some_and(|h| !h.is_empty()) {
189 lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler, hooks).await
190 } else {
191 handler.call(req, request_data).await
192 }
193}
194
195fn create_method_router(
198 method: crate::Method,
199 has_path_params: bool,
200 handler: Arc<dyn Handler>,
201 hooks: Option<Arc<crate::LifecycleHooks>>,
202 include_raw_query_params: bool,
203 include_query_params_json: bool,
204) -> axum::routing::MethodRouter {
205 let expects_body = method_expects_body(&method);
206 let include_headers = handler.wants_headers();
207 let include_cookies = handler.wants_cookies();
208 let without_body_options = request_extraction::WithoutBodyExtractionOptions {
209 include_raw_query_params,
210 include_query_params_json,
211 include_headers,
212 include_cookies,
213 };
214
215 if expects_body {
216 if has_path_params {
217 let handler_clone = handler.clone();
218 let hooks_clone = hooks.clone();
219 match method {
220 crate::Method::Post => axum::routing::post(
221 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
222 let (parts, body) = req.into_parts();
223 let request_data = match request_extraction::create_request_data_with_body(
224 &parts,
225 path_params.0,
226 body,
227 include_raw_query_params,
228 include_query_params_json,
229 include_headers,
230 include_cookies,
231 )
232 .await
233 {
234 Ok(data) => data,
235 Err((status, body)) => return error_to_response(status, body),
236 };
237 let req = axum::extract::Request::from_parts(parts, Body::empty());
238 handler_result_to_response(
239 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
240 )
241 },
242 ),
243 crate::Method::Put => axum::routing::put(
244 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
245 let (parts, body) = req.into_parts();
246 let request_data = match request_extraction::create_request_data_with_body(
247 &parts,
248 path_params.0,
249 body,
250 include_raw_query_params,
251 include_query_params_json,
252 include_headers,
253 include_cookies,
254 )
255 .await
256 {
257 Ok(data) => data,
258 Err((status, body)) => return error_to_response(status, body),
259 };
260 let req = axum::extract::Request::from_parts(parts, Body::empty());
261 handler_result_to_response(
262 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
263 )
264 },
265 ),
266 crate::Method::Patch => axum::routing::patch(
267 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
268 let (parts, body) = req.into_parts();
269 let request_data = match request_extraction::create_request_data_with_body(
270 &parts,
271 path_params.0,
272 body,
273 include_raw_query_params,
274 include_query_params_json,
275 include_headers,
276 include_cookies,
277 )
278 .await
279 {
280 Ok(data) => data,
281 Err((status, body)) => return error_to_response(status, body),
282 };
283 let req = axum::extract::Request::from_parts(parts, Body::empty());
284 handler_result_to_response(
285 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
286 )
287 },
288 ),
289 crate::Method::Get
290 | crate::Method::Delete
291 | crate::Method::Head
292 | crate::Method::Options
293 | crate::Method::Trace => MethodRouter::new(),
294 }
295 } else {
296 let handler_clone = handler.clone();
297 let hooks_clone = hooks.clone();
298 match method {
299 crate::Method::Post => axum::routing::post(move |req: axum::extract::Request| async move {
300 let (parts, body) = req.into_parts();
301 let request_data = match request_extraction::create_request_data_with_body(
302 &parts,
303 HashMap::new(),
304 body,
305 include_raw_query_params,
306 include_query_params_json,
307 include_headers,
308 include_cookies,
309 )
310 .await
311 {
312 Ok(data) => data,
313 Err((status, body)) => return error_to_response(status, body),
314 };
315 let req = axum::extract::Request::from_parts(parts, Body::empty());
316 handler_result_to_response(
317 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
318 )
319 }),
320 crate::Method::Put => axum::routing::put(move |req: axum::extract::Request| async move {
321 let (parts, body) = req.into_parts();
322 let request_data = match request_extraction::create_request_data_with_body(
323 &parts,
324 HashMap::new(),
325 body,
326 include_raw_query_params,
327 include_query_params_json,
328 include_headers,
329 include_cookies,
330 )
331 .await
332 {
333 Ok(data) => data,
334 Err((status, body)) => return error_to_response(status, body),
335 };
336 let req = axum::extract::Request::from_parts(parts, Body::empty());
337 handler_result_to_response(
338 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
339 )
340 }),
341 crate::Method::Patch => axum::routing::patch(move |req: axum::extract::Request| async move {
342 let (parts, body) = req.into_parts();
343 let request_data = match request_extraction::create_request_data_with_body(
344 &parts,
345 HashMap::new(),
346 body,
347 include_raw_query_params,
348 include_query_params_json,
349 include_headers,
350 include_cookies,
351 )
352 .await
353 {
354 Ok(data) => data,
355 Err((status, body)) => return error_to_response(status, body),
356 };
357 let req = axum::extract::Request::from_parts(parts, Body::empty());
358 handler_result_to_response(
359 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
360 )
361 }),
362 crate::Method::Get
363 | crate::Method::Delete
364 | crate::Method::Head
365 | crate::Method::Options
366 | crate::Method::Trace => MethodRouter::new(),
367 }
368 }
369 } else if has_path_params {
370 let handler_clone = handler.clone();
371 let hooks_clone = hooks.clone();
372 match method {
373 crate::Method::Get => axum::routing::get(
374 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
375 let request_data = request_extraction::create_request_data_without_body(
376 req.uri(),
377 req.method(),
378 req.headers(),
379 path_params.0,
380 without_body_options,
381 );
382 handler_result_to_response(
383 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
384 )
385 },
386 ),
387 crate::Method::Delete => axum::routing::delete(
388 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
389 let request_data = request_extraction::create_request_data_without_body(
390 req.uri(),
391 req.method(),
392 req.headers(),
393 path_params.0,
394 without_body_options,
395 );
396 handler_result_to_response(
397 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
398 )
399 },
400 ),
401 crate::Method::Head => axum::routing::head(
402 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
403 let request_data = request_extraction::create_request_data_without_body(
404 req.uri(),
405 req.method(),
406 req.headers(),
407 path_params.0,
408 without_body_options,
409 );
410 handler_result_to_response(
411 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
412 )
413 },
414 ),
415 crate::Method::Trace => axum::routing::trace(
416 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
417 let request_data = request_extraction::create_request_data_without_body(
418 req.uri(),
419 req.method(),
420 req.headers(),
421 path_params.0,
422 without_body_options,
423 );
424 handler_result_to_response(
425 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
426 )
427 },
428 ),
429 crate::Method::Options => axum::routing::options(
430 move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
431 let request_data = request_extraction::create_request_data_without_body(
432 req.uri(),
433 req.method(),
434 req.headers(),
435 path_params.0,
436 without_body_options,
437 );
438 handler_result_to_response(
439 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
440 )
441 },
442 ),
443 crate::Method::Post | crate::Method::Put | crate::Method::Patch => MethodRouter::new(),
444 }
445 } else {
446 let handler_clone = handler.clone();
447 let hooks_clone = hooks.clone();
448 match method {
449 crate::Method::Get => axum::routing::get(move |req: axum::extract::Request| async move {
450 let request_data = request_extraction::create_request_data_without_body(
451 req.uri(),
452 req.method(),
453 req.headers(),
454 HashMap::new(),
455 without_body_options,
456 );
457 handler_result_to_response(
458 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
459 )
460 }),
461 crate::Method::Delete => axum::routing::delete(move |req: axum::extract::Request| async move {
462 let request_data = request_extraction::create_request_data_without_body(
463 req.uri(),
464 req.method(),
465 req.headers(),
466 HashMap::new(),
467 without_body_options,
468 );
469 handler_result_to_response(
470 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
471 )
472 }),
473 crate::Method::Head => axum::routing::head(move |req: axum::extract::Request| async move {
474 let request_data = request_extraction::create_request_data_without_body(
475 req.uri(),
476 req.method(),
477 req.headers(),
478 HashMap::new(),
479 without_body_options,
480 );
481 handler_result_to_response(
482 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
483 )
484 }),
485 crate::Method::Trace => axum::routing::trace(move |req: axum::extract::Request| async move {
486 let request_data = request_extraction::create_request_data_without_body(
487 req.uri(),
488 req.method(),
489 req.headers(),
490 HashMap::new(),
491 without_body_options,
492 );
493 handler_result_to_response(
494 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
495 )
496 }),
497 crate::Method::Options => axum::routing::options(move |req: axum::extract::Request| async move {
498 let request_data = request_extraction::create_request_data_without_body(
499 req.uri(),
500 req.method(),
501 req.headers(),
502 HashMap::new(),
503 without_body_options,
504 );
505 handler_result_to_response(
506 call_with_optional_hooks(req, request_data, handler_clone, hooks_clone).await,
507 )
508 }),
509 crate::Method::Post | crate::Method::Put | crate::Method::Patch => MethodRouter::new(),
510 }
511 }
512}
513
514#[derive(Clone, Default)]
516struct MakeRequestUuid;
517
518impl MakeRequestId for MakeRequestUuid {
519 fn make_request_id<B>(&mut self, _request: &axum::http::Request<B>) -> Option<RequestId> {
520 let id = Uuid::new_v4().to_string().parse().ok()?;
521 Some(RequestId::new(id))
522 }
523}
524
525#[cfg(not(tarpaulin_include))]
529async fn shutdown_signal() {
530 let ctrl_c = async {
531 tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
532 };
533
534 #[cfg(unix)]
535 let terminate = async {
536 tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
537 .expect("failed to install signal handler")
538 .recv()
539 .await;
540 };
541
542 #[cfg(not(unix))]
543 let terminate = std::future::pending::<()>();
544
545 tokio::select! {
546 _ = ctrl_c => {
547 tracing::info!("Received SIGINT (Ctrl+C), starting graceful shutdown");
548 },
549 _ = terminate => {
550 tracing::info!("Received SIGTERM, starting graceful shutdown");
551 },
552 }
553}
554
555#[cfg(not(feature = "di"))]
557pub fn build_router_with_handlers(
558 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
559 hooks: Option<Arc<crate::LifecycleHooks>>,
560) -> Result<AxumRouter, String> {
561 build_router_with_handlers_inner(routes, hooks, None, true)
562}
563
564#[cfg(feature = "di")]
566pub fn build_router_with_handlers(
567 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
568 hooks: Option<Arc<crate::LifecycleHooks>>,
569 di_container: Option<Arc<spikard_core::di::DependencyContainer>>,
570) -> Result<AxumRouter, String> {
571 build_router_with_handlers_inner(routes, hooks, di_container, true)
572}
573
574fn build_router_with_handlers_inner(
575 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
576 hooks: Option<Arc<crate::LifecycleHooks>>,
577 #[cfg(feature = "di")] di_container: Option<Arc<spikard_core::di::DependencyContainer>>,
578 #[cfg(not(feature = "di"))] _di_container: Option<()>,
579 enable_http_trace: bool,
580) -> Result<AxumRouter, String> {
581 let mut app = AxumRouter::new();
582 let mut fast_router = fast_router::FastRouter::new();
583
584 let mut routes_by_path: HashMap<String, Vec<RouteHandlerPair>> = HashMap::new();
585 for (route, handler) in routes {
586 routes_by_path
587 .entry(route.path.clone())
588 .or_default()
589 .push((route, handler));
590 }
591
592 let mut sorted_paths: Vec<String> = routes_by_path.keys().cloned().collect();
593 sorted_paths.sort();
594
595 for path in sorted_paths {
596 let route_handlers = routes_by_path
597 .remove(&path)
598 .ok_or_else(|| format!("Missing handlers for path '{}'", path))?;
599
600 type RouteEntry = (crate::Route, Arc<dyn Handler>, Option<crate::StaticResponse>);
601 let mut handlers_by_method: HashMap<crate::Method, RouteEntry> = HashMap::new();
602 for (route, handler) in route_handlers {
603 #[cfg(feature = "di")]
604 let handler = if let Some(ref container) = di_container {
605 let required_deps = extract_handler_dependencies(&route);
606 if !required_deps.is_empty() {
607 Arc::new(crate::di_handler::DependencyInjectingHandler::new(
608 handler,
609 Arc::clone(container),
610 required_deps,
611 )) as Arc<dyn Handler>
612 } else {
613 handler
614 }
615 } else {
616 handler
617 };
618
619 let static_resp = handler.static_response();
622 let validating_handler = Arc::new(handler::ValidatingHandler::new(handler, &route));
623 handlers_by_method.insert(route.method.clone(), (route, validating_handler, static_resp));
624 }
625
626 let cors_config: Option<CorsConfig> = handlers_by_method
627 .values()
628 .find_map(|(route, _, _)| route.cors.as_ref())
629 .cloned();
630
631 let has_options_handler = handlers_by_method.keys().any(|m| m.as_str() == "OPTIONS");
632
633 let mut combined_router: Option<MethodRouter> = None;
634 let has_path_params = path.contains('{');
635
636 for (_method, (route, handler, static_resp_opt)) in handlers_by_method {
637 let method = route.method.clone();
638
639 if let Some(static_resp) = static_resp_opt {
652 let resp_status = static_resp.status;
653
654 if !has_path_params {
655 let axum_path_for_fast = spikard_core::type_hints::strip_type_hints(&path);
656 let http_method: axum::http::Method = route.method.as_str().parse().map_err(|_| {
657 format!(
658 "Invalid HTTP method '{}' for static route {}",
659 route.method.as_str(),
660 path
661 )
662 })?;
663 fast_router.insert(http_method, &axum_path_for_fast, &static_resp);
664 }
665
666 let static_handler = move || {
669 let resp = static_resp.to_response();
670 async move { resp }
671 };
672
673 let method_router: MethodRouter = match method {
674 crate::Method::Get => axum::routing::get(static_handler),
675 crate::Method::Post => axum::routing::post(static_handler),
676 crate::Method::Put => axum::routing::put(static_handler),
677 crate::Method::Patch => axum::routing::patch(static_handler),
678 crate::Method::Delete => axum::routing::delete(static_handler),
679 crate::Method::Head => axum::routing::head(static_handler),
680 crate::Method::Options => axum::routing::options(static_handler),
681 crate::Method::Trace => axum::routing::trace(static_handler),
682 };
683
684 combined_router = Some(match combined_router {
685 None => method_router,
686 Some(existing) => existing.merge(method_router),
687 });
688
689 tracing::info!(
690 "Registered static route: {} {} (status {})",
691 route.method.as_str(),
692 path,
693 resp_status,
694 );
695 continue;
696 }
697
698 let method_router: MethodRouter = match method {
699 crate::Method::Options => {
700 if let Some(ref cors_cfg) = route.cors {
701 let cors_config = cors_cfg.clone();
702 axum::routing::options(move |req: axum::extract::Request| async move {
703 crate::cors::handle_preflight(req.headers(), &cors_config).map_err(|e| *e)
704 })
705 } else {
706 let include_raw_query_params = route.parameter_validator.is_some();
707 let include_query_params_json = !handler.prefers_parameter_extraction();
708 create_method_router(
709 method,
710 has_path_params,
711 handler,
712 hooks.clone(),
713 include_raw_query_params,
714 include_query_params_json,
715 )
716 }
717 }
718 method => {
719 let include_raw_query_params = route.parameter_validator.is_some();
720 let include_query_params_json = !handler.prefers_parameter_extraction();
721 create_method_router(
722 method,
723 has_path_params,
724 handler,
725 hooks.clone(),
726 include_raw_query_params,
727 include_query_params_json,
728 )
729 }
730 };
731
732 let method_router = if matches!(
737 route.method,
738 crate::Method::Post | crate::Method::Put | crate::Method::Patch
739 ) && (route.expects_json_body || route.file_params.is_some())
740 {
741 method_router.layer(axum::middleware::from_fn_with_state(
742 crate::middleware::RouteInfo {
743 expects_json_body: route.expects_json_body,
744 },
745 crate::middleware::validate_content_type_middleware,
746 ))
747 } else {
748 method_router
749 };
750
751 combined_router = Some(match combined_router {
752 None => method_router,
753 Some(existing) => existing.merge(method_router),
754 });
755
756 tracing::info!("Registered route: {} {}", route.method.as_str(), path);
757 }
758
759 if let Some(ref cors_cfg) = cors_config
760 && !has_options_handler
761 {
762 let cors_config_clone: CorsConfig = cors_cfg.clone();
763 let options_router = axum::routing::options(move |req: axum::extract::Request| async move {
764 crate::cors::handle_preflight(req.headers(), &cors_config_clone).map_err(|e| *e)
765 });
766
767 combined_router = Some(match combined_router {
768 None => options_router,
769 Some(existing) => existing.merge(options_router),
770 });
771
772 tracing::info!("Auto-generated OPTIONS handler for CORS preflight: {}", path);
773 }
774
775 if let Some(router) = combined_router {
776 let mut axum_path = type_hints::strip_type_hints(&path);
777 if !axum_path.starts_with('/') {
778 axum_path = format!("/{}", axum_path);
779 }
780 app = app.route(&axum_path, router);
781 }
782 }
783
784 if enable_http_trace {
785 app = app.layer(TraceLayer::new_for_http());
786 }
787
788 if fast_router.has_routes() {
791 let fast_router = Arc::new(fast_router);
792 app = app.layer(axum::middleware::from_fn(
793 move |req: axum::extract::Request, next: axum::middleware::Next| {
794 let fast_router = Arc::clone(&fast_router);
795 async move {
796 if let Some(resp) = fast_router.lookup(req.method(), req.uri().path()) {
797 return resp;
798 }
799 next.run(req).await
800 }
801 },
802 ));
803 }
804
805 Ok(app)
806}
807
808pub fn build_router_with_handlers_and_config(
810 routes: Vec<RouteHandlerPair>,
811 config: ServerConfig,
812 route_metadata: Vec<crate::RouteMetadata>,
813) -> Result<AxumRouter, String> {
814 build_router_with_handlers_and_config_and_grpc(routes, config, route_metadata, None)
815}
816
817pub fn build_router_with_handlers_and_config_and_grpc(
819 routes: Vec<RouteHandlerPair>,
820 config: ServerConfig,
821 route_metadata: Vec<crate::RouteMetadata>,
822 grpc_registry: Option<Arc<crate::grpc::GrpcRegistry>>,
823) -> Result<AxumRouter, String> {
824 #[cfg(all(feature = "di", debug_assertions))]
825 if let Some(di_container) = config.di_container.as_ref() {
826 eprintln!(
827 "[spikard-di] build_router: di_container has keys: {:?}",
828 di_container.keys()
829 );
830 } else {
831 eprintln!("[spikard-di] build_router: di_container is None");
832 }
833 let hooks = config.lifecycle_hooks.clone();
834
835 let jsonrpc_registry = if let Some(ref jsonrpc_config) = config.jsonrpc {
836 if jsonrpc_config.enabled {
837 let registry = Arc::new(crate::jsonrpc::JsonRpcMethodRegistry::new());
838
839 for (route, handler) in &routes {
840 if let Some(ref jsonrpc_info) = route.jsonrpc_method {
841 let method_name = jsonrpc_info.method_name.clone();
842
843 let metadata = crate::jsonrpc::MethodMetadata::new(&method_name)
844 .with_params_schema(jsonrpc_info.params_schema.clone().unwrap_or(serde_json::json!({})))
845 .with_result_schema(jsonrpc_info.result_schema.clone().unwrap_or(serde_json::json!({})));
846
847 let metadata = if let Some(ref description) = jsonrpc_info.description {
848 metadata.with_description(description.clone())
849 } else {
850 metadata
851 };
852
853 let metadata = if jsonrpc_info.deprecated {
854 metadata.mark_deprecated()
855 } else {
856 metadata
857 };
858
859 let mut metadata = metadata;
860 for tag in &jsonrpc_info.tags {
861 metadata = metadata.with_tag(tag.clone());
862 }
863
864 if let Err(e) = registry.register(&method_name, Arc::clone(handler), metadata) {
865 tracing::warn!(
866 "Failed to register JSON-RPC method '{}' for route {}: {}",
867 method_name,
868 route.path,
869 e
870 );
871 } else {
872 tracing::debug!(
873 "Registered JSON-RPC method '{}' for route {} {} (handler: {})",
874 method_name,
875 route.method,
876 route.path,
877 route.handler_name
878 );
879 }
880 }
881 }
882
883 Some(registry)
884 } else {
885 None
886 }
887 } else {
888 None
889 };
890
891 #[cfg(feature = "di")]
892 let mut app =
893 build_router_with_handlers_inner(routes, hooks, config.di_container.clone(), config.enable_http_trace)?;
894 #[cfg(not(feature = "di"))]
895 let mut app = build_router_with_handlers_inner(routes, hooks, None, config.enable_http_trace)?;
896
897 if let (Some(grpc_config), Some(registry)) = (config.grpc.clone(), grpc_registry)
898 && !registry.is_empty()
899 {
900 let state = GrpcMiddlewareState {
901 registry,
902 config: grpc_config,
903 };
904 app = app.layer(axum::middleware::from_fn_with_state(state, grpc_routing_middleware));
905 }
906
907 if config.jwt_auth.is_some() || config.api_key_auth.is_some() {
911 app = app.layer(SetSensitiveRequestHeadersLayer::new([
912 axum::http::header::AUTHORIZATION,
913 axum::http::header::COOKIE,
914 ]));
915 }
916
917 if let Some(ref compression) = config.compression {
918 let mut compression_layer = CompressionLayer::new();
919 if !compression.gzip {
920 compression_layer = compression_layer.gzip(false);
921 }
922 if !compression.brotli {
923 compression_layer = compression_layer.br(false);
924 }
925
926 let min_threshold = compression.min_size.min(u16::MAX as usize) as u16;
927 let predicate = SizeAbove::new(min_threshold)
928 .and(NotForContentType::GRPC)
929 .and(NotForContentType::IMAGES)
930 .and(NotForContentType::SSE);
931 let compression_layer = compression_layer.compress_when(predicate);
932
933 app = app.layer(compression_layer);
934 }
935
936 if let Some(ref rate_limit) = config.rate_limit {
937 if rate_limit.ip_based {
938 let governor_conf = Arc::new(
939 GovernorConfigBuilder::default()
940 .per_second(rate_limit.per_second)
941 .burst_size(rate_limit.burst)
942 .finish()
943 .ok_or_else(|| "Failed to create rate limiter".to_string())?,
944 );
945 app = app.layer(tower_governor::GovernorLayer::new(governor_conf));
946 } else {
947 let governor_conf = Arc::new(
948 GovernorConfigBuilder::default()
949 .per_second(rate_limit.per_second)
950 .burst_size(rate_limit.burst)
951 .key_extractor(GlobalKeyExtractor)
952 .finish()
953 .ok_or_else(|| "Failed to create rate limiter".to_string())?,
954 );
955 app = app.layer(tower_governor::GovernorLayer::new(governor_conf));
956 }
957 }
958
959 if let Some(ref jwt_config) = config.jwt_auth {
960 let jwt_config_clone = jwt_config.clone();
961 app = app.layer(axum::middleware::from_fn(move |headers, req, next| {
962 crate::auth::jwt_auth_middleware(jwt_config_clone.clone(), headers, req, next)
963 }));
964 }
965
966 if let Some(ref api_key_config) = config.api_key_auth {
967 let api_key_config_clone = api_key_config.clone();
968 app = app.layer(axum::middleware::from_fn(move |headers, req, next| {
969 crate::auth::api_key_auth_middleware(api_key_config_clone.clone(), headers, req, next)
970 }));
971 }
972
973 if let Some(timeout_secs) = config.request_timeout {
974 app = app.layer(TimeoutLayer::with_status_code(
975 StatusCode::REQUEST_TIMEOUT,
976 Duration::from_secs(timeout_secs),
977 ));
978 }
979
980 if config.enable_request_id {
981 app = app
982 .layer(PropagateRequestIdLayer::x_request_id())
983 .layer(SetRequestIdLayer::x_request_id(MakeRequestUuid));
984 }
985
986 if let Some(max_size) = config.max_body_size {
990 app = app.layer(DefaultBodyLimit::max(max_size));
991 }
992
993 for static_config in &config.static_files {
994 let mut serve_dir = ServeDir::new(&static_config.directory);
995 if static_config.index_file {
996 serve_dir = serve_dir.append_index_html_on_directories(true);
997 }
998
999 let mut static_router = AxumRouter::new().fallback_service(serve_dir);
1000 if let Some(ref cache_control) = static_config.cache_control {
1001 let header_value = axum::http::HeaderValue::from_str(cache_control)
1002 .map_err(|e| format!("Invalid cache-control header: {}", e))?;
1003 static_router = static_router.layer(SetResponseHeaderLayer::overriding(
1004 axum::http::header::CACHE_CONTROL,
1005 header_value,
1006 ));
1007 }
1008
1009 app = app.nest_service(&static_config.route_prefix, static_router);
1010
1011 tracing::info!(
1012 "Serving static files from '{}' at '{}'",
1013 static_config.directory,
1014 static_config.route_prefix
1015 );
1016 }
1017
1018 if let Some(ref openapi_config) = config.openapi
1019 && openapi_config.enabled
1020 {
1021 use axum::response::{Html, Json};
1022
1023 let schema_registry = crate::SchemaRegistry::new();
1024 let openapi_spec =
1025 crate::openapi::generate_openapi_spec(&route_metadata, openapi_config, &schema_registry, Some(&config))
1026 .map_err(|e| format!("Failed to generate OpenAPI spec: {}", e))?;
1027
1028 let spec_json =
1029 serde_json::to_string(&openapi_spec).map_err(|e| format!("Failed to serialize OpenAPI spec: {}", e))?;
1030 let spec_value = serde_json::from_str::<serde_json::Value>(&spec_json)
1031 .map_err(|e| format!("Failed to parse OpenAPI spec: {}", e))?;
1032
1033 let openapi_json_path = openapi_config.openapi_json_path.clone();
1034 app = app.route(&openapi_json_path, get(move || async move { Json(spec_value) }));
1035
1036 let swagger_html = format!(
1037 r#"<!DOCTYPE html>
1038<html>
1039<head>
1040 <title>Swagger UI</title>
1041 <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist/swagger-ui.css">
1042</head>
1043<body>
1044 <div id="swagger-ui"></div>
1045 <script src="https://unpkg.com/swagger-ui-dist/swagger-ui-bundle.js"></script>
1046 <script>
1047 SwaggerUIBundle({{
1048 url: '{}',
1049 dom_id: '#swagger-ui',
1050 }});
1051 </script>
1052</body>
1053</html>"#,
1054 openapi_json_path
1055 );
1056 let swagger_ui_path = openapi_config.swagger_ui_path.clone();
1057 app = app.route(&swagger_ui_path, get(move || async move { Html(swagger_html) }));
1058
1059 let redoc_html = format!(
1060 r#"<!DOCTYPE html>
1061<html>
1062<head>
1063 <title>Redoc</title>
1064</head>
1065<body>
1066 <redoc spec-url='{}'></redoc>
1067 <script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
1068</body>
1069</html>"#,
1070 openapi_json_path
1071 );
1072 let redoc_path = openapi_config.redoc_path.clone();
1073 app = app.route(&redoc_path, get(move || async move { Html(redoc_html) }));
1074
1075 tracing::info!("OpenAPI documentation enabled at {}", openapi_json_path);
1076 }
1077
1078 if let Some(ref jsonrpc_config) = config.jsonrpc
1079 && jsonrpc_config.enabled
1080 && let Some(registry) = jsonrpc_registry
1081 {
1082 use axum::response::Json;
1083
1084 let jsonrpc_router = Arc::new(crate::jsonrpc::JsonRpcRouter::new(
1085 Arc::clone(®istry),
1086 jsonrpc_config.enable_batch,
1087 jsonrpc_config.max_batch_size,
1088 ));
1089
1090 let state = Arc::new(crate::jsonrpc::JsonRpcState { router: jsonrpc_router });
1091
1092 let endpoint_path = jsonrpc_config.endpoint_path.clone();
1093 app = app.route(&endpoint_path, post(crate::jsonrpc::handle_jsonrpc).with_state(state));
1094 let openrpc_spec = crate::jsonrpc::generate_openrpc_spec(®istry, &config)?;
1095 app = app.route("/openrpc.json", get(move || async move { Json(openrpc_spec) }));
1096
1097 tracing::info!("JSON-RPC endpoint enabled at {}", endpoint_path);
1098 tracing::info!("OpenRPC documentation enabled at /openrpc.json");
1099 }
1100
1101 Ok(app)
1102}
1103
1104pub struct Server;
1106
1107impl Server {
1108 pub fn with_handlers(
1115 config: ServerConfig,
1116 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1117 ) -> Result<AxumRouter, String> {
1118 let metadata: Vec<crate::RouteMetadata> = routes.iter().map(|(route, _)| route_to_metadata(route)).collect();
1119 build_router_with_handlers_and_config(routes, config, metadata)
1120 }
1121
1122 pub fn with_handlers_and_metadata(
1124 config: ServerConfig,
1125 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1126 metadata: Vec<crate::RouteMetadata>,
1127 ) -> Result<AxumRouter, String> {
1128 build_router_with_handlers_and_config(routes, config, metadata)
1129 }
1130
1131 pub fn with_handlers_metadata_and_grpc(
1133 config: ServerConfig,
1134 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1135 metadata: Vec<crate::RouteMetadata>,
1136 grpc_registry: Arc<crate::grpc::GrpcRegistry>,
1137 ) -> Result<AxumRouter, String> {
1138 build_router_with_handlers_and_config_and_grpc(routes, config, metadata, Some(grpc_registry))
1139 }
1140
1141 #[cfg(not(tarpaulin_include))]
1145 pub async fn run_with_config(app: AxumRouter, config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
1146 let addr = format!("{}:{}", config.host, config.port);
1147 let socket_addr: SocketAddr = addr.parse()?;
1148 let listener = TcpListener::bind(socket_addr).await?;
1149
1150 tracing::info!("Listening on http://{}", socket_addr);
1151
1152 if config.graceful_shutdown {
1153 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
1154 .with_graceful_shutdown(shutdown_signal())
1155 .await?;
1156 } else {
1157 axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
1158 }
1159
1160 Ok(())
1161 }
1162
1163 pub fn init_logging() {
1170 let _ = tracing_subscriber::registry()
1171 .with(
1172 tracing_subscriber::EnvFilter::try_from_default_env()
1173 .unwrap_or_else(|_| "spikard=info,tower_http=info".into()),
1174 )
1175 .with(tracing_subscriber::fmt::layer())
1176 .try_init();
1177 }
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183 use std::pin::Pin;
1184 use std::sync::Arc;
1185
1186 struct TestHandler;
1187
1188 impl Handler for TestHandler {
1189 fn call(
1190 &self,
1191 _request: axum::http::Request<Body>,
1192 _request_data: crate::handler_trait::RequestData,
1193 ) -> Pin<Box<dyn std::future::Future<Output = crate::handler_trait::HandlerResult> + Send + '_>> {
1194 Box::pin(async { Ok(axum::http::Response::builder().status(200).body(Body::empty()).unwrap()) })
1195 }
1196 }
1197
1198 fn build_test_route(path: &str, method: &str, handler_name: &str, expects_json_body: bool) -> crate::Route {
1199 use std::str::FromStr;
1200 crate::Route {
1201 path: path.to_string(),
1202 method: spikard_core::Method::from_str(method).expect("valid method"),
1203 handler_name: handler_name.to_string(),
1204 expects_json_body,
1205 cors: None,
1206 is_async: true,
1207 file_params: None,
1208 request_validator: None,
1209 response_validator: None,
1210 parameter_validator: None,
1211 jsonrpc_method: None,
1212 #[cfg(feature = "di")]
1213 handler_dependencies: vec![],
1214 }
1215 }
1216
1217 fn build_test_route_with_cors(
1218 path: &str,
1219 method: &str,
1220 handler_name: &str,
1221 expects_json_body: bool,
1222 cors: crate::CorsConfig,
1223 ) -> crate::Route {
1224 use std::str::FromStr;
1225 crate::Route {
1226 path: path.to_string(),
1227 method: spikard_core::Method::from_str(method).expect("valid method"),
1228 handler_name: handler_name.to_string(),
1229 expects_json_body,
1230 cors: Some(cors),
1231 is_async: true,
1232 file_params: None,
1233 request_validator: None,
1234 response_validator: None,
1235 parameter_validator: None,
1236 jsonrpc_method: None,
1237 #[cfg(feature = "di")]
1238 handler_dependencies: vec![],
1239 }
1240 }
1241
1242 #[test]
1243 fn test_method_expects_body_post() {
1244 assert!(method_expects_body(&crate::Method::Post));
1245 }
1246
1247 #[test]
1248 fn test_method_expects_body_put() {
1249 assert!(method_expects_body(&crate::Method::Put));
1250 }
1251
1252 #[test]
1253 fn test_method_expects_body_patch() {
1254 assert!(method_expects_body(&crate::Method::Patch));
1255 }
1256
1257 #[test]
1258 fn test_method_expects_body_get() {
1259 assert!(!method_expects_body(&crate::Method::Get));
1260 }
1261
1262 #[test]
1263 fn test_method_expects_body_delete() {
1264 assert!(!method_expects_body(&crate::Method::Delete));
1265 }
1266
1267 #[test]
1268 fn test_method_expects_body_head() {
1269 assert!(!method_expects_body(&crate::Method::Head));
1270 }
1271
1272 #[test]
1273 fn test_method_expects_body_options() {
1274 assert!(!method_expects_body(&crate::Method::Options));
1275 }
1276
1277 #[test]
1278 fn test_method_expects_body_trace() {
1279 assert!(!method_expects_body(&crate::Method::Trace));
1280 }
1281
1282 #[test]
1283 fn test_make_request_uuid_generates_valid_uuid() {
1284 let mut maker = MakeRequestUuid;
1285 let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1286
1287 let id = maker.make_request_id(&request);
1288
1289 assert!(id.is_some());
1290 let id_val = id.unwrap();
1291 let id_str = id_val.header_value().to_str().expect("valid utf8");
1292 assert!(!id_str.is_empty());
1293 assert!(Uuid::parse_str(id_str).is_ok());
1294 }
1295
1296 #[test]
1297 fn test_make_request_uuid_unique_per_call() {
1298 let mut maker = MakeRequestUuid;
1299 let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1300
1301 let id1 = maker.make_request_id(&request).unwrap();
1302 let id2 = maker.make_request_id(&request).unwrap();
1303
1304 let id1_str = id1.header_value().to_str().expect("valid utf8");
1305 let id2_str = id2.header_value().to_str().expect("valid utf8");
1306 assert_ne!(id1_str, id2_str);
1307 }
1308
1309 #[test]
1310 fn test_make_request_uuid_v4_format() {
1311 let mut maker = MakeRequestUuid;
1312 let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1313
1314 let id = maker.make_request_id(&request).unwrap();
1315 let id_str = id.header_value().to_str().expect("valid utf8");
1316
1317 let uuid = Uuid::parse_str(id_str).expect("valid UUID");
1318 assert_eq!(uuid.get_version(), Some(uuid::Version::Random));
1319 }
1320
1321 #[test]
1322 fn test_make_request_uuid_multiple_independent_makers() {
1323 let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1324
1325 let id1 = {
1326 let mut maker1 = MakeRequestUuid;
1327 maker1.make_request_id(&request).unwrap()
1328 };
1329 let id2 = {
1330 let mut maker2 = MakeRequestUuid;
1331 maker2.make_request_id(&request).unwrap()
1332 };
1333
1334 let id1_str = id1.header_value().to_str().expect("valid utf8");
1335 let id2_str = id2.header_value().to_str().expect("valid utf8");
1336 assert_ne!(id1_str, id2_str);
1337 }
1338
1339 #[test]
1340 fn test_make_request_uuid_clone_independence() {
1341 let mut maker1 = MakeRequestUuid;
1342 let mut maker2 = maker1.clone();
1343 let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1344
1345 let id1 = maker1.make_request_id(&request).unwrap();
1346 let id2 = maker2.make_request_id(&request).unwrap();
1347
1348 let id1_str = id1.header_value().to_str().expect("valid utf8");
1349 let id2_str = id2.header_value().to_str().expect("valid utf8");
1350 assert_ne!(id1_str, id2_str);
1351 }
1352
1353 #[test]
1354 fn test_server_config_default_values() {
1355 let config = ServerConfig::default();
1356
1357 assert_eq!(config.host, "127.0.0.1");
1358 assert_eq!(config.port, 8000);
1359 assert_eq!(config.workers, 1);
1360 assert!(!config.enable_request_id);
1361 assert!(config.max_body_size.is_some());
1362 assert!(config.request_timeout.is_none());
1363 assert!(config.graceful_shutdown);
1364 }
1365
1366 #[test]
1367 fn test_server_config_builder_pattern() {
1368 let config = ServerConfig::builder().port(9000).host("0.0.0.0".to_string()).build();
1369
1370 assert_eq!(config.port, 9000);
1371 assert_eq!(config.host, "0.0.0.0");
1372 }
1373
1374 #[cfg(feature = "di")]
1375 fn build_router_for_tests(
1376 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1377 hooks: Option<Arc<crate::LifecycleHooks>>,
1378 ) -> Result<AxumRouter, String> {
1379 build_router_with_handlers(routes, hooks, None)
1380 }
1381
1382 #[cfg(not(feature = "di"))]
1383 fn build_router_for_tests(
1384 routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1385 hooks: Option<Arc<crate::LifecycleHooks>>,
1386 ) -> Result<AxumRouter, String> {
1387 build_router_with_handlers(routes, hooks)
1388 }
1389
1390 #[test]
1391 fn test_route_registry_empty_routes() {
1392 let routes: Vec<(crate::Route, Arc<dyn Handler>)> = vec![];
1393 let _result = build_router_for_tests(routes, None);
1394 }
1395
1396 #[test]
1397 fn test_route_registry_single_route() {
1398 let route = build_test_route("/test", "GET", "test_handler", false);
1399
1400 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1401 let routes = vec![(route, handler)];
1402
1403 let result = build_router_for_tests(routes, None);
1404 assert!(result.is_ok());
1405 }
1406
1407 #[test]
1408 fn test_route_path_normalization_without_leading_slash() {
1409 let route = build_test_route("api/users", "GET", "list_users", false);
1410
1411 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1412 let routes = vec![(route, handler)];
1413
1414 let result = build_router_for_tests(routes, None);
1415 assert!(result.is_ok());
1416 }
1417
1418 #[test]
1419 fn test_route_path_normalization_with_leading_slash() {
1420 let route = build_test_route("/api/users", "GET", "list_users", false);
1421
1422 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1423 let routes = vec![(route, handler)];
1424
1425 let result = build_router_for_tests(routes, None);
1426 assert!(result.is_ok());
1427 }
1428
1429 #[test]
1430 fn test_multiple_routes_same_path_different_methods() {
1431 let get_route = build_test_route("/users", "GET", "list_users", false);
1432 let post_route = build_test_route("/users", "POST", "create_user", true);
1433
1434 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1435 let routes = vec![(get_route, handler.clone()), (post_route, handler)];
1436
1437 let result = build_router_for_tests(routes, None);
1438 assert!(result.is_ok());
1439 }
1440
1441 #[test]
1442 fn test_multiple_different_routes() {
1443 let users_route = build_test_route("/users", "GET", "list_users", false);
1444 let posts_route = build_test_route("/posts", "GET", "list_posts", false);
1445
1446 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1447 let routes = vec![(users_route, handler.clone()), (posts_route, handler)];
1448
1449 let result = build_router_for_tests(routes, None);
1450 assert!(result.is_ok());
1451 }
1452
1453 #[test]
1454 fn test_route_with_single_path_parameter() {
1455 let route = build_test_route("/users/{id}", "GET", "get_user", false);
1456
1457 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1458 let routes = vec![(route, handler)];
1459
1460 let result = build_router_for_tests(routes, None);
1461 assert!(result.is_ok());
1462 }
1463
1464 #[test]
1465 fn test_route_with_multiple_path_parameters() {
1466 let route = build_test_route("/users/{user_id}/posts/{post_id}", "GET", "get_user_post", false);
1467
1468 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1469 let routes = vec![(route, handler)];
1470
1471 let result = build_router_for_tests(routes, None);
1472 assert!(result.is_ok());
1473 }
1474
1475 #[test]
1476 fn test_route_with_path_parameter_post_with_body() {
1477 let route = build_test_route("/users/{id}", "PUT", "update_user", true);
1478
1479 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1480 let routes = vec![(route, handler)];
1481
1482 let result = build_router_for_tests(routes, None);
1483 assert!(result.is_ok());
1484 }
1485
1486 #[test]
1487 fn test_route_with_path_parameter_delete() {
1488 let route = build_test_route("/users/{id}", "DELETE", "delete_user", false);
1489
1490 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1491 let routes = vec![(route, handler)];
1492
1493 let result = build_router_for_tests(routes, None);
1494 assert!(result.is_ok());
1495 }
1496
1497 #[test]
1498 fn test_route_post_method_with_body() {
1499 let route = build_test_route("/users", "POST", "create_user", true);
1500
1501 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1502 let routes = vec![(route, handler)];
1503
1504 let result = build_router_for_tests(routes, None);
1505 assert!(result.is_ok());
1506 }
1507
1508 #[test]
1509 fn test_route_put_method_with_body() {
1510 let route = build_test_route("/users/{id}", "PUT", "update_user", true);
1511
1512 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1513 let routes = vec![(route, handler)];
1514
1515 let result = build_router_for_tests(routes, None);
1516 assert!(result.is_ok());
1517 }
1518
1519 #[test]
1520 fn test_route_patch_method_with_body() {
1521 let route = build_test_route("/users/{id}", "PATCH", "patch_user", true);
1522
1523 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1524 let routes = vec![(route, handler)];
1525
1526 let result = build_router_for_tests(routes, None);
1527 assert!(result.is_ok());
1528 }
1529
1530 #[test]
1531 fn test_route_head_method() {
1532 let route = build_test_route("/users", "HEAD", "head_users", false);
1533
1534 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1535 let routes = vec![(route, handler)];
1536
1537 let result = build_router_for_tests(routes, None);
1538 assert!(result.is_ok());
1539 }
1540
1541 #[test]
1542 fn test_route_options_method() {
1543 let route = build_test_route("/users", "OPTIONS", "options_users", false);
1544
1545 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1546 let routes = vec![(route, handler)];
1547
1548 let result = build_router_for_tests(routes, None);
1549 assert!(result.is_ok());
1550 }
1551
1552 #[test]
1553 fn test_route_trace_method() {
1554 let route = build_test_route("/users", "TRACE", "trace_users", false);
1555
1556 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1557 let routes = vec![(route, handler)];
1558
1559 let result = build_router_for_tests(routes, None);
1560 assert!(result.is_ok());
1561 }
1562
1563 #[test]
1564 fn test_route_with_cors_config() {
1565 let cors_config = crate::CorsConfig {
1566 allowed_origins: vec!["https://example.com".to_string()],
1567 allowed_methods: vec!["GET".to_string(), "POST".to_string()],
1568 allowed_headers: vec!["Content-Type".to_string()],
1569 expose_headers: None,
1570 max_age: Some(3600),
1571 allow_credentials: Some(true),
1572 ..Default::default()
1573 };
1574
1575 let route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config);
1576
1577 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1578 let routes = vec![(route, handler)];
1579
1580 let result = build_router_for_tests(routes, None);
1581 assert!(result.is_ok());
1582 }
1583
1584 #[test]
1585 fn test_multiple_routes_with_cors_same_path() {
1586 let cors_config = crate::CorsConfig {
1587 allowed_origins: vec!["https://example.com".to_string()],
1588 allowed_methods: vec!["GET".to_string(), "POST".to_string()],
1589 allowed_headers: vec!["Content-Type".to_string()],
1590 expose_headers: None,
1591 max_age: Some(3600),
1592 allow_credentials: Some(true),
1593 ..Default::default()
1594 };
1595
1596 let get_route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config.clone());
1597 let post_route = build_test_route_with_cors("/users", "POST", "create_user", true, cors_config);
1598
1599 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1600 let routes = vec![(get_route, handler.clone()), (post_route, handler)];
1601
1602 let result = build_router_for_tests(routes, None);
1603 assert!(result.is_ok());
1604 }
1605
1606 #[test]
1607 fn test_routes_sorted_by_path() {
1608 let zebra_route = build_test_route("/zebra", "GET", "get_zebra", false);
1609 let alpha_route = build_test_route("/alpha", "GET", "get_alpha", false);
1610
1611 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1612 let routes = vec![(zebra_route, handler.clone()), (alpha_route, handler)];
1613
1614 let result = build_router_for_tests(routes, None);
1615 assert!(result.is_ok());
1616 }
1617
1618 #[test]
1619 fn test_routes_with_nested_paths() {
1620 let parent_route = build_test_route("/api", "GET", "get_api", false);
1621 let child_route = build_test_route("/api/users", "GET", "get_users", false);
1622
1623 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1624 let routes = vec![(parent_route, handler.clone()), (child_route, handler)];
1625
1626 let result = build_router_for_tests(routes, None);
1627 assert!(result.is_ok());
1628 }
1629
1630 #[test]
1631 fn test_routes_with_lifecycle_hooks() {
1632 let hooks = crate::LifecycleHooks::new();
1633 let hooks = Arc::new(hooks);
1634
1635 let route = build_test_route("/users", "GET", "list_users", false);
1636
1637 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1638 let routes = vec![(route, handler)];
1639
1640 let result = build_router_for_tests(routes, Some(hooks));
1641 assert!(result.is_ok());
1642 }
1643
1644 #[test]
1645 fn test_routes_without_lifecycle_hooks() {
1646 let route = build_test_route("/users", "GET", "list_users", false);
1647
1648 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1649 let routes = vec![(route, handler)];
1650
1651 let result = build_router_for_tests(routes, None);
1652 assert!(result.is_ok());
1653 }
1654
1655 #[test]
1656 fn test_route_with_trailing_slash() {
1657 let route = build_test_route("/users/", "GET", "list_users", false);
1658
1659 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1660 let routes = vec![(route, handler)];
1661
1662 let result = build_router_for_tests(routes, None);
1663 assert!(result.is_ok());
1664 }
1665
1666 #[test]
1667 fn test_route_with_root_path() {
1668 let route = build_test_route("/", "GET", "root_handler", false);
1669
1670 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1671 let routes = vec![(route, handler)];
1672
1673 let result = build_router_for_tests(routes, None);
1674 assert!(result.is_ok());
1675 }
1676
1677 #[test]
1678 fn test_large_number_of_routes() {
1679 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1680 let mut routes = vec![];
1681
1682 for i in 0..50 {
1683 let route = build_test_route(&format!("/route{}", i), "GET", &format!("handler_{}", i), false);
1684 routes.push((route, handler.clone()));
1685 }
1686
1687 let result = build_router_for_tests(routes, None);
1688 assert!(result.is_ok());
1689 }
1690
1691 #[test]
1692 fn test_route_with_query_params_in_path_definition() {
1693 let route = build_test_route("/search", "GET", "search", false);
1694
1695 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1696 let routes = vec![(route, handler)];
1697
1698 let result = build_router_for_tests(routes, None);
1699 assert!(result.is_ok());
1700 }
1701
1702 #[test]
1703 fn test_all_http_methods_on_same_path() {
1704 let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1705 let methods = vec!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1706
1707 let mut routes = vec![];
1708 for method in methods {
1709 let expects_body = matches!(method, "POST" | "PUT" | "PATCH");
1710 let route = build_test_route("/resource", method, &format!("handler_{}", method), expects_body);
1711 routes.push((route, handler.clone()));
1712 }
1713
1714 let result = build_router_for_tests(routes, None);
1715 assert!(result.is_ok());
1716 }
1717}