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