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