spikard 0.5.0 → 0.6.1
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 +674 -674
- data/ext/spikard_rb/Cargo.toml +17 -17
- data/ext/spikard_rb/extconf.rb +13 -10
- data/ext/spikard_rb/src/lib.rs +6 -6
- data/lib/spikard/app.rb +405 -405
- 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 +256 -256
- 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 +366 -366
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
- data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
- data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
- 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 +127 -127
- data/vendor/crates/spikard-core/src/di/container.rs +702 -702
- 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 +534 -534
- data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
- data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
- data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
- data/vendor/crates/spikard-core/src/di/value.rs +281 -281
- data/vendor/crates/spikard-core/src/errors.rs +69 -69
- data/vendor/crates/spikard-core/src/http.rs +415 -415
- data/vendor/crates/spikard-core/src/lib.rs +29 -29
- data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
- data/vendor/crates/spikard-core/src/metadata.rs +389 -389
- data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
- data/vendor/crates/spikard-core/src/problem.rs +344 -344
- data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
- data/vendor/crates/spikard-core/src/router.rs +510 -510
- 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/error_mapper.rs +696 -688
- data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
- data/vendor/crates/spikard-http/Cargo.toml +62 -64
- data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
- data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
- data/vendor/crates/spikard-http/src/auth.rs +296 -296
- data/vendor/crates/spikard-http/src/background.rs +1860 -1860
- 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 +1005 -1005
- data/vendor/crates/spikard-http/src/debug.rs +128 -128
- data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
- data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
- data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
- data/vendor/crates/spikard-http/src/lib.rs +534 -534
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
- data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
- data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
- data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
- data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
- data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
- data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
- data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
- data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
- data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
- data/vendor/crates/spikard-http/src/response.rs +720 -720
- data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
- data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
- data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
- data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
- data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
- data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
- data/vendor/crates/spikard-http/src/testing.rs +406 -377
- data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
- data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
- data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
- data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
- data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
- data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
- data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
- data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
- data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
- data/vendor/crates/spikard-rb/Cargo.toml +48 -48
- data/vendor/crates/spikard-rb/build.rs +199 -199
- data/vendor/crates/spikard-rb/src/background.rs +63 -63
- data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
- data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
- data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
- data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
- data/vendor/crates/spikard-rb/src/handler.rs +618 -618
- data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
- data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
- data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
- data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
- data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
- data/vendor/crates/spikard-rb/src/server.rs +305 -308
- data/vendor/crates/spikard-rb/src/sse.rs +231 -231
- data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
- data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
- data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
- data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
- metadata +15 -1
|
@@ -1,374 +1,377 @@
|
|
|
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 std::sync::mpsc;
|
|
10
|
-
use tokio::sync::oneshot;
|
|
11
|
-
use tracing::{debug, error};
|
|
12
|
-
|
|
13
|
-
/// Ruby implementation of WebSocketHandler
|
|
14
|
-
pub struct RubyWebSocketHandler {
|
|
15
|
-
/// Handler name for debugging
|
|
16
|
-
name: String,
|
|
17
|
-
/// Ruby proc/callable for handle_message (Opaque for Send safety)
|
|
18
|
-
#[allow(dead_code)]
|
|
19
|
-
handle_message_proc: Opaque<Value>,
|
|
20
|
-
/// Ruby proc/callable for on_connect (Opaque for Send safety)
|
|
21
|
-
on_connect_proc: Option<Opaque<Value>>,
|
|
22
|
-
/// Ruby proc/callable for on_disconnect (Opaque for Send safety)
|
|
23
|
-
on_disconnect_proc: Option<Opaque<Value>>,
|
|
24
|
-
/// Work queue for executing Ruby callbacks on a Ruby thread
|
|
25
|
-
work_tx: mpsc::Sender<WebSocketWorkItem>,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
enum WebSocketWorkItem {
|
|
29
|
-
HandleMessage {
|
|
30
|
-
message: JsonValue,
|
|
31
|
-
reply: oneshot::Sender<Result<Option<JsonValue>, String>>,
|
|
32
|
-
},
|
|
33
|
-
OnConnect {
|
|
34
|
-
reply: oneshot::Sender<Result<(), String>>,
|
|
35
|
-
},
|
|
36
|
-
OnDisconnect {
|
|
37
|
-
reply: oneshot::Sender<Result<(), String>>,
|
|
38
|
-
},
|
|
39
|
-
Shutdown,
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
impl RubyWebSocketHandler {
|
|
43
|
-
/// Create a new Ruby WebSocket handler
|
|
44
|
-
#[allow(dead_code)]
|
|
45
|
-
pub fn new(
|
|
46
|
-
ruby: &magnus::Ruby,
|
|
47
|
-
name: String,
|
|
48
|
-
handle_message_proc: Value,
|
|
49
|
-
on_connect_proc: Option<Value>,
|
|
50
|
-
on_disconnect_proc: Option<Value>,
|
|
51
|
-
) -> Self {
|
|
52
|
-
let handle_message_proc = Opaque::from(handle_message_proc);
|
|
53
|
-
let on_connect_proc = on_connect_proc.map(Opaque::from);
|
|
54
|
-
let on_disconnect_proc = on_disconnect_proc.map(Opaque::from);
|
|
55
|
-
let (work_tx, work_rx) = mpsc::channel();
|
|
56
|
-
let handler_name = name.clone();
|
|
57
|
-
|
|
58
|
-
let handle_message_proc_for_thread = handle_message_proc;
|
|
59
|
-
let on_connect_proc_for_thread = on_connect_proc;
|
|
60
|
-
let on_disconnect_proc_for_thread = on_disconnect_proc;
|
|
61
|
-
|
|
62
|
-
ruby.thread_create_from_fn(move |ruby| {
|
|
63
|
-
websocket_worker_loop(
|
|
64
|
-
ruby,
|
|
65
|
-
&handler_name,
|
|
66
|
-
handle_message_proc_for_thread,
|
|
67
|
-
on_connect_proc_for_thread,
|
|
68
|
-
on_disconnect_proc_for_thread,
|
|
69
|
-
work_rx,
|
|
70
|
-
);
|
|
71
|
-
ruby.qnil()
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
Self {
|
|
75
|
-
name,
|
|
76
|
-
handle_message_proc,
|
|
77
|
-
on_connect_proc,
|
|
78
|
-
on_disconnect_proc,
|
|
79
|
-
work_tx,
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/// Convert Ruby value to JSON
|
|
84
|
-
fn ruby_to_json(ruby: &magnus::Ruby, value: Value) -> Result<JsonValue, String> {
|
|
85
|
-
if value.is_nil() {
|
|
86
|
-
return Ok(JsonValue::Null);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
let json_module: Value = ruby
|
|
90
|
-
.class_object()
|
|
91
|
-
.const_get("JSON")
|
|
92
|
-
.map_err(|e| format!("JSON module not available: {}", e))?;
|
|
93
|
-
|
|
94
|
-
let json_str: String = json_module
|
|
95
|
-
.funcall("generate", (value,))
|
|
96
|
-
.map_err(|e| format!("Failed to generate JSON: {}", e))?;
|
|
97
|
-
|
|
98
|
-
serde_json::from_str(&json_str).map_err(|e| format!("Failed to parse JSON: {}", e))
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/// Convert JSON value to Ruby value
|
|
102
|
-
fn json_to_ruby(ruby: &magnus::Ruby, value: &JsonValue) -> Result<Value, String> {
|
|
103
|
-
match value {
|
|
104
|
-
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
105
|
-
JsonValue::Bool(b) => Ok(if *b {
|
|
106
|
-
ruby.qtrue().as_value()
|
|
107
|
-
} else {
|
|
108
|
-
ruby.qfalse().as_value()
|
|
109
|
-
}),
|
|
110
|
-
JsonValue::Number(num) => {
|
|
111
|
-
if let Some(i) = num.as_i64() {
|
|
112
|
-
Ok(ruby.integer_from_i64(i).as_value())
|
|
113
|
-
} else if let Some(f) = num.as_f64() {
|
|
114
|
-
Ok(ruby.float_from_f64(f).as_value())
|
|
115
|
-
} else {
|
|
116
|
-
Ok(ruby.qnil().as_value())
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
120
|
-
JsonValue::Array(arr) => {
|
|
121
|
-
let ruby_array = ruby.ary_new();
|
|
122
|
-
for item in arr {
|
|
123
|
-
ruby_array
|
|
124
|
-
.push(Self::json_to_ruby(ruby, item)?)
|
|
125
|
-
.map_err(|e| format!("Failed to push to array: {}", e))?;
|
|
126
|
-
}
|
|
127
|
-
Ok(ruby_array.as_value())
|
|
128
|
-
}
|
|
129
|
-
JsonValue::Object(obj) => {
|
|
130
|
-
let ruby_hash = RHash::new();
|
|
131
|
-
for (key, val) in obj {
|
|
132
|
-
ruby_hash
|
|
133
|
-
.aset(ruby.str_new(key), Self::json_to_ruby(ruby, val)?)
|
|
134
|
-
.map_err(|e| format!("Failed to set hash value: {}", e))?;
|
|
135
|
-
}
|
|
136
|
-
Ok(ruby_hash.as_value())
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
impl WebSocketHandler for RubyWebSocketHandler {
|
|
143
|
-
async fn handle_message(&self, message: JsonValue) -> Option<JsonValue> {
|
|
144
|
-
debug!("Ruby WebSocket handler '{}': handle_message", self.name);
|
|
145
|
-
|
|
146
|
-
let (reply_tx, reply_rx) = oneshot::channel();
|
|
147
|
-
if self
|
|
148
|
-
.work_tx
|
|
149
|
-
.send(WebSocketWorkItem::HandleMessage {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
let
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
let
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
.
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
.
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
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 std::sync::mpsc;
|
|
10
|
+
use tokio::sync::oneshot;
|
|
11
|
+
use tracing::{debug, error};
|
|
12
|
+
|
|
13
|
+
/// Ruby implementation of WebSocketHandler
|
|
14
|
+
pub struct RubyWebSocketHandler {
|
|
15
|
+
/// Handler name for debugging
|
|
16
|
+
name: String,
|
|
17
|
+
/// Ruby proc/callable for handle_message (Opaque for Send safety)
|
|
18
|
+
#[allow(dead_code)]
|
|
19
|
+
handle_message_proc: Opaque<Value>,
|
|
20
|
+
/// Ruby proc/callable for on_connect (Opaque for Send safety)
|
|
21
|
+
on_connect_proc: Option<Opaque<Value>>,
|
|
22
|
+
/// Ruby proc/callable for on_disconnect (Opaque for Send safety)
|
|
23
|
+
on_disconnect_proc: Option<Opaque<Value>>,
|
|
24
|
+
/// Work queue for executing Ruby callbacks on a Ruby thread
|
|
25
|
+
work_tx: mpsc::Sender<WebSocketWorkItem>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
enum WebSocketWorkItem {
|
|
29
|
+
HandleMessage {
|
|
30
|
+
message: JsonValue,
|
|
31
|
+
reply: oneshot::Sender<Result<Option<JsonValue>, String>>,
|
|
32
|
+
},
|
|
33
|
+
OnConnect {
|
|
34
|
+
reply: oneshot::Sender<Result<(), String>>,
|
|
35
|
+
},
|
|
36
|
+
OnDisconnect {
|
|
37
|
+
reply: oneshot::Sender<Result<(), String>>,
|
|
38
|
+
},
|
|
39
|
+
Shutdown,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
impl RubyWebSocketHandler {
|
|
43
|
+
/// Create a new Ruby WebSocket handler
|
|
44
|
+
#[allow(dead_code)]
|
|
45
|
+
pub fn new(
|
|
46
|
+
ruby: &magnus::Ruby,
|
|
47
|
+
name: String,
|
|
48
|
+
handle_message_proc: Value,
|
|
49
|
+
on_connect_proc: Option<Value>,
|
|
50
|
+
on_disconnect_proc: Option<Value>,
|
|
51
|
+
) -> Self {
|
|
52
|
+
let handle_message_proc = Opaque::from(handle_message_proc);
|
|
53
|
+
let on_connect_proc = on_connect_proc.map(Opaque::from);
|
|
54
|
+
let on_disconnect_proc = on_disconnect_proc.map(Opaque::from);
|
|
55
|
+
let (work_tx, work_rx) = mpsc::channel();
|
|
56
|
+
let handler_name = name.clone();
|
|
57
|
+
|
|
58
|
+
let handle_message_proc_for_thread = handle_message_proc;
|
|
59
|
+
let on_connect_proc_for_thread = on_connect_proc;
|
|
60
|
+
let on_disconnect_proc_for_thread = on_disconnect_proc;
|
|
61
|
+
|
|
62
|
+
ruby.thread_create_from_fn(move |ruby| {
|
|
63
|
+
websocket_worker_loop(
|
|
64
|
+
ruby,
|
|
65
|
+
&handler_name,
|
|
66
|
+
handle_message_proc_for_thread,
|
|
67
|
+
on_connect_proc_for_thread,
|
|
68
|
+
on_disconnect_proc_for_thread,
|
|
69
|
+
work_rx,
|
|
70
|
+
);
|
|
71
|
+
ruby.qnil()
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
Self {
|
|
75
|
+
name,
|
|
76
|
+
handle_message_proc,
|
|
77
|
+
on_connect_proc,
|
|
78
|
+
on_disconnect_proc,
|
|
79
|
+
work_tx,
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Convert Ruby value to JSON
|
|
84
|
+
fn ruby_to_json(ruby: &magnus::Ruby, value: Value) -> Result<JsonValue, String> {
|
|
85
|
+
if value.is_nil() {
|
|
86
|
+
return Ok(JsonValue::Null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let json_module: Value = ruby
|
|
90
|
+
.class_object()
|
|
91
|
+
.const_get("JSON")
|
|
92
|
+
.map_err(|e| format!("JSON module not available: {}", e))?;
|
|
93
|
+
|
|
94
|
+
let json_str: String = json_module
|
|
95
|
+
.funcall("generate", (value,))
|
|
96
|
+
.map_err(|e| format!("Failed to generate JSON: {}", e))?;
|
|
97
|
+
|
|
98
|
+
serde_json::from_str(&json_str).map_err(|e| format!("Failed to parse JSON: {}", e))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Convert JSON value to Ruby value
|
|
102
|
+
fn json_to_ruby(ruby: &magnus::Ruby, value: &JsonValue) -> Result<Value, String> {
|
|
103
|
+
match value {
|
|
104
|
+
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
105
|
+
JsonValue::Bool(b) => Ok(if *b {
|
|
106
|
+
ruby.qtrue().as_value()
|
|
107
|
+
} else {
|
|
108
|
+
ruby.qfalse().as_value()
|
|
109
|
+
}),
|
|
110
|
+
JsonValue::Number(num) => {
|
|
111
|
+
if let Some(i) = num.as_i64() {
|
|
112
|
+
Ok(ruby.integer_from_i64(i).as_value())
|
|
113
|
+
} else if let Some(f) = num.as_f64() {
|
|
114
|
+
Ok(ruby.float_from_f64(f).as_value())
|
|
115
|
+
} else {
|
|
116
|
+
Ok(ruby.qnil().as_value())
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
120
|
+
JsonValue::Array(arr) => {
|
|
121
|
+
let ruby_array = ruby.ary_new();
|
|
122
|
+
for item in arr {
|
|
123
|
+
ruby_array
|
|
124
|
+
.push(Self::json_to_ruby(ruby, item)?)
|
|
125
|
+
.map_err(|e| format!("Failed to push to array: {}", e))?;
|
|
126
|
+
}
|
|
127
|
+
Ok(ruby_array.as_value())
|
|
128
|
+
}
|
|
129
|
+
JsonValue::Object(obj) => {
|
|
130
|
+
let ruby_hash = RHash::new();
|
|
131
|
+
for (key, val) in obj {
|
|
132
|
+
ruby_hash
|
|
133
|
+
.aset(ruby.str_new(key), Self::json_to_ruby(ruby, val)?)
|
|
134
|
+
.map_err(|e| format!("Failed to set hash value: {}", e))?;
|
|
135
|
+
}
|
|
136
|
+
Ok(ruby_hash.as_value())
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
impl WebSocketHandler for RubyWebSocketHandler {
|
|
143
|
+
async fn handle_message(&self, message: JsonValue) -> Option<JsonValue> {
|
|
144
|
+
debug!("Ruby WebSocket handler '{}': handle_message", self.name);
|
|
145
|
+
|
|
146
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
147
|
+
if self
|
|
148
|
+
.work_tx
|
|
149
|
+
.send(WebSocketWorkItem::HandleMessage {
|
|
150
|
+
message,
|
|
151
|
+
reply: reply_tx,
|
|
152
|
+
})
|
|
153
|
+
.is_err()
|
|
154
|
+
{
|
|
155
|
+
error!("Ruby WebSocket handler '{}' worker thread closed", self.name);
|
|
156
|
+
return None;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
let result = match reply_rx.await {
|
|
160
|
+
Ok(result) => result,
|
|
161
|
+
Err(_) => {
|
|
162
|
+
error!("Ruby WebSocket handler '{}' response channel closed", self.name);
|
|
163
|
+
return None;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
match result {
|
|
168
|
+
Ok(value) => value,
|
|
169
|
+
Err(e) => {
|
|
170
|
+
error!("Ruby error in handle_message: {}", e);
|
|
171
|
+
None
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async fn on_connect(&self) {
|
|
177
|
+
debug!("Ruby WebSocket handler '{}': on_connect", self.name);
|
|
178
|
+
|
|
179
|
+
if self.on_connect_proc.is_some() {
|
|
180
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
181
|
+
if self
|
|
182
|
+
.work_tx
|
|
183
|
+
.send(WebSocketWorkItem::OnConnect { reply: reply_tx })
|
|
184
|
+
.is_err()
|
|
185
|
+
{
|
|
186
|
+
error!("Ruby WebSocket handler '{}' worker thread closed", self.name);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let result = match reply_rx.await {
|
|
191
|
+
Ok(result) => result,
|
|
192
|
+
Err(_) => {
|
|
193
|
+
error!("Ruby WebSocket handler '{}' on_connect channel closed", self.name);
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
if let Err(e) = result {
|
|
199
|
+
error!("on_connect error: {}", e);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
debug!("Ruby WebSocket handler '{}': on_connect completed", self.name);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async fn on_disconnect(&self) {
|
|
207
|
+
debug!("Ruby WebSocket handler '{}': on_disconnect", self.name);
|
|
208
|
+
|
|
209
|
+
if self.on_disconnect_proc.is_some() {
|
|
210
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
211
|
+
if self
|
|
212
|
+
.work_tx
|
|
213
|
+
.send(WebSocketWorkItem::OnDisconnect { reply: reply_tx })
|
|
214
|
+
.is_err()
|
|
215
|
+
{
|
|
216
|
+
error!("Ruby WebSocket handler '{}' worker thread closed", self.name);
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
let result = match reply_rx.await {
|
|
221
|
+
Ok(result) => result,
|
|
222
|
+
Err(_) => {
|
|
223
|
+
error!("Ruby WebSocket handler '{}' on_disconnect channel closed", self.name);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
if let Err(e) = result {
|
|
229
|
+
error!("on_disconnect error: {}", e);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
debug!("Ruby WebSocket handler '{}': on_disconnect completed", self.name);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
impl Drop for RubyWebSocketHandler {
|
|
238
|
+
fn drop(&mut self) {
|
|
239
|
+
let _ = self.work_tx.send(WebSocketWorkItem::Shutdown);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
fn websocket_worker_loop(
|
|
244
|
+
ruby: &magnus::Ruby,
|
|
245
|
+
handler_name: &str,
|
|
246
|
+
handle_message_proc: Opaque<Value>,
|
|
247
|
+
on_connect_proc: Option<Opaque<Value>>,
|
|
248
|
+
on_disconnect_proc: Option<Opaque<Value>>,
|
|
249
|
+
work_rx: mpsc::Receiver<WebSocketWorkItem>,
|
|
250
|
+
) {
|
|
251
|
+
let work_rx_ref = &work_rx;
|
|
252
|
+
loop {
|
|
253
|
+
let work = crate::call_without_gvl!(
|
|
254
|
+
recv_work_item,
|
|
255
|
+
args: (work_rx_ref, &mpsc::Receiver<WebSocketWorkItem>),
|
|
256
|
+
return_type: Option<WebSocketWorkItem>
|
|
257
|
+
);
|
|
258
|
+
let Some(work) = work else {
|
|
259
|
+
break;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
match work {
|
|
263
|
+
WebSocketWorkItem::HandleMessage { message, reply } => {
|
|
264
|
+
let result = (|| {
|
|
265
|
+
let message_ruby = RubyWebSocketHandler::json_to_ruby(ruby, &message)?;
|
|
266
|
+
let proc_value = ruby.get_inner(handle_message_proc);
|
|
267
|
+
let result: Value = proc_value
|
|
268
|
+
.funcall("call", (message_ruby,))
|
|
269
|
+
.map_err(|e| format!("Handler '{}' call failed: {}", handler_name, e))?;
|
|
270
|
+
if result.is_nil() {
|
|
271
|
+
Ok(None)
|
|
272
|
+
} else {
|
|
273
|
+
RubyWebSocketHandler::ruby_to_json(ruby, result).map(Some)
|
|
274
|
+
}
|
|
275
|
+
})();
|
|
276
|
+
let _ = reply.send(result);
|
|
277
|
+
}
|
|
278
|
+
WebSocketWorkItem::OnConnect { reply } => {
|
|
279
|
+
let result = on_connect_proc
|
|
280
|
+
.map(|proc| {
|
|
281
|
+
let proc_value = ruby.get_inner(proc);
|
|
282
|
+
proc_value
|
|
283
|
+
.funcall::<_, _, Value>("call", ())
|
|
284
|
+
.map_err(|e| format!("on_connect '{}' call failed: {}", handler_name, e))?;
|
|
285
|
+
Ok(())
|
|
286
|
+
})
|
|
287
|
+
.unwrap_or(Ok(()));
|
|
288
|
+
let _ = reply.send(result);
|
|
289
|
+
}
|
|
290
|
+
WebSocketWorkItem::OnDisconnect { reply } => {
|
|
291
|
+
let result = on_disconnect_proc
|
|
292
|
+
.map(|proc| {
|
|
293
|
+
let proc_value = ruby.get_inner(proc);
|
|
294
|
+
proc_value
|
|
295
|
+
.funcall::<_, _, Value>("call", ())
|
|
296
|
+
.map_err(|e| format!("on_disconnect '{}' call failed: {}", handler_name, e))?;
|
|
297
|
+
Ok(())
|
|
298
|
+
})
|
|
299
|
+
.unwrap_or(Ok(()));
|
|
300
|
+
let _ = reply.send(result);
|
|
301
|
+
}
|
|
302
|
+
WebSocketWorkItem::Shutdown => {
|
|
303
|
+
break;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
fn recv_work_item(receiver: &mpsc::Receiver<WebSocketWorkItem>) -> Option<WebSocketWorkItem> {
|
|
310
|
+
receiver.recv().ok()
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
unsafe impl Send for RubyWebSocketHandler {}
|
|
314
|
+
unsafe impl Sync for RubyWebSocketHandler {}
|
|
315
|
+
|
|
316
|
+
/// Create WebSocketState from Ruby handler object
|
|
317
|
+
///
|
|
318
|
+
/// This function is designed to be called from Ruby to register WebSocket handlers.
|
|
319
|
+
#[allow(dead_code)]
|
|
320
|
+
pub fn create_websocket_state(
|
|
321
|
+
ruby: &magnus::Ruby,
|
|
322
|
+
handler_obj: Value,
|
|
323
|
+
) -> Result<spikard_http::WebSocketState<RubyWebSocketHandler>, magnus::Error> {
|
|
324
|
+
let handle_message_proc: Value = handler_obj
|
|
325
|
+
.funcall("method", (ruby.to_symbol("handle_message"),))
|
|
326
|
+
.map_err(|e| {
|
|
327
|
+
magnus::Error::new(
|
|
328
|
+
ruby.exception_arg_error(),
|
|
329
|
+
format!("handle_message method not found: {}", e),
|
|
330
|
+
)
|
|
331
|
+
})?;
|
|
332
|
+
|
|
333
|
+
let on_connect_proc = handler_obj
|
|
334
|
+
.funcall::<_, _, Value>("method", (ruby.to_symbol("on_connect"),))
|
|
335
|
+
.ok();
|
|
336
|
+
|
|
337
|
+
let on_disconnect_proc = handler_obj
|
|
338
|
+
.funcall::<_, _, Value>("method", (ruby.to_symbol("on_disconnect"),))
|
|
339
|
+
.ok();
|
|
340
|
+
|
|
341
|
+
let message_schema = handler_obj
|
|
342
|
+
.funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_message_schema"),))
|
|
343
|
+
.ok()
|
|
344
|
+
.and_then(|v| {
|
|
345
|
+
if v.is_nil() {
|
|
346
|
+
None
|
|
347
|
+
} else {
|
|
348
|
+
RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
let response_schema = handler_obj
|
|
353
|
+
.funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_response_schema"),))
|
|
354
|
+
.ok()
|
|
355
|
+
.and_then(|v| {
|
|
356
|
+
if v.is_nil() {
|
|
357
|
+
None
|
|
358
|
+
} else {
|
|
359
|
+
RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
let ruby_handler = RubyWebSocketHandler::new(
|
|
364
|
+
ruby,
|
|
365
|
+
"WebSocketHandler".to_string(),
|
|
366
|
+
handle_message_proc,
|
|
367
|
+
on_connect_proc,
|
|
368
|
+
on_disconnect_proc,
|
|
369
|
+
);
|
|
370
|
+
|
|
371
|
+
if message_schema.is_some() || response_schema.is_some() {
|
|
372
|
+
spikard_http::WebSocketState::with_schemas(ruby_handler, message_schema, response_schema)
|
|
373
|
+
.map_err(|e| magnus::Error::new(ruby.exception_runtime_error(), e))
|
|
374
|
+
} else {
|
|
375
|
+
Ok(spikard_http::WebSocketState::new(ruby_handler))
|
|
376
|
+
}
|
|
377
|
+
}
|