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