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