spikard 0.8.2 → 0.10.1

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 (115) 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 +3 -3
  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 +11 -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 +63 -25
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +20 -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 +25 -22
  16. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
  17. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +24 -10
  18. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +17 -11
  22. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +51 -73
  23. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +442 -4
  24. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  25. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +22 -10
  26. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
  27. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  28. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  29. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  30. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  31. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  32. data/vendor/crates/spikard-core/Cargo.toml +11 -3
  33. data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
  34. data/vendor/crates/spikard-core/src/debug.rs +2 -2
  35. data/vendor/crates/spikard-core/src/di/container.rs +2 -2
  36. data/vendor/crates/spikard-core/src/di/error.rs +1 -1
  37. data/vendor/crates/spikard-core/src/di/factory.rs +9 -5
  38. data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
  39. data/vendor/crates/spikard-core/src/di/resolved.rs +25 -2
  40. data/vendor/crates/spikard-core/src/di/value.rs +2 -1
  41. data/vendor/crates/spikard-core/src/errors.rs +3 -0
  42. data/vendor/crates/spikard-core/src/http.rs +94 -18
  43. data/vendor/crates/spikard-core/src/lifecycle.rs +85 -61
  44. data/vendor/crates/spikard-core/src/parameters.rs +75 -54
  45. data/vendor/crates/spikard-core/src/problem.rs +19 -5
  46. data/vendor/crates/spikard-core/src/request_data.rs +16 -24
  47. data/vendor/crates/spikard-core/src/router.rs +26 -6
  48. data/vendor/crates/spikard-core/src/schema_registry.rs +25 -11
  49. data/vendor/crates/spikard-core/src/type_hints.rs +14 -7
  50. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +30 -16
  51. data/vendor/crates/spikard-core/src/validation/mod.rs +46 -33
  52. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  53. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  54. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  55. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  56. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  57. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  58. data/vendor/crates/spikard-http/Cargo.toml +11 -2
  59. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  60. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  61. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  62. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  63. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  64. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  65. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  66. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  67. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  68. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  69. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  70. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  71. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  72. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  73. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  74. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  75. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  76. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  77. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  78. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  79. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  80. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  81. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  82. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  83. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  84. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  85. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  86. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  87. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  88. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  89. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  90. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  91. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  92. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  93. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  94. data/vendor/crates/spikard-rb/Cargo.toml +11 -1
  95. data/vendor/crates/spikard-rb/build.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  97. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  98. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  99. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  100. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  101. data/vendor/crates/spikard-rb/src/lib.rs +502 -62
  102. data/vendor/crates/spikard-rb/src/lifecycle.rs +31 -3
  103. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  104. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  105. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  106. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  107. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  108. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  109. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  110. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  111. data/vendor/crates/spikard-rb-macros/Cargo.toml +9 -1
  112. data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
  113. metadata +14 -4
  114. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  115. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
