spikard 0.13.0 → 0.15.2

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