spikard 0.8.3 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +19 -10
- data/ext/spikard_rb/Cargo.lock +234 -162
- data/ext/spikard_rb/Cargo.toml +2 -2
- data/ext/spikard_rb/extconf.rb +4 -3
- data/lib/spikard/config.rb +88 -12
- data/lib/spikard/testing.rb +3 -1
- data/lib/spikard/version.rb +1 -1
- data/lib/spikard.rb +11 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +3 -6
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
- data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
- data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
- data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
- data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
- data/vendor/crates/spikard-core/Cargo.toml +3 -3
- data/vendor/crates/spikard-core/src/di/container.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
- data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
- data/vendor/crates/spikard-core/src/di/value.rs +1 -1
- data/vendor/crates/spikard-core/src/http.rs +75 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
- data/vendor/crates/spikard-core/src/parameters.rs +14 -19
- data/vendor/crates/spikard-core/src/problem.rs +1 -1
- data/vendor/crates/spikard-core/src/request_data.rs +7 -16
- data/vendor/crates/spikard-core/src/router.rs +6 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
- data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
- data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
- data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
- data/vendor/crates/spikard-http/Cargo.toml +4 -2
- data/vendor/crates/spikard-http/src/cors.rs +32 -11
- data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
- data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
- data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
- data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
- data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
- data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
- data/vendor/crates/spikard-http/src/lib.rs +1 -1
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
- data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
- data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
- data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
- data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
- data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
- data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
- data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
- data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
- data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
- data/vendor/crates/spikard-rb/Cargo.toml +3 -1
- data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
- data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
- data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
- data/vendor/crates/spikard-rb/src/handler.rs +169 -91
- data/vendor/crates/spikard-rb/src/lib.rs +444 -62
- data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
- data/vendor/crates/spikard-rb/src/request.rs +117 -20
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
- data/vendor/crates/spikard-rb/src/server.rs +23 -14
- data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
- data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
- data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
- data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
- metadata +14 -4
- data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
- data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
//! Request parsing and data extraction utilities
|
|
2
|
+
//!
|
|
3
|
+
//! Performance optimizations in this module:
|
|
4
|
+
//! - Static singletons for empty collections avoid repeated allocations
|
|
5
|
+
//! - HashMap::with_capacity pre-allocates based on expected sizes
|
|
6
|
+
//! - Arc wrapping enables cheap cloning of RequestData
|
|
2
7
|
|
|
3
8
|
use crate::handler_trait::RequestData;
|
|
4
9
|
use crate::query_parser::{parse_query_pairs_to_json, parse_query_string};
|
|
@@ -9,6 +14,28 @@ use std::collections::HashMap;
|
|
|
9
14
|
use std::sync::Arc;
|
|
10
15
|
use std::sync::OnceLock;
|
|
11
16
|
|
|
17
|
+
// Performance: Static singletons for empty collections.
|
|
18
|
+
// These avoid allocating new empty HashMaps/Values for every request that doesn't
|
|
19
|
+
// need them (e.g., requests without query params, headers disabled, etc.).
|
|
20
|
+
|
|
21
|
+
/// Static empty JSON object value, shared across all requests without query params.
|
|
22
|
+
fn empty_json_object() -> Arc<Value> {
|
|
23
|
+
static EMPTY: OnceLock<Arc<Value>> = OnceLock::new();
|
|
24
|
+
Arc::clone(EMPTY.get_or_init(|| Arc::new(Value::Object(serde_json::Map::new()))))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Static null JSON value for requests without bodies.
|
|
28
|
+
fn null_json_value() -> Arc<Value> {
|
|
29
|
+
static NULL: OnceLock<Arc<Value>> = OnceLock::new();
|
|
30
|
+
Arc::clone(NULL.get_or_init(|| Arc::new(Value::Null)))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Static empty path params map.
|
|
34
|
+
fn empty_path_params() -> Arc<HashMap<String, String>> {
|
|
35
|
+
static EMPTY: OnceLock<Arc<HashMap<String, String>>> = OnceLock::new();
|
|
36
|
+
Arc::clone(EMPTY.get_or_init(|| Arc::new(HashMap::new())))
|
|
37
|
+
}
|
|
38
|
+
|
|
12
39
|
#[derive(Debug, Clone, Copy)]
|
|
13
40
|
pub struct WithoutBodyExtractionOptions {
|
|
14
41
|
pub include_raw_query_params: bool,
|
|
@@ -24,6 +51,7 @@ fn extract_query_params_and_raw(
|
|
|
24
51
|
) -> (Value, HashMap<String, Vec<String>>) {
|
|
25
52
|
let query_string = uri.query().unwrap_or("");
|
|
26
53
|
if query_string.is_empty() {
|
|
54
|
+
// Performance: Return empty object for empty query string.
|
|
27
55
|
return (Value::Object(serde_json::Map::new()), HashMap::new());
|
|
28
56
|
}
|
|
29
57
|
|
|
@@ -34,22 +62,23 @@ fn extract_query_params_and_raw(
|
|
|
34
62
|
HashMap::new(),
|
|
35
63
|
),
|
|
36
64
|
(true, false) => {
|
|
37
|
-
let
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
65
|
+
let pairs = parse_query_string(query_string.as_bytes(), '&');
|
|
66
|
+
// Performance: Pre-allocate HashMap with estimated unique key count.
|
|
67
|
+
// In practice, most keys are unique, so pairs.len() is a good estimate.
|
|
68
|
+
let mut raw = HashMap::with_capacity(pairs.len());
|
|
69
|
+
for (k, v) in pairs {
|
|
70
|
+
raw.entry(k).or_insert_with(Vec::new).push(v);
|
|
71
|
+
}
|
|
44
72
|
(Value::Null, raw)
|
|
45
73
|
}
|
|
46
74
|
(true, true) => {
|
|
47
75
|
let pairs = parse_query_string(query_string.as_bytes(), '&');
|
|
48
76
|
let json = parse_query_pairs_to_json(&pairs, true);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
77
|
+
// Performance: Pre-allocate HashMap with estimated unique key count.
|
|
78
|
+
let mut raw = HashMap::with_capacity(pairs.len());
|
|
79
|
+
for (k, v) in pairs {
|
|
80
|
+
raw.entry(k).or_insert_with(Vec::new).push(v);
|
|
81
|
+
}
|
|
53
82
|
(json, raw)
|
|
54
83
|
}
|
|
55
84
|
}
|
|
@@ -67,18 +96,21 @@ pub fn extract_query_params(uri: &axum::http::Uri) -> Value {
|
|
|
67
96
|
|
|
68
97
|
/// Extract raw query parameters as strings (no type conversion)
|
|
69
98
|
/// Used for validation error messages to show the actual input values
|
|
99
|
+
///
|
|
100
|
+
/// Performance: Pre-allocates HashMap based on parsed pair count.
|
|
70
101
|
pub fn extract_raw_query_params(uri: &axum::http::Uri) -> HashMap<String, Vec<String>> {
|
|
71
102
|
let query_string = uri.query().unwrap_or("");
|
|
72
103
|
if query_string.is_empty() {
|
|
73
|
-
HashMap::new()
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
104
|
+
return HashMap::new();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let pairs = parse_query_string(query_string.as_bytes(), '&');
|
|
108
|
+
// Performance: Pre-allocate with estimated unique key count.
|
|
109
|
+
let mut map = HashMap::with_capacity(pairs.len());
|
|
110
|
+
for (k, v) in pairs {
|
|
111
|
+
map.entry(k).or_insert_with(Vec::new).push(v);
|
|
81
112
|
}
|
|
113
|
+
map
|
|
82
114
|
}
|
|
83
115
|
|
|
84
116
|
/// Extract headers from request
|
|
@@ -107,13 +139,21 @@ fn extract_content_type_header(headers: &axum::http::HeaderMap) -> Arc<HashMap<S
|
|
|
107
139
|
}
|
|
108
140
|
|
|
109
141
|
/// Extract cookies from request headers
|
|
142
|
+
///
|
|
143
|
+
/// Performance: Pre-allocates HashMap capacity based on estimated cookie count
|
|
144
|
+
/// by counting semicolons in the cookie string (each cookie separated by "; ").
|
|
110
145
|
pub fn extract_cookies(headers: &axum::http::HeaderMap) -> HashMap<String, String> {
|
|
111
|
-
let
|
|
146
|
+
let Some(cookie_str) = headers.get(axum::http::header::COOKIE).and_then(|h| h.to_str().ok()) else {
|
|
147
|
+
return HashMap::new();
|
|
148
|
+
};
|
|
112
149
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
150
|
+
// Performance: Estimate cookie count by counting semicolons + 1.
|
|
151
|
+
// Typical cookie string: "session=abc; user=123; theme=dark"
|
|
152
|
+
let estimated_count = cookie_str.bytes().filter(|&b| b == b';').count() + 1;
|
|
153
|
+
let mut cookies = HashMap::with_capacity(estimated_count);
|
|
154
|
+
|
|
155
|
+
for cookie in cookie::Cookie::split_parse(cookie_str).flatten() {
|
|
156
|
+
cookies.insert(cookie.name().to_string(), cookie.value().to_string());
|
|
117
157
|
}
|
|
118
158
|
|
|
119
159
|
cookies
|
|
@@ -132,6 +172,10 @@ fn empty_raw_query_map() -> Arc<HashMap<String, Vec<String>>> {
|
|
|
132
172
|
/// Create RequestData from request parts (for requests without body)
|
|
133
173
|
///
|
|
134
174
|
/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
|
|
175
|
+
///
|
|
176
|
+
/// Performance optimizations:
|
|
177
|
+
/// - Uses static singletons for empty path params, body, headers, cookies
|
|
178
|
+
/// - Only allocates when data is present
|
|
135
179
|
pub fn create_request_data_without_body(
|
|
136
180
|
uri: &axum::http::Uri,
|
|
137
181
|
method: &axum::http::Method,
|
|
@@ -141,9 +185,26 @@ pub fn create_request_data_without_body(
|
|
|
141
185
|
) -> RequestData {
|
|
142
186
|
let (query_params, raw_query_params) =
|
|
143
187
|
extract_query_params_and_raw(uri, options.include_raw_query_params, options.include_query_params_json);
|
|
188
|
+
|
|
189
|
+
// Performance: Use static singleton for empty path params (common for routes without params).
|
|
190
|
+
let path_params_arc = if path_params.is_empty() {
|
|
191
|
+
empty_path_params()
|
|
192
|
+
} else {
|
|
193
|
+
Arc::new(path_params)
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Performance: Use static singleton for empty query params.
|
|
197
|
+
let query_params_arc = if matches!(query_params, Value::Object(ref m) if m.is_empty()) {
|
|
198
|
+
empty_json_object()
|
|
199
|
+
} else if matches!(query_params, Value::Null) {
|
|
200
|
+
null_json_value()
|
|
201
|
+
} else {
|
|
202
|
+
Arc::new(query_params)
|
|
203
|
+
};
|
|
204
|
+
|
|
144
205
|
RequestData {
|
|
145
|
-
path_params:
|
|
146
|
-
query_params,
|
|
206
|
+
path_params: path_params_arc,
|
|
207
|
+
query_params: query_params_arc,
|
|
147
208
|
raw_query_params: if raw_query_params.is_empty() {
|
|
148
209
|
empty_raw_query_map()
|
|
149
210
|
} else {
|
|
@@ -160,7 +221,8 @@ pub fn create_request_data_without_body(
|
|
|
160
221
|
} else {
|
|
161
222
|
empty_string_map()
|
|
162
223
|
},
|
|
163
|
-
|
|
224
|
+
// Performance: Use static null value singleton.
|
|
225
|
+
body: null_json_value(),
|
|
164
226
|
raw_body: None,
|
|
165
227
|
method: method.as_str().to_string(),
|
|
166
228
|
path: uri.path().to_string(),
|
|
@@ -172,8 +234,11 @@ pub fn create_request_data_without_body(
|
|
|
172
234
|
/// Create RequestData from request parts (for requests with body)
|
|
173
235
|
///
|
|
174
236
|
/// Wraps HashMaps in Arc to enable cheap cloning without duplicating data.
|
|
175
|
-
///
|
|
176
|
-
///
|
|
237
|
+
///
|
|
238
|
+
/// Performance optimizations:
|
|
239
|
+
/// - Stores raw body bytes without parsing JSON (deferred parsing)
|
|
240
|
+
/// - Uses static singletons for empty collections
|
|
241
|
+
/// - Pre-read body bytes are reused if available in extensions
|
|
177
242
|
pub async fn create_request_data_with_body(
|
|
178
243
|
parts: &axum::http::request::Parts,
|
|
179
244
|
path_params: HashMap<String, String>,
|
|
@@ -200,15 +265,32 @@ pub async fn create_request_data_with_body(
|
|
|
200
265
|
let (query_params, raw_query_params) =
|
|
201
266
|
extract_query_params_and_raw(&parts.uri, include_raw_query_params, include_query_params_json);
|
|
202
267
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
268
|
+
// Performance: Use static singleton for empty path params.
|
|
269
|
+
let path_params_arc = if path_params.is_empty() {
|
|
270
|
+
empty_path_params()
|
|
271
|
+
} else {
|
|
272
|
+
Arc::new(path_params)
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// Performance: Use static singleton for empty/null query params.
|
|
276
|
+
let query_params_arc = if matches!(query_params, Value::Object(ref m) if m.is_empty()) {
|
|
277
|
+
empty_json_object()
|
|
278
|
+
} else if matches!(query_params, Value::Null) {
|
|
279
|
+
null_json_value()
|
|
280
|
+
} else {
|
|
281
|
+
Arc::new(query_params)
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// Performance: Reuse pre-parsed JSON if available, otherwise use static null.
|
|
285
|
+
let body_arc = if let Some(parsed) = parts.extensions.get::<crate::middleware::PreParsedJson>() {
|
|
286
|
+
Arc::new(parsed.0.clone())
|
|
287
|
+
} else {
|
|
288
|
+
null_json_value()
|
|
289
|
+
};
|
|
208
290
|
|
|
209
291
|
Ok(RequestData {
|
|
210
|
-
path_params:
|
|
211
|
-
query_params,
|
|
292
|
+
path_params: path_params_arc,
|
|
293
|
+
query_params: query_params_arc,
|
|
212
294
|
raw_query_params: if raw_query_params.is_empty() {
|
|
213
295
|
empty_raw_query_map()
|
|
214
296
|
} else {
|
|
@@ -225,7 +307,7 @@ pub async fn create_request_data_with_body(
|
|
|
225
307
|
} else {
|
|
226
308
|
empty_string_map()
|
|
227
309
|
},
|
|
228
|
-
body:
|
|
310
|
+
body: body_arc,
|
|
229
311
|
raw_body: if body_bytes.is_empty() { None } else { Some(body_bytes) },
|
|
230
312
|
method: parts.method.as_str().to_string(),
|
|
231
313
|
path: parts.uri.path().to_string(),
|
|
@@ -477,11 +559,11 @@ mod tests {
|
|
|
477
559
|
assert_eq!(result.method, "GET");
|
|
478
560
|
assert_eq!(result.path, "/test");
|
|
479
561
|
assert!(result.path_params.is_empty());
|
|
480
|
-
assert_eq!(result.query_params, json!({}));
|
|
562
|
+
assert_eq!(*result.query_params, json!({}));
|
|
481
563
|
assert!(result.raw_query_params.is_empty());
|
|
482
564
|
assert!(result.headers.is_empty());
|
|
483
565
|
assert!(result.cookies.is_empty());
|
|
484
|
-
assert_eq!(result.body, Value::Null);
|
|
566
|
+
assert_eq!(*result.body, Value::Null);
|
|
485
567
|
assert!(result.raw_body.is_none());
|
|
486
568
|
}
|
|
487
569
|
|
|
@@ -507,7 +589,7 @@ mod tests {
|
|
|
507
589
|
|
|
508
590
|
let result = create_request_data_without_body(&uri, &method, &headers, path_params, OPTIONS_ALL);
|
|
509
591
|
|
|
510
|
-
assert_eq!(result.query_params, json!({"q": "rust", "limit": 10}));
|
|
592
|
+
assert_eq!(*result.query_params, json!({"q": "rust", "limit": 10}));
|
|
511
593
|
assert_eq!(result.raw_query_params.get("q"), Some(&vec!["rust".to_string()]));
|
|
512
594
|
assert_eq!(result.raw_query_params.get("limit"), Some(&vec!["10".to_string()]));
|
|
513
595
|
}
|
|
@@ -603,7 +685,7 @@ mod tests {
|
|
|
603
685
|
|
|
604
686
|
assert_eq!(result.method, "POST");
|
|
605
687
|
assert_eq!(result.path, "/test");
|
|
606
|
-
assert_eq!(result.body, Value::Null);
|
|
688
|
+
assert_eq!(*result.body, Value::Null);
|
|
607
689
|
assert!(result.raw_body.is_none());
|
|
608
690
|
}
|
|
609
691
|
|
|
@@ -624,7 +706,7 @@ mod tests {
|
|
|
624
706
|
.unwrap();
|
|
625
707
|
|
|
626
708
|
assert_eq!(result.method, "POST");
|
|
627
|
-
assert_eq!(result.body, Value::Null);
|
|
709
|
+
assert_eq!(*result.body, Value::Null);
|
|
628
710
|
assert!(result.raw_body.is_some());
|
|
629
711
|
assert_eq!(result.raw_body.as_ref().unwrap().as_ref(), br#"{"key":"value"}"#);
|
|
630
712
|
}
|
|
@@ -645,7 +727,7 @@ mod tests {
|
|
|
645
727
|
.await
|
|
646
728
|
.unwrap();
|
|
647
729
|
|
|
648
|
-
assert_eq!(result.query_params, json!({"foo": "bar", "baz": "qux"}));
|
|
730
|
+
assert_eq!(*result.query_params, json!({"foo": "bar", "baz": "qux"}));
|
|
649
731
|
}
|
|
650
732
|
|
|
651
733
|
#[tokio::test]
|
|
@@ -741,7 +823,7 @@ mod tests {
|
|
|
741
823
|
assert_eq!(result.method, "PUT");
|
|
742
824
|
assert_eq!(result.path, "/users/42");
|
|
743
825
|
assert_eq!(result.path_params.get("user_id"), Some(&"42".to_string()));
|
|
744
|
-
assert_eq!(result.query_params, json!({"action": "update"}));
|
|
826
|
+
assert_eq!(*result.query_params, json!({"action": "update"}));
|
|
745
827
|
assert!(result.headers.contains_key("authorization"));
|
|
746
828
|
assert!(result.cookies.contains_key("session"));
|
|
747
829
|
assert!(result.raw_body.is_some());
|
|
@@ -19,6 +19,10 @@ use std::sync::Arc;
|
|
|
19
19
|
use super::lifecycle_execution;
|
|
20
20
|
use super::request_extraction;
|
|
21
21
|
|
|
22
|
+
/// Execute handler with optional lifecycle hooks.
|
|
23
|
+
///
|
|
24
|
+
/// Performance: Checks if hooks are present and non-empty before incurring
|
|
25
|
+
/// the overhead of lifecycle execution. Most requests don't have hooks.
|
|
22
26
|
#[inline]
|
|
23
27
|
async fn call_with_optional_hooks(
|
|
24
28
|
req: Request<Body>,
|
|
@@ -26,8 +30,10 @@ async fn call_with_optional_hooks(
|
|
|
26
30
|
handler: Arc<dyn Handler>,
|
|
27
31
|
hooks: Option<Arc<crate::LifecycleHooks>>,
|
|
28
32
|
) -> HandlerResult {
|
|
33
|
+
// Performance: Fast path for requests without hooks (common case).
|
|
34
|
+
// Only invoke lifecycle execution when hooks are actually registered.
|
|
29
35
|
if hooks.as_ref().is_some_and(|h| !h.is_empty()) {
|
|
30
|
-
|
|
36
|
+
lifecycle_execution::execute_with_lifecycle_hooks(req, request_data, handler, hooks).await
|
|
31
37
|
} else {
|
|
32
38
|
handler.call(req, request_data).await
|
|
33
39
|
}
|
|
@@ -122,6 +128,9 @@ impl MethodRouterFactory {
|
|
|
122
128
|
}
|
|
123
129
|
|
|
124
130
|
/// Create a method router for a route with path parameters
|
|
131
|
+
///
|
|
132
|
+
/// Performance: Each match arm only clones the Arc when needed for that specific
|
|
133
|
+
/// HTTP method, avoiding redundant clones at the function level.
|
|
125
134
|
fn create_with_path_params(
|
|
126
135
|
method: HttpMethod,
|
|
127
136
|
handler: Arc<dyn Handler>,
|
|
@@ -131,8 +140,7 @@ impl MethodRouterFactory {
|
|
|
131
140
|
include_headers: bool,
|
|
132
141
|
include_cookies: bool,
|
|
133
142
|
) -> MethodRouter {
|
|
134
|
-
|
|
135
|
-
let hooks_clone = hooks.clone();
|
|
143
|
+
// Performance: Removed redundant outer clones. Each match arm clones only when needed.
|
|
136
144
|
let without_body_options = request_extraction::WithoutBodyExtractionOptions {
|
|
137
145
|
include_raw_query_params,
|
|
138
146
|
include_query_params_json,
|
|
@@ -143,11 +151,12 @@ impl MethodRouterFactory {
|
|
|
143
151
|
if method.expects_body() {
|
|
144
152
|
match method {
|
|
145
153
|
HttpMethod::Post => {
|
|
146
|
-
|
|
147
|
-
let
|
|
154
|
+
// Performance: Clone directly from parameters, avoiding intermediate clone.
|
|
155
|
+
let handler_for_closure = handler.clone();
|
|
156
|
+
let hooks_for_closure = hooks.clone();
|
|
148
157
|
axum::routing::post(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
149
|
-
let handler =
|
|
150
|
-
let hooks =
|
|
158
|
+
let handler = handler_for_closure.clone();
|
|
159
|
+
let hooks = hooks_for_closure.clone();
|
|
151
160
|
async move {
|
|
152
161
|
let (parts, body) = req.into_parts();
|
|
153
162
|
let request_data = request_extraction::create_request_data_with_body(
|
|
@@ -172,11 +181,11 @@ impl MethodRouterFactory {
|
|
|
172
181
|
})
|
|
173
182
|
}
|
|
174
183
|
HttpMethod::Put => {
|
|
175
|
-
let
|
|
176
|
-
let
|
|
184
|
+
let handler_for_closure = handler.clone();
|
|
185
|
+
let hooks_for_closure = hooks.clone();
|
|
177
186
|
axum::routing::put(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
178
|
-
let handler =
|
|
179
|
-
let hooks =
|
|
187
|
+
let handler = handler_for_closure.clone();
|
|
188
|
+
let hooks = hooks_for_closure.clone();
|
|
180
189
|
async move {
|
|
181
190
|
let (parts, body) = req.into_parts();
|
|
182
191
|
let request_data = request_extraction::create_request_data_with_body(
|
|
@@ -201,11 +210,11 @@ impl MethodRouterFactory {
|
|
|
201
210
|
})
|
|
202
211
|
}
|
|
203
212
|
HttpMethod::Patch => {
|
|
204
|
-
let
|
|
205
|
-
let
|
|
213
|
+
let handler_for_closure = handler.clone();
|
|
214
|
+
let hooks_for_closure = hooks.clone();
|
|
206
215
|
axum::routing::patch(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
207
|
-
let handler =
|
|
208
|
-
let hooks =
|
|
216
|
+
let handler = handler_for_closure.clone();
|
|
217
|
+
let hooks = hooks_for_closure.clone();
|
|
209
218
|
async move {
|
|
210
219
|
let (parts, body) = req.into_parts();
|
|
211
220
|
let request_data = request_extraction::create_request_data_with_body(
|
|
@@ -234,11 +243,11 @@ impl MethodRouterFactory {
|
|
|
234
243
|
} else {
|
|
235
244
|
match method {
|
|
236
245
|
HttpMethod::Get => {
|
|
237
|
-
let
|
|
238
|
-
let
|
|
246
|
+
let handler_for_closure = handler.clone();
|
|
247
|
+
let hooks_for_closure = hooks.clone();
|
|
239
248
|
axum::routing::get(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
240
|
-
let handler =
|
|
241
|
-
let hooks =
|
|
249
|
+
let handler = handler_for_closure.clone();
|
|
250
|
+
let hooks = hooks_for_closure.clone();
|
|
242
251
|
async move {
|
|
243
252
|
let request_data = request_extraction::create_request_data_without_body(
|
|
244
253
|
req.uri(),
|
|
@@ -256,11 +265,11 @@ impl MethodRouterFactory {
|
|
|
256
265
|
})
|
|
257
266
|
}
|
|
258
267
|
HttpMethod::Delete => {
|
|
259
|
-
let
|
|
260
|
-
let
|
|
268
|
+
let handler_for_closure = handler.clone();
|
|
269
|
+
let hooks_for_closure = hooks.clone();
|
|
261
270
|
axum::routing::delete(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
262
|
-
let handler =
|
|
263
|
-
let hooks =
|
|
271
|
+
let handler = handler_for_closure.clone();
|
|
272
|
+
let hooks = hooks_for_closure.clone();
|
|
264
273
|
async move {
|
|
265
274
|
let request_data = request_extraction::create_request_data_without_body(
|
|
266
275
|
req.uri(),
|
|
@@ -278,11 +287,11 @@ impl MethodRouterFactory {
|
|
|
278
287
|
})
|
|
279
288
|
}
|
|
280
289
|
HttpMethod::Head => {
|
|
281
|
-
let
|
|
282
|
-
let
|
|
290
|
+
let handler_for_closure = handler.clone();
|
|
291
|
+
let hooks_for_closure = hooks.clone();
|
|
283
292
|
axum::routing::head(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
284
|
-
let handler =
|
|
285
|
-
let hooks =
|
|
293
|
+
let handler = handler_for_closure.clone();
|
|
294
|
+
let hooks = hooks_for_closure.clone();
|
|
286
295
|
async move {
|
|
287
296
|
let request_data = request_extraction::create_request_data_without_body(
|
|
288
297
|
req.uri(),
|
|
@@ -300,11 +309,11 @@ impl MethodRouterFactory {
|
|
|
300
309
|
})
|
|
301
310
|
}
|
|
302
311
|
HttpMethod::Trace => {
|
|
303
|
-
let
|
|
304
|
-
let
|
|
312
|
+
let handler_for_closure = handler.clone();
|
|
313
|
+
let hooks_for_closure = hooks.clone();
|
|
305
314
|
axum::routing::trace(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
306
|
-
let handler =
|
|
307
|
-
let hooks =
|
|
315
|
+
let handler = handler_for_closure.clone();
|
|
316
|
+
let hooks = hooks_for_closure.clone();
|
|
308
317
|
async move {
|
|
309
318
|
let request_data = request_extraction::create_request_data_without_body(
|
|
310
319
|
req.uri(),
|
|
@@ -322,11 +331,11 @@ impl MethodRouterFactory {
|
|
|
322
331
|
})
|
|
323
332
|
}
|
|
324
333
|
HttpMethod::Options => {
|
|
325
|
-
let
|
|
326
|
-
let
|
|
334
|
+
let handler_for_closure = handler.clone();
|
|
335
|
+
let hooks_for_closure = hooks.clone();
|
|
327
336
|
axum::routing::options(move |path_params: Path<HashMap<String, String>>, req: AxumRequest| {
|
|
328
|
-
let handler =
|
|
329
|
-
let hooks =
|
|
337
|
+
let handler = handler_for_closure.clone();
|
|
338
|
+
let hooks = hooks_for_closure.clone();
|
|
330
339
|
async move {
|
|
331
340
|
let request_data = request_extraction::create_request_data_without_body(
|
|
332
341
|
req.uri(),
|
|
@@ -349,6 +358,9 @@ impl MethodRouterFactory {
|
|
|
349
358
|
}
|
|
350
359
|
|
|
351
360
|
/// Create a method router for a route without path parameters
|
|
361
|
+
///
|
|
362
|
+
/// Performance: Each match arm only clones the Arc when needed for that specific
|
|
363
|
+
/// HTTP method, avoiding redundant clones at the function level.
|
|
352
364
|
fn create_without_path_params(
|
|
353
365
|
method: HttpMethod,
|
|
354
366
|
handler: Arc<dyn Handler>,
|
|
@@ -358,8 +370,7 @@ impl MethodRouterFactory {
|
|
|
358
370
|
include_headers: bool,
|
|
359
371
|
include_cookies: bool,
|
|
360
372
|
) -> MethodRouter {
|
|
361
|
-
|
|
362
|
-
let hooks_clone = hooks.clone();
|
|
373
|
+
// Performance: Removed redundant outer clones. Each match arm clones only when needed.
|
|
363
374
|
let without_body_options = request_extraction::WithoutBodyExtractionOptions {
|
|
364
375
|
include_raw_query_params,
|
|
365
376
|
include_query_params_json,
|
|
@@ -370,11 +381,11 @@ impl MethodRouterFactory {
|
|
|
370
381
|
if method.expects_body() {
|
|
371
382
|
match method {
|
|
372
383
|
HttpMethod::Post => {
|
|
373
|
-
let
|
|
374
|
-
let
|
|
384
|
+
let handler_for_closure = handler.clone();
|
|
385
|
+
let hooks_for_closure = hooks.clone();
|
|
375
386
|
axum::routing::post(move |req: AxumRequest| {
|
|
376
|
-
let handler =
|
|
377
|
-
let hooks =
|
|
387
|
+
let handler = handler_for_closure.clone();
|
|
388
|
+
let hooks = hooks_for_closure.clone();
|
|
378
389
|
async move {
|
|
379
390
|
let (parts, body) = req.into_parts();
|
|
380
391
|
let request_data = request_extraction::create_request_data_with_body(
|
|
@@ -399,11 +410,11 @@ impl MethodRouterFactory {
|
|
|
399
410
|
})
|
|
400
411
|
}
|
|
401
412
|
HttpMethod::Put => {
|
|
402
|
-
let
|
|
403
|
-
let
|
|
413
|
+
let handler_for_closure = handler.clone();
|
|
414
|
+
let hooks_for_closure = hooks.clone();
|
|
404
415
|
axum::routing::put(move |req: AxumRequest| {
|
|
405
|
-
let handler =
|
|
406
|
-
let hooks =
|
|
416
|
+
let handler = handler_for_closure.clone();
|
|
417
|
+
let hooks = hooks_for_closure.clone();
|
|
407
418
|
async move {
|
|
408
419
|
let (parts, body) = req.into_parts();
|
|
409
420
|
let request_data = request_extraction::create_request_data_with_body(
|
|
@@ -428,11 +439,11 @@ impl MethodRouterFactory {
|
|
|
428
439
|
})
|
|
429
440
|
}
|
|
430
441
|
HttpMethod::Patch => {
|
|
431
|
-
let
|
|
432
|
-
let
|
|
442
|
+
let handler_for_closure = handler.clone();
|
|
443
|
+
let hooks_for_closure = hooks.clone();
|
|
433
444
|
axum::routing::patch(move |req: AxumRequest| {
|
|
434
|
-
let handler =
|
|
435
|
-
let hooks =
|
|
445
|
+
let handler = handler_for_closure.clone();
|
|
446
|
+
let hooks = hooks_for_closure.clone();
|
|
436
447
|
async move {
|
|
437
448
|
let (parts, body) = req.into_parts();
|
|
438
449
|
let request_data = request_extraction::create_request_data_with_body(
|
|
@@ -461,11 +472,11 @@ impl MethodRouterFactory {
|
|
|
461
472
|
} else {
|
|
462
473
|
match method {
|
|
463
474
|
HttpMethod::Get => {
|
|
464
|
-
let
|
|
465
|
-
let
|
|
475
|
+
let handler_for_closure = handler.clone();
|
|
476
|
+
let hooks_for_closure = hooks.clone();
|
|
466
477
|
axum::routing::get(move |req: AxumRequest| {
|
|
467
|
-
let handler =
|
|
468
|
-
let hooks =
|
|
478
|
+
let handler = handler_for_closure.clone();
|
|
479
|
+
let hooks = hooks_for_closure.clone();
|
|
469
480
|
async move {
|
|
470
481
|
let request_data = request_extraction::create_request_data_without_body(
|
|
471
482
|
req.uri(),
|
|
@@ -483,11 +494,11 @@ impl MethodRouterFactory {
|
|
|
483
494
|
})
|
|
484
495
|
}
|
|
485
496
|
HttpMethod::Delete => {
|
|
486
|
-
let
|
|
487
|
-
let
|
|
497
|
+
let handler_for_closure = handler.clone();
|
|
498
|
+
let hooks_for_closure = hooks.clone();
|
|
488
499
|
axum::routing::delete(move |req: AxumRequest| {
|
|
489
|
-
let handler =
|
|
490
|
-
let hooks =
|
|
500
|
+
let handler = handler_for_closure.clone();
|
|
501
|
+
let hooks = hooks_for_closure.clone();
|
|
491
502
|
async move {
|
|
492
503
|
let request_data = request_extraction::create_request_data_without_body(
|
|
493
504
|
req.uri(),
|
|
@@ -505,11 +516,11 @@ impl MethodRouterFactory {
|
|
|
505
516
|
})
|
|
506
517
|
}
|
|
507
518
|
HttpMethod::Head => {
|
|
508
|
-
let
|
|
509
|
-
let
|
|
519
|
+
let handler_for_closure = handler.clone();
|
|
520
|
+
let hooks_for_closure = hooks.clone();
|
|
510
521
|
axum::routing::head(move |req: AxumRequest| {
|
|
511
|
-
let handler =
|
|
512
|
-
let hooks =
|
|
522
|
+
let handler = handler_for_closure.clone();
|
|
523
|
+
let hooks = hooks_for_closure.clone();
|
|
513
524
|
async move {
|
|
514
525
|
let request_data = request_extraction::create_request_data_without_body(
|
|
515
526
|
req.uri(),
|
|
@@ -527,11 +538,11 @@ impl MethodRouterFactory {
|
|
|
527
538
|
})
|
|
528
539
|
}
|
|
529
540
|
HttpMethod::Trace => {
|
|
530
|
-
let
|
|
531
|
-
let
|
|
541
|
+
let handler_for_closure = handler.clone();
|
|
542
|
+
let hooks_for_closure = hooks.clone();
|
|
532
543
|
axum::routing::trace(move |req: AxumRequest| {
|
|
533
|
-
let handler =
|
|
534
|
-
let hooks =
|
|
544
|
+
let handler = handler_for_closure.clone();
|
|
545
|
+
let hooks = hooks_for_closure.clone();
|
|
535
546
|
async move {
|
|
536
547
|
let request_data = request_extraction::create_request_data_without_body(
|
|
537
548
|
req.uri(),
|
|
@@ -549,11 +560,11 @@ impl MethodRouterFactory {
|
|
|
549
560
|
})
|
|
550
561
|
}
|
|
551
562
|
HttpMethod::Options => {
|
|
552
|
-
let
|
|
553
|
-
let
|
|
563
|
+
let handler_for_closure = handler.clone();
|
|
564
|
+
let hooks_for_closure = hooks.clone();
|
|
554
565
|
axum::routing::options(move |req: AxumRequest| {
|
|
555
|
-
let handler =
|
|
556
|
-
let hooks =
|
|
566
|
+
let handler = handler_for_closure.clone();
|
|
567
|
+
let hooks = hooks_for_closure.clone();
|
|
557
568
|
async move {
|
|
558
569
|
let request_data = request_extraction::create_request_data_without_body(
|
|
559
570
|
req.uri(),
|
|
@@ -207,10 +207,10 @@ mod tests {
|
|
|
207
207
|
fn create_test_request_data() -> RequestData {
|
|
208
208
|
RequestData {
|
|
209
209
|
path_params: Arc::new(HashMap::new()),
|
|
210
|
-
query_params: serde_json::Value::Null,
|
|
210
|
+
query_params: Arc::new(serde_json::Value::Null),
|
|
211
211
|
validated_params: None,
|
|
212
212
|
raw_query_params: Arc::new(HashMap::new()),
|
|
213
|
-
body: json!({"test": "data"}),
|
|
213
|
+
body: Arc::new(json!({"test": "data"})),
|
|
214
214
|
raw_body: None,
|
|
215
215
|
headers: Arc::new(HashMap::new()),
|
|
216
216
|
cookies: Arc::new(HashMap::new()),
|