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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -10
  3. data/ext/spikard_rb/Cargo.lock +234 -162
  4. data/ext/spikard_rb/Cargo.toml +2 -2
  5. data/ext/spikard_rb/extconf.rb +4 -3
  6. data/lib/spikard/config.rb +88 -12
  7. data/lib/spikard/testing.rb +3 -1
  8. data/lib/spikard/version.rb +1 -1
  9. data/lib/spikard.rb +11 -0
  10. data/vendor/crates/spikard-bindings-shared/Cargo.toml +3 -6
  11. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
  12. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
  14. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
  15. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
  16. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
  17. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  18. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
  22. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
  23. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  24. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
  25. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  26. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  27. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  28. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  29. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  30. data/vendor/crates/spikard-core/Cargo.toml +3 -3
  31. data/vendor/crates/spikard-core/src/di/container.rs +1 -1
  32. data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
  34. data/vendor/crates/spikard-core/src/di/value.rs +1 -1
  35. data/vendor/crates/spikard-core/src/http.rs +75 -0
  36. data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
  37. data/vendor/crates/spikard-core/src/parameters.rs +14 -19
  38. data/vendor/crates/spikard-core/src/problem.rs +1 -1
  39. data/vendor/crates/spikard-core/src/request_data.rs +7 -16
  40. data/vendor/crates/spikard-core/src/router.rs +6 -0
  41. data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
  42. data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
  43. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
  44. data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
  45. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  46. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  47. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  48. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  49. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  50. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  51. data/vendor/crates/spikard-http/Cargo.toml +4 -2
  52. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  53. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  54. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  55. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  56. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  57. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  58. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  59. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  60. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  61. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  62. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  63. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  64. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  65. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  66. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  67. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  68. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  69. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  70. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  71. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  72. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  73. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  74. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  75. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  76. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  77. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  78. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  79. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  80. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  81. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  82. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  83. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  84. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  85. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  86. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  87. data/vendor/crates/spikard-rb/Cargo.toml +3 -1
  88. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  89. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  90. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  91. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  92. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  93. data/vendor/crates/spikard-rb/src/lib.rs +444 -62
  94. data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
  95. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  96. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  97. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  98. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  99. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  100. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  101. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  102. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  103. data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
  104. metadata +14 -4
  105. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  106. 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 raw =
38
- parse_query_string(query_string.as_bytes(), '&')
39
- .into_iter()
40
- .fold(HashMap::new(), |mut acc, (k, v)| {
41
- acc.entry(k).or_insert_with(Vec::new).push(v);
42
- acc
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
- let raw = pairs.into_iter().fold(HashMap::new(), |mut acc, (k, v)| {
50
- acc.entry(k).or_insert_with(Vec::new).push(v);
51
- acc
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
- } else {
75
- parse_query_string(query_string.as_bytes(), '&')
76
- .into_iter()
77
- .fold(HashMap::new(), |mut acc, (k, v)| {
78
- acc.entry(k).or_insert_with(Vec::new).push(v);
79
- acc
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 mut cookies = HashMap::new();
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
- if let Some(cookie_str) = headers.get(axum::http::header::COOKIE).and_then(|h| h.to_str().ok()) {
114
- for cookie in cookie::Cookie::split_parse(cookie_str).flatten() {
115
- cookies.insert(cookie.name().to_string(), cookie.value().to_string());
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: Arc::new(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
- body: Value::Null,
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
- /// Performance optimization: stores raw body bytes without parsing JSON.
176
- /// JSON parsing is deferred until actually needed (e.g., for validation).
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
- let body_value = parts
204
- .extensions
205
- .get::<crate::middleware::PreParsedJson>()
206
- .map(|parsed| parsed.0.clone())
207
- .unwrap_or(Value::Null);
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: Arc::new(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: body_value,
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
- call_with_optional_hooks(req, request_data, handler, hooks).await
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
- let handler_clone = handler.clone();
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
- let handler_clone = handler_clone.clone();
147
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
150
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
176
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
179
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
205
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
208
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
238
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
241
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
260
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
263
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
282
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
285
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
304
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
307
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
326
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
329
- let hooks = hooks_clone.clone();
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
- let handler_clone = handler.clone();
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 handler_clone = handler_clone.clone();
374
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
377
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
403
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
406
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
432
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
435
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
465
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
468
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
487
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
490
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
509
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
512
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
531
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
534
- let hooks = hooks_clone.clone();
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 handler_clone = handler_clone.clone();
553
- let hooks_clone = hooks_clone.clone();
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 = handler_clone.clone();
556
- let hooks = hooks_clone.clone();
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()),