spikard 0.3.6 → 0.5.0

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
@@ -13,7 +13,8 @@ use axum::Router as AxumRouter;
13
13
  use axum::body::Body;
14
14
  use axum::extract::{DefaultBodyLimit, Path};
15
15
  use axum::http::StatusCode;
16
- use axum::routing::{MethodRouter, get};
16
+ use axum::routing::{MethodRouter, get, post};
17
+ use spikard_core::type_hints;
17
18
  use std::collections::HashMap;
18
19
  use std::net::SocketAddr;
19
20
  use std::sync::Arc;
@@ -44,237 +45,280 @@ fn extract_handler_dependencies(route: &crate::Route) -> Vec<String> {
44
45
  }
45
46
 
46
47
  /// Determines if a method typically has a request body
47
- fn method_expects_body(method: &str) -> bool {
48
- matches!(method, "POST" | "PUT" | "PATCH")
48
+ fn method_expects_body(method: &crate::Method) -> bool {
49
+ matches!(method, crate::Method::Post | crate::Method::Put | crate::Method::Patch)
49
50
  }
50
51
 
51
- /// Creates a method router for the given HTTP method
52
- /// Handles both path parameters and non-path variants
52
+ /// Creates a method router for the given HTTP method.
53
+ /// Handles both path parameters and non-path variants.
53
54
  fn create_method_router(
54
- method: &str,
55
+ method: crate::Method,
55
56
  has_path_params: bool,
56
57
  handler: Arc<dyn Handler>,
57
58
  hooks: Option<Arc<crate::LifecycleHooks>>,
59
+ include_raw_query_params: bool,
60
+ include_query_params_json: bool,
58
61
  ) -> axum::routing::MethodRouter {
59
- let expects_body = method_expects_body(method);
62
+ let expects_body = method_expects_body(&method);
63
+ let include_headers = handler.wants_headers();
64
+ let include_cookies = handler.wants_cookies();
65
+ let without_body_options = request_extraction::WithoutBodyExtractionOptions {
66
+ include_raw_query_params,
67
+ include_query_params_json,
68
+ include_headers,
69
+ include_cookies,
70
+ };
60
71
 
61
72
  if expects_body {
62
- // POST, PUT, PATCH - need to handle body
63
73
  if has_path_params {
64
74
  let handler_clone = handler.clone();
65
75
  let hooks_clone = hooks.clone();
66
76
  match method {
67
- "POST" => axum::routing::post(
77
+ crate::Method::Post => axum::routing::post(
68
78
  move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
69
79
  let (parts, body) = req.into_parts();
70
- let request_data =
71
- request_extraction::create_request_data_with_body(&parts, path_params.0, body).await?;
80
+ let request_data = request_extraction::create_request_data_with_body(
81
+ &parts,
82
+ path_params.0,
83
+ body,
84
+ include_raw_query_params,
85
+ include_query_params_json,
86
+ include_headers,
87
+ include_cookies,
88
+ )
89
+ .await?;
72
90
  let req = axum::extract::Request::from_parts(parts, Body::empty());
73
91
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
74
92
  .await
75
93
  },
76
94
  ),
77
- "PUT" => axum::routing::put(
95
+ crate::Method::Put => axum::routing::put(
78
96
  move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
79
97
  let (parts, body) = req.into_parts();
80
- let request_data =
81
- request_extraction::create_request_data_with_body(&parts, path_params.0, body).await?;
98
+ let request_data = request_extraction::create_request_data_with_body(
99
+ &parts,
100
+ path_params.0,
101
+ body,
102
+ include_raw_query_params,
103
+ include_query_params_json,
104
+ include_headers,
105
+ include_cookies,
106
+ )
107
+ .await?;
82
108
  let req = axum::extract::Request::from_parts(parts, Body::empty());
83
109
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
84
110
  .await
85
111
  },
86
112
  ),
87
- "PATCH" => axum::routing::patch(
113
+ crate::Method::Patch => axum::routing::patch(
88
114
  move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
89
115
  let (parts, body) = req.into_parts();
90
- let request_data =
91
- request_extraction::create_request_data_with_body(&parts, path_params.0, body).await?;
116
+ let request_data = request_extraction::create_request_data_with_body(
117
+ &parts,
118
+ path_params.0,
119
+ body,
120
+ include_raw_query_params,
121
+ include_query_params_json,
122
+ include_headers,
123
+ include_cookies,
124
+ )
125
+ .await?;
92
126
  let req = axum::extract::Request::from_parts(parts, Body::empty());
93
127
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
94
128
  .await
95
129
  },
96
130
  ),
97
- _ => {
98
- eprintln!(
99
- "[spikard-router] unsupported HTTP method with path params: {} (defaulting to 405)",
100
- method
101
- );
102
- MethodRouter::new()
103
- }
131
+ crate::Method::Get
132
+ | crate::Method::Delete
133
+ | crate::Method::Head
134
+ | crate::Method::Options
135
+ | crate::Method::Trace => MethodRouter::new(),
104
136
  }
105
137
  } else {
106
138
  let handler_clone = handler.clone();
107
139
  let hooks_clone = hooks.clone();
108
140
  match method {
109
- "POST" => axum::routing::post(move |req: axum::extract::Request| async move {
141
+ crate::Method::Post => axum::routing::post(move |req: axum::extract::Request| async move {
110
142
  let (parts, body) = req.into_parts();
111
- let request_data =
112
- request_extraction::create_request_data_with_body(&parts, HashMap::new(), body).await?;
143
+ let request_data = request_extraction::create_request_data_with_body(
144
+ &parts,
145
+ HashMap::new(),
146
+ body,
147
+ include_raw_query_params,
148
+ include_query_params_json,
149
+ include_headers,
150
+ include_cookies,
151
+ )
152
+ .await?;
113
153
  let req = axum::extract::Request::from_parts(parts, Body::empty());
114
154
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
115
155
  .await
116
156
  }),
117
- "PUT" => axum::routing::put(move |req: axum::extract::Request| async move {
157
+ crate::Method::Put => axum::routing::put(move |req: axum::extract::Request| async move {
118
158
  let (parts, body) = req.into_parts();
119
- let request_data =
120
- request_extraction::create_request_data_with_body(&parts, HashMap::new(), body).await?;
159
+ let request_data = request_extraction::create_request_data_with_body(
160
+ &parts,
161
+ HashMap::new(),
162
+ body,
163
+ include_raw_query_params,
164
+ include_query_params_json,
165
+ include_headers,
166
+ include_cookies,
167
+ )
168
+ .await?;
121
169
  let req = axum::extract::Request::from_parts(parts, Body::empty());
122
170
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
123
171
  .await
124
172
  }),
125
- "PATCH" => axum::routing::patch(move |req: axum::extract::Request| async move {
173
+ crate::Method::Patch => axum::routing::patch(move |req: axum::extract::Request| async move {
126
174
  let (parts, body) = req.into_parts();
127
- let request_data =
128
- request_extraction::create_request_data_with_body(&parts, HashMap::new(), body).await?;
175
+ let request_data = request_extraction::create_request_data_with_body(
176
+ &parts,
177
+ HashMap::new(),
178
+ body,
179
+ include_raw_query_params,
180
+ include_query_params_json,
181
+ include_headers,
182
+ include_cookies,
183
+ )
184
+ .await?;
129
185
  let req = axum::extract::Request::from_parts(parts, Body::empty());
130
186
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
131
187
  .await
132
188
  }),
133
- _ => {
134
- eprintln!(
135
- "[spikard-router] unsupported HTTP method without path params: {} (defaulting to 405)",
136
- method
137
- );
138
- MethodRouter::new()
139
- }
189
+ crate::Method::Get
190
+ | crate::Method::Delete
191
+ | crate::Method::Head
192
+ | crate::Method::Options
193
+ | crate::Method::Trace => MethodRouter::new(),
140
194
  }
141
195
  }
142
- } else {
143
- // GET, DELETE, HEAD, TRACE - no body handling
144
- if has_path_params {
145
- let handler_clone = handler.clone();
146
- let hooks_clone = hooks.clone();
147
- match method {
148
- "GET" => axum::routing::get(
149
- move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
150
- let request_data = request_extraction::create_request_data_without_body(
151
- req.uri(),
152
- req.method(),
153
- req.headers(),
154
- path_params.0,
155
- );
156
- lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
157
- .await
158
- },
159
- ),
160
- "DELETE" => axum::routing::delete(
161
- move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
162
- let request_data = request_extraction::create_request_data_without_body(
163
- req.uri(),
164
- req.method(),
165
- req.headers(),
166
- path_params.0,
167
- );
168
- lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
169
- .await
170
- },
171
- ),
172
- "HEAD" => axum::routing::head(
173
- move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
174
- let request_data = request_extraction::create_request_data_without_body(
175
- req.uri(),
176
- req.method(),
177
- req.headers(),
178
- path_params.0,
179
- );
180
- lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
181
- .await
182
- },
183
- ),
184
- "TRACE" => axum::routing::trace(
185
- move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
186
- let request_data = request_extraction::create_request_data_without_body(
187
- req.uri(),
188
- req.method(),
189
- req.headers(),
190
- path_params.0,
191
- );
192
- lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
193
- .await
194
- },
195
- ),
196
- "OPTIONS" => axum::routing::options(
197
- move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
198
- let request_data = request_extraction::create_request_data_without_body(
199
- req.uri(),
200
- req.method(),
201
- req.headers(),
202
- path_params.0,
203
- );
204
- lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
205
- .await
206
- },
207
- ),
208
- _ => {
209
- eprintln!(
210
- "[spikard-router] unsupported HTTP method with path params: {} (defaulting to 405)",
211
- method
212
- );
213
- MethodRouter::new()
214
- }
215
- }
216
- } else {
217
- let handler_clone = handler.clone();
218
- let hooks_clone = hooks.clone();
219
- match method {
220
- "GET" => axum::routing::get(move |req: axum::extract::Request| async move {
196
+ } else if has_path_params {
197
+ let handler_clone = handler.clone();
198
+ let hooks_clone = hooks.clone();
199
+ match method {
200
+ crate::Method::Get => axum::routing::get(
201
+ move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
221
202
  let request_data = request_extraction::create_request_data_without_body(
222
203
  req.uri(),
223
204
  req.method(),
224
205
  req.headers(),
225
- HashMap::new(),
206
+ path_params.0,
207
+ without_body_options,
226
208
  );
227
209
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
228
210
  .await
229
- }),
230
- "DELETE" => axum::routing::delete(move |req: axum::extract::Request| async move {
211
+ },
212
+ ),
213
+ crate::Method::Delete => axum::routing::delete(
214
+ move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
231
215
  let request_data = request_extraction::create_request_data_without_body(
232
216
  req.uri(),
233
217
  req.method(),
234
218
  req.headers(),
235
- HashMap::new(),
219
+ path_params.0,
220
+ without_body_options,
236
221
  );
237
222
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
238
223
  .await
239
- }),
240
- "HEAD" => axum::routing::head(move |req: axum::extract::Request| async move {
224
+ },
225
+ ),
226
+ crate::Method::Head => axum::routing::head(
227
+ move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
241
228
  let request_data = request_extraction::create_request_data_without_body(
242
229
  req.uri(),
243
230
  req.method(),
244
231
  req.headers(),
245
- HashMap::new(),
232
+ path_params.0,
233
+ without_body_options,
246
234
  );
247
235
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
248
236
  .await
249
- }),
250
- "TRACE" => axum::routing::trace(move |req: axum::extract::Request| async move {
237
+ },
238
+ ),
239
+ crate::Method::Trace => axum::routing::trace(
240
+ move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
251
241
  let request_data = request_extraction::create_request_data_without_body(
252
242
  req.uri(),
253
243
  req.method(),
254
244
  req.headers(),
255
- HashMap::new(),
245
+ path_params.0,
246
+ without_body_options,
256
247
  );
257
248
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
258
249
  .await
259
- }),
260
- "OPTIONS" => axum::routing::options(move |req: axum::extract::Request| async move {
250
+ },
251
+ ),
252
+ crate::Method::Options => axum::routing::options(
253
+ move |path_params: Path<HashMap<String, String>>, req: axum::extract::Request| async move {
261
254
  let request_data = request_extraction::create_request_data_without_body(
262
255
  req.uri(),
263
256
  req.method(),
264
257
  req.headers(),
265
- HashMap::new(),
258
+ path_params.0,
259
+ without_body_options,
266
260
  );
267
261
  lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone)
268
262
  .await
269
- }),
270
- _ => {
271
- eprintln!(
272
- "[spikard-router] unsupported HTTP method without path params: {} (defaulting to 405)",
273
- method
274
- );
275
- MethodRouter::new()
276
- }
277
- }
263
+ },
264
+ ),
265
+ crate::Method::Post | crate::Method::Put | crate::Method::Patch => MethodRouter::new(),
266
+ }
267
+ } else {
268
+ let handler_clone = handler.clone();
269
+ let hooks_clone = hooks.clone();
270
+ match method {
271
+ crate::Method::Get => axum::routing::get(move |req: axum::extract::Request| async move {
272
+ let request_data = request_extraction::create_request_data_without_body(
273
+ req.uri(),
274
+ req.method(),
275
+ req.headers(),
276
+ HashMap::new(),
277
+ without_body_options,
278
+ );
279
+ lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone).await
280
+ }),
281
+ crate::Method::Delete => axum::routing::delete(move |req: axum::extract::Request| async move {
282
+ let request_data = request_extraction::create_request_data_without_body(
283
+ req.uri(),
284
+ req.method(),
285
+ req.headers(),
286
+ HashMap::new(),
287
+ without_body_options,
288
+ );
289
+ lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone).await
290
+ }),
291
+ crate::Method::Head => axum::routing::head(move |req: axum::extract::Request| async move {
292
+ let request_data = request_extraction::create_request_data_without_body(
293
+ req.uri(),
294
+ req.method(),
295
+ req.headers(),
296
+ HashMap::new(),
297
+ without_body_options,
298
+ );
299
+ lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone).await
300
+ }),
301
+ crate::Method::Trace => axum::routing::trace(move |req: axum::extract::Request| async move {
302
+ let request_data = request_extraction::create_request_data_without_body(
303
+ req.uri(),
304
+ req.method(),
305
+ req.headers(),
306
+ HashMap::new(),
307
+ without_body_options,
308
+ );
309
+ lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone).await
310
+ }),
311
+ crate::Method::Options => axum::routing::options(move |req: axum::extract::Request| async move {
312
+ let request_data = request_extraction::create_request_data_without_body(
313
+ req.uri(),
314
+ req.method(),
315
+ req.headers(),
316
+ HashMap::new(),
317
+ without_body_options,
318
+ );
319
+ lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler_clone, hooks_clone).await
320
+ }),
321
+ crate::Method::Post | crate::Method::Put | crate::Method::Patch => MethodRouter::new(),
278
322
  }
