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.
- checksums.yaml +4 -4
- data/README.md +21 -6
- data/ext/spikard_rb/Cargo.toml +2 -2
- data/lib/spikard/app.rb +33 -14
- data/lib/spikard/testing.rb +47 -12
- data/lib/spikard/version.rb +1 -1
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
- data/vendor/crates/spikard-core/Cargo.toml +4 -4
- data/vendor/crates/spikard-core/src/debug.rs +64 -0
- data/vendor/crates/spikard-core/src/di/container.rs +3 -27
- data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
- data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
- data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
- data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
- data/vendor/crates/spikard-core/src/di/value.rs +2 -4
- data/vendor/crates/spikard-core/src/errors.rs +30 -0
- data/vendor/crates/spikard-core/src/http.rs +262 -0
- data/vendor/crates/spikard-core/src/lib.rs +1 -1
- data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
- data/vendor/crates/spikard-core/src/metadata.rs +389 -0
- data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
- data/vendor/crates/spikard-core/src/problem.rs +34 -0
- data/vendor/crates/spikard-core/src/request_data.rs +966 -1
- data/vendor/crates/spikard-core/src/router.rs +263 -2
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
- data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
- data/vendor/crates/spikard-http/Cargo.toml +12 -16
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
- data/vendor/crates/spikard-http/src/auth.rs +65 -16
- data/vendor/crates/spikard-http/src/background.rs +1614 -3
- data/vendor/crates/spikard-http/src/cors.rs +515 -0
- data/vendor/crates/spikard-http/src/debug.rs +65 -0
- data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
- data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
- data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
- data/vendor/crates/spikard-http/src/lib.rs +33 -28
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
- data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
- data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
- data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
- data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
- data/vendor/crates/spikard-http/src/response.rs +321 -0
- data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
- data/vendor/crates/spikard-http/src/sse.rs +983 -21
- data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
- data/vendor/crates/spikard-http/src/testing.rs +7 -7
- data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
- data/vendor/crates/spikard-rb/Cargo.toml +10 -4
- data/vendor/crates/spikard-rb/build.rs +196 -5
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
- data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
- data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
- data/vendor/crates/spikard-rb/src/handler.rs +100 -107
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
- data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
- data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
- data/vendor/crates/spikard-rb/src/server.rs +47 -22
- data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
- data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
- metadata +46 -13
- data/vendor/crates/spikard-http/src/parameters.rs +0 -1
- data/vendor/crates/spikard-http/src/problem.rs +0 -1
- data/vendor/crates/spikard-http/src/router.rs +0 -1
- data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
- data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
- data/vendor/crates/spikard-http/src/validation.rs +0 -1
- data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
- /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: &
|
|
48
|
-
matches!(method,
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
418
|
-
|
|
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
|
-
|
|
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 =>
|
|
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 =
|
|
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
|
-
|
|
465
|
-
|
|
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 =
|
|
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 =
|
|
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=
|
|
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
|
-
|
|
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
|
}
|