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
|
@@ -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
|
|
13
|
+
use spikard_core::problem::ProblemDetails;
|
|
13
14
|
use spikard_http::testing::MultipartFilePart;
|
|
14
15
|
use std::collections::HashMap;
|
|
15
16
|
|
|
16
|
-
use crate::
|
|
17
|
+
use crate::testing::client::{RequestBody, RequestConfig, TestResponseData};
|
|
17
18
|
|
|
18
19
|
/// Convert a Ruby value to JSON.
|
|
19
20
|
///
|
|
20
|
-
///
|
|
21
|
-
///
|
|
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.
|
|
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
|
-
|
|
86
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
213
|
+
let hash = ruby.hash_new_capa(map.len());
|
|
114
214
|
for (key, values) in map {
|
|
115
|
-
let array = ruby.
|
|
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.
|
|
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.
|
|
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()
|
|
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
|
|
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
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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 {
|