Skip to main content

spikard_http/server/
mod.rs

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