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