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