279
323
  }
280
324
  }
@@ -291,6 +335,9 @@ impl MakeRequestId for MakeRequestUuid {
291
335
  }
292
336
 
293
337
  /// Graceful shutdown signal handler
338
+ ///
339
+ /// Coverage: Tested via integration tests (Unix signal handling not easily unit testable)
340
+ #[cfg(not(tarpaulin_include))]
294
341
  async fn shutdown_signal() {
295
342
  let ctrl_c = async {
296
343
  tokio::signal::ctrl_c().await.expect("failed to install Ctrl+C handler");
@@ -323,7 +370,7 @@ pub fn build_router_with_handlers(
323
370
  routes: Vec<(crate::Route, Arc<dyn Handler>)>,
324
371
  hooks: Option<Arc<crate::LifecycleHooks>>,
325
372
  ) -> Result<AxumRouter, String> {
326
- build_router_with_handlers_inner(routes, hooks, None)
373
+ build_router_with_handlers_inner(routes, hooks, None, true)
327
374
  }
328
375
 
329
376
  /// Build an Axum router from routes and foreign handlers with optional DI container
@@ -333,7 +380,7 @@ pub fn build_router_with_handlers(
333
380
  hooks: Option<Arc<crate::LifecycleHooks>>,
334
381
  di_container: Option<Arc<spikard_core::di::DependencyContainer>>,
335
382
  ) -> Result<AxumRouter, String> {
336
- build_router_with_handlers_inner(routes, hooks, di_container)
383
+ build_router_with_handlers_inner(routes, hooks, di_container, true)
337
384
  }
338
385
 
339
386
  fn build_router_with_handlers_inner(
@@ -341,26 +388,10 @@ fn build_router_with_handlers_inner(
341
388
  hooks: Option<Arc<crate::LifecycleHooks>>,
342
389
  #[cfg(feature = "di")] di_container: Option<Arc<spikard_core::di::DependencyContainer>>,
343
390
  #[cfg(not(feature = "di"))] _di_container: Option<()>,
391
+ enable_http_trace: bool,
344
392
  ) -> Result<AxumRouter, String> {
345
393
  let mut app = AxumRouter::new();
346
394
 
347
- let mut registry = HashMap::new();
348
- for (route, _) in &routes {
349
- let axum_path = crate::type_hints::strip_type_hints(&route.path);
350
- let axum_path = if axum_path.starts_with('/') {
351
- axum_path
352
- } else {
353
- format!("/{}", axum_path)
354
- };
355
- registry.insert(
356
- (route.method.as_str().to_string(), axum_path),
357
- crate::middleware::RouteInfo {
358
- expects_json_body: route.expects_json_body,
359
- },
360
- );
361
- }
362
- let route_registry: crate::middleware::RouteRegistry = Arc::new(registry);
363
-
364
395
  let mut routes_by_path: HashMap<String, Vec<RouteHandlerPair>> = HashMap::new();
365
396
  for (route, handler) in routes {
366
397
  routes_by_path
@@ -414,20 +445,48 @@ fn build_router_with_handlers_inner(
414
445
  let has_path_params = path.contains('{');
415
446
 
416
447
  for (_method, (route, handler)) in handlers_by_method {
417
- let method_router: MethodRouter = match route.method.as_str() {
418
- "OPTIONS" => {
448
+ let method = route.method.clone();
449
+ let method_router: MethodRouter = match method {
450
+ crate::Method::Options => {
419
451
  if let Some(ref cors_cfg) = route.cors {
420
452
  let cors_config = cors_cfg.clone();
421
453
  axum::routing::options(move |req: axum::extract::Request| async move {
422
454
  crate::cors::handle_preflight(req.headers(), &cors_config).map_err(|e| *e)
423
455
  })
424
456
  } else {
425
- create_method_router(route.method.as_str(), has_path_params, handler, hooks.clone())
457
+ let include_raw_query_params = route.parameter_validator.is_some();
458
+ let include_query_params_json = !handler.prefers_parameter_extraction();
459
+ create_method_router(
460
+ method,
461
+ has_path_params,
462
+ handler,
463
+ hooks.clone(),
464
+ include_raw_query_params,
465
+ include_query_params_json,
466
+ )
426
467
  }
427
468
  }
428
- method => create_method_router(method, has_path_params, handler, hooks.clone()),
469
+ method => {
470
+ let include_raw_query_params = route.parameter_validator.is_some();
471
+ let include_query_params_json = !handler.prefers_parameter_extraction();
472
+ create_method_router(
473
+ method,
474
+ has_path_params,
475
+ handler,
476
+ hooks.clone(),
477
+ include_raw_query_params,
478
+ include_query_params_json,
479
+ )
480
+ }
429
481
  };
430
482
 
483
+ let method_router = method_router.layer(axum::middleware::from_fn_with_state(
484
+ crate::middleware::RouteInfo {
485
+ expects_json_body: route.expects_json_body,
486
+ },
487
+ crate::middleware::validate_content_type_middleware,
488
+ ));
489
+
431
490
  combined_router = Some(match combined_router {
432
491
  None => method_router,
433
492
  Some(existing) => existing.merge(method_router),
@@ -453,7 +512,7 @@ fn build_router_with_handlers_inner(
453
512
  }
454
513
 
455
514
  if let Some(router) = combined_router {
456
- let mut axum_path = crate::type_hints::strip_type_hints(&path);
515
+ let mut axum_path = type_hints::strip_type_hints(&path);
457
516
  if !axum_path.starts_with('/') {
458
517
  axum_path = format!("/{}", axum_path);
459
518
  }
@@ -461,12 +520,9 @@ fn build_router_with_handlers_inner(
461
520
  }
462
521
  }
463
522
 
464
- app = app.layer(axum::middleware::from_fn(
465
- crate::middleware::validate_content_type_middleware,
466
- ));
467
- app = app.layer(TraceLayer::new_for_http());
468
-
469
- app = app.layer(axum::Extension(route_registry));
523
+ if enable_http_trace {
524
+ app = app.layer(TraceLayer::new_for_http());
525
+ }
470
526
 
471
527
  Ok(app)
472
528
  }
@@ -488,10 +544,67 @@ pub fn build_router_with_handlers_and_config(
488
544
  }
489
545
  let hooks = config.lifecycle_hooks.clone();
490
546
 
547
+ let jsonrpc_registry = if let Some(ref jsonrpc_config) = config.jsonrpc {
548
+ if jsonrpc_config.enabled {
549
+ let registry = Arc::new(crate::jsonrpc::JsonRpcMethodRegistry::new());
550
+
551
+ for (route, handler) in &routes {
552
+ if let Some(ref jsonrpc_info) = route.jsonrpc_method {
553
+ let method_name = jsonrpc_info.method_name.clone();
554
+
555
+ let metadata = crate::jsonrpc::MethodMetadata::new(&method_name)
556
+ .with_params_schema(jsonrpc_info.params_schema.clone().unwrap_or(serde_json::json!({})))
557
+ .with_result_schema(jsonrpc_info.result_schema.clone().unwrap_or(serde_json::json!({})));
558
+
559
+ let metadata = if let Some(ref description) = jsonrpc_info.description {
560
+ metadata.with_description(description.clone())
561
+ } else {
562
+ metadata
563
+ };
564
+
565
+ let metadata = if jsonrpc_info.deprecated {
566
+ metadata.mark_deprecated()
567
+ } else {
568
+ metadata
569
+ };
570
+
571
+ let mut metadata = metadata;
572
+ for tag in &jsonrpc_info.tags {
573
+ metadata = metadata.with_tag(tag.clone());
574
+ }
575
+
576
+ if let Err(e) = registry.register(&method_name, Arc::clone(handler), metadata) {
577
+ tracing::warn!(
578
+ "Failed to register JSON-RPC method '{}' for route {}: {}",
579
+ method_name,
580
+ route.path,
581
+ e
582
+ );
583
+ } else {
584
+ tracing::debug!(
585
+ "Registered JSON-RPC method '{}' for route {} {} (handler: {})",
586
+ method_name,
587
+ route.method,
588
+ route.path,
589
+ route.handler_name
590
+ );
591
+ }
592
+ }
593
+ }
594
+
595
+ Some(registry)
596
+ } else {
597
+ None
598
+ }
599
+ } else {
600
+ None
601
+ };
602
+
491
603
  #[cfg(feature = "di")]
492
- let mut app = build_router_with_handlers(routes, hooks, config.di_container.clone())?;
604
+ let mut app =
605
+ build_router_with_handlers_inner(routes, hooks, config.di_container.clone(), config.enable_http_trace)?;
493
606
  #[cfg(not(feature = "di"))]
494
- let mut app = build_router_with_handlers(routes, hooks)?;
607
+ let mut app = build_router_with_handlers_inner(routes, hooks, None, config.enable_http_trace)?;
495
608
 
496
609
  app = app.layer(SetSensitiveRequestHeadersLayer::new([
497
610
  axum::http::header::AUTHORIZATION,
@@ -658,6 +771,29 @@ pub fn build_router_with_handlers_and_config(
658
771
  tracing::info!("OpenAPI documentation enabled at {}", openapi_json_path);
659
772
  }
660
773
 
774
+ if let Some(ref jsonrpc_config) = config.jsonrpc
775
+ && jsonrpc_config.enabled
776
+ && let Some(registry) = jsonrpc_registry
777
+ {
778
+ let jsonrpc_router = Arc::new(crate::jsonrpc::JsonRpcRouter::new(
779
+ registry,
780
+ jsonrpc_config.enable_batch,
781
+ jsonrpc_config.max_batch_size,
782
+ ));
783
+
784
+ let state = Arc::new(crate::jsonrpc::JsonRpcState { router: jsonrpc_router });
785
+
786
+ let endpoint_path = jsonrpc_config.endpoint_path.clone();
787
+ app = app.route(&endpoint_path, post(crate::jsonrpc::handle_jsonrpc).with_state(state));
788
+
789
+ // TODO: Add per-method routes if enabled
790
+ // TODO: Add WebSocket endpoint if enabled
791
+ // TODO: Add SSE endpoint if enabled
792
+ // TODO: Add OpenRPC spec endpoint if enabled
793
+
794
+ tracing::info!("JSON-RPC endpoint enabled at {}", endpoint_path);
795
+ }
796
+
661
797
  Ok(app)
662
798
  }
663
799
 
@@ -700,6 +836,10 @@ impl Server {
700
836
  cors: route.cors.clone(),
701
837
  body_param_name: None,
702
838
  handler_dependencies: Some(route.handler_dependencies.clone()),
839
+ jsonrpc_method: route
840
+ .jsonrpc_method
841
+ .as_ref()
842
+ .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
703
843
  }
704
844
  }
705
845
  #[cfg(not(feature = "di"))]
@@ -715,6 +855,10 @@ impl Server {
715
855
  is_async: route.is_async,
716
856
  cors: route.cors.clone(),
717
857
  body_param_name: None,
858
+ jsonrpc_method: route
859
+ .jsonrpc_method
860
+ .as_ref()
861
+ .map(|info| serde_json::to_value(info).unwrap_or(serde_json::json!(null))),
718
862
  }
719
863
  }
720
864
  })
@@ -732,6 +876,9 @@ impl Server {
732
876
  }
733
877
 
734
878
  /// Run the server with the Axum router and config
879
+ ///
880
+ /// Coverage: Production-only, tested via integration tests
881
+ #[cfg(not(tarpaulin_include))]
735
882
  pub async fn run_with_config(app: AxumRouter, config: ServerConfig) -> Result<(), Box<dyn std::error::Error>> {
736
883
  let addr = format!("{}:{}", config.host, config.port);
737
884
  let socket_addr: SocketAddr = addr.parse()?;
@@ -740,11 +887,11 @@ impl Server {
740
887
  tracing::info!("Listening on http://{}", socket_addr);
741
888
 
742
889
  if config.graceful_shutdown {
743
- axum::serve(listener, app)
890
+ axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
744
891
  .with_graceful_shutdown(shutdown_signal())
745
892
  .await?;
746
893
  } else {
747
- axum::serve(listener, app).await?;
894
+ axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
748
895
  }
749
896
 
750
897
  Ok(())
@@ -755,13 +902,16 @@ impl Server {
755
902
  tracing_subscriber::registry()
756
903
  .with(
757
904
  tracing_subscriber::EnvFilter::try_from_default_env()
758
- .unwrap_or_else(|_| "spikard=debug,tower_http=debug".into()),
905
+ .unwrap_or_else(|_| "spikard=info,tower_http=info".into()),
759
906
  )
760
907
  .with(tracing_subscriber::fmt::layer())
761
908
  .init();
762
909
  }
763
910
 
764
911
  /// Start the server
912
+ ///
913
+ /// Coverage: Production-only, tested via integration tests
914
+ #[cfg(not(tarpaulin_include))]
765
915
  pub async fn serve(self) -> Result<(), Box<dyn std::error::Error>> {
766
916
  tracing::info!("Starting server with {} routes", self.router.route_count());
767
917
 
@@ -773,7 +923,7 @@ impl Server {
773
923
 
774
924
  tracing::info!("Listening on http://{}", socket_addr);
775
925
 
776
- axum::serve(listener, app).await?;
926
+ axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>()).await?;
777
927
 
778
928
  Ok(())
779
929
  }
@@ -786,7 +936,9 @@ impl Server {
786
936
 
787
937
  // TODO: Add routes from self.router
788
938
 
789
- app = app.layer(TraceLayer::new_for_http());
939
+ if self.config.enable_http_trace {
940
+ app = app.layer(TraceLayer::new_for_http());
941
+ }
790
942
 
791
943
  app
792
944
  }
@@ -795,6 +947,175 @@ impl Server {
795
947
  #[cfg(test)]
796
948
  mod tests {
797
949
  use super::*;
950
+ use std::pin::Pin;
951
+ use std::sync::Arc;
952
+
953
+ struct TestHandler;
954
+
955
+ impl Handler for TestHandler {
956
+ fn call(
957
+ &self,
958
+ _request: axum::http::Request<Body>,
959
+ _request_data: crate::handler_trait::RequestData,
960
+ ) -> Pin<Box<dyn std::future::Future<Output = crate::handler_trait::HandlerResult> + Send + '_>> {
961
+ Box::pin(async { Ok(axum::http::Response::builder().status(200).body(Body::empty()).unwrap()) })
962
+ }
963
+ }
964
+
965
+ fn build_test_route(path: &str, method: &str, handler_name: &str, expects_json_body: bool) -> crate::Route {
966
+ use std::str::FromStr;
967
+ crate::Route {
968
+ path: path.to_string(),
969
+ method: spikard_core::Method::from_str(method).expect("valid method"),
970
+ handler_name: handler_name.to_string(),
971
+ expects_json_body,
972
+ cors: None,
973
+ is_async: true,
974
+ file_params: None,
975
+ request_validator: None,
976
+ response_validator: None,
977
+ parameter_validator: None,
978
+ jsonrpc_method: None,
979
+ #[cfg(feature = "di")]
980
+ handler_dependencies: vec![],
981
+ }
982
+ }
983
+
984
+ fn build_test_route_with_cors(
985
+ path: &str,
986
+ method: &str,
987
+ handler_name: &str,
988
+ expects_json_body: bool,
989
+ cors: crate::CorsConfig,
990
+ ) -> crate::Route {
991
+ use std::str::FromStr;
992
+ crate::Route {
993
+ path: path.to_string(),
994
+ method: spikard_core::Method::from_str(method).expect("valid method"),
995
+ handler_name: handler_name.to_string(),
996
+ expects_json_body,
997
+ cors: Some(cors),
998
+ is_async: true,
999
+ file_params: None,
1000
+ request_validator: None,
1001
+ response_validator: None,
1002
+ parameter_validator: None,
1003
+ jsonrpc_method: None,
1004
+ #[cfg(feature = "di")]
1005
+ handler_dependencies: vec![],
1006
+ }
1007
+ }
1008
+
1009
+ #[test]
1010
+ fn test_method_expects_body_post() {
1011
+ assert!(method_expects_body(&crate::Method::Post));
1012
+ }
1013
+
1014
+ #[test]
1015
+ fn test_method_expects_body_put() {
1016
+ assert!(method_expects_body(&crate::Method::Put));
1017
+ }
1018
+
1019
+ #[test]
1020
+ fn test_method_expects_body_patch() {
1021
+ assert!(method_expects_body(&crate::Method::Patch));
1022
+ }
1023
+
1024
+ #[test]
1025
+ fn test_method_expects_body_get() {
1026
+ assert!(!method_expects_body(&crate::Method::Get));
1027
+ }
1028
+
1029
+ #[test]
1030
+ fn test_method_expects_body_delete() {
1031
+ assert!(!method_expects_body(&crate::Method::Delete));
1032
+ }
1033
+
1034
+ #[test]
1035
+ fn test_method_expects_body_head() {
1036
+ assert!(!method_expects_body(&crate::Method::Head));
1037
+ }
1038
+
1039
+ #[test]
1040
+ fn test_method_expects_body_options() {
1041
+ assert!(!method_expects_body(&crate::Method::Options));
1042
+ }
1043
+
1044
+ #[test]
1045
+ fn test_method_expects_body_trace() {
1046
+ assert!(!method_expects_body(&crate::Method::Trace));
1047
+ }
1048
+
1049
+ #[test]
1050
+ fn test_make_request_uuid_generates_valid_uuid() {
1051
+ let mut maker = MakeRequestUuid;
1052
+ let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1053
+
1054
+ let id = maker.make_request_id(&request);
1055
+
1056
+ assert!(id.is_some());
1057
+ let id_val = id.unwrap();
1058
+ let id_str = id_val.header_value().to_str().expect("valid utf8");
1059
+ assert!(!id_str.is_empty());
1060
+ assert!(Uuid::parse_str(id_str).is_ok());
1061
+ }
1062
+
1063
+ #[test]
1064
+ fn test_make_request_uuid_unique_per_call() {
1065
+ let mut maker = MakeRequestUuid;
1066
+ let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1067
+
1068
+ let id1 = maker.make_request_id(&request).unwrap();
1069
+ let id2 = maker.make_request_id(&request).unwrap();
1070
+
1071
+ let id1_str = id1.header_value().to_str().expect("valid utf8");
1072
+ let id2_str = id2.header_value().to_str().expect("valid utf8");
1073
+ assert_ne!(id1_str, id2_str);
1074
+ }
1075
+
1076
+ #[test]
1077
+ fn test_make_request_uuid_v4_format() {
1078
+ let mut maker = MakeRequestUuid;
1079
+ let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1080
+
1081
+ let id = maker.make_request_id(&request).unwrap();
1082
+ let id_str = id.header_value().to_str().expect("valid utf8");
1083
+
1084
+ let uuid = Uuid::parse_str(id_str).expect("valid UUID");
1085
+ assert_eq!(uuid.get_version(), Some(uuid::Version::Random));
1086
+ }
1087
+
1088
+ #[test]
1089
+ fn test_make_request_uuid_multiple_independent_makers() {
1090
+ let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1091
+
1092
+ let id1 = {
1093
+ let mut maker1 = MakeRequestUuid;
1094
+ maker1.make_request_id(&request).unwrap()
1095
+ };
1096
+ let id2 = {
1097
+ let mut maker2 = MakeRequestUuid;
1098
+ maker2.make_request_id(&request).unwrap()
1099
+ };
1100
+
1101
+ let id1_str = id1.header_value().to_str().expect("valid utf8");
1102
+ let id2_str = id2.header_value().to_str().expect("valid utf8");
1103
+ assert_ne!(id1_str, id2_str);
1104
+ }
1105
+
1106
+ #[test]
1107
+ fn test_make_request_uuid_clone_independence() {
1108
+ let mut maker1 = MakeRequestUuid;
1109
+ let mut maker2 = maker1.clone();
1110
+ let request = axum::http::Request::builder().body(Body::empty()).unwrap();
1111
+
1112
+ let id1 = maker1.make_request_id(&request).unwrap();
1113
+ let id2 = maker2.make_request_id(&request).unwrap();
1114
+
1115
+ let id1_str = id1.header_value().to_str().expect("valid utf8");
1116
+ let id2_str = id2.header_value().to_str().expect("valid utf8");
1117
+ assert_ne!(id1_str, id2_str);
1118
+ }
798
1119
 
799
1120
  #[test]
800
1121
  fn test_server_creation() {
@@ -802,4 +1123,380 @@ mod tests {
802
1123
  let router = Router::new();
803
1124
  let _server = Server::new(config, router);
804
1125
  }
1126
+
1127
+ #[test]
1128
+ fn test_server_creation_with_custom_host_port() {
1129
+ let mut config = ServerConfig::default();
1130
+ config.host = "0.0.0.0".to_string();
1131
+ config.port = 3000;
1132
+
1133
+ let router = Router::new();
1134
+ let server = Server::new(config.clone(), router);
1135
+
1136
+ assert_eq!(server.config.host, "0.0.0.0");
1137
+ assert_eq!(server.config.port, 3000);
1138
+ }
1139
+
1140
+ #[test]
1141
+ fn test_server_config_default_values() {
1142
+ let config = ServerConfig::default();
1143
+
1144
+ assert_eq!(config.host, "127.0.0.1");
1145
+ assert_eq!(config.port, 8000);
1146
+ assert_eq!(config.workers, 1);
1147
+ assert!(!config.enable_request_id);
1148
+ assert!(config.max_body_size.is_some());
1149
+ assert!(config.request_timeout.is_none());
1150
+ assert!(config.graceful_shutdown);
1151
+ }
1152
+
1153
+ #[test]
1154
+ fn test_server_config_builder_pattern() {
1155
+ let config = ServerConfig::builder().port(9000).host("0.0.0.0".to_string()).build();
1156
+
1157
+ assert_eq!(config.port, 9000);
1158
+ assert_eq!(config.host, "0.0.0.0");
1159
+ }
1160
+
1161
+ #[cfg(feature = "di")]
1162
+ fn build_router_for_tests(
1163
+ routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1164
+ hooks: Option<Arc<crate::LifecycleHooks>>,
1165
+ ) -> Result<AxumRouter, String> {
1166
+ build_router_with_handlers(routes, hooks, None)
1167
+ }
1168
+
1169
+ #[cfg(not(feature = "di"))]
1170
+ fn build_router_for_tests(
1171
+ routes: Vec<(crate::Route, Arc<dyn Handler>)>,
1172
+ hooks: Option<Arc<crate::LifecycleHooks>>,
1173
+ ) -> Result<AxumRouter, String> {
1174
+ build_router_with_handlers(routes, hooks)
1175
+ }
1176
+
1177
+ #[test]
1178
+ fn test_route_registry_empty_routes() {
1179
+ let routes: Vec<(crate::Route, Arc<dyn Handler>)> = vec![];
1180
+ let _result = build_router_for_tests(routes, None);
1181
+ }
1182
+
1183
+ #[test]
1184
+ fn test_route_registry_single_route() {
1185
+ let route = build_test_route("/test", "GET", "test_handler", false);
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_path_normalization_without_leading_slash() {
1196
+ let route = build_test_route("api/users", "GET", "list_users", 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_path_normalization_with_leading_slash() {
1207
+ let route = build_test_route("/api/users", "GET", "list_users", false);
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_multiple_routes_same_path_different_methods() {
1218
+ let get_route = build_test_route("/users", "GET", "list_users", false);
1219
+ let post_route = build_test_route("/users", "POST", "create_user", true);
1220
+
1221
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1222
+ let routes = vec![(get_route, handler.clone()), (post_route, handler)];
1223
+
1224
+ let result = build_router_for_tests(routes, None);
1225
+ assert!(result.is_ok());
1226
+ }
1227
+
1228
+ #[test]
1229
+ fn test_multiple_different_routes() {
1230
+ let users_route = build_test_route("/users", "GET", "list_users", false);
1231
+ let posts_route = build_test_route("/posts", "GET", "list_posts", false);
1232
+
1233
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1234
+ let routes = vec![(users_route, handler.clone()), (posts_route, handler)];
1235
+
1236
+ let result = build_router_for_tests(routes, None);
1237
+ assert!(result.is_ok());
1238
+ }
1239
+
1240
+ #[test]
1241
+ fn test_route_with_single_path_parameter() {
1242
+ let route = build_test_route("/users/{id}", "GET", "get_user", false);
1243
+
1244
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1245
+ let routes = vec![(route, handler)];
1246
+
1247
+ let result = build_router_for_tests(routes, None);
1248
+ assert!(result.is_ok());
1249
+ }
1250
+
1251
+ #[test]
1252
+ fn test_route_with_multiple_path_parameters() {
1253
+ let route = build_test_route("/users/{user_id}/posts/{post_id}", "GET", "get_user_post", false);
1254
+
1255
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1256
+ let routes = vec![(route, handler)];
1257
+
1258
+ let result = build_router_for_tests(routes, None);
1259
+ assert!(result.is_ok());
1260
+ }
1261
+
1262
+ #[test]
1263
+ fn test_route_with_path_parameter_post_with_body() {
1264
+ let route = build_test_route("/users/{id}", "PUT", "update_user", true);
1265
+
1266
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1267
+ let routes = vec![(route, handler)];
1268
+
1269
+ let result = build_router_for_tests(routes, None);
1270
+ assert!(result.is_ok());
1271
+ }
1272
+
1273
+ #[test]
1274
+ fn test_route_with_path_parameter_delete() {
1275
+ let route = build_test_route("/users/{id}", "DELETE", "delete_user", false);
1276
+
1277
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1278
+ let routes = vec![(route, handler)];
1279
+
1280
+ let result = build_router_for_tests(routes, None);
1281
+ assert!(result.is_ok());
1282
+ }
1283
+
1284
+ #[test]
1285
+ fn test_route_post_method_with_body() {
1286
+ let route = build_test_route("/users", "POST", "create_user", true);
1287
+
1288
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1289
+ let routes = vec![(route, handler)];
1290
+
1291
+ let result = build_router_for_tests(routes, None);
1292
+ assert!(result.is_ok());
1293
+ }
1294
+
1295
+ #[test]
1296
+ fn test_route_put_method_with_body() {
1297
+ let route = build_test_route("/users/{id}", "PUT", "update_user", true);
1298
+
1299
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1300
+ let routes = vec![(route, handler)];
1301
+
1302
+ let result = build_router_for_tests(routes, None);
1303
+ assert!(result.is_ok());
1304
+ }
1305
+
1306
+ #[test]
1307
+ fn test_route_patch_method_with_body() {
1308
+ let route = build_test_route("/users/{id}", "PATCH", "patch_user", true);
1309
+
1310
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1311
+ let routes = vec![(route, handler)];
1312
+
1313
+ let result = build_router_for_tests(routes, None);
1314
+ assert!(result.is_ok());
1315
+ }
1316
+
1317
+ #[test]
1318
+ fn test_route_head_method() {
1319
+ let route = build_test_route("/users", "HEAD", "head_users", false);
1320
+
1321
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1322
+ let routes = vec![(route, handler)];
1323
+
1324
+ let result = build_router_for_tests(routes, None);
1325
+ assert!(result.is_ok());
1326
+ }
1327
+
1328
+ #[test]
1329
+ fn test_route_options_method() {
1330
+ let route = build_test_route("/users", "OPTIONS", "options_users", false);
1331
+
1332
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1333
+ let routes = vec![(route, handler)];
1334
+
1335
+ let result = build_router_for_tests(routes, None);
1336
+ assert!(result.is_ok());
1337
+ }
1338
+
1339
+ #[test]
1340
+ fn test_route_trace_method() {
1341
+ let route = build_test_route("/users", "TRACE", "trace_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, None);
1347
+ assert!(result.is_ok());
1348
+ }
1349
+
1350
+ #[test]
1351
+ fn test_route_with_cors_config() {
1352
+ let cors_config = crate::CorsConfig {
1353
+ allowed_origins: vec!["https://example.com".to_string()],
1354
+ allowed_methods: vec!["GET".to_string(), "POST".to_string()],
1355
+ allowed_headers: vec!["Content-Type".to_string()],
1356
+ expose_headers: None,
1357
+ max_age: Some(3600),
1358
+ allow_credentials: Some(true),
1359
+ };
1360
+
1361
+ let route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config);
1362
+
1363
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1364
+ let routes = vec![(route, handler)];
1365
+
1366
+ let result = build_router_for_tests(routes, None);
1367
+ assert!(result.is_ok());
1368
+ }
1369
+
1370
+ #[test]
1371
+ fn test_multiple_routes_with_cors_same_path() {
1372
+ let cors_config = crate::CorsConfig {
1373
+ allowed_origins: vec!["https://example.com".to_string()],
1374
+ allowed_methods: vec!["GET".to_string(), "POST".to_string()],
1375
+ allowed_headers: vec!["Content-Type".to_string()],
1376
+ expose_headers: None,
1377
+ max_age: Some(3600),
1378
+ allow_credentials: Some(true),
1379
+ };
1380
+
1381
+ let get_route = build_test_route_with_cors("/users", "GET", "list_users", false, cors_config.clone());
1382
+ let post_route = build_test_route_with_cors("/users", "POST", "create_user", true, cors_config);
1383
+
1384
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1385
+ let routes = vec![(get_route, handler.clone()), (post_route, handler)];
1386
+
1387
+ let result = build_router_for_tests(routes, None);
1388
+ assert!(result.is_ok());
1389
+ }
1390
+
1391
+ #[test]
1392
+ fn test_routes_sorted_by_path() {
1393
+ let zebra_route = build_test_route("/zebra", "GET", "get_zebra", false);
1394
+ let alpha_route = build_test_route("/alpha", "GET", "get_alpha", false);
1395
+
1396
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1397
+ let routes = vec![(zebra_route, handler.clone()), (alpha_route, handler)];
1398
+
1399
+ let result = build_router_for_tests(routes, None);
1400
+ assert!(result.is_ok());
1401
+ }
1402
+
1403
+ #[test]
1404
+ fn test_routes_with_nested_paths() {
1405
+ let parent_route = build_test_route("/api", "GET", "get_api", false);
1406
+ let child_route = build_test_route("/api/users", "GET", "get_users", false);
1407
+
1408
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1409
+ let routes = vec![(parent_route, handler.clone()), (child_route, handler)];
1410
+
1411
+ let result = build_router_for_tests(routes, None);
1412
+ assert!(result.is_ok());
1413
+ }
1414
+
1415
+ #[test]
1416
+ fn test_routes_with_lifecycle_hooks() {
1417
+ let hooks = crate::LifecycleHooks::new();
1418
+ let hooks = Arc::new(hooks);
1419
+
1420
+ let route = build_test_route("/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, Some(hooks));
1426
+ assert!(result.is_ok());
1427
+ }
1428
+
1429
+ #[test]
1430
+ fn test_routes_without_lifecycle_hooks() {
1431
+ let route = build_test_route("/users", "GET", "list_users", false);
1432
+
1433
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1434
+ let routes = vec![(route, handler)];
1435
+
1436
+ let result = build_router_for_tests(routes, None);
1437
+ assert!(result.is_ok());
1438
+ }
1439
+
1440
+ #[test]
1441
+ fn test_route_with_trailing_slash() {
1442
+ let route = build_test_route("/users/", "GET", "list_users", false);
1443
+
1444
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1445
+ let routes = vec![(route, handler)];
1446
+
1447
+ let result = build_router_for_tests(routes, None);
1448
+ assert!(result.is_ok());
1449
+ }
1450
+
1451
+ #[test]
1452
+ fn test_route_with_root_path() {
1453
+ let route = build_test_route("/", "GET", "root_handler", false);
1454
+
1455
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1456
+ let routes = vec![(route, handler)];
1457
+
1458
+ let result = build_router_for_tests(routes, None);
1459
+ assert!(result.is_ok());
1460
+ }
1461
+
1462
+ #[test]
1463
+ fn test_large_number_of_routes() {
1464
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1465
+ let mut routes = vec![];
1466
+
1467
+ for i in 0..50 {
1468
+ let route = build_test_route(&format!("/route{}", i), "GET", &format!("handler_{}", i), false);
1469
+ routes.push((route, handler.clone()));
1470
+ }
1471
+
1472
+ let result = build_router_for_tests(routes, None);
1473
+ assert!(result.is_ok());
1474
+ }
1475
+
1476
+ #[test]
1477
+ fn test_route_with_query_params_in_path_definition() {
1478
+ let route = build_test_route("/search", "GET", "search", false);
1479
+
1480
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1481
+ let routes = vec![(route, handler)];
1482
+
1483
+ let result = build_router_for_tests(routes, None);
1484
+ assert!(result.is_ok());
1485
+ }
1486
+
1487
+ #[test]
1488
+ fn test_all_http_methods_on_same_path() {
1489
+ let handler: Arc<dyn Handler> = Arc::new(TestHandler);
1490
+ let methods = vec!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"];
1491
+
1492
+ let mut routes = vec![];
1493
+ for method in methods {
1494
+ let expects_body = matches!(method, "POST" | "PUT" | "PATCH");
1495
+ let route = build_test_route("/resource", method, &format!("handler_{}", method), expects_body);
1496
+ routes.push((route, handler.clone()));
1497
+ }
1498
+
1499
+ let result = build_router_for_tests(routes, None);
1500
+ assert!(result.is_ok());
1501
+ }
805
1502
  }