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