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