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
@@ -4,26 +4,37 @@
4
4
  //! including JSON conversion, string conversion, and request/response building.
5
5
 
6
6
  #![allow(dead_code)]
7
+ #![deny(clippy::unwrap_used)]
7
8
 
8
9
  use bytes::Bytes;
9
10
  use magnus::prelude::*;
10
- use magnus::{Error, RArray, RHash, RString, Ruby, TryConvert, Value};
11
+ use magnus::{Error, RArray, RHash, RString, Ruby, Symbol, TryConvert, Value};
11
12
  use serde_json::Value as JsonValue;
12
- use spikard_http::problem::ProblemDetails;
13
+ use spikard_core::problem::ProblemDetails;
13
14
  use spikard_http::testing::MultipartFilePart;
14
15
  use std::collections::HashMap;
15
16
 
16
- use crate::test_client::{RequestBody, RequestConfig, TestResponseData};
17
+ use crate::testing::client::{RequestBody, RequestConfig, TestResponseData};
17
18
 
18
19
  /// Convert a Ruby value to JSON.
19
20
  ///
20
- /// Uses Ruby's JSON.generate method to serialize the Ruby object
21
- /// and then parses the result.
21
+ /// Fast-path converts common Ruby types directly in Rust to avoid
22
+ /// `JSON.generate` + `serde_json::from_str` overhead.
23
+ ///
24
+ /// Falls back to Ruby JSON for unsupported types to preserve behavior.
22
25
  pub fn ruby_value_to_json(ruby: &Ruby, json_module: Value, value: Value) -> Result<JsonValue, Error> {
23
26
  if value.is_nil() {
24
27
  return Ok(JsonValue::Null);
25
28
  }
26
29
 
30
+ if let Some(converted) = ruby_value_to_json_fast(ruby, json_module, value, 0)? {
31
+ return Ok(converted);
32
+ }
33
+
34
+ ruby_value_to_json_fallback(ruby, json_module, value)
35
+ }
36
+
37
+ fn ruby_value_to_json_fallback(ruby: &Ruby, json_module: Value, value: Value) -> Result<JsonValue, Error> {
27
38
  let json_string: String = json_module.funcall("generate", (value,))?;
28
39
  serde_json::from_str(&json_string).map_err(|err| {
29
40
  Error::new(
@@ -33,6 +44,95 @@ pub fn ruby_value_to_json(ruby: &Ruby, json_module: Value, value: Value) -> Resu
33
44
  })
34
45
  }
35
46
 
47
+ fn ruby_value_to_json_fast(
48
+ ruby: &Ruby,
49
+ json_module: Value,
50
+ value: Value,
51
+ depth: usize,
52
+ ) -> Result<Option<JsonValue>, Error> {
53
+ // Cycle detection is non-trivial without tracking Ruby object IDs; a shallow
54
+ // recursion guard avoids worst-case behavior and defers to Ruby JSON.
55
+ if depth > 64 {
56
+ return Ok(None);
57
+ }
58
+
59
+ if value.is_nil() {
60
+ return Ok(Some(JsonValue::Null));
61
+ }
62
+
63
+ if value.is_kind_of(ruby.class_true_class()) {
64
+ return Ok(Some(JsonValue::Bool(true)));
65
+ }
66
+
67
+ if value.is_kind_of(ruby.class_false_class()) {
68
+ return Ok(Some(JsonValue::Bool(false)));
69
+ }
70
+
71
+ if let Ok(text) = RString::try_convert(value) {
72
+ let slice = unsafe { text.as_slice() };
73
+ return Ok(Some(JsonValue::String(String::from_utf8_lossy(slice).to_string())));
74
+ }
75
+
76
+ if value.is_kind_of(ruby.class_float()) {
77
+ let n = f64::try_convert(value)?;
78
+ let number = serde_json::Number::from_f64(n).ok_or_else(|| {
79
+ Error::new(
80
+ ruby.exception_runtime_error(),
81
+ "Failed to convert Ruby Float to JSON number",
82
+ )
83
+ })?;
84
+ return Ok(Some(JsonValue::Number(number)));
85
+ }
86
+
87
+ if value.is_kind_of(ruby.class_integer()) {
88
+ if let Ok(n) = i64::try_convert(value) {
89
+ return Ok(Some(JsonValue::from(n)));
90
+ }
91
+ if let Ok(n) = u64::try_convert(value) {
92
+ return Ok(Some(JsonValue::from(n)));
93
+ }
94
+ }
95
+
96
+ if let Some(array) = RArray::from_value(value) {
97
+ let mut out = Vec::with_capacity(array.len());
98
+ for idx in 0..array.len() {
99
+ let item: Value = array.entry(idx as isize)?;
100
+ let json_item = match ruby_value_to_json_fast(ruby, json_module, item, depth + 1)? {
101
+ Some(v) => v,
102
+ None => ruby_value_to_json_fallback(ruby, json_module, item)?,
103
+ };
104
+ out.push(json_item);
105
+ }
106
+ return Ok(Some(JsonValue::Array(out)));
107
+ }
108
+
109
+ if let Some(hash) = RHash::from_value(value) {
110
+ let mut map = serde_json::Map::with_capacity(hash.len());
111
+ hash.foreach(|key: Value, val: Value| {
112
+ let key_str = if let Ok(key_text) = RString::try_convert(key) {
113
+ let slice = unsafe { key_text.as_slice() };
114
+ String::from_utf8_lossy(slice).to_string()
115
+ } else if let Ok(sym) = Symbol::try_convert(key) {
116
+ sym.name()?.into_owned()
117
+ } else {
118
+ let key_as_str: String = key.funcall("to_s", ())?;
119
+ key_as_str
120
+ };
121
+
122
+ let json_val = match ruby_value_to_json_fast(ruby, json_module, val, depth + 1)? {
123
+ Some(v) => v,
124
+ None => ruby_value_to_json_fallback(ruby, json_module, val)?,
125
+ };
126
+
127
+ map.insert(key_str, json_val);
128
+ Ok(magnus::r_hash::ForEach::Continue)
129
+ })?;
130
+ return Ok(Some(JsonValue::Object(map)));
131
+ }
132
+
133
+ Ok(None)
134
+ }
135
+
36
136
  /// Convert JSON to a Ruby value.
37
137
  ///
38
138
  /// Recursively converts JSON types to native Ruby types:
@@ -74,20 +174,20 @@ pub fn json_to_ruby_with_uploads(
74
174
  }
75
175
  JsonValue::String(str_val) => Ok(ruby.str_new(str_val).as_value()),
76
176
  JsonValue::Array(items) => {
77
- let array = ruby.ary_new();
177
+ let array = ruby.ary_new_capa(items.len());
78
178
  for item in items {
79
179
  array.push(json_to_ruby_with_uploads(ruby, item, upload_file_class)?)?;
80
180
  }
81
181
  Ok(array.as_value())
82
182
  }
83
183
  JsonValue::Object(map) => {
84
- if let Some(upload_file) = upload_file_class {
85
- if let Some(upload) = try_build_upload_file(ruby, upload_file, map)? {
86
- return Ok(upload);
87
- }
184
+ if let Some(upload_file) = upload_file_class
185
+ && let Some(upload) = try_build_upload_file(ruby, upload_file, map)?
186
+ {
187
+ return Ok(upload);
88
188
  }
89
189
 
90
- let hash = ruby.hash_new();
190
+ let hash = ruby.hash_new_capa(map.len());
91
191
  for (key, item) in map {
92
192
  hash.aset(
93
193
  ruby.str_new(key),
@@ -101,7 +201,7 @@ pub fn json_to_ruby_with_uploads(
101
201
 
102
202
  /// Convert a HashMap to a Ruby Hash.
103
203
  pub fn map_to_ruby_hash(ruby: &Ruby, map: &HashMap<String, String>) -> Result<Value, Error> {
104
- let hash = ruby.hash_new();
204
+ let hash = ruby.hash_new_capa(map.len());
105
205
  for (key, value) in map {
106
206
  hash.aset(ruby.str_new(key), ruby.str_new(value))?;
107
207
  }
@@ -110,9 +210,9 @@ pub fn map_to_ruby_hash(ruby: &Ruby, map: &HashMap<String, String>) -> Result<Va
110
210
 
111
211
  /// Convert a HashMap of Vecs to a Ruby Hash with array values.
112
212
  pub fn multimap_to_ruby_hash(ruby: &Ruby, map: &HashMap<String, Vec<String>>) -> Result<Value, Error> {
113
- let hash = ruby.hash_new();
213
+ let hash = ruby.hash_new_capa(map.len());
114
214
  for (key, values) in map {
115
- let array = ruby.ary_new();
215
+ let array = ruby.ary_new_capa(values.len());
116
216
  for value in values {
117
217
  array.push(ruby.str_new(value))?;
118
218
  }
@@ -165,10 +265,6 @@ fn try_build_upload_file(
165
265
  /// Accepts either String or Array of bytes.
166
266
  pub fn ruby_value_to_bytes(value: Value) -> Result<Bytes, std::io::Error> {
167
267
  if let Ok(str_value) = RString::try_convert(value) {
168
- // SAFETY: Magnus guarantees RString::as_slice() returns valid UTF-8 (or binary)
169
- // bytes for the lifetime of the RString. The slice is only used within this
170
- // function scope to copy into a Bytes buffer, and does not outlive the RString
171
- // reference. The copy_from_slice operation is safe for the borrowed data.
172
268
  let slice = unsafe { str_value.as_slice() };
173
269
  return Ok(Bytes::copy_from_slice(slice));
174
270
  }
@@ -296,10 +392,15 @@ pub fn parse_request_config(ruby: &Ruby, options: Value) -> Result<RequestConfig
296
392
  };
297
393
 
298
394
  let files_opt = get_kw(ruby, hash, "files");
299
- let has_files = files_opt.is_some() && !files_opt.unwrap().is_nil();
395
+ let has_files = files_opt.as_ref().is_some_and(|f| !f.is_nil());
300
396
 
301
397
  let body = if has_files {
302
- let files_value = files_opt.unwrap();
398
+ let files_value = files_opt.ok_or_else(|| {
399
+ Error::new(
400
+ ruby.exception_runtime_error(),
401
+ "Files option should be Some if has_files is true",
402
+ )
403
+ })?;
303
404
  let files = extract_files(ruby, files_value)?;
304
405
 
305
406
  let mut form_data = Vec::new();
@@ -0,0 +1,100 @@
1
+ //! Dependency injection container building from Ruby objects.
2
+ //!
3
+ //! This module handles constructing DependencyContainer instances from Ruby
4
+ //! hashes and factories.
5
+
6
+ use magnus::prelude::*;
7
+ use magnus::{Error, RHash, Ruby, TryConvert, Value, r_hash::ForEach};
8
+ use spikard_core::di::DependencyContainer;
9
+ use std::sync::Arc;
10
+
11
+ /// Build a DependencyContainer from Ruby dependency definitions
12
+ pub fn build_dependency_container(ruby: &Ruby, dependencies: Value) -> Result<DependencyContainer, Error> {
13
+ if dependencies.is_nil() {
14
+ return Ok(DependencyContainer::new());
15
+ }
16
+
17
+ let mut container = DependencyContainer::new();
18
+ let deps_hash = RHash::try_convert(dependencies)?;
19
+
20
+ deps_hash.foreach(|key: String, value: Value| -> Result<ForEach, Error> {
21
+ if let Ok(dep_hash) = RHash::try_convert(value) {
22
+ let dep_type: Option<String> = get_kw(ruby, dep_hash, "type").and_then(|v| {
23
+ if let Ok(sym) = magnus::Symbol::try_convert(v) {
24
+ Some(sym.name().ok()?.to_string())
25
+ } else {
26
+ String::try_convert(v).ok()
27
+ }
28
+ });
29
+
30
+ match dep_type.as_deref() {
31
+ Some("factory") => {
32
+ let factory = get_kw(ruby, dep_hash, "factory")
33
+ .ok_or_else(|| Error::new(ruby.exception_runtime_error(), "Factory missing 'factory' key"))?;
34
+
35
+ let depends_on: Vec<String> = get_kw(ruby, dep_hash, "depends_on")
36
+ .and_then(|v| Vec::<String>::try_convert(v).ok())
37
+ .unwrap_or_default();
38
+
39
+ let singleton: bool = get_kw(ruby, dep_hash, "singleton")
40
+ .and_then(|v| bool::try_convert(v).ok())
41
+ .unwrap_or(false);
42
+
43
+ let cacheable: bool = get_kw(ruby, dep_hash, "cacheable")
44
+ .and_then(|v| bool::try_convert(v).ok())
45
+ .unwrap_or(true);
46
+
47
+ let factory_dep =
48
+ crate::di::RubyFactoryDependency::new(key.clone(), factory, depends_on, singleton, cacheable);
49
+
50
+ container.register(key.clone(), Arc::new(factory_dep)).map_err(|e| {
51
+ Error::new(
52
+ ruby.exception_runtime_error(),
53
+ format!("Failed to register factory '{}': {}", key, e),
54
+ )
55
+ })?;
56
+ }
57
+ Some("value") => {
58
+ let value_data = get_kw(ruby, dep_hash, "value").ok_or_else(|| {
59
+ Error::new(ruby.exception_runtime_error(), "Value dependency missing 'value' key")
60
+ })?;
61
+
62
+ let value_dep = crate::di::RubyValueDependency::new(key.clone(), value_data);
63
+
64
+ container.register(key.clone(), Arc::new(value_dep)).map_err(|e| {
65
+ Error::new(
66
+ ruby.exception_runtime_error(),
67
+ format!("Failed to register value '{}': {}", key, e),
68
+ )
69
+ })?;
70
+ }
71
+ _ => {
72
+ return Err(Error::new(
73
+ ruby.exception_runtime_error(),
74
+ format!("Invalid dependency type for '{}'", key),
75
+ ));
76
+ }
77
+ }
78
+ } else {
79
+ let value_dep = crate::di::RubyValueDependency::new(key.clone(), value);
80
+ container.register(key.clone(), Arc::new(value_dep)).map_err(|e| {
81
+ Error::new(
82
+ ruby.exception_runtime_error(),
83
+ format!("Failed to register value '{}': {}", key, e),
84
+ )
85
+ })?;
86
+ }
87
+
88
+ Ok(ForEach::Continue)
89
+ })?;
90
+
91
+ Ok(container)
92
+ }
93
+
94
+ /// Get a keyword argument from a Ruby hash (returns None if not present or nil)
95
+ fn get_kw(ruby: &Ruby, hash: RHash, name: &str) -> Option<Value> {
96
+ match hash.get(ruby.to_symbol(name)) {
97
+ Some(v) if !v.is_nil() => Some(v),
98
+ _ => None,
99
+ }
100
+ }
@@ -5,6 +5,10 @@
5
5
 
6
6
  #![allow(dead_code)]
7
7
 
8
+ pub mod builder;
9
+
10
+ pub use builder::build_dependency_container;
11
+
8
12
  use http::Request;
9
13
  use magnus::prelude::*;
10
14
  use magnus::value::{InnerValue, Opaque};
@@ -39,7 +43,7 @@ impl Dependency for RubyValueDependency {
39
43
  }
40
44
 
41
45
  fn depends_on(&self) -> Vec<String> {
42
- Vec::new() // Value dependencies have no dependencies
46
+ Vec::new()
43
47
  }
44
48
 
45
49
  fn resolve(
@@ -50,12 +54,10 @@ impl Dependency for RubyValueDependency {
50
54
  ) -> Pin<Box<dyn std::future::Future<Output = Result<Arc<dyn Any + Send + Sync>, DependencyError>> + Send + '_>>
51
55
  {
52
56
  Box::pin(async move {
53
- // Get the Ruby value
54
57
  let ruby = Ruby::get().map_err(|e| DependencyError::ResolutionFailed { message: e.to_string() })?;
55
58
 
56
59
  let value = self.value.get_inner_with(&ruby);
57
60
 
58
- // Convert to JSON and back to make it Send + Sync
59
61
  let json_value = ruby_value_to_json(&ruby, value)
60
62
  .map_err(|e| DependencyError::ResolutionFailed { message: e.to_string() })?;
61
63
 
@@ -64,7 +66,7 @@ impl Dependency for RubyValueDependency {
64
66
  }
65
67
 
66
68
  fn singleton(&self) -> bool {
67
- true // Value dependencies are always singletons
69
+ true
68
70
  }
69
71
 
70
72
  fn cacheable(&self) -> bool {
@@ -111,30 +113,23 @@ impl Dependency for RubyFactoryDependency {
111
113
  resolved: &ResolvedDependencies,
112
114
  ) -> Pin<Box<dyn std::future::Future<Output = Result<Arc<dyn Any + Send + Sync>, DependencyError>> + Send + '_>>
113
115
  {
114
- // Clone data needed in async block
115
116
  let factory = self.factory;
116
117
  let depends_on = self.depends_on.clone();
117
118
  let key = self.key.clone();
118
119
  let is_singleton = self.singleton;
119
120
  let resolved_clone = resolved.clone();
120
121
 
121
- // Extract resolved dependencies now (before async)
122
- // Need to handle both JsonValue and RubyValueWrapper types
123
122
  let resolved_deps: Vec<(String, JsonValue)> = depends_on
124
123
  .iter()
125
124
  .filter_map(|dep_key| {
126
- // Try JsonValue first
127
125
  if let Some(json_value) = resolved.get::<JsonValue>(dep_key) {
128
126
  return Some((dep_key.clone(), (*json_value).clone()));
129
127
  }
130
- // Try RubyValueWrapper (for singletons)
131
- if let Some(wrapper) = resolved.get::<RubyValueWrapper>(dep_key) {
132
- // Convert wrapper to JSON synchronously
133
- if let Ok(ruby) = Ruby::get()
134
- && let Ok(json) = wrapper.to_json(&ruby)
135
- {
136
- return Some((dep_key.clone(), json));
137
- }
128
+ if let Some(wrapper) = resolved.get::<RubyValueWrapper>(dep_key)
129
+ && let Ok(ruby) = Ruby::get()
130
+ && let Ok(json) = wrapper.to_json(&ruby)
131
+ {
132
+ return Some((dep_key.clone(), json));
138
133
  }
139
134
  None
140
135
  })
@@ -143,15 +138,9 @@ impl Dependency for RubyFactoryDependency {
143
138
  Box::pin(async move {
144
139
  let ruby = Ruby::get().map_err(|e| DependencyError::ResolutionFailed { message: e.to_string() })?;
145
140
 
146
- // Build positional arguments array from resolved dependencies
147
- // Dependencies must be passed in the order specified by depends_on
148
- // Important: preserve the order from depends_on, not from resolved_deps iteration
149
141
  let args: Result<Vec<Value>, DependencyError> = depends_on
150
142
  .iter()
151
- .filter_map(|dep_key| {
152
- // Find this dependency in resolved_deps
153
- resolved_deps.iter().find(|(k, _)| k == dep_key).map(|(_, v)| v)
154
- })
143
+ .filter_map(|dep_key| resolved_deps.iter().find(|(k, _)| k == dep_key).map(|(_, v)| v))
155
144
  .map(|dep_value| {
156
145
  json_to_ruby(&ruby, dep_value).map_err(|e| DependencyError::ResolutionFailed {
157
146
  message: format!("Failed to convert dependency value: {}", e),
@@ -160,10 +149,8 @@ impl Dependency for RubyFactoryDependency {
160
149
  .collect();
161
150
  let args = args?;
162
151
 
163
- // Call the factory Proc with positional arguments
164
152
  let factory_value = factory.get_inner_with(&ruby);
165
153
 
166
- // Check if factory responds to call
167
154
  if !factory_value
168
155
  .respond_to("call", false)
169
156
  .map_err(|e| DependencyError::ResolutionFailed { message: e.to_string() })?
@@ -173,10 +160,7 @@ impl Dependency for RubyFactoryDependency {
173
160
  });
174
161
  }
175
162
 
176
- // Call factory with positional arguments
177
- // Use a Ruby helper to call with splatted arguments
178
163
  let result: Value = if !args.is_empty() {
179
- // Create a Ruby array of arguments
180
164
  let args_array = ruby.ary_new();
181
165
  for arg in &args {
182
166
  args_array.push(*arg).map_err(|e| DependencyError::ResolutionFailed {
@@ -184,8 +168,6 @@ impl Dependency for RubyFactoryDependency {
184
168
  })?;
185
169
  }
186
170
 
187
- // Use Ruby's send with * to splat arguments
188
- // Equivalent to: factory_value.call(*args_array)
189
171
  let splat_lambda = ruby
190
172
  .eval::<Value>("lambda { |proc, args| proc.call(*args) }")
191
173
  .map_err(|e| DependencyError::ResolutionFailed {
@@ -200,7 +182,6 @@ impl Dependency for RubyFactoryDependency {
200
182
  message: format!("Failed to call factory for '{}': {}", key, e),
201
183
  })?;
202
184
 
203
- // Check if result is an array with cleanup callback (Ruby pattern: [resource, cleanup_proc])
204
185
  let (value_to_convert, cleanup_callback) = if result.is_kind_of(ruby.class_array()) {
205
186
  let array = magnus::RArray::from_value(result).ok_or_else(|| DependencyError::ResolutionFailed {
206
187
  message: format!("Failed to convert result to array for '{}'", key),
@@ -208,50 +189,40 @@ impl Dependency for RubyFactoryDependency {
208
189
 
209
190
  let len = array.len();
210
191
  if len == 2 {
211
- // Extract the resource (first element)
212
192
  let resource: Value = array.entry(0).map_err(|e| DependencyError::ResolutionFailed {
213
193
  message: format!("Failed to extract resource from array for '{}': {}", key, e),
214
194
  })?;
215
195
 
216
- // Extract cleanup callback (second element)
217
196
  let cleanup: Value = array.entry(1).map_err(|e| DependencyError::ResolutionFailed {
218
197
  message: format!("Failed to extract cleanup callback for '{}': {}", key, e),
219
198
  })?;
220
199
 
221
200
  (resource, Some(cleanup))
222
201
  } else {
223
- // Not a cleanup pattern, use the array as-is
224
202
  (result, None)
225
203
  }
226
204
  } else {
227
- // Not an array, use the value as-is
228
205
  (result, None)
229
206
  };
230
207
 
231
- // Register cleanup callback if present
232
208
  if let Some(cleanup_proc) = cleanup_callback {
233
209
  let cleanup_opaque = Opaque::from(cleanup_proc);
234
210
 
235
211
  resolved_clone.add_cleanup_task(Box::new(move || {
236
212
  Box::pin(async move {
237
- // Get Ruby runtime and call cleanup proc
238
213
  if let Ok(ruby) = Ruby::get() {
239
214
  let proc = cleanup_opaque.get_inner_with(&ruby);
240
- // Call the cleanup proc - ignore errors during cleanup
241
215
  let _ = proc.funcall::<_, _, Value>("call", ());
242
216
  }
243
217
  })
244
218
  }));
245
219
  }
246
220
 
247
- // For singleton dependencies, store Ruby value wrapper to preserve mutations
248
- // For non-singleton, convert to JSON immediately (no need to preserve mutations)
249
221
  if is_singleton {
250
222
  let wrapper = RubyValueWrapper::new(value_to_convert);
251
223
  return Ok(Arc::new(wrapper) as Arc<dyn Any + Send + Sync>);
252
224
  }
253
225
 
254
- // Convert result to JSON for non-singleton dependencies
255
226
  let json_value = ruby_value_to_json(&ruby, value_to_convert)
256
227
  .map_err(|e| DependencyError::ResolutionFailed { message: e.to_string() })?;
257
228
 
@@ -305,8 +276,6 @@ impl RubyValueWrapper {
305
276
  }
306
277
  }
307
278
 
308
- // Safety: Opaque<Value> is designed to be Send + Sync by magnus
309
- // It holds a stable pointer that's safe to share across threads
310
279
  unsafe impl Send for RubyValueWrapper {}
311
280
  unsafe impl Sync for RubyValueWrapper {}
312
281
 
@@ -374,7 +343,6 @@ pub fn extract_di_options(ruby: &Ruby, options: Value) -> Result<(Vec<String>, b
374
343
 
375
344
  let hash = RHash::try_convert(options)?;
376
345
 
377
- // Extract depends_on
378
346
  let depends_on = if let Some(deps_value) = get_kw(ruby, hash, "depends_on") {
379
347
  if deps_value.is_nil() {
380
348
  Vec::new()
@@ -385,14 +353,12 @@ pub fn extract_di_options(ruby: &Ruby, options: Value) -> Result<(Vec<String>, b
385
353
  Vec::new()
386
354
  };
387
355
 
388
- // Extract singleton (default false)
389
356
  let singleton = if let Some(singleton_value) = get_kw(ruby, hash, "singleton") {
390
357
  bool::try_convert(singleton_value).unwrap_or(false)
391
358
  } else {
392
359
  false
393
360
  };
394
361
 
395
- // Extract cacheable (default true)
396
362
  let cacheable = if let Some(cacheable_value) = get_kw(ruby, hash, "cacheable") {
397
363
  bool::try_convert(cacheable_value).unwrap_or(true)
398
364
  } else {