spikard 0.3.2 → 0.3.3
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/LICENSE +1 -1
- data/README.md +659 -659
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +10 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +386 -386
- data/lib/spikard/background.rb +27 -27
- data/lib/spikard/config.rb +396 -396
- data/lib/spikard/converters.rb +13 -13
- data/lib/spikard/handler_wrapper.rb +113 -113
- data/lib/spikard/provide.rb +214 -214
- data/lib/spikard/response.rb +173 -173
- data/lib/spikard/schema.rb +243 -243
- data/lib/spikard/sse.rb +111 -111
- data/lib/spikard/streaming_response.rb +44 -44
- data/lib/spikard/testing.rb +221 -221
- data/lib/spikard/upload_file.rb +131 -131
- data/lib/spikard/version.rb +5 -5
- data/lib/spikard/websocket.rb +59 -59
- data/lib/spikard.rb +43 -43
- data/sig/spikard.rbs +360 -360
- data/vendor/crates/spikard-core/Cargo.toml +40 -40
- data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/crates/spikard-core/src/debug.rs +63 -63
- data/vendor/crates/spikard-core/src/di/container.rs +726 -726
- data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/crates/spikard-core/src/di/error.rs +118 -118
- data/vendor/crates/spikard-core/src/di/factory.rs +538 -538
- data/vendor/crates/spikard-core/src/di/graph.rs +545 -545
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +411 -411
- data/vendor/crates/spikard-core/src/di/value.rs +283 -283
- data/vendor/crates/spikard-core/src/errors.rs +39 -39
- data/vendor/crates/spikard-core/src/http.rs +153 -153
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +422 -422
- data/vendor/crates/spikard-core/src/parameters.rs +722 -722
- data/vendor/crates/spikard-core/src/problem.rs +310 -310
- data/vendor/crates/spikard-core/src/request_data.rs +189 -189
- data/vendor/crates/spikard-core/src/router.rs +249 -249
- data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
- data/vendor/crates/spikard-core/src/validation.rs +699 -699
- data/vendor/crates/spikard-http/Cargo.toml +58 -58
- data/vendor/crates/spikard-http/src/auth.rs +247 -247
- data/vendor/crates/spikard-http/src/background.rs +249 -249
- data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/crates/spikard-http/src/cors.rs +490 -490
- data/vendor/crates/spikard-http/src/debug.rs +63 -63
- data/vendor/crates/spikard-http/src/di_handler.rs +423 -423
- data/vendor/crates/spikard-http/src/handler_response.rs +190 -190
- data/vendor/crates/spikard-http/src/handler_trait.rs +228 -228
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +284 -284
- data/vendor/crates/spikard-http/src/lib.rs +529 -529
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +149 -149
- data/vendor/crates/spikard-http/src/lifecycle.rs +428 -428
- data/vendor/crates/spikard-http/src/middleware/mod.rs +285 -285
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +86 -86
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +147 -147
- data/vendor/crates/spikard-http/src/middleware/validation.rs +287 -287
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +190 -190
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +308 -308
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +195 -195
- data/vendor/crates/spikard-http/src/parameters.rs +1 -1
- data/vendor/crates/spikard-http/src/problem.rs +1 -1
- data/vendor/crates/spikard-http/src/query_parser.rs +369 -369
- data/vendor/crates/spikard-http/src/response.rs +399 -399
- data/vendor/crates/spikard-http/src/router.rs +1 -1
- data/vendor/crates/spikard-http/src/schema_registry.rs +1 -1
- data/vendor/crates/spikard-http/src/server/handler.rs +87 -87
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +98 -98
- data/vendor/crates/spikard-http/src/server/mod.rs +805 -805
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +119 -119
- data/vendor/crates/spikard-http/src/sse.rs +447 -447
- data/vendor/crates/spikard-http/src/testing/form.rs +14 -14
- data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +285 -285
- data/vendor/crates/spikard-http/src/testing.rs +377 -377
- data/vendor/crates/spikard-http/src/type_hints.rs +1 -1
- data/vendor/crates/spikard-http/src/validation.rs +1 -1
- data/vendor/crates/spikard-http/src/websocket.rs +324 -324
- data/vendor/crates/spikard-rb/Cargo.toml +42 -42
- data/vendor/crates/spikard-rb/build.rs +8 -8
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config.rs +294 -294
- data/vendor/crates/spikard-rb/src/conversion.rs +453 -453
- data/vendor/crates/spikard-rb/src/di.rs +409 -409
- data/vendor/crates/spikard-rb/src/handler.rs +625 -625
- data/vendor/crates/spikard-rb/src/lib.rs +2771 -2771
- data/vendor/crates/spikard-rb/src/lifecycle.rs +274 -274
- data/vendor/crates/spikard-rb/src/server.rs +283 -283
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/test_client.rs +404 -404
- data/vendor/crates/spikard-rb/src/test_sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/test_websocket.rs +221 -221
- data/vendor/crates/spikard-rb/src/websocket.rs +233 -233
- data/vendor/spikard-core/Cargo.toml +40 -40
- data/vendor/spikard-core/src/bindings/mod.rs +3 -3
- data/vendor/spikard-core/src/bindings/response.rs +133 -133
- data/vendor/spikard-core/src/debug.rs +63 -63
- data/vendor/spikard-core/src/di/container.rs +726 -726
- data/vendor/spikard-core/src/di/dependency.rs +273 -273
- data/vendor/spikard-core/src/di/error.rs +118 -118
- data/vendor/spikard-core/src/di/factory.rs +538 -538
- data/vendor/spikard-core/src/di/graph.rs +545 -545
- data/vendor/spikard-core/src/di/mod.rs +192 -192
- data/vendor/spikard-core/src/di/resolved.rs +411 -411
- data/vendor/spikard-core/src/di/value.rs +283 -283
- data/vendor/spikard-core/src/http.rs +153 -153
- data/vendor/spikard-core/src/lib.rs +28 -28
- data/vendor/spikard-core/src/lifecycle.rs +422 -422
- data/vendor/spikard-core/src/parameters.rs +719 -719
- data/vendor/spikard-core/src/problem.rs +310 -310
- data/vendor/spikard-core/src/request_data.rs +189 -189
- data/vendor/spikard-core/src/router.rs +249 -249
- data/vendor/spikard-core/src/schema_registry.rs +183 -183
- data/vendor/spikard-core/src/type_hints.rs +304 -304
- data/vendor/spikard-core/src/validation.rs +699 -699
- data/vendor/spikard-http/Cargo.toml +58 -58
- data/vendor/spikard-http/src/auth.rs +247 -247
- data/vendor/spikard-http/src/background.rs +249 -249
- data/vendor/spikard-http/src/bindings/mod.rs +3 -3
- data/vendor/spikard-http/src/bindings/response.rs +1 -1
- data/vendor/spikard-http/src/body_metadata.rs +8 -8
- data/vendor/spikard-http/src/cors.rs +490 -490
- data/vendor/spikard-http/src/debug.rs +63 -63
- data/vendor/spikard-http/src/di_handler.rs +423 -423
- data/vendor/spikard-http/src/handler_response.rs +190 -190
- data/vendor/spikard-http/src/handler_trait.rs +228 -228
- data/vendor/spikard-http/src/handler_trait_tests.rs +284 -284
- data/vendor/spikard-http/src/lib.rs +529 -529
- data/vendor/spikard-http/src/lifecycle/adapter.rs +149 -149
- data/vendor/spikard-http/src/lifecycle.rs +428 -428
- data/vendor/spikard-http/src/middleware/mod.rs +285 -285
- data/vendor/spikard-http/src/middleware/multipart.rs +86 -86
- data/vendor/spikard-http/src/middleware/urlencoded.rs +147 -147
- data/vendor/spikard-http/src/middleware/validation.rs +287 -287
- data/vendor/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/spikard-http/src/openapi/parameter_extraction.rs +190 -190
- data/vendor/spikard-http/src/openapi/schema_conversion.rs +308 -308
- data/vendor/spikard-http/src/openapi/spec_generation.rs +195 -195
- data/vendor/spikard-http/src/parameters.rs +1 -1
- data/vendor/spikard-http/src/problem.rs +1 -1
- data/vendor/spikard-http/src/query_parser.rs +369 -369
- data/vendor/spikard-http/src/response.rs +399 -399
- data/vendor/spikard-http/src/router.rs +1 -1
- data/vendor/spikard-http/src/schema_registry.rs +1 -1
- data/vendor/spikard-http/src/server/handler.rs +80 -80
- data/vendor/spikard-http/src/server/lifecycle_execution.rs +98 -98
- data/vendor/spikard-http/src/server/mod.rs +805 -805
- data/vendor/spikard-http/src/server/request_extraction.rs +119 -119
- data/vendor/spikard-http/src/sse.rs +447 -447
- data/vendor/spikard-http/src/testing/form.rs +14 -14
- data/vendor/spikard-http/src/testing/multipart.rs +60 -60
- data/vendor/spikard-http/src/testing/test_client.rs +285 -285
- data/vendor/spikard-http/src/testing.rs +377 -377
- data/vendor/spikard-http/src/type_hints.rs +1 -1
- data/vendor/spikard-http/src/validation.rs +1 -1
- data/vendor/spikard-http/src/websocket.rs +324 -324
- data/vendor/spikard-rb/Cargo.toml +42 -42
- data/vendor/spikard-rb/build.rs +8 -8
- data/vendor/spikard-rb/src/background.rs +63 -63
- data/vendor/spikard-rb/src/config.rs +294 -294
- data/vendor/spikard-rb/src/conversion.rs +392 -392
- data/vendor/spikard-rb/src/di.rs +409 -409
- data/vendor/spikard-rb/src/handler.rs +534 -534
- data/vendor/spikard-rb/src/lib.rs +2020 -2020
- data/vendor/spikard-rb/src/lifecycle.rs +267 -267
- data/vendor/spikard-rb/src/server.rs +283 -283
- data/vendor/spikard-rb/src/sse.rs +231 -231
- data/vendor/spikard-rb/src/test_client.rs +404 -404
- data/vendor/spikard-rb/src/test_sse.rs +143 -143
- data/vendor/spikard-rb/src/test_websocket.rs +221 -221
- data/vendor/spikard-rb/src/websocket.rs +233 -233
- metadata +1 -1
|
@@ -1,233 +1,233 @@
|
|
|
1
|
-
//! Ruby WebSocket handler bindings
|
|
2
|
-
//!
|
|
3
|
-
//! This module provides the bridge between Ruby blocks/procs and Rust's WebSocket system.
|
|
4
|
-
//! Uses magnus to safely call Ruby code from Rust async tasks.
|
|
5
|
-
|
|
6
|
-
use magnus::{RHash, Value, prelude::*, value::Opaque};
|
|
7
|
-
use serde_json::Value as JsonValue;
|
|
8
|
-
use spikard_http::WebSocketHandler;
|
|
9
|
-
use tracing::{debug, error};
|
|
10
|
-
|
|
11
|
-
/// Ruby implementation of WebSocketHandler
|
|
12
|
-
pub struct RubyWebSocketHandler {
|
|
13
|
-
/// Handler name for debugging
|
|
14
|
-
name: String,
|
|
15
|
-
/// Ruby proc/callable for handle_message (Opaque for Send safety)
|
|
16
|
-
handle_message_proc: Opaque<Value>,
|
|
17
|
-
/// Ruby proc/callable for on_connect (Opaque for Send safety)
|
|
18
|
-
on_connect_proc: Option<Opaque<Value>>,
|
|
19
|
-
/// Ruby proc/callable for on_disconnect (Opaque for Send safety)
|
|
20
|
-
on_disconnect_proc: Option<Opaque<Value>>,
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
impl RubyWebSocketHandler {
|
|
24
|
-
/// Create a new Ruby WebSocket handler
|
|
25
|
-
#[allow(dead_code)]
|
|
26
|
-
pub fn new(
|
|
27
|
-
name: String,
|
|
28
|
-
handle_message_proc: Value,
|
|
29
|
-
on_connect_proc: Option<Value>,
|
|
30
|
-
on_disconnect_proc: Option<Value>,
|
|
31
|
-
) -> Self {
|
|
32
|
-
Self {
|
|
33
|
-
name,
|
|
34
|
-
handle_message_proc: handle_message_proc.into(),
|
|
35
|
-
on_connect_proc: on_connect_proc.map(|v| v.into()),
|
|
36
|
-
on_disconnect_proc: on_disconnect_proc.map(|v| v.into()),
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// Convert Ruby value to JSON
|
|
41
|
-
fn ruby_to_json(ruby: &magnus::Ruby, value: Value) -> Result<JsonValue, String> {
|
|
42
|
-
if value.is_nil() {
|
|
43
|
-
return Ok(JsonValue::Null);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
let json_module: Value = ruby
|
|
47
|
-
.class_object()
|
|
48
|
-
.const_get("JSON")
|
|
49
|
-
.map_err(|e| format!("JSON module not available: {}", e))?;
|
|
50
|
-
|
|
51
|
-
let json_str: String = json_module
|
|
52
|
-
.funcall("generate", (value,))
|
|
53
|
-
.map_err(|e| format!("Failed to generate JSON: {}", e))?;
|
|
54
|
-
|
|
55
|
-
serde_json::from_str(&json_str).map_err(|e| format!("Failed to parse JSON: {}", e))
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/// Convert JSON value to Ruby value
|
|
59
|
-
fn json_to_ruby(ruby: &magnus::Ruby, value: &JsonValue) -> Result<Value, String> {
|
|
60
|
-
match value {
|
|
61
|
-
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
62
|
-
JsonValue::Bool(b) => Ok(if *b {
|
|
63
|
-
ruby.qtrue().as_value()
|
|
64
|
-
} else {
|
|
65
|
-
ruby.qfalse().as_value()
|
|
66
|
-
}),
|
|
67
|
-
JsonValue::Number(num) => {
|
|
68
|
-
if let Some(i) = num.as_i64() {
|
|
69
|
-
Ok(ruby.integer_from_i64(i).as_value())
|
|
70
|
-
} else if let Some(f) = num.as_f64() {
|
|
71
|
-
Ok(ruby.float_from_f64(f).as_value())
|
|
72
|
-
} else {
|
|
73
|
-
Ok(ruby.qnil().as_value())
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
77
|
-
JsonValue::Array(arr) => {
|
|
78
|
-
let ruby_array = ruby.ary_new();
|
|
79
|
-
for item in arr {
|
|
80
|
-
ruby_array
|
|
81
|
-
.push(Self::json_to_ruby(ruby, item)?)
|
|
82
|
-
.map_err(|e| format!("Failed to push to array: {}", e))?;
|
|
83
|
-
}
|
|
84
|
-
Ok(ruby_array.as_value())
|
|
85
|
-
}
|
|
86
|
-
JsonValue::Object(obj) => {
|
|
87
|
-
let ruby_hash = RHash::new();
|
|
88
|
-
for (key, val) in obj {
|
|
89
|
-
ruby_hash
|
|
90
|
-
.aset(ruby.str_new(key), Self::json_to_ruby(ruby, val)?)
|
|
91
|
-
.map_err(|e| format!("Failed to set hash value: {}", e))?;
|
|
92
|
-
}
|
|
93
|
-
Ok(ruby_hash.as_value())
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
impl WebSocketHandler for RubyWebSocketHandler {
|
|
100
|
-
async fn handle_message(&self, message: JsonValue) -> Option<JsonValue> {
|
|
101
|
-
debug!("Ruby WebSocket handler '{}': handle_message", self.name);
|
|
102
|
-
|
|
103
|
-
match magnus::Ruby::get()
|
|
104
|
-
.map_err(|e| format!("Failed to get Ruby: {}", e))
|
|
105
|
-
.and_then(|ruby| {
|
|
106
|
-
let message_ruby = Self::json_to_ruby(&ruby, &message)?;
|
|
107
|
-
|
|
108
|
-
let proc_value = ruby.get_inner(self.handle_message_proc);
|
|
109
|
-
let result: Value = proc_value
|
|
110
|
-
.funcall("call", (message_ruby,))
|
|
111
|
-
.map_err(|e| format!("Handler '{}' call failed: {}", self.name, e))?;
|
|
112
|
-
|
|
113
|
-
if result.is_nil() {
|
|
114
|
-
Ok(None)
|
|
115
|
-
} else {
|
|
116
|
-
Self::ruby_to_json(&ruby, result).map(Some)
|
|
117
|
-
}
|
|
118
|
-
}) {
|
|
119
|
-
Ok(value) => value,
|
|
120
|
-
Err(e) => {
|
|
121
|
-
error!("Ruby error in handle_message: {}", e);
|
|
122
|
-
None
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async fn on_connect(&self) {
|
|
128
|
-
debug!("Ruby WebSocket handler '{}': on_connect", self.name);
|
|
129
|
-
|
|
130
|
-
if let Some(on_connect_proc) = self.on_connect_proc {
|
|
131
|
-
if let Err(e) = magnus::Ruby::get()
|
|
132
|
-
.map_err(|e| format!("Failed to get Ruby: {}", e))
|
|
133
|
-
.and_then(|ruby| {
|
|
134
|
-
let proc_value = ruby.get_inner(on_connect_proc);
|
|
135
|
-
proc_value
|
|
136
|
-
.funcall::<_, _, Value>("call", ())
|
|
137
|
-
.map_err(|e| format!("on_connect '{}' call failed: {}", self.name, e))?;
|
|
138
|
-
Ok(())
|
|
139
|
-
})
|
|
140
|
-
{
|
|
141
|
-
error!("on_connect error: {}", e);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
debug!("Ruby WebSocket handler '{}': on_connect completed", self.name);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
async fn on_disconnect(&self) {
|
|
149
|
-
debug!("Ruby WebSocket handler '{}': on_disconnect", self.name);
|
|
150
|
-
|
|
151
|
-
if let Some(on_disconnect_proc) = self.on_disconnect_proc {
|
|
152
|
-
if let Err(e) = magnus::Ruby::get()
|
|
153
|
-
.map_err(|e| format!("Failed to get Ruby: {}", e))
|
|
154
|
-
.and_then(|ruby| {
|
|
155
|
-
let proc_value = ruby.get_inner(on_disconnect_proc);
|
|
156
|
-
proc_value
|
|
157
|
-
.funcall::<_, _, Value>("call", ())
|
|
158
|
-
.map_err(|e| format!("on_disconnect '{}' call failed: {}", self.name, e))?;
|
|
159
|
-
Ok(())
|
|
160
|
-
})
|
|
161
|
-
{
|
|
162
|
-
error!("on_disconnect error: {}", e);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
debug!("Ruby WebSocket handler '{}': on_disconnect completed", self.name);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
unsafe impl Send for RubyWebSocketHandler {}
|
|
171
|
-
unsafe impl Sync for RubyWebSocketHandler {}
|
|
172
|
-
|
|
173
|
-
/// Create WebSocketState from Ruby handler object
|
|
174
|
-
///
|
|
175
|
-
/// This function is designed to be called from Ruby to register WebSocket handlers.
|
|
176
|
-
#[allow(dead_code)]
|
|
177
|
-
pub fn create_websocket_state(
|
|
178
|
-
ruby: &magnus::Ruby,
|
|
179
|
-
handler_obj: Value,
|
|
180
|
-
) -> Result<spikard_http::WebSocketState<RubyWebSocketHandler>, magnus::Error> {
|
|
181
|
-
let handle_message_proc: Value = handler_obj
|
|
182
|
-
.funcall("method", (ruby.to_symbol("handle_message"),))
|
|
183
|
-
.map_err(|e| {
|
|
184
|
-
magnus::Error::new(
|
|
185
|
-
ruby.exception_arg_error(),
|
|
186
|
-
format!("handle_message method not found: {}", e),
|
|
187
|
-
)
|
|
188
|
-
})?;
|
|
189
|
-
|
|
190
|
-
let on_connect_proc = handler_obj
|
|
191
|
-
.funcall::<_, _, Value>("method", (ruby.to_symbol("on_connect"),))
|
|
192
|
-
.ok();
|
|
193
|
-
|
|
194
|
-
let on_disconnect_proc = handler_obj
|
|
195
|
-
.funcall::<_, _, Value>("method", (ruby.to_symbol("on_disconnect"),))
|
|
196
|
-
.ok();
|
|
197
|
-
|
|
198
|
-
let message_schema = handler_obj
|
|
199
|
-
.funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_message_schema"),))
|
|
200
|
-
.ok()
|
|
201
|
-
.and_then(|v| {
|
|
202
|
-
if v.is_nil() {
|
|
203
|
-
None
|
|
204
|
-
} else {
|
|
205
|
-
RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
let response_schema = handler_obj
|
|
210
|
-
.funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_response_schema"),))
|
|
211
|
-
.ok()
|
|
212
|
-
.and_then(|v| {
|
|
213
|
-
if v.is_nil() {
|
|
214
|
-
None
|
|
215
|
-
} else {
|
|
216
|
-
RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
let ruby_handler = RubyWebSocketHandler::new(
|
|
221
|
-
"WebSocketHandler".to_string(),
|
|
222
|
-
handle_message_proc,
|
|
223
|
-
on_connect_proc,
|
|
224
|
-
on_disconnect_proc,
|
|
225
|
-
);
|
|
226
|
-
|
|
227
|
-
if message_schema.is_some() || response_schema.is_some() {
|
|
228
|
-
spikard_http::WebSocketState::with_schemas(ruby_handler, message_schema, response_schema)
|
|
229
|
-
.map_err(|e| magnus::Error::new(ruby.exception_runtime_error(), e))
|
|
230
|
-
} else {
|
|
231
|
-
Ok(spikard_http::WebSocketState::new(ruby_handler))
|
|
232
|
-
}
|
|
233
|
-
}
|
|
1
|
+
//! Ruby WebSocket handler bindings
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides the bridge between Ruby blocks/procs and Rust's WebSocket system.
|
|
4
|
+
//! Uses magnus to safely call Ruby code from Rust async tasks.
|
|
5
|
+
|
|
6
|
+
use magnus::{RHash, Value, prelude::*, value::Opaque};
|
|
7
|
+
use serde_json::Value as JsonValue;
|
|
8
|
+
use spikard_http::WebSocketHandler;
|
|
9
|
+
use tracing::{debug, error};
|
|
10
|
+
|
|
11
|
+
/// Ruby implementation of WebSocketHandler
|
|
12
|
+
pub struct RubyWebSocketHandler {
|
|
13
|
+
/// Handler name for debugging
|
|
14
|
+
name: String,
|
|
15
|
+
/// Ruby proc/callable for handle_message (Opaque for Send safety)
|
|
16
|
+
handle_message_proc: Opaque<Value>,
|
|
17
|
+
/// Ruby proc/callable for on_connect (Opaque for Send safety)
|
|
18
|
+
on_connect_proc: Option<Opaque<Value>>,
|
|
19
|
+
/// Ruby proc/callable for on_disconnect (Opaque for Send safety)
|
|
20
|
+
on_disconnect_proc: Option<Opaque<Value>>,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
impl RubyWebSocketHandler {
|
|
24
|
+
/// Create a new Ruby WebSocket handler
|
|
25
|
+
#[allow(dead_code)]
|
|
26
|
+
pub fn new(
|
|
27
|
+
name: String,
|
|
28
|
+
handle_message_proc: Value,
|
|
29
|
+
on_connect_proc: Option<Value>,
|
|
30
|
+
on_disconnect_proc: Option<Value>,
|
|
31
|
+
) -> Self {
|
|
32
|
+
Self {
|
|
33
|
+
name,
|
|
34
|
+
handle_message_proc: handle_message_proc.into(),
|
|
35
|
+
on_connect_proc: on_connect_proc.map(|v| v.into()),
|
|
36
|
+
on_disconnect_proc: on_disconnect_proc.map(|v| v.into()),
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Convert Ruby value to JSON
|
|
41
|
+
fn ruby_to_json(ruby: &magnus::Ruby, value: Value) -> Result<JsonValue, String> {
|
|
42
|
+
if value.is_nil() {
|
|
43
|
+
return Ok(JsonValue::Null);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let json_module: Value = ruby
|
|
47
|
+
.class_object()
|
|
48
|
+
.const_get("JSON")
|
|
49
|
+
.map_err(|e| format!("JSON module not available: {}", e))?;
|
|
50
|
+
|
|
51
|
+
let json_str: String = json_module
|
|
52
|
+
.funcall("generate", (value,))
|
|
53
|
+
.map_err(|e| format!("Failed to generate JSON: {}", e))?;
|
|
54
|
+
|
|
55
|
+
serde_json::from_str(&json_str).map_err(|e| format!("Failed to parse JSON: {}", e))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// Convert JSON value to Ruby value
|
|
59
|
+
fn json_to_ruby(ruby: &magnus::Ruby, value: &JsonValue) -> Result<Value, String> {
|
|
60
|
+
match value {
|
|
61
|
+
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
62
|
+
JsonValue::Bool(b) => Ok(if *b {
|
|
63
|
+
ruby.qtrue().as_value()
|
|
64
|
+
} else {
|
|
65
|
+
ruby.qfalse().as_value()
|
|
66
|
+
}),
|
|
67
|
+
JsonValue::Number(num) => {
|
|
68
|
+
if let Some(i) = num.as_i64() {
|
|
69
|
+
Ok(ruby.integer_from_i64(i).as_value())
|
|
70
|
+
} else if let Some(f) = num.as_f64() {
|
|
71
|
+
Ok(ruby.float_from_f64(f).as_value())
|
|
72
|
+
} else {
|
|
73
|
+
Ok(ruby.qnil().as_value())
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
77
|
+
JsonValue::Array(arr) => {
|
|
78
|
+
let ruby_array = ruby.ary_new();
|
|
79
|
+
for item in arr {
|
|
80
|
+
ruby_array
|
|
81
|
+
.push(Self::json_to_ruby(ruby, item)?)
|
|
82
|
+
.map_err(|e| format!("Failed to push to array: {}", e))?;
|
|
83
|
+
}
|
|
84
|
+
Ok(ruby_array.as_value())
|
|
85
|
+
}
|
|
86
|
+
JsonValue::Object(obj) => {
|
|
87
|
+
let ruby_hash = RHash::new();
|
|
88
|
+
for (key, val) in obj {
|
|
89
|
+
ruby_hash
|
|
90
|
+
.aset(ruby.str_new(key), Self::json_to_ruby(ruby, val)?)
|
|
91
|
+
.map_err(|e| format!("Failed to set hash value: {}", e))?;
|
|
92
|
+
}
|
|
93
|
+
Ok(ruby_hash.as_value())
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
impl WebSocketHandler for RubyWebSocketHandler {
|
|
100
|
+
async fn handle_message(&self, message: JsonValue) -> Option<JsonValue> {
|
|
101
|
+
debug!("Ruby WebSocket handler '{}': handle_message", self.name);
|
|
102
|
+
|
|
103
|
+
match magnus::Ruby::get()
|
|
104
|
+
.map_err(|e| format!("Failed to get Ruby: {}", e))
|
|
105
|
+
.and_then(|ruby| {
|
|
106
|
+
let message_ruby = Self::json_to_ruby(&ruby, &message)?;
|
|
107
|
+
|
|
108
|
+
let proc_value = ruby.get_inner(self.handle_message_proc);
|
|
109
|
+
let result: Value = proc_value
|
|
110
|
+
.funcall("call", (message_ruby,))
|
|
111
|
+
.map_err(|e| format!("Handler '{}' call failed: {}", self.name, e))?;
|
|
112
|
+
|
|
113
|
+
if result.is_nil() {
|
|
114
|
+
Ok(None)
|
|
115
|
+
} else {
|
|
116
|
+
Self::ruby_to_json(&ruby, result).map(Some)
|
|
117
|
+
}
|
|
118
|
+
}) {
|
|
119
|
+
Ok(value) => value,
|
|
120
|
+
Err(e) => {
|
|
121
|
+
error!("Ruby error in handle_message: {}", e);
|
|
122
|
+
None
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async fn on_connect(&self) {
|
|
128
|
+
debug!("Ruby WebSocket handler '{}': on_connect", self.name);
|
|
129
|
+
|
|
130
|
+
if let Some(on_connect_proc) = self.on_connect_proc {
|
|
131
|
+
if let Err(e) = magnus::Ruby::get()
|
|
132
|
+
.map_err(|e| format!("Failed to get Ruby: {}", e))
|
|
133
|
+
.and_then(|ruby| {
|
|
134
|
+
let proc_value = ruby.get_inner(on_connect_proc);
|
|
135
|
+
proc_value
|
|
136
|
+
.funcall::<_, _, Value>("call", ())
|
|
137
|
+
.map_err(|e| format!("on_connect '{}' call failed: {}", self.name, e))?;
|
|
138
|
+
Ok(())
|
|
139
|
+
})
|
|
140
|
+
{
|
|
141
|
+
error!("on_connect error: {}", e);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
debug!("Ruby WebSocket handler '{}': on_connect completed", self.name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async fn on_disconnect(&self) {
|
|
149
|
+
debug!("Ruby WebSocket handler '{}': on_disconnect", self.name);
|
|
150
|
+
|
|
151
|
+
if let Some(on_disconnect_proc) = self.on_disconnect_proc {
|
|
152
|
+
if let Err(e) = magnus::Ruby::get()
|
|
153
|
+
.map_err(|e| format!("Failed to get Ruby: {}", e))
|
|
154
|
+
.and_then(|ruby| {
|
|
155
|
+
let proc_value = ruby.get_inner(on_disconnect_proc);
|
|
156
|
+
proc_value
|
|
157
|
+
.funcall::<_, _, Value>("call", ())
|
|
158
|
+
.map_err(|e| format!("on_disconnect '{}' call failed: {}", self.name, e))?;
|
|
159
|
+
Ok(())
|
|
160
|
+
})
|
|
161
|
+
{
|
|
162
|
+
error!("on_disconnect error: {}", e);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
debug!("Ruby WebSocket handler '{}': on_disconnect completed", self.name);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
unsafe impl Send for RubyWebSocketHandler {}
|
|
171
|
+
unsafe impl Sync for RubyWebSocketHandler {}
|
|
172
|
+
|
|
173
|
+
/// Create WebSocketState from Ruby handler object
|
|
174
|
+
///
|
|
175
|
+
/// This function is designed to be called from Ruby to register WebSocket handlers.
|
|
176
|
+
#[allow(dead_code)]
|
|
177
|
+
pub fn create_websocket_state(
|
|
178
|
+
ruby: &magnus::Ruby,
|
|
179
|
+
handler_obj: Value,
|
|
180
|
+
) -> Result<spikard_http::WebSocketState<RubyWebSocketHandler>, magnus::Error> {
|
|
181
|
+
let handle_message_proc: Value = handler_obj
|
|
182
|
+
.funcall("method", (ruby.to_symbol("handle_message"),))
|
|
183
|
+
.map_err(|e| {
|
|
184
|
+
magnus::Error::new(
|
|
185
|
+
ruby.exception_arg_error(),
|
|
186
|
+
format!("handle_message method not found: {}", e),
|
|
187
|
+
)
|
|
188
|
+
})?;
|
|
189
|
+
|
|
190
|
+
let on_connect_proc = handler_obj
|
|
191
|
+
.funcall::<_, _, Value>("method", (ruby.to_symbol("on_connect"),))
|
|
192
|
+
.ok();
|
|
193
|
+
|
|
194
|
+
let on_disconnect_proc = handler_obj
|
|
195
|
+
.funcall::<_, _, Value>("method", (ruby.to_symbol("on_disconnect"),))
|
|
196
|
+
.ok();
|
|
197
|
+
|
|
198
|
+
let message_schema = handler_obj
|
|
199
|
+
.funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_message_schema"),))
|
|
200
|
+
.ok()
|
|
201
|
+
.and_then(|v| {
|
|
202
|
+
if v.is_nil() {
|
|
203
|
+
None
|
|
204
|
+
} else {
|
|
205
|
+
RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let response_schema = handler_obj
|
|
210
|
+
.funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_response_schema"),))
|
|
211
|
+
.ok()
|
|
212
|
+
.and_then(|v| {
|
|
213
|
+
if v.is_nil() {
|
|
214
|
+
None
|
|
215
|
+
} else {
|
|
216
|
+
RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
let ruby_handler = RubyWebSocketHandler::new(
|
|
221
|
+
"WebSocketHandler".to_string(),
|
|
222
|
+
handle_message_proc,
|
|
223
|
+
on_connect_proc,
|
|
224
|
+
on_disconnect_proc,
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if message_schema.is_some() || response_schema.is_some() {
|
|
228
|
+
spikard_http::WebSocketState::with_schemas(ruby_handler, message_schema, response_schema)
|
|
229
|
+
.map_err(|e| magnus::Error::new(ruby.exception_runtime_error(), e))
|
|
230
|
+
} else {
|
|
231
|
+
Ok(spikard_http::WebSocketState::new(ruby_handler))
|
|
232
|
+
}
|
|
233
|
+
}
|