Skip to main content

spikard_http/server/
mod.rs

1//! HTTP server implementation using Tokio and Axum
2//!
3//! This module provides the main server builder and routing infrastructure, with
4//! focused submodules for handler validation, request extraction, and lifecycle execution.
5
6pub 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
38/// Type alias for route handler pairs
39type RouteHandlerPair = (crate::Route, Arc<dyn Handler>);
40
41/// Extract required dependencies from route metadata
42///
43/// Placeholder implementation until routes can declare dependencies via metadata.
44#[cfg(feature = "di")]
45fn extract_handler_dependencies(route: &crate::Route) -> Vec<String> {
46    route.handler_dependencies.clone()
47}
48
49/// Determines if a method typically has a request body
50fn 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
112/// Creates a method router for the given HTTP method.
113/// Handles both path parameters and non-path variants.
114fn 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/// Request ID generator using UUIDs
432#[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/// Graceful shutdown signal handler
443///
444/// Coverage: Tested via integration tests (Unix signal handling not easily unit testable)
445#[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/// Build an Axum router from routes and foreign handlers
473#[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/// Build an Axum router from routes and foreign handlers with optional DI container
482#[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            // Check for static_response before wrapping in ValidatingHandler,
537            // since ValidatingHandler doesn't delegate static_response().
538            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            // Fast-path: if the handler declares a static response, bypass the
557            // entire middleware pipeline (validation, hooks, request extraction).
558            //
559            // NOTE: static routes also bypass CORS handling, content-type
560            // validation, and HTTP tracing. If CORS headers are needed they
561            // must be included in `StaticResponse.headers` explicitly.
562            //
563            // Non-parameterized paths are also inserted into the FastRouter for
564            // O(1) HashMap-based lookup as the outermost middleware. The Axum
565            // route below serves as fallback — it handles the same request if
566            // the FastRouter layer is somehow bypassed and also covers
567            // parameterized static routes that cannot go into the FastRouter.
568            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                // Axum fallback handler — uses the same `to_response()` as the
584                // FastRouter and `StaticResponseHandler::call`.
585                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            // Only apply content-type validation middleware for methods that
650            // carry a request body. GET/DELETE/HEAD/OPTIONS/TRACE never have
651            // meaningful content-type headers, so the middleware just adds
652            // into_parts/from_parts overhead for those methods.
653            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    // Install the fast-router as the outermost middleware so that static-response
706    // routes are served without entering the Axum routing tree at all.
707    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
725/// Build router with handlers and apply middleware based on config
726pub 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    // Only add the sensitive-header redaction layer when auth middleware is
805    // configured — without auth there is nothing to redact, and the layer
806    // otherwise adds per-request overhead.
807    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    // Only add the body-limit layer when a limit is explicitly configured.
884    // Omitting the layer entirely (instead of `disable()`) avoids a no-op
885    // middleware dispatch on every request.
886    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        // TODO: Add per-method routes if enabled
991        // TODO: Add WebSocket endpoint if enabled
992        // TODO: Add SSE endpoint if enabled
993        // TODO: Add OpenRPC spec endpoint if enabled
994
995        tracing::info!("JSON-RPC endpoint enabled at {}", endpoint_path);
996    }
997
998    Ok(app)
999}
1000
1001/// HTTP Server
1002pub struct Server;
1003
1004impl Server {
1005    /// Build a server router with runtime handlers.
1006    ///
1007    /// Build router with trait-based handlers
1008    /// Routes are grouped by path before registration to support multiple HTTP methods
1009    /// for the same path (e.g., GET /data and POST /data). Axum requires that all methods
1010    /// for a path be merged into a single MethodRouter before calling `.route()`.
1011    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    /// Build a server router with runtime handlers and explicit metadata for OpenAPI.
1065    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    /// Run the server with the Axum router and config
1074    ///
1075    /// Coverage: Production-only, tested via integration tests
1076    #[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    /// Initialize logging
1096    ///
1097    /// This function is idempotent - calling it multiple times is safe.
1098    /// It uses `try_init()` instead of `init()` to avoid panics when the
1099    /// global subscriber has already been set (e.g., by a language runtime
1100    /// or a previous call).
1101    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}