@@ -15,6 +15,8 @@ pub struct RawResponse {
15
15
 
16
16
  impl RawResponse {
17
17
  /// Construct a new response.
18
+ #[must_use]
19
+ #[allow(clippy::missing_const_for_fn)]
18
20
  pub fn new(status: u16, headers: HashMap<String, String>, body: Vec<u8>) -> Self {
19
21
  Self { status, headers, body }
20
22
  }
@@ -35,19 +37,13 @@ impl RawResponse {
35
37
  return;
36
38
  }
37
39
 
38
- let accept_encoding = header_value(request_headers, "Accept-Encoding").map(|value| value.to_ascii_lowercase());
39
- let accepts_brotli = accept_encoding
40
- .as_ref()
41
- .map(|value| value.contains("br"))
42
- .unwrap_or(false);
40
+ let accept_encoding = header_value(request_headers, "Accept-Encoding").map(str::to_ascii_lowercase);
41
+ let accepts_brotli = accept_encoding.as_ref().is_some_and(|value| value.contains("br"));
43
42
  if compression.brotli && accepts_brotli && self.try_compress_brotli(compression) {
44
43
  return;
45
44
  }
46
45
 
47
- let accepts_gzip = accept_encoding
48
- .as_ref()
49
- .map(|value| value.contains("gzip"))
50
- .unwrap_or(false);
46
+ let accepts_gzip = accept_encoding.as_ref().is_some_and(|value| value.contains("gzip"));
51
47
  if compression.gzip && accepts_gzip {
52
48
  self.try_compress_gzip(compression);
53
49
  }
@@ -110,6 +106,7 @@ pub struct StaticAsset {
110
106
 
111
107
  impl StaticAsset {
112
108
  /// Build a response snapshot if the incoming request targets this asset.
109
+ #[must_use]
113
110
  pub fn serve(&self, method: &str, normalized_path: &str) -> Option<RawResponse> {
114
111
  if !method.eq_ignore_ascii_case("GET") && !method.eq_ignore_ascii_case("HEAD") {
115
112
  return None;
@@ -1,8 +1,8 @@
1
1
  //! Debug logging utilities for spikard-http
2
2
  //!
3
3
  //! This module provides debug logging that can be enabled via:
4
- //! - Building in debug mode (cfg(debug_assertions))
5
- //! - Setting SPIKARD_DEBUG=1 environment variable
4
+ //! - Building in debug mode (`cfg(debug_assertions)`)
5
+ //! - Setting `SPIKARD_DEBUG=1` environment variable
6
6
 
7
7
  use std::sync::atomic::{AtomicBool, Ordering};
8
8
 
@@ -365,7 +365,7 @@ impl std::fmt::Debug for DependencyContainer {
365
365
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366
366
  f.debug_struct("DependencyContainer")
367
367
  .field("dependencies", &self.dependencies.keys())
368
- .finish()
368
+ .finish_non_exhaustive()
369
369
  }
370
370
  }
371
371
 
@@ -696,7 +696,7 @@ mod tests {
696
696
  let dep = ValueDependency::new("test", 42i32);
697
697
  container.register("test".to_string(), Arc::new(dep)).unwrap();
698
698
 
699
- let debug_str = format!("{:?}", container);
699
+ let debug_str = format!("{container:?}");
700
700
  assert!(debug_str.contains("DependencyContainer"));
701
701
  }
702
702
  }
@@ -33,7 +33,7 @@ pub enum DependencyError {
33
33
  /// ```
34
34
  #[error("Circular dependency detected: {cycle:?}")]
35
35
  CircularDependency {
36
- /// The cycle of dependencies (e.g., ["A", "B", "C", "A"])
36
+ /// The cycle of dependencies (e.g., `["A", "B", "C", "A"]`)
37
37
  cycle: Vec<String>,
38
38
  },
39
39
 
@@ -134,7 +134,7 @@ impl std::fmt::Debug for FactoryDependency {
134
134
  .field("dependencies", &self.dependencies)
135
135
  .field("cacheable", &self.cacheable)
136
136
  .field("singleton", &self.singleton)
137
- .finish()
137
+ .finish_non_exhaustive()
138
138
  }
139
139
  }
140
140
 
@@ -209,6 +209,7 @@ impl FactoryDependencyBuilder {
209
209
  /// })
210
210
  /// .build();
211
211
  /// ```
212
+ #[must_use]
212
213
  pub fn factory<F>(mut self, factory: F) -> Self
213
214
  where
214
215
  F: Fn(
@@ -245,6 +246,7 @@ impl FactoryDependencyBuilder {
245
246
  /// })
246
247
  /// .build();
247
248
  /// ```
249
+ #[must_use]
248
250
  pub fn depends_on(mut self, dependencies: Vec<String>) -> Self {
249
251
  self.dependencies = dependencies;
250
252
  self
@@ -272,7 +274,8 @@ impl FactoryDependencyBuilder {
272
274
  /// .cacheable(true) // Same ID for all uses in one request
273
275
  /// .build();
274
276
  /// ```
275
- pub fn cacheable(mut self, cacheable: bool) -> Self {
277
+ #[must_use]
278
+ pub const fn cacheable(mut self, cacheable: bool) -> Self {
276
279
  self.cacheable = cacheable;
277
280
  self
278
281
  }
@@ -300,7 +303,8 @@ impl FactoryDependencyBuilder {
300
303
  /// .singleton(true) // Share across all requests
301
304
  /// .build();
302
305
  /// ```
303
- pub fn singleton(mut self, singleton: bool) -> Self {
306
+ #[must_use]
307
+ pub const fn singleton(mut self, singleton: bool) -> Self {
304
308
  self.singleton = singleton;
305
309
  self
306
310
  }
@@ -436,7 +440,7 @@ mod tests {
436
440
  let config: Option<Arc<String>> = resolved.get("config");
437
441
  let config_value = config.map(|c| (*c).clone()).unwrap_or_default();
438
442
 
439
- Ok(Arc::new(format!("Service using {}", config_value)) as Arc<dyn Any + Send + Sync>)
443
+ Ok(Arc::new(format!("Service using {config_value}")) as Arc<dyn Any + Send + Sync>)
440
444
  })
441
445
  })
442
446
  .build();
@@ -520,7 +524,7 @@ mod tests {
520
524
  .singleton(false)
521
525
  .build();
522
526
 
523
- let debug_str = format!("{:?}", factory);
527
+ let debug_str = format!("{factory:?}");
524
528
  assert!(debug_str.contains("FactoryDependency"));
525
529
  assert!(debug_str.contains("test"));
526
530
  assert!(debug_str.contains("dep1"));
@@ -133,6 +133,7 @@ impl DependencyGraph {
133
133
  /// // Adding c -> [] would not
134
134
  /// assert!(!graph.has_cycle_with("c", &[]));
135
135
  /// ```
136
+ #[must_use]
136
137
  pub fn has_cycle_with(&self, new_key: &str, new_deps: &[String]) -> bool {
137
138
  let mut temp_graph = self.graph.clone();
138
139
  temp_graph.insert(new_key.to_string(), new_deps.to_vec());
@@ -31,7 +31,7 @@ type CleanupTask = Box<dyn FnOnce() -> BoxFuture<'static, ()> + Send>;
31
31
  ///
32
32
  /// // Insert a dependency
33
33
  /// let value = Arc::new(42i32);
34
- /// resolved.insert("answer".to_string(), value.clone());
34
+ /// resolved.insert("answer".to_string(), value);
35
35
  ///
36
36
  /// // Retrieve with type safety
37
37
  /// let retrieved: Option<Arc<i32>> = resolved.get("answer");
@@ -88,6 +88,9 @@ impl ResolvedDependencies {
88
88
  /// let config = Arc::new("production".to_string());
89
89
  /// resolved.insert("config".to_string(), config);
90
90
  /// ```
91
+ ///
92
+ /// # Panics
93
+ /// Panics if the lock is poisoned.
91
94
  pub fn insert(&mut self, key: String, value: Arc<dyn Any + Send + Sync>) {
92
95
  self.dependencies.lock().unwrap().insert(key, value);
93
96
  }
@@ -126,6 +129,10 @@ impl ResolvedDependencies {
126
129
  /// let missing: Option<Arc<i32>> = resolved.get("missing");
127
130
  /// assert!(missing.is_none());
128
131
  /// ```
132
+ ///
133
+ /// # Panics
134
+ /// Panics if the lock is poisoned.
135
+ #[must_use]
129
136
  pub fn get<T: Send + Sync + 'static>(&self, key: &str) -> Option<Arc<T>> {
130
137
  self.dependencies
131
138
  .lock()
@@ -155,6 +162,10 @@ impl ResolvedDependencies {
155
162
  /// let any_ref = resolved.get_arc("data");
156
163
  /// assert!(any_ref.is_some());
157
164
  /// ```
165
+ ///
166
+ /// # Panics
167
+ /// Panics if the lock is poisoned.
168
+ #[must_use]
158
169
  pub fn get_arc(&self, key: &str) -> Option<Arc<dyn Any + Send + Sync>> {
159
170
  self.dependencies.lock().unwrap().get(key).cloned()
160
171
  }
@@ -177,6 +188,9 @@ impl ResolvedDependencies {
177
188
  /// assert!(resolved.contains("exists"));
178
189
  /// assert!(!resolved.contains("missing"));
179
190
  /// ```
191
+ ///
192
+ /// # Panics
193
+ /// Panics if the lock is poisoned.
180
194
  #[must_use]
181
195
  pub fn contains(&self, key: &str) -> bool {
182
196
  self.dependencies.lock().unwrap().contains_key(key)
@@ -202,6 +216,9 @@ impl ResolvedDependencies {
202
216
  /// assert!(keys.contains(&"config".to_string()));
203
217
  /// assert!(keys.contains(&"db".to_string()));
204
218
  /// ```
219
+ ///
220
+ /// # Panics
221
+ /// Panics if the lock is poisoned.
205
222
  #[must_use]
206
223
  pub fn keys(&self) -> Vec<String> {
207
224
  self.dependencies.lock().unwrap().keys().cloned().collect()
@@ -233,6 +250,9 @@ impl ResolvedDependencies {
233
250
  /// resolved.cleanup().await;
234
251
  /// # });
235
252
  /// ```
253
+ ///
254
+ /// # Panics
255
+ /// Panics if the lock is poisoned.
236
256
  pub fn add_cleanup_task(&self, task: CleanupTask) {
237
257
  self.cleanup_tasks.lock().unwrap().push(task);
238
258
  }
@@ -275,6 +295,9 @@ impl ResolvedDependencies {
275
295
  /// assert_eq!(*order.lock().unwrap(), vec![2, 1]);
276
296
  /// # });
277
297
  /// ```
298
+ ///
299
+ /// # Panics
300
+ /// Panics if the lock is poisoned.
278
301
  pub async fn cleanup(self) {
279
302
  let tasks = {
280
303
  let mut cleanup_tasks = self.cleanup_tasks.lock().unwrap();
@@ -312,7 +335,7 @@ mod tests {
312
335
  fn test_insert_and_get() {
313
336
  let mut resolved = ResolvedDependencies::new();
314
337
  let value = Arc::new(42i32);
315
- resolved.insert("answer".to_string(), value.clone());
338
+ resolved.insert("answer".to_string(), value);
316
339
 
317
340
  let retrieved: Option<Arc<i32>> = resolved.get("answer");
318
341
  assert_eq!(retrieved.map(|v| *v), Some(42));
@@ -134,6 +134,7 @@ impl<T: Clone + Send + Sync + 'static> std::fmt::Debug for ValueDependency<T> {
134
134
  f.debug_struct("ValueDependency")
135
135
  .field("key", &self.key)
136
136
  .field("value_type", &std::any::type_name::<T>())
137
+ .field("value", &"<T>")
137
138
  .finish()
138
139
  }
139
140
  }
@@ -274,7 +275,7 @@ mod tests {
274
275
  #[test]
275
276
  fn test_debug() {
276
277
  let dep = ValueDependency::new("test", 42i32);
277
- let debug_str = format!("{:?}", dep);
278
+ let debug_str = format!("{dep:?}");
278
279
  assert!(debug_str.contains("ValueDependency"));
279
280
  assert!(debug_str.contains("test"));
280
281
  }
@@ -31,6 +31,9 @@ impl StructuredError {
31
31
  }
32
32
 
33
33
  /// Catch panics and convert to a structured error so they don't cross FFI boundaries.
34
+ ///
35
+ /// # Errors
36
+ /// Returns a structured error if a panic occurs during function execution.
34
37
  pub fn shield_panic<T, F>(f: F) -> Result<T, StructuredError>
35
38
  where
36
39
  F: FnOnce() -> T + UnwindSafe,
@@ -1,5 +1,6 @@
1
1
  use serde::{Deserialize, Serialize};
2
2
  use serde_json::Value;
3
+ use std::sync::OnceLock;
3
4
 
4
5
  /// HTTP method
5
6
  #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -15,16 +16,17 @@ pub enum Method {
15
16
  }
16
17
 
17
18
  impl Method {
18
- pub fn as_str(&self) -> &'static str {
19
+ #[must_use]
20
+ pub const fn as_str(&self) -> &'static str {
19
21
  match self {
20
- Method::Get => "GET",
21
- Method::Post => "POST",
22
- Method::Put => "PUT",
23
- Method::Patch => "PATCH",
24
- Method::Delete => "DELETE",
25
- Method::Head => "HEAD",
26
- Method::Options => "OPTIONS",
27
- Method::Trace => "TRACE",
22
+ Self::Get => "GET",
23
+ Self::Post => "POST",
24
+ Self::Put => "PUT",
25
+ Self::Patch => "PATCH",
26
+ Self::Delete => "DELETE",
27
+ Self::Head => "HEAD",
28
+ Self::Options => "OPTIONS",
29
+ Self::Trace => "TRACE",
28
30
  }
29
31
  }
30
32
  }
@@ -40,15 +42,15 @@ impl std::str::FromStr for Method {
40
42
 
41
43
  fn from_str(s: &str) -> Result<Self, Self::Err> {
42
44
  match s.to_uppercase().as_str() {
43
- "GET" => Ok(Method::Get),
44
- "POST" => Ok(Method::Post),
45
- "PUT" => Ok(Method::Put),
46
- "PATCH" => Ok(Method::Patch),
47
- "DELETE" => Ok(Method::Delete),
48
- "HEAD" => Ok(Method::Head),
49
- "OPTIONS" => Ok(Method::Options),
50
- "TRACE" => Ok(Method::Trace),
51
- _ => Err(format!("Unknown HTTP method: {}", s)),
45
+ "GET" => Ok(Self::Get),
46
+ "POST" => Ok(Self::Post),
47
+ "PUT" => Ok(Self::Put),
48
+ "PATCH" => Ok(Self::Patch),
49
+ "DELETE" => Ok(Self::Delete),
50
+ "HEAD" => Ok(Self::Head),
51
+ "OPTIONS" => Ok(Self::Options),
52
+ "TRACE" => Ok(Self::Trace),
53
+ _ => Err(format!("Unknown HTTP method: {s}")),
52
54
  }
53
55
  }
54
56
  }
@@ -66,6 +68,73 @@ pub struct CorsConfig {
66
68
  pub max_age: Option<u32>,
67
69
  #[serde(skip_serializing_if = "Option::is_none")]
68
70
  pub allow_credentials: Option<bool>,
71
+
72
+ // Optimized caches (lazy-initialized on first use)
73
+ #[serde(skip)]
74
+ #[doc(hidden)]
75
+ pub methods_joined_cache: OnceLock<String>,
76
+ #[serde(skip)]
77
+ #[doc(hidden)]
78
+ pub headers_joined_cache: OnceLock<String>,
79
+ }
80
+
81
+ impl CorsConfig {
82
+ /// Get the cached joined methods string for preflight responses
83
+ pub fn allowed_methods_joined(&self) -> &str {
84
+ self.methods_joined_cache
85
+ .get_or_init(|| self.allowed_methods.join(", "))
86
+ }
87
+
88
+ /// Get the cached joined headers string for preflight responses
89
+ pub fn allowed_headers_joined(&self) -> &str {
90
+ self.headers_joined_cache
91
+ .get_or_init(|| self.allowed_headers.join(", "))
92
+ }
93
+
94
+ /// Check if an origin is allowed (O(1) with wildcard, O(n) for exact match)
95
+ pub fn is_origin_allowed(&self, origin: &str) -> bool {
96
+ if origin.is_empty() {
97
+ return false;
98
+ }
99
+ self.allowed_origins.iter().any(|o| o == "*" || o == origin)
100
+ }
101
+
102
+ /// Check if a method is allowed (O(1) with wildcard, O(n) for exact match)
103
+ pub fn is_method_allowed(&self, method: &str) -> bool {
104
+ self.allowed_methods
105
+ .iter()
106
+ .any(|m| m == "*" || m.eq_ignore_ascii_case(method))
107
+ }
108
+
109
+ /// Check if all requested headers are allowed (O(n) where n = num requested headers)
110
+ pub fn are_headers_allowed(&self, requested: &[&str]) -> bool {
111
+ // Check if wildcard is set
112
+ if self.allowed_headers.iter().any(|h| h == "*") {
113
+ return true;
114
+ }
115
+
116
+ // Check each requested header
117
+ requested.iter().all(|req_header| {
118
+ self.allowed_headers
119
+ .iter()
120
+ .any(|h| h.to_lowercase() == req_header.to_lowercase())
121
+ })
122
+ }
123
+ }
124
+
125
+ impl Default for CorsConfig {
126
+ fn default() -> Self {
127
+ Self {
128
+ allowed_origins: vec!["*".to_string()],
129
+ allowed_methods: vec!["*".to_string()],
130
+ allowed_headers: vec![],
131
+ expose_headers: None,
132
+ max_age: None,
133
+ allow_credentials: None,
134
+ methods_joined_cache: OnceLock::new(),
135
+ headers_joined_cache: OnceLock::new(),
136
+ }
137
+ }
69
138
  }
70
139
 
71
140
  /// Route metadata extracted from bindings
@@ -92,6 +161,11 @@ pub struct RouteMetadata {
92
161
  /// JSON-RPC method metadata (if this route is exposed as a JSON-RPC method)
93
162
  #[serde(skip_serializing_if = "Option::is_none")]
94
163
  pub jsonrpc_method: Option<Value>,
164
+ /// Optional static response configuration: `{"status": 200, "body": "OK", "content_type": "text/plain"}`
165
+ /// When present, the handler is replaced by a `StaticResponseHandler` that bypasses the full
166
+ /// middleware pipeline for maximum throughput.
167
+ #[serde(skip_serializing_if = "Option::is_none")]
168
+ pub static_response: Option<Value>,
95
169
  }
96
170
 
97
171
  /// Compression configuration shared across runtimes
@@ -385,6 +459,7 @@ mod tests {
385
459
  expose_headers: None,
386
460
  max_age: None,
387
461
  allow_credentials: None,
462
+ ..Default::default()
388
463
  };
389
464
  assert_eq!(cors.allowed_origins.len(), 1);
390
465
  assert_eq!(cors.allowed_methods.len(), 2);
@@ -407,6 +482,7 @@ mod tests {
407
482
  #[cfg(feature = "di")]
408
483
  handler_dependencies: None,
409
484
  jsonrpc_method: None,
485
+ static_response: None,
410
486
  };
411
487
  assert_eq!(metadata.method, "GET");
412
488
  assert_eq!(metadata.path, "/api/users");