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.
- checksums.yaml +4 -4
- data/README.md +19 -10
- data/ext/spikard_rb/Cargo.lock +234 -162
- data/ext/spikard_rb/Cargo.toml +3 -3
- 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 +11 -6
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +63 -25
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +20 -4
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +25 -22
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +14 -12
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +24 -10
- 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 +17 -11
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +51 -73
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +442 -4
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +22 -10
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +28 -10
- 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 +11 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +6 -9
- data/vendor/crates/spikard-core/src/debug.rs +2 -2
- data/vendor/crates/spikard-core/src/di/container.rs +2 -2
- data/vendor/crates/spikard-core/src/di/error.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +9 -5
- data/vendor/crates/spikard-core/src/di/graph.rs +1 -0
- data/vendor/crates/spikard-core/src/di/resolved.rs +25 -2
- data/vendor/crates/spikard-core/src/di/value.rs +2 -1
- data/vendor/crates/spikard-core/src/errors.rs +3 -0
- data/vendor/crates/spikard-core/src/http.rs +94 -18
- data/vendor/crates/spikard-core/src/lifecycle.rs +85 -61
- data/vendor/crates/spikard-core/src/parameters.rs +75 -54
- data/vendor/crates/spikard-core/src/problem.rs +19 -5
- data/vendor/crates/spikard-core/src/request_data.rs +16 -24
- data/vendor/crates/spikard-core/src/router.rs +26 -6
- data/vendor/crates/spikard-core/src/schema_registry.rs +25 -11
- data/vendor/crates/spikard-core/src/type_hints.rs +14 -7
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +30 -16
- data/vendor/crates/spikard-core/src/validation/mod.rs +46 -33
- 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 +11 -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 +11 -1
- data/vendor/crates/spikard-rb/build.rs +1 -0
- 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 +502 -62
- data/vendor/crates/spikard-rb/src/lifecycle.rs +31 -3
- 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 +9 -1
- data/vendor/crates/spikard-rb-macros/src/lib.rs +4 -5
- 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
|
@@ -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(
|
|
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
|
-
.
|
|
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!("{:?}"
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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 {}"
|
|
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!("{:?}"
|
|
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
|
|
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
|
|
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!("{:?}"
|
|
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
|
-
|
|
19
|
+
#[must_use]
|
|
20
|
+
pub const fn as_str(&self) -> &'static str {
|
|
19
21
|
match self {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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(
|
|
44
|
-
"POST" => Ok(
|
|
45
|
-
"PUT" => Ok(
|
|
46
|
-
"PATCH" => Ok(
|
|
47
|
-
"DELETE" => Ok(
|
|
48
|
-
"HEAD" => Ok(
|
|
49
|
-
"OPTIONS" => Ok(
|
|
50
|
-
"TRACE" => Ok(
|
|
51
|
-
_ => Err(format!("Unknown HTTP method: {}"
|
|
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");
|