spikard 0.3.6 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +21 -6
- data/ext/spikard_rb/Cargo.toml +2 -2
- data/lib/spikard/app.rb +33 -14
- data/lib/spikard/testing.rb +47 -12
- data/lib/spikard/version.rb +1 -1
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
- data/vendor/crates/spikard-core/Cargo.toml +4 -4
- data/vendor/crates/spikard-core/src/debug.rs +64 -0
- data/vendor/crates/spikard-core/src/di/container.rs +3 -27
- data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
- data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
- data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
- data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
- data/vendor/crates/spikard-core/src/di/value.rs +2 -4
- data/vendor/crates/spikard-core/src/errors.rs +30 -0
- data/vendor/crates/spikard-core/src/http.rs +262 -0
- data/vendor/crates/spikard-core/src/lib.rs +1 -1
- data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
- data/vendor/crates/spikard-core/src/metadata.rs +389 -0
- data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
- data/vendor/crates/spikard-core/src/problem.rs +34 -0
- data/vendor/crates/spikard-core/src/request_data.rs +966 -1
- data/vendor/crates/spikard-core/src/router.rs +263 -2
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
- data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
- data/vendor/crates/spikard-http/Cargo.toml +12 -16
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
- data/vendor/crates/spikard-http/src/auth.rs +65 -16
- data/vendor/crates/spikard-http/src/background.rs +1614 -3
- data/vendor/crates/spikard-http/src/cors.rs +515 -0
- data/vendor/crates/spikard-http/src/debug.rs +65 -0
- data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
- data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
- data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
- data/vendor/crates/spikard-http/src/lib.rs +33 -28
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
- data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
- data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
- data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
- data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
- data/vendor/crates/spikard-http/src/response.rs +321 -0
- data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
- data/vendor/crates/spikard-http/src/sse.rs +983 -21
- data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
- data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
- data/vendor/crates/spikard-http/src/testing.rs +7 -7
- data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
- data/vendor/crates/spikard-rb/Cargo.toml +10 -4
- data/vendor/crates/spikard-rb/build.rs +196 -5
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
- data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
- data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
- data/vendor/crates/spikard-rb/src/handler.rs +100 -107
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
- data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
- data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
- data/vendor/crates/spikard-rb/src/server.rs +47 -22
- data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
- data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
- metadata +46 -13
- data/vendor/crates/spikard-http/src/parameters.rs +0 -1
- data/vendor/crates/spikard-http/src/problem.rs +0 -1
- data/vendor/crates/spikard-http/src/router.rs +0 -1
- data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
- data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
- data/vendor/crates/spikard-http/src/validation.rs +0 -1
- data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
- /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
|
@@ -61,3 +61,67 @@ macro_rules! debug_log_value {
|
|
|
61
61
|
}
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
|
|
65
|
+
#[cfg(test)]
|
|
66
|
+
mod tests {
|
|
67
|
+
use super::*;
|
|
68
|
+
use std::sync::Mutex;
|
|
69
|
+
use std::sync::atomic::Ordering;
|
|
70
|
+
|
|
71
|
+
static FLAG_LOCK: Mutex<()> = Mutex::new(());
|
|
72
|
+
|
|
73
|
+
struct DebugFlagGuard {
|
|
74
|
+
previous_flag: bool,
|
|
75
|
+
previous_env: Option<String>,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl Drop for DebugFlagGuard {
|
|
79
|
+
fn drop(&mut self) {
|
|
80
|
+
DEBUG_ENABLED.store(self.previous_flag, Ordering::Relaxed);
|
|
81
|
+
if let Some(prev) = &self.previous_env {
|
|
82
|
+
unsafe { std::env::set_var("SPIKARD_DEBUG", prev) };
|
|
83
|
+
} else {
|
|
84
|
+
unsafe { std::env::remove_var("SPIKARD_DEBUG") };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[test]
|
|
90
|
+
fn init_sets_debug_enabled_in_tests() {
|
|
91
|
+
let _lock = FLAG_LOCK.lock().unwrap();
|
|
92
|
+
let previous = DEBUG_ENABLED.load(Ordering::Relaxed);
|
|
93
|
+
let previous_env = std::env::var("SPIKARD_DEBUG").ok();
|
|
94
|
+
let _guard = DebugFlagGuard {
|
|
95
|
+
previous_flag: previous,
|
|
96
|
+
previous_env,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
unsafe { std::env::set_var("SPIKARD_DEBUG", "1") };
|
|
100
|
+
|
|
101
|
+
init();
|
|
102
|
+
assert!(is_enabled(), "init should enable debug when SPIKARD_DEBUG is set");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
#[test]
|
|
106
|
+
fn macros_follow_debug_flag() {
|
|
107
|
+
let _lock = FLAG_LOCK.lock().unwrap();
|
|
108
|
+
let previous = DEBUG_ENABLED.load(Ordering::Relaxed);
|
|
109
|
+
let previous_env = std::env::var("SPIKARD_DEBUG").ok();
|
|
110
|
+
let _guard = DebugFlagGuard {
|
|
111
|
+
previous_flag: previous,
|
|
112
|
+
previous_env,
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
DEBUG_ENABLED.store(false, Ordering::Relaxed);
|
|
116
|
+
debug_log!("disabled branch");
|
|
117
|
+
debug_log_module!("core", "disabled");
|
|
118
|
+
debug_log_value!("counter", 0_u8);
|
|
119
|
+
assert!(!is_enabled());
|
|
120
|
+
|
|
121
|
+
DEBUG_ENABLED.store(true, Ordering::Relaxed);
|
|
122
|
+
debug_log!("enabled branch {}", 1);
|
|
123
|
+
debug_log_module!("core", "enabled");
|
|
124
|
+
debug_log_value!("counter", 2_i32);
|
|
125
|
+
assert!(is_enabled());
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -50,6 +50,7 @@ use tokio::sync::RwLock;
|
|
|
50
50
|
/// let request_data = RequestData {
|
|
51
51
|
/// path_params: Arc::new(HashMap::new()),
|
|
52
52
|
/// query_params: serde_json::Value::Null,
|
|
53
|
+
/// validated_params: None,
|
|
53
54
|
/// raw_query_params: Arc::new(HashMap::new()),
|
|
54
55
|
/// body: serde_json::Value::Null,
|
|
55
56
|
/// raw_body: None,
|
|
@@ -128,10 +129,8 @@ impl DependencyContainer {
|
|
|
128
129
|
/// container.register("config".to_string(), Arc::new(config)).unwrap();
|
|
129
130
|
/// ```
|
|
130
131
|
pub fn register(&mut self, key: String, dep: Arc<dyn Dependency>) -> Result<&mut Self, DependencyError> {
|
|
131
|
-
// Add to dependency graph (this checks for cycles and duplicates)
|
|
132
132
|
self.dependency_graph.add_dependency(&key, dep.depends_on())?;
|
|
133
133
|
|
|
134
|
-
// Store the dependency
|
|
135
134
|
self.dependencies.insert(key, dep);
|
|
136
135
|
|
|
137
136
|
Ok(self)
|
|
@@ -195,6 +194,7 @@ impl DependencyContainer {
|
|
|
195
194
|
/// let request_data = RequestData {
|
|
196
195
|
/// path_params: Arc::new(HashMap::new()),
|
|
197
196
|
/// query_params: serde_json::Value::Null,
|
|
197
|
+
/// validated_params: None,
|
|
198
198
|
/// raw_query_params: Arc::new(HashMap::new()),
|
|
199
199
|
/// body: serde_json::Value::Null,
|
|
200
200
|
/// raw_body: None,
|
|
@@ -225,31 +225,23 @@ impl DependencyContainer {
|
|
|
225
225
|
}
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
// Calculate resolution batches
|
|
229
228
|
let batches = self.dependency_graph.calculate_batches(deps)?;
|
|
230
229
|
|
|
231
230
|
let mut resolved = ResolvedDependencies::new();
|
|
232
231
|
let mut request_cache: HashMap<String, Arc<dyn Any + Send + Sync>> = HashMap::new();
|
|
233
232
|
|
|
234
|
-
// Process each batch sequentially
|
|
235
233
|
for batch in batches {
|
|
236
|
-
// Sort keys within batch by registration order for deterministic resolution
|
|
237
|
-
// This ensures cleanup happens in a predictable reverse order
|
|
238
234
|
// NOTE: We resolve sequentially within each batch to ensure cleanup tasks
|
|
239
|
-
// are registered in a deterministic order (LIFO on cleanup)
|
|
240
235
|
let mut sorted_keys: Vec<_> = batch.iter().collect();
|
|
241
236
|
|
|
242
|
-
// Sort by insertion order (index in IndexMap) instead of alphabetically
|
|
243
237
|
sorted_keys.sort_by_key(|key| self.dependencies.get_index_of(*key).unwrap_or(usize::MAX));
|
|
244
238
|
|
|
245
239
|
for key in sorted_keys {
|
|
246
|
-
// Get the dependency
|
|
247
240
|
let dep = self
|
|
248
241
|
.dependencies
|
|
249
242
|
.get(key)
|
|
250
243
|
.ok_or_else(|| DependencyError::NotFound { key: key.clone() })?;
|
|
251
244
|
|
|
252
|
-
// Check singleton cache first
|
|
253
245
|
if dep.singleton() {
|
|
254
246
|
let cache = self.singleton_cache.read().await;
|
|
255
247
|
if let Some(cached) = cache.get(key) {
|
|
@@ -258,7 +250,6 @@ impl DependencyContainer {
|
|
|
258
250
|
}
|
|
259
251
|
}
|
|
260
252
|
|
|
261
|
-
// Check request cache
|
|
262
253
|
if dep.cacheable()
|
|
263
254
|
&& let Some(cached) = request_cache.get(key)
|
|
264
255
|
{
|
|
@@ -266,10 +257,8 @@ impl DependencyContainer {
|
|
|
266
257
|
continue;
|
|
267
258
|
}
|
|
268
259
|
|
|
269
|
-
// Need to resolve - do it sequentially to preserve cleanup order
|
|
270
260
|
let result = dep.resolve(req, data, &resolved).await?;
|
|
271
261
|
|
|
272
|
-
// Store in appropriate cache
|
|
273
262
|
if dep.singleton() {
|
|
274
263
|
let mut cache = self.singleton_cache.write().await;
|
|
275
264
|
cache.insert(key.clone(), Arc::clone(&result));
|
|
@@ -277,7 +266,6 @@ impl DependencyContainer {
|
|
|
277
266
|
request_cache.insert(key.clone(), Arc::clone(&result));
|
|
278
267
|
}
|
|
279
268
|
|
|
280
|
-
// Always store in resolved
|
|
281
269
|
resolved.insert(key.clone(), result);
|
|
282
270
|
}
|
|
283
271
|
}
|
|
@@ -396,6 +384,7 @@ mod tests {
|
|
|
396
384
|
RequestData {
|
|
397
385
|
path_params: Arc::new(HashMap::new()),
|
|
398
386
|
query_params: serde_json::Value::Null,
|
|
387
|
+
validated_params: None,
|
|
399
388
|
raw_query_params: Arc::new(HashMap::new()),
|
|
400
389
|
body: serde_json::Value::Null,
|
|
401
390
|
raw_body: None,
|
|
@@ -507,11 +496,9 @@ mod tests {
|
|
|
507
496
|
async fn test_resolve_nested() {
|
|
508
497
|
let mut container = DependencyContainer::new();
|
|
509
498
|
|
|
510
|
-
// config (no dependencies)
|
|
511
499
|
let config = ValueDependency::new("config", "production".to_string());
|
|
512
500
|
container.register("config".to_string(), Arc::new(config)).unwrap();
|
|
513
501
|
|
|
514
|
-
// database (depends on config)
|
|
515
502
|
let database = FactoryDependency::builder("database")
|
|
516
503
|
.depends_on(vec!["config".to_string()])
|
|
517
504
|
.factory(|_req, _data, resolved| {
|
|
@@ -541,10 +528,8 @@ mod tests {
|
|
|
541
528
|
async fn test_resolve_batched() {
|
|
542
529
|
let mut container = DependencyContainer::new();
|
|
543
530
|
|
|
544
|
-
// Track resolution order
|
|
545
531
|
let counter = Arc::new(AtomicU32::new(0));
|
|
546
532
|
|
|
547
|
-
// config (no deps)
|
|
548
533
|
let counter1 = Arc::clone(&counter);
|
|
549
534
|
let config = FactoryDependency::builder("config")
|
|
550
535
|
.factory(move |_req, _data, _resolved| {
|
|
@@ -557,7 +542,6 @@ mod tests {
|
|
|
557
542
|
.build();
|
|
558
543
|
container.register("config".to_string(), Arc::new(config)).unwrap();
|
|
559
544
|
|
|
560
|
-
// db and cache (both depend on config, can run in parallel)
|
|
561
545
|
let counter2 = Arc::clone(&counter);
|
|
562
546
|
let database = FactoryDependency::builder("database")
|
|
563
547
|
.depends_on(vec!["config".to_string()])
|
|
@@ -592,11 +576,9 @@ mod tests {
|
|
|
592
576
|
.await
|
|
593
577
|
.unwrap();
|
|
594
578
|
|
|
595
|
-
// config should be resolved first (order 0)
|
|
596
579
|
let config_order: Arc<u32> = resolved.get("config").unwrap();
|
|
597
580
|
assert_eq!(*config_order, 0);
|
|
598
581
|
|
|
599
|
-
// db and cache should be resolved after config (order 1 and 2, in either order)
|
|
600
582
|
let db_order: Arc<u32> = resolved.get("database").unwrap();
|
|
601
583
|
let cache_order: Arc<u32> = resolved.get("cache").unwrap();
|
|
602
584
|
assert!(*db_order >= 1);
|
|
@@ -628,7 +610,6 @@ mod tests {
|
|
|
628
610
|
let request = make_request();
|
|
629
611
|
let request_data = make_request_data();
|
|
630
612
|
|
|
631
|
-
// Resolve multiple times
|
|
632
613
|
for _ in 0..3 {
|
|
633
614
|
let resolved = container
|
|
634
615
|
.resolve_for_handler(&["singleton".to_string()], &request, &request_data)
|
|
@@ -636,11 +617,9 @@ mod tests {
|
|
|
636
617
|
.unwrap();
|
|
637
618
|
|
|
638
619
|
let value: Arc<u32> = resolved.get("singleton").unwrap();
|
|
639
|
-
// Should always be 0 (resolved only once)
|
|
640
620
|
assert_eq!(*value, 0);
|
|
641
621
|
}
|
|
642
622
|
|
|
643
|
-
// Counter should only have been incremented once
|
|
644
623
|
assert_eq!(counter.load(Ordering::SeqCst), 1);
|
|
645
624
|
}
|
|
646
625
|
|
|
@@ -669,7 +648,6 @@ mod tests {
|
|
|
669
648
|
let request = make_request();
|
|
670
649
|
let request_data = make_request_data();
|
|
671
650
|
|
|
672
|
-
// First resolve
|
|
673
651
|
let resolved1 = container
|
|
674
652
|
.resolve_for_handler(&["singleton".to_string()], &request, &request_data)
|
|
675
653
|
.await
|
|
@@ -677,10 +655,8 @@ mod tests {
|
|
|
677
655
|
let value1: Arc<u32> = resolved1.get("singleton").unwrap();
|
|
678
656
|
assert_eq!(*value1, 0);
|
|
679
657
|
|
|
680
|
-
// Clear cache
|
|
681
658
|
container.clear_singleton_cache().await;
|
|
682
659
|
|
|
683
|
-
// Second resolve should re-execute factory
|
|
684
660
|
let resolved2 = container
|
|
685
661
|
.resolve_for_handler(&["singleton".to_string()], &request, &request_data)
|
|
686
662
|
.await
|
|
@@ -107,7 +107,6 @@ impl Dependency for FactoryDependency {
|
|
|
107
107
|
request_data: &RequestData,
|
|
108
108
|
resolved: &ResolvedDependencies,
|
|
109
109
|
) -> Pin<Box<dyn Future<Output = Result<Arc<dyn Any + Send + Sync>, DependencyError>> + Send>> {
|
|
110
|
-
// Call the factory function
|
|
111
110
|
(self.factory)(request, request_data, resolved)
|
|
112
111
|
}
|
|
113
112
|
|
|
@@ -350,6 +349,7 @@ mod tests {
|
|
|
350
349
|
RequestData {
|
|
351
350
|
path_params: Arc::new(HashMap::new()),
|
|
352
351
|
query_params: serde_json::Value::Null,
|
|
352
|
+
validated_params: None,
|
|
353
353
|
raw_query_params: Arc::new(HashMap::new()),
|
|
354
354
|
body: serde_json::Value::Null,
|
|
355
355
|
raw_body: None,
|
|
@@ -406,7 +406,6 @@ mod tests {
|
|
|
406
406
|
let factory = FactoryDependency::builder("async_value")
|
|
407
407
|
.factory(|_req, _data, _resolved| {
|
|
408
408
|
Box::pin(async {
|
|
409
|
-
// Simulate async work
|
|
410
409
|
tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
|
|
411
410
|
Ok(Arc::new(100i32) as Arc<dyn Any + Send + Sync>)
|
|
412
411
|
})
|
|
@@ -426,7 +425,6 @@ mod tests {
|
|
|
426
425
|
|
|
427
426
|
#[tokio::test]
|
|
428
427
|
async fn test_factory_depends_on() {
|
|
429
|
-
// Create resolved dependencies with a value
|
|
430
428
|
let mut resolved = ResolvedDependencies::new();
|
|
431
429
|
resolved.insert("config".to_string(), Arc::new("test_config".to_string()));
|
|
432
430
|
|
|
@@ -435,7 +433,6 @@ mod tests {
|
|
|
435
433
|
.factory(|_req, _data, resolved| {
|
|
436
434
|
let resolved = resolved.clone();
|
|
437
435
|
Box::pin(async move {
|
|
438
|
-
// Access the config dependency
|
|
439
436
|
let config: Option<Arc<String>> = resolved.get("config");
|
|
440
437
|
let config_value = config.map(|c| (*c).clone()).unwrap_or_default();
|
|
441
438
|
|
|
@@ -505,7 +502,6 @@ mod tests {
|
|
|
505
502
|
let request_data = make_request_data();
|
|
506
503
|
let resolved = ResolvedDependencies::new();
|
|
507
504
|
|
|
508
|
-
// Call factory multiple times
|
|
509
505
|
for i in 0..3 {
|
|
510
506
|
let result = factory.resolve(&request, &request_data, &resolved).await;
|
|
511
507
|
let value: Arc<u32> = result.unwrap().downcast().unwrap();
|
|
@@ -96,13 +96,10 @@ impl DependencyGraph {
|
|
|
96
96
|
/// assert!(result.is_err());
|
|
97
97
|
/// ```
|
|
98
98
|
pub fn add_dependency(&mut self, key: &str, depends_on: Vec<String>) -> Result<(), DependencyError> {
|
|
99
|
-
// Check for duplicate
|
|
100
99
|
if self.graph.contains_key(key) {
|
|
101
100
|
return Err(DependencyError::DuplicateKey { key: key.to_string() });
|
|
102
101
|
}
|
|
103
102
|
|
|
104
|
-
// Don't check for cycles here - allow registration and detect at resolution time
|
|
105
|
-
// This allows the server to start and return proper HTTP error responses
|
|
106
103
|
self.graph.insert(key.to_string(), depends_on);
|
|
107
104
|
Ok(())
|
|
108
105
|
}
|
|
@@ -137,11 +134,9 @@ impl DependencyGraph {
|
|
|
137
134
|
/// assert!(!graph.has_cycle_with("c", &[]));
|
|
138
135
|
/// ```
|
|
139
136
|
pub fn has_cycle_with(&self, new_key: &str, new_deps: &[String]) -> bool {
|
|
140
|
-
// Build temporary graph with the new dependency
|
|
141
137
|
let mut temp_graph = self.graph.clone();
|
|
142
138
|
temp_graph.insert(new_key.to_string(), new_deps.to_vec());
|
|
143
139
|
|
|
144
|
-
// DFS cycle detection
|
|
145
140
|
let mut visited = HashSet::new();
|
|
146
141
|
let mut rec_stack = HashSet::new();
|
|
147
142
|
|
|
@@ -182,7 +177,6 @@ impl DependencyGraph {
|
|
|
182
177
|
return true;
|
|
183
178
|
}
|
|
184
179
|
} else if rec_stack.contains(dep) {
|
|
185
|
-
// Found a back edge (cycle)
|
|
186
180
|
return true;
|
|
187
181
|
}
|
|
188
182
|
}
|
|
@@ -238,7 +232,6 @@ impl DependencyGraph {
|
|
|
238
232
|
/// assert!(batches[1].contains("c"));
|
|
239
233
|
/// ```
|
|
240
234
|
pub fn calculate_batches(&self, keys: &[String]) -> Result<Vec<HashSet<String>>, DependencyError> {
|
|
241
|
-
// Build subgraph with only the requested keys and their transitive dependencies
|
|
242
235
|
let mut subgraph = HashMap::new();
|
|
243
236
|
let mut to_visit: VecDeque<String> = keys.iter().cloned().collect();
|
|
244
237
|
let mut visited = HashSet::new();
|
|
@@ -255,12 +248,10 @@ impl DependencyGraph {
|
|
|
255
248
|
to_visit.push_back(dep.clone());
|
|
256
249
|
}
|
|
257
250
|
} else {
|
|
258
|
-
// Key not in graph - treat as having no dependencies
|
|
259
251
|
subgraph.insert(key.clone(), vec![]);
|
|
260
252
|
}
|
|
261
253
|
}
|
|
262
254
|
|
|
263
|
-
// Calculate in-degree for each node in the subgraph
|
|
264
255
|
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
|
265
256
|
for key in subgraph.keys() {
|
|
266
257
|
in_degree.entry(key.clone()).or_insert(0);
|
|
@@ -271,7 +262,6 @@ impl DependencyGraph {
|
|
|
271
262
|
}
|
|
272
263
|
}
|
|
273
264
|
|
|
274
|
-
// Kahn's algorithm for topological sort with batching
|
|
275
265
|
let mut batches = Vec::new();
|
|
276
266
|
let mut queue: VecDeque<String> = in_degree
|
|
277
267
|
.iter()
|
|
@@ -283,17 +273,14 @@ impl DependencyGraph {
|
|
|
283
273
|
let total = subgraph.len();
|
|
284
274
|
|
|
285
275
|
while !queue.is_empty() {
|
|
286
|
-
// All items in the queue can be processed in parallel (same batch)
|
|
287
276
|
let mut batch = HashSet::new();
|
|
288
277
|
|
|
289
|
-
// Process all nodes with in-degree 0
|
|
290
278
|
let batch_size = queue.len();
|
|
291
279
|
for _ in 0..batch_size {
|
|
292
280
|
if let Some(node) = queue.pop_front() {
|
|
293
281
|
batch.insert(node.clone());
|
|
294
282
|
processed += 1;
|
|
295
283
|
|
|
296
|
-
// Reduce in-degree for dependents
|
|
297
284
|
if let Some(deps) = subgraph.get(&node) {
|
|
298
285
|
for dep in deps {
|
|
299
286
|
if let Some(degree) = in_degree.get_mut(dep) {
|
|
@@ -312,9 +299,7 @@ impl DependencyGraph {
|
|
|
312
299
|
}
|
|
313
300
|
}
|
|
314
301
|
|
|
315
|
-
// Check if we processed all nodes (if not, there's a cycle)
|
|
316
302
|
if processed < total {
|
|
317
|
-
// Find a cycle by tracing from any unprocessed node
|
|
318
303
|
let unprocessed: Vec<String> = subgraph
|
|
319
304
|
.keys()
|
|
320
305
|
.filter(|k| in_degree.get(*k).is_some_and(|&d| d > 0))
|
|
@@ -322,17 +307,14 @@ impl DependencyGraph {
|
|
|
322
307
|
.collect();
|
|
323
308
|
|
|
324
309
|
if let Some(start) = unprocessed.first() {
|
|
325
|
-
// Trace the cycle path
|
|
326
310
|
let mut cycle = vec![start.clone()];
|
|
327
311
|
let mut current = start;
|
|
328
312
|
let mut visited_in_path = HashSet::new();
|
|
329
313
|
visited_in_path.insert(start.clone());
|
|
330
314
|
|
|
331
|
-
// Follow dependencies until we find the cycle
|
|
332
315
|
while let Some(deps) = subgraph.get(current) {
|
|
333
316
|
if let Some(next) = deps.iter().find(|d| unprocessed.contains(d)) {
|
|
334
317
|
if visited_in_path.contains(next) {
|
|
335
|
-
// Found the cycle - add the closing node
|
|
336
318
|
cycle.push(next.clone());
|
|
337
319
|
break;
|
|
338
320
|
}
|
|
@@ -344,32 +326,23 @@ impl DependencyGraph {
|
|
|
344
326
|
}
|
|
345
327
|
}
|
|
346
328
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
// After rotation, update the closing element to match the new first element
|
|
356
|
-
if let Some(first) = cycle.first().cloned()
|
|
357
|
-
&& let Some(last) = cycle.last_mut()
|
|
358
|
-
{
|
|
359
|
-
*last = first;
|
|
360
|
-
}
|
|
329
|
+
if cycle.len() > 1
|
|
330
|
+
&& let Some((min_idx, _)) = cycle[..cycle.len() - 1].iter().enumerate().min_by_key(|(_, s)| *s)
|
|
331
|
+
{
|
|
332
|
+
cycle.rotate_left(min_idx);
|
|
333
|
+
if let Some(first) = cycle.first().cloned()
|
|
334
|
+
&& let Some(last) = cycle.last_mut()
|
|
335
|
+
{
|
|
336
|
+
*last = first;
|
|
361
337
|
}
|
|
362
338
|
}
|
|
363
339
|
|
|
364
340
|
return Err(DependencyError::CircularDependency { cycle });
|
|
365
341
|
}
|
|
366
342
|
|
|
367
|
-
// Fallback if we can't trace the cycle
|
|
368
343
|
return Err(DependencyError::CircularDependency { cycle: unprocessed });
|
|
369
344
|
}
|
|
370
345
|
|
|
371
|
-
// Reverse the batches because we built them in reverse order
|
|
372
|
-
// (dependencies come before dependents in our graph structure)
|
|
373
346
|
batches.reverse();
|
|
374
347
|
|
|
375
348
|
Ok(batches)
|
|
@@ -406,7 +379,6 @@ mod tests {
|
|
|
406
379
|
let mut graph = DependencyGraph::new();
|
|
407
380
|
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
408
381
|
|
|
409
|
-
// a -> b -> a would be a cycle
|
|
410
382
|
assert!(graph.has_cycle_with("b", &["a".to_string()]));
|
|
411
383
|
}
|
|
412
384
|
|
|
@@ -416,7 +388,6 @@ mod tests {
|
|
|
416
388
|
graph.add_dependency("a", vec!["b".to_string()]).unwrap();
|
|
417
389
|
graph.add_dependency("b", vec!["c".to_string()]).unwrap();
|
|
418
390
|
|
|
419
|
-
// c -> a would create cycle: a -> b -> c -> a
|
|
420
391
|
assert!(graph.has_cycle_with("c", &["a".to_string()]));
|
|
421
392
|
}
|
|
422
393
|
|
|
@@ -424,7 +395,6 @@ mod tests {
|
|
|
424
395
|
fn test_has_cycle_self_loop() {
|
|
425
396
|
let graph = DependencyGraph::new();
|
|
426
397
|
|
|
427
|
-
// Self-loop: a -> a
|
|
428
398
|
assert!(graph.has_cycle_with("a", &["a".to_string()]));
|
|
429
399
|
}
|
|
430
400
|
|
|
@@ -434,7 +404,6 @@ mod tests {
|
|
|
434
404
|
graph.add_dependency("a", vec![]).unwrap();
|
|
435
405
|
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
436
406
|
|
|
437
|
-
// c -> a is fine (no cycle)
|
|
438
407
|
assert!(!graph.has_cycle_with("c", &["a".to_string()]));
|
|
439
408
|
}
|
|
440
409
|
|
|
@@ -459,7 +428,6 @@ mod tests {
|
|
|
459
428
|
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
460
429
|
.unwrap();
|
|
461
430
|
|
|
462
|
-
// Should be 3 batches in order: a, b, c
|
|
463
431
|
assert_eq!(batches.len(), 3);
|
|
464
432
|
assert!(batches[0].contains("a"));
|
|
465
433
|
assert!(batches[1].contains("b"));
|
|
@@ -479,8 +447,6 @@ mod tests {
|
|
|
479
447
|
.calculate_batches(&["a".to_string(), "b".to_string(), "c".to_string()])
|
|
480
448
|
.unwrap();
|
|
481
449
|
|
|
482
|
-
// Batch 1: a and b (parallel)
|
|
483
|
-
// Batch 2: c
|
|
484
450
|
assert_eq!(batches.len(), 2);
|
|
485
451
|
assert_eq!(batches[0].len(), 2);
|
|
486
452
|
assert!(batches[0].contains("a"));
|
|
@@ -507,9 +473,6 @@ mod tests {
|
|
|
507
473
|
])
|
|
508
474
|
.unwrap();
|
|
509
475
|
|
|
510
|
-
// Batch 1: config
|
|
511
|
-
// Batch 2: database, cache (parallel)
|
|
512
|
-
// Batch 3: service
|
|
513
476
|
assert_eq!(batches.len(), 3);
|
|
514
477
|
assert!(batches[0].contains("config"));
|
|
515
478
|
assert_eq!(batches[1].len(), 2);
|
|
@@ -525,7 +488,6 @@ mod tests {
|
|
|
525
488
|
graph.add_dependency("b", vec!["a".to_string()]).unwrap();
|
|
526
489
|
graph.add_dependency("c", vec!["a".to_string()]).unwrap();
|
|
527
490
|
|
|
528
|
-
// Only request b (should also include its dependency a)
|
|
529
491
|
let batches = graph.calculate_batches(&["b".to_string()]).unwrap();
|
|
530
492
|
|
|
531
493
|
assert_eq!(batches.len(), 2);
|
|
@@ -538,7 +500,6 @@ mod tests {
|
|
|
538
500
|
let mut graph = DependencyGraph::new();
|
|
539
501
|
graph.add_dependency("a", vec!["missing".to_string()]).unwrap();
|
|
540
502
|
|
|
541
|
-
// Should handle missing dependencies gracefully
|
|
542
503
|
let batches = graph.calculate_batches(&["a".to_string()]).unwrap();
|
|
543
504
|
assert!(!batches.is_empty());
|
|
544
505
|
}
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
//! let request_data = RequestData {
|
|
64
64
|
//! path_params: Arc::new(HashMap::new()),
|
|
65
65
|
//! query_params: serde_json::Value::Null,
|
|
66
|
+
//! validated_params: None,
|
|
66
67
|
//! raw_query_params: Arc::new(HashMap::new()),
|
|
67
68
|
//! body: serde_json::Value::Null,
|
|
68
69
|
//! raw_body: None,
|
|
@@ -182,7 +183,6 @@ mod graph;
|
|
|
182
183
|
mod resolved;
|
|
183
184
|
mod value;
|
|
184
185
|
|
|
185
|
-
// Public exports
|
|
186
186
|
pub use container::DependencyContainer;
|
|
187
187
|
pub use dependency::Dependency;
|
|
188
188
|
pub use error::DependencyError;
|
|
@@ -276,13 +276,11 @@ impl ResolvedDependencies {
|
|
|
276
276
|
/// # });
|
|
277
277
|
/// ```
|
|
278
278
|
pub async fn cleanup(self) {
|
|
279
|
-
// Take ownership of cleanup tasks
|
|
280
279
|
let tasks = {
|
|
281
280
|
let mut cleanup_tasks = self.cleanup_tasks.lock().unwrap();
|
|
282
281
|
std::mem::take(&mut *cleanup_tasks)
|
|
283
282
|
};
|
|
284
283
|
|
|
285
|
-
// Run cleanup tasks in reverse order (LIFO)
|
|
286
284
|
for task in tasks.into_iter().rev() {
|
|
287
285
|
task().await;
|
|
288
286
|
}
|
|
@@ -325,7 +323,6 @@ mod tests {
|
|
|
325
323
|
let mut resolved = ResolvedDependencies::new();
|
|
326
324
|
resolved.insert("number".to_string(), Arc::new(42i32));
|
|
327
325
|
|
|
328
|
-
// Wrong type returns None
|
|
329
326
|
let wrong: Option<Arc<String>> = resolved.get("number");
|
|
330
327
|
assert!(wrong.is_none());
|
|
331
328
|
}
|
|
@@ -345,7 +342,6 @@ mod tests {
|
|
|
345
342
|
let any_ref = resolved.get_arc("data");
|
|
346
343
|
assert!(any_ref.is_some());
|
|
347
344
|
|
|
348
|
-
// Can downcast manually
|
|
349
345
|
let vec_ref = any_ref.unwrap().downcast::<Vec<i32>>().ok();
|
|
350
346
|
assert!(vec_ref.is_some());
|
|
351
347
|
}
|
|
@@ -388,14 +384,13 @@ mod tests {
|
|
|
388
384
|
|
|
389
385
|
resolved.cleanup().await;
|
|
390
386
|
|
|
391
|
-
// Tasks run in reverse order (LIFO)
|
|
392
387
|
assert_eq!(*order.lock().unwrap(), vec![3, 2, 1]);
|
|
393
388
|
}
|
|
394
389
|
|
|
395
390
|
#[tokio::test]
|
|
396
391
|
async fn test_cleanup_empty() {
|
|
397
392
|
let resolved = ResolvedDependencies::new();
|
|
398
|
-
resolved.cleanup().await;
|
|
393
|
+
resolved.cleanup().await;
|
|
399
394
|
}
|
|
400
395
|
|
|
401
396
|
#[test]
|
|
@@ -403,7 +398,6 @@ mod tests {
|
|
|
403
398
|
let mut resolved1 = ResolvedDependencies::new();
|
|
404
399
|
resolved1.insert("key".to_string(), Arc::new(42i32));
|
|
405
400
|
|
|
406
|
-
// Clone shares the same underlying data
|
|
407
401
|
let resolved2 = resolved1.clone();
|
|
408
402
|
let value: Option<Arc<i32>> = resolved2.get("key");
|
|
409
403
|
assert_eq!(value.map(|v| *v), Some(42));
|
|
@@ -42,6 +42,7 @@ use std::sync::Arc;
|
|
|
42
42
|
/// let request_data = RequestData {
|
|
43
43
|
/// path_params: Arc::new(HashMap::new()),
|
|
44
44
|
/// query_params: serde_json::Value::Null,
|
|
45
|
+
/// validated_params: None,
|
|
45
46
|
/// raw_query_params: Arc::new(HashMap::new()),
|
|
46
47
|
/// body: serde_json::Value::Null,
|
|
47
48
|
/// raw_body: None,
|
|
@@ -116,17 +117,14 @@ impl<T: Clone + Send + Sync + 'static> Dependency for ValueDependency<T> {
|
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
fn depends_on(&self) -> Vec<String> {
|
|
119
|
-
// Value dependencies have no dependencies
|
|
120
120
|
vec![]
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
fn cacheable(&self) -> bool {
|
|
124
|
-
// Values are inherently cacheable (they never change)
|
|
125
124
|
true
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
fn singleton(&self) -> bool {
|
|
129
|
-
// Values can be singletons (they never change)
|
|
130
128
|
true
|
|
131
129
|
}
|
|
132
130
|
}
|
|
@@ -149,6 +147,7 @@ mod tests {
|
|
|
149
147
|
RequestData {
|
|
150
148
|
path_params: Arc::new(HashMap::new()),
|
|
151
149
|
query_params: serde_json::Value::Null,
|
|
150
|
+
validated_params: None,
|
|
152
151
|
raw_query_params: Arc::new(HashMap::new()),
|
|
153
152
|
body: serde_json::Value::Null,
|
|
154
153
|
raw_body: None,
|
|
@@ -225,7 +224,6 @@ mod tests {
|
|
|
225
224
|
let request = Request::builder().body(()).unwrap();
|
|
226
225
|
let request_data = make_request_data();
|
|
227
226
|
|
|
228
|
-
// Resolve concurrently from multiple tasks
|
|
229
227
|
let handles: Vec<_> = (0..10)
|
|
230
228
|
.map(|_| {
|
|
231
229
|
let dep = Arc::clone(&dep);
|
|
@@ -37,3 +37,33 @@ where
|
|
|
37
37
|
{
|
|
38
38
|
catch_unwind(f).map_err(|_| StructuredError::simple("panic", "Unexpected panic in Rust code"))
|
|
39
39
|
}
|
|
40
|
+
|
|
41
|
+
#[cfg(test)]
|
|
42
|
+
mod tests {
|
|
43
|
+
use super::*;
|
|
44
|
+
use serde_json::json;
|
|
45
|
+
|
|
46
|
+
#[test]
|
|
47
|
+
fn structured_error_constructors_populate_fields() {
|
|
48
|
+
let details = json!({"field": "name"});
|
|
49
|
+
let err = StructuredError::new("invalid", "bad input", details.clone());
|
|
50
|
+
assert_eq!(err.code, "invalid");
|
|
51
|
+
assert_eq!(err.error, "bad input");
|
|
52
|
+
assert_eq!(err.details, details);
|
|
53
|
+
|
|
54
|
+
let simple = StructuredError::simple("missing", "not found");
|
|
55
|
+
assert_eq!(simple.code, "missing");
|
|
56
|
+
assert_eq!(simple.error, "not found");
|
|
57
|
+
assert!(simple.details.is_object());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
#[test]
|
|
61
|
+
fn shield_panic_returns_ok_or_structured_error() {
|
|
62
|
+
let ok = shield_panic(|| 42);
|
|
63
|
+
assert_eq!(ok.unwrap(), 42);
|
|
64
|
+
|
|
65
|
+
let err = shield_panic(|| panic!("boom")).unwrap_err();
|
|
66
|
+
assert_eq!(err.code, "panic");
|
|
67
|
+
assert!(err.error.contains("Unexpected panic"));
|
|
68
|
+
}
|
|
69
|
+
}
|