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