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