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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /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
- // Normalize the cycle to start with the lexicographically smallest element
348
- // This makes cycle detection deterministic
349
- // The cycle includes the closing element (first element repeated at end)
350
- // e.g., [A, B, A] or [B, A, B]
351
- if cycle.len() > 1 {
352
- // Find the index of the smallest element (ignoring the last closing element)
353
- if let Some((min_idx, _)) = cycle[..cycle.len() - 1].iter().enumerate().min_by_key(|(_, s)| *s) {
354
- cycle.rotate_left(min_idx);
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; // Should not panic
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
+ }