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