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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
use magnus::{RHash, Value, prelude::*, value::Opaque};
|
|
7
7
|
use serde_json::Value as JsonValue;
|
|
8
8
|
use spikard_http::WebSocketHandler;
|
|
9
|
+
use std::sync::mpsc;
|
|
10
|
+
use tokio::sync::oneshot;
|
|
9
11
|
use tracing::{debug, error};
|
|
10
12
|
|
|
11
13
|
/// Ruby implementation of WebSocketHandler
|
|
@@ -13,27 +15,68 @@ pub struct RubyWebSocketHandler {
|
|
|
13
15
|
/// Handler name for debugging
|
|
14
16
|
name: String,
|
|
15
17
|
/// Ruby proc/callable for handle_message (Opaque for Send safety)
|
|
18
|
+
#[allow(dead_code)]
|
|
16
19
|
handle_message_proc: Opaque<Value>,
|
|
17
20
|
/// Ruby proc/callable for on_connect (Opaque for Send safety)
|
|
18
21
|
on_connect_proc: Option<Opaque<Value>>,
|
|
19
22
|
/// Ruby proc/callable for on_disconnect (Opaque for Send safety)
|
|
20
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,
|
|
21
40
|
}
|
|
22
41
|
|
|
23
42
|
impl RubyWebSocketHandler {
|
|
24
43
|
/// Create a new Ruby WebSocket handler
|
|
25
44
|
#[allow(dead_code)]
|
|
26
45
|
pub fn new(
|
|
46
|
+
ruby: &magnus::Ruby,
|
|
27
47
|
name: String,
|
|
28
48
|
handle_message_proc: Value,
|
|
29
49
|
on_connect_proc: Option<Value>,
|
|
30
50
|
on_disconnect_proc: Option<Value>,
|
|
31
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
|
+
|
|
32
74
|
Self {
|
|
33
75
|
name,
|
|
34
|
-
handle_message_proc
|
|
35
|
-
on_connect_proc
|
|
36
|
-
on_disconnect_proc
|
|
76
|
+
handle_message_proc,
|
|
77
|
+
on_connect_proc,
|
|
78
|
+
on_disconnect_proc,
|
|
79
|
+
work_tx,
|
|
37
80
|
}
|
|
38
81
|
}
|
|
39
82
|
|
|
@@ -100,22 +143,25 @@ impl WebSocketHandler for RubyWebSocketHandler {
|
|
|
100
143
|
async fn handle_message(&self, message: JsonValue) -> Option<JsonValue> {
|
|
101
144
|
debug!("Ruby WebSocket handler '{}': handle_message", self.name);
|
|
102
145
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
146
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
147
|
+
if self
|
|
148
|
+
.work_tx
|
|
149
|
+
.send(WebSocketWorkItem::HandleMessage { message, reply: reply_tx })
|
|
150
|
+
.is_err()
|
|
151
|
+
{
|
|
152
|
+
error!("Ruby WebSocket handler '{}' worker thread closed", self.name);
|
|
153
|
+
return None;
|
|
154
|
+
}
|
|
107
155
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
156
|
+
let result = match reply_rx.await {
|
|
157
|
+
Ok(result) => result,
|
|
158
|
+
Err(_) => {
|
|
159
|
+
error!("Ruby WebSocket handler '{}' response channel closed", self.name);
|
|
160
|
+
return None;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
112
163
|
|
|
113
|
-
|
|
114
|
-
Ok(None)
|
|
115
|
-
} else {
|
|
116
|
-
Self::ruby_to_json(&ruby, result).map(Some)
|
|
117
|
-
}
|
|
118
|
-
}) {
|
|
164
|
+
match result {
|
|
119
165
|
Ok(value) => value,
|
|
120
166
|
Err(e) => {
|
|
121
167
|
error!("Ruby error in handle_message: {}", e);
|
|
@@ -127,17 +173,26 @@ impl WebSocketHandler for RubyWebSocketHandler {
|
|
|
127
173
|
async fn on_connect(&self) {
|
|
128
174
|
debug!("Ruby WebSocket handler '{}': on_connect", self.name);
|
|
129
175
|
|
|
130
|
-
if
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.funcall::<_, _, Value>("call", ())
|
|
137
|
-
.map_err(|e| format!("on_connect '{}' call failed: {}", self.name, e))?;
|
|
138
|
-
Ok(())
|
|
139
|
-
})
|
|
176
|
+
if self.on_connect_proc.is_some() {
|
|
177
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
178
|
+
if self
|
|
179
|
+
.work_tx
|
|
180
|
+
.send(WebSocketWorkItem::OnConnect { reply: reply_tx })
|
|
181
|
+
.is_err()
|
|
140
182
|
{
|
|
183
|
+
error!("Ruby WebSocket handler '{}' worker thread closed", self.name);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let result = match reply_rx.await {
|
|
188
|
+
Ok(result) => result,
|
|
189
|
+
Err(_) => {
|
|
190
|
+
error!("Ruby WebSocket handler '{}' on_connect channel closed", self.name);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
if let Err(e) = result {
|
|
141
196
|
error!("on_connect error: {}", e);
|
|
142
197
|
}
|
|
143
198
|
|
|
@@ -148,17 +203,26 @@ impl WebSocketHandler for RubyWebSocketHandler {
|
|
|
148
203
|
async fn on_disconnect(&self) {
|
|
149
204
|
debug!("Ruby WebSocket handler '{}': on_disconnect", self.name);
|
|
150
205
|
|
|
151
|
-
if
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
.funcall::<_, _, Value>("call", ())
|
|
158
|
-
.map_err(|e| format!("on_disconnect '{}' call failed: {}", self.name, e))?;
|
|
159
|
-
Ok(())
|
|
160
|
-
})
|
|
206
|
+
if self.on_disconnect_proc.is_some() {
|
|
207
|
+
let (reply_tx, reply_rx) = oneshot::channel();
|
|
208
|
+
if self
|
|
209
|
+
.work_tx
|
|
210
|
+
.send(WebSocketWorkItem::OnDisconnect { reply: reply_tx })
|
|
211
|
+
.is_err()
|
|
161
212
|
{
|
|
213
|
+
error!("Ruby WebSocket handler '{}' worker thread closed", self.name);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let result = match reply_rx.await {
|
|
218
|
+
Ok(result) => result,
|
|
219
|
+
Err(_) => {
|
|
220
|
+
error!("Ruby WebSocket handler '{}' on_disconnect channel closed", self.name);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
if let Err(e) = result {
|
|
162
226
|
error!("on_disconnect error: {}", e);
|
|
163
227
|
}
|
|
164
228
|
|
|
@@ -167,6 +231,82 @@ impl WebSocketHandler for RubyWebSocketHandler {
|
|
|
167
231
|
}
|
|
168
232
|
}
|
|
169
233
|
|
|
234
|
+
impl Drop for RubyWebSocketHandler {
|
|
235
|
+
fn drop(&mut self) {
|
|
236
|
+
let _ = self.work_tx.send(WebSocketWorkItem::Shutdown);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn websocket_worker_loop(
|
|
241
|
+
ruby: &magnus::Ruby,
|
|
242
|
+
handler_name: &str,
|
|
243
|
+
handle_message_proc: Opaque<Value>,
|
|
244
|
+
on_connect_proc: Option<Opaque<Value>>,
|
|
245
|
+
on_disconnect_proc: Option<Opaque<Value>>,
|
|
246
|
+
work_rx: mpsc::Receiver<WebSocketWorkItem>,
|
|
247
|
+
) {
|
|
248
|
+
let work_rx_ref = &work_rx;
|
|
249
|
+
loop {
|
|
250
|
+
let work = crate::call_without_gvl!(
|
|
251
|
+
recv_work_item,
|
|
252
|
+
args: (work_rx_ref, &mpsc::Receiver<WebSocketWorkItem>),
|
|
253
|
+
return_type: Option<WebSocketWorkItem>
|
|
254
|
+
);
|
|
255
|
+
let Some(work) = work else {
|
|
256
|
+
break;
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
match work {
|
|
260
|
+
WebSocketWorkItem::HandleMessage { message, reply } => {
|
|
261
|
+
let result = (|| {
|
|
262
|
+
let message_ruby = RubyWebSocketHandler::json_to_ruby(ruby, &message)?;
|
|
263
|
+
let proc_value = ruby.get_inner(handle_message_proc);
|
|
264
|
+
let result: Value = proc_value
|
|
265
|
+
.funcall("call", (message_ruby,))
|
|
266
|
+
.map_err(|e| format!("Handler '{}' call failed: {}", handler_name, e))?;
|
|
267
|
+
if result.is_nil() {
|
|
268
|
+
Ok(None)
|
|
269
|
+
} else {
|
|
270
|
+
RubyWebSocketHandler::ruby_to_json(ruby, result).map(Some)
|
|
271
|
+
}
|
|
272
|
+
})();
|
|
273
|
+
let _ = reply.send(result);
|
|
274
|
+
}
|
|
275
|
+
WebSocketWorkItem::OnConnect { reply } => {
|
|
276
|
+
let result = on_connect_proc
|
|
277
|
+
.map(|proc| {
|
|
278
|
+
let proc_value = ruby.get_inner(proc);
|
|
279
|
+
proc_value
|
|
280
|
+
.funcall::<_, _, Value>("call", ())
|
|
281
|
+
.map_err(|e| format!("on_connect '{}' call failed: {}", handler_name, e))?;
|
|
282
|
+
Ok(())
|
|
283
|
+
})
|
|
284
|
+
.unwrap_or(Ok(()));
|
|
285
|
+
let _ = reply.send(result);
|
|
286
|
+
}
|
|
287
|
+
WebSocketWorkItem::OnDisconnect { reply } => {
|
|
288
|
+
let result = on_disconnect_proc
|
|
289
|
+
.map(|proc| {
|
|
290
|
+
let proc_value = ruby.get_inner(proc);
|
|
291
|
+
proc_value
|
|
292
|
+
.funcall::<_, _, Value>("call", ())
|
|
293
|
+
.map_err(|e| format!("on_disconnect '{}' call failed: {}", handler_name, e))?;
|
|
294
|
+
Ok(())
|
|
295
|
+
})
|
|
296
|
+
.unwrap_or(Ok(()));
|
|
297
|
+
let _ = reply.send(result);
|
|
298
|
+
}
|
|
299
|
+
WebSocketWorkItem::Shutdown => {
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
fn recv_work_item(receiver: &mpsc::Receiver<WebSocketWorkItem>) -> Option<WebSocketWorkItem> {
|
|
307
|
+
receiver.recv().ok()
|
|
308
|
+
}
|
|
309
|
+
|
|
170
310
|
unsafe impl Send for RubyWebSocketHandler {}
|
|
171
311
|
unsafe impl Sync for RubyWebSocketHandler {}
|
|
172
312
|
|
|
@@ -218,6 +358,7 @@ pub fn create_websocket_state(
|
|
|
218
358
|
});
|
|
219
359
|
|
|
220
360
|
let ruby_handler = RubyWebSocketHandler::new(
|
|
361
|
+
ruby,
|
|
221
362
|
"WebSocketHandler".to_string(),
|
|
222
363
|
handle_message_proc,
|
|
223
364
|
on_connect_proc,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spikard
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Na'aman Hirschfeld
|
|
@@ -69,6 +69,22 @@ files:
|
|
|
69
69
|
- sig/spikard.rbs
|
|
70
70
|
- vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml
|
|
71
71
|
- vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml
|
|
72
|
+
- vendor/crates/spikard-bindings-shared/Cargo.toml
|
|
73
|
+
- vendor/crates/spikard-bindings-shared/examples/config_extraction.rs
|
|
74
|
+
- vendor/crates/spikard-bindings-shared/src/config_extractor.rs
|
|
75
|
+
- vendor/crates/spikard-bindings-shared/src/conversion_traits.rs
|
|
76
|
+
- vendor/crates/spikard-bindings-shared/src/di_traits.rs
|
|
77
|
+
- vendor/crates/spikard-bindings-shared/src/error_response.rs
|
|
78
|
+
- vendor/crates/spikard-bindings-shared/src/handler_base.rs
|
|
79
|
+
- vendor/crates/spikard-bindings-shared/src/lib.rs
|
|
80
|
+
- vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs
|
|
81
|
+
- vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs
|
|
82
|
+
- vendor/crates/spikard-bindings-shared/src/response_builder.rs
|
|
83
|
+
- vendor/crates/spikard-bindings-shared/src/test_client_base.rs
|
|
84
|
+
- vendor/crates/spikard-bindings-shared/src/validation_helpers.rs
|
|
85
|
+
- vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs
|
|
86
|
+
- vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs
|
|
87
|
+
- vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs
|
|
72
88
|
- vendor/crates/spikard-core/Cargo.toml
|
|
73
89
|
- vendor/crates/spikard-core/src/bindings/mod.rs
|
|
74
90
|
- vendor/crates/spikard-core/src/bindings/response.rs
|
|
@@ -85,14 +101,18 @@ files:
|
|
|
85
101
|
- vendor/crates/spikard-core/src/http.rs
|
|
86
102
|
- vendor/crates/spikard-core/src/lib.rs
|
|
87
103
|
- vendor/crates/spikard-core/src/lifecycle.rs
|
|
104
|
+
- vendor/crates/spikard-core/src/metadata.rs
|
|
88
105
|
- vendor/crates/spikard-core/src/parameters.rs
|
|
89
106
|
- vendor/crates/spikard-core/src/problem.rs
|
|
90
107
|
- vendor/crates/spikard-core/src/request_data.rs
|
|
91
108
|
- vendor/crates/spikard-core/src/router.rs
|
|
92
109
|
- vendor/crates/spikard-core/src/schema_registry.rs
|
|
93
110
|
- vendor/crates/spikard-core/src/type_hints.rs
|
|
94
|
-
- vendor/crates/spikard-core/src/validation.rs
|
|
111
|
+
- vendor/crates/spikard-core/src/validation/error_mapper.rs
|
|
112
|
+
- vendor/crates/spikard-core/src/validation/mod.rs
|
|
95
113
|
- vendor/crates/spikard-http/Cargo.toml
|
|
114
|
+
- vendor/crates/spikard-http/examples/sse-notifications.rs
|
|
115
|
+
- vendor/crates/spikard-http/examples/websocket-chat.rs
|
|
96
116
|
- vendor/crates/spikard-http/src/auth.rs
|
|
97
117
|
- vendor/crates/spikard-http/src/background.rs
|
|
98
118
|
- vendor/crates/spikard-http/src/bindings/mod.rs
|
|
@@ -115,38 +135,51 @@ files:
|
|
|
115
135
|
- vendor/crates/spikard-http/src/openapi/parameter_extraction.rs
|
|
116
136
|
- vendor/crates/spikard-http/src/openapi/schema_conversion.rs
|
|
117
137
|
- vendor/crates/spikard-http/src/openapi/spec_generation.rs
|
|
118
|
-
- vendor/crates/spikard-http/src/parameters.rs
|
|
119
|
-
- vendor/crates/spikard-http/src/problem.rs
|
|
120
138
|
- vendor/crates/spikard-http/src/query_parser.rs
|
|
121
139
|
- vendor/crates/spikard-http/src/response.rs
|
|
122
|
-
- vendor/crates/spikard-http/src/router.rs
|
|
123
|
-
- vendor/crates/spikard-http/src/schema_registry.rs
|
|
124
140
|
- vendor/crates/spikard-http/src/server/handler.rs
|
|
125
141
|
- vendor/crates/spikard-http/src/server/lifecycle_execution.rs
|
|
126
142
|
- vendor/crates/spikard-http/src/server/mod.rs
|
|
127
143
|
- vendor/crates/spikard-http/src/server/request_extraction.rs
|
|
144
|
+
- vendor/crates/spikard-http/src/server/routing_factory.rs
|
|
128
145
|
- vendor/crates/spikard-http/src/sse.rs
|
|
129
146
|
- vendor/crates/spikard-http/src/testing.rs
|
|
130
147
|
- vendor/crates/spikard-http/src/testing/form.rs
|
|
131
148
|
- vendor/crates/spikard-http/src/testing/multipart.rs
|
|
132
149
|
- vendor/crates/spikard-http/src/testing/test_client.rs
|
|
133
|
-
- vendor/crates/spikard-http/src/type_hints.rs
|
|
134
|
-
- vendor/crates/spikard-http/src/validation.rs
|
|
135
150
|
- vendor/crates/spikard-http/src/websocket.rs
|
|
151
|
+
- vendor/crates/spikard-http/tests/background_behavior.rs
|
|
152
|
+
- vendor/crates/spikard-http/tests/common/handlers.rs
|
|
153
|
+
- vendor/crates/spikard-http/tests/common/mod.rs
|
|
154
|
+
- vendor/crates/spikard-http/tests/di_integration.rs
|
|
155
|
+
- vendor/crates/spikard-http/tests/doc_snippets.rs
|
|
156
|
+
- vendor/crates/spikard-http/tests/lifecycle_execution.rs
|
|
157
|
+
- vendor/crates/spikard-http/tests/multipart_behavior.rs
|
|
158
|
+
- vendor/crates/spikard-http/tests/server_config_builder.rs
|
|
159
|
+
- vendor/crates/spikard-http/tests/sse_behavior.rs
|
|
160
|
+
- vendor/crates/spikard-http/tests/websocket_behavior.rs
|
|
136
161
|
- vendor/crates/spikard-rb/Cargo.toml
|
|
137
162
|
- vendor/crates/spikard-rb/build.rs
|
|
138
163
|
- vendor/crates/spikard-rb/src/background.rs
|
|
139
|
-
- vendor/crates/spikard-rb/src/config.rs
|
|
164
|
+
- vendor/crates/spikard-rb/src/config/mod.rs
|
|
165
|
+
- vendor/crates/spikard-rb/src/config/server_config.rs
|
|
140
166
|
- vendor/crates/spikard-rb/src/conversion.rs
|
|
141
|
-
- vendor/crates/spikard-rb/src/di.rs
|
|
167
|
+
- vendor/crates/spikard-rb/src/di/builder.rs
|
|
168
|
+
- vendor/crates/spikard-rb/src/di/mod.rs
|
|
142
169
|
- vendor/crates/spikard-rb/src/handler.rs
|
|
170
|
+
- vendor/crates/spikard-rb/src/integration/mod.rs
|
|
143
171
|
- vendor/crates/spikard-rb/src/lib.rs
|
|
144
172
|
- vendor/crates/spikard-rb/src/lifecycle.rs
|
|
173
|
+
- vendor/crates/spikard-rb/src/metadata/mod.rs
|
|
174
|
+
- vendor/crates/spikard-rb/src/metadata/route_extraction.rs
|
|
175
|
+
- vendor/crates/spikard-rb/src/runtime/mod.rs
|
|
176
|
+
- vendor/crates/spikard-rb/src/runtime/server_runner.rs
|
|
145
177
|
- vendor/crates/spikard-rb/src/server.rs
|
|
146
178
|
- vendor/crates/spikard-rb/src/sse.rs
|
|
147
|
-
- vendor/crates/spikard-rb/src/
|
|
148
|
-
- vendor/crates/spikard-rb/src/
|
|
149
|
-
- vendor/crates/spikard-rb/src/
|
|
179
|
+
- vendor/crates/spikard-rb/src/testing/client.rs
|
|
180
|
+
- vendor/crates/spikard-rb/src/testing/mod.rs
|
|
181
|
+
- vendor/crates/spikard-rb/src/testing/sse.rs
|
|
182
|
+
- vendor/crates/spikard-rb/src/testing/websocket.rs
|
|
150
183
|
- vendor/crates/spikard-rb/src/websocket.rs
|
|
151
184
|
homepage: https://github.com/Goldziher/spikard
|
|
152
185
|
licenses:
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pub use spikard_core::parameters::*;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pub use spikard_core::problem::*;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pub use spikard_core::router::*;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pub use spikard_core::schema_registry::*;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pub use spikard_core::type_hints::*;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
pub use spikard_core::validation::*;
|
|
@@ -1,221 +0,0 @@
|
|
|
1
|
-
//! WebSocket test client bindings for Ruby
|
|
2
|
-
|
|
3
|
-
use magnus::prelude::*;
|
|
4
|
-
use magnus::{Error, Ruby, Value, method};
|
|
5
|
-
use serde_json::Value as JsonValue;
|
|
6
|
-
use spikard_http::testing::{WebSocketConnection as RustWebSocketConnection, WebSocketMessage as RustWebSocketMessage};
|
|
7
|
-
use std::cell::RefCell;
|
|
8
|
-
|
|
9
|
-
use crate::server::GLOBAL_RUNTIME;
|
|
10
|
-
|
|
11
|
-
/// Ruby wrapper for WebSocket test client
|
|
12
|
-
#[derive(Default)]
|
|
13
|
-
#[magnus::wrap(class = "Spikard::Native::WebSocketTestConnection", free_immediately)]
|
|
14
|
-
pub struct WebSocketTestConnection {
|
|
15
|
-
inner: RefCell<Option<RustWebSocketConnection>>,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
impl WebSocketTestConnection {
|
|
19
|
-
/// Create a new WebSocket test connection (public for lib.rs)
|
|
20
|
-
pub(crate) fn new(inner: RustWebSocketConnection) -> Self {
|
|
21
|
-
Self {
|
|
22
|
-
inner: RefCell::new(Some(inner)),
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/// Send a text message
|
|
27
|
-
fn send_text(&self, text: String) -> Result<(), Error> {
|
|
28
|
-
let mut inner = self.inner.borrow_mut();
|
|
29
|
-
let ws = inner
|
|
30
|
-
.as_mut()
|
|
31
|
-
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "WebSocket closed"))?;
|
|
32
|
-
|
|
33
|
-
GLOBAL_RUNTIME.block_on(async {
|
|
34
|
-
ws.send_text(text).await;
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
Ok(())
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/// Send a JSON message
|
|
41
|
-
fn send_json(ruby: &Ruby, this: &Self, obj: Value) -> Result<(), Error> {
|
|
42
|
-
let json_value = ruby_to_json(ruby, obj)?;
|
|
43
|
-
let mut inner = this.inner.borrow_mut();
|
|
44
|
-
let ws = inner
|
|
45
|
-
.as_mut()
|
|
46
|
-
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "WebSocket closed"))?;
|
|
47
|
-
|
|
48
|
-
GLOBAL_RUNTIME.block_on(async {
|
|
49
|
-
ws.send_json(&json_value).await;
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
Ok(())
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/// Receive a text message
|
|
56
|
-
fn receive_text(&self) -> Result<String, Error> {
|
|
57
|
-
let mut inner = self.inner.borrow_mut();
|
|
58
|
-
let ws = inner
|
|
59
|
-
.as_mut()
|
|
60
|
-
.ok_or_else(|| Error::new(magnus::exception::runtime_error(), "WebSocket closed"))?;
|
|
61
|
-
|
|
62
|
-
let text = GLOBAL_RUNTIME.block_on(async { ws.receive_text().await });
|
|
63
|
-
|
|
64
|
-
Ok(text)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/// Receive and parse a JSON message
|
|
68
|
-
fn receive_json(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
69
|
-
let mut inner = this.inner.borrow_mut();
|
|
70
|
-
let ws = inner
|
|
71
|
-
.as_mut()
|
|
72
|
-
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "WebSocket closed"))?;
|
|
73
|
-
|
|
74
|
-
let json_value: JsonValue = GLOBAL_RUNTIME.block_on(async { ws.receive_json().await });
|
|
75
|
-
|
|
76
|
-
json_to_ruby(ruby, &json_value)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/// Receive raw bytes
|
|
80
|
-
fn receive_bytes(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
81
|
-
let mut inner = this.inner.borrow_mut();
|
|
82
|
-
let ws = inner
|
|
83
|
-
.as_mut()
|
|
84
|
-
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "WebSocket closed"))?;
|
|
85
|
-
|
|
86
|
-
let bytes = GLOBAL_RUNTIME.block_on(async { ws.receive_bytes().await });
|
|
87
|
-
|
|
88
|
-
Ok(ruby.str_from_slice(&bytes).as_value())
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/// Receive a message and return WebSocketMessage
|
|
92
|
-
fn receive_message(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
93
|
-
let mut inner = this.inner.borrow_mut();
|
|
94
|
-
let ws = inner
|
|
95
|
-
.as_mut()
|
|
96
|
-
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "WebSocket closed"))?;
|
|
97
|
-
|
|
98
|
-
let msg = GLOBAL_RUNTIME.block_on(async { ws.receive_message().await });
|
|
99
|
-
|
|
100
|
-
let ws_msg = WebSocketMessage::new(msg);
|
|
101
|
-
Ok(ruby.obj_wrap(ws_msg).as_value())
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/// Close the WebSocket connection
|
|
105
|
-
fn close(&self) -> Result<(), Error> {
|
|
106
|
-
self.inner.borrow_mut().take();
|
|
107
|
-
Ok(())
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/// Ruby wrapper for WebSocket messages
|
|
112
|
-
#[magnus::wrap(class = "Spikard::Native::WebSocketMessage", free_immediately)]
|
|
113
|
-
pub struct WebSocketMessage {
|
|
114
|
-
inner: RustWebSocketMessage,
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
impl WebSocketMessage {
|
|
118
|
-
pub fn new(inner: RustWebSocketMessage) -> Self {
|
|
119
|
-
Self { inner }
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/// Get message as text if it's a text message
|
|
123
|
-
fn as_text(&self) -> Result<Option<String>, Error> {
|
|
124
|
-
Ok(self.inner.as_text().map(|s| s.to_string()))
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/// Get message as JSON if it's a text message containing JSON
|
|
128
|
-
fn as_json(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
129
|
-
match this.inner.as_json() {
|
|
130
|
-
Ok(value) => json_to_ruby(ruby, &value),
|
|
131
|
-
Err(_) => Ok(ruby.qnil().as_value()),
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/// Get message as binary if it's a binary message
|
|
136
|
-
fn as_binary(ruby: &Ruby, this: &Self) -> Result<Value, Error> {
|
|
137
|
-
match this.inner.as_binary() {
|
|
138
|
-
Some(bytes) => Ok(ruby.str_from_slice(bytes).as_value()),
|
|
139
|
-
None => Ok(ruby.qnil().as_value()),
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/// Check if this is a close message
|
|
144
|
-
fn is_close(&self) -> bool {
|
|
145
|
-
self.inner.is_close()
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/// Helper to convert Ruby object to JSON
|
|
150
|
-
fn ruby_to_json(ruby: &Ruby, value: Value) -> Result<JsonValue, Error> {
|
|
151
|
-
let json_module = ruby.class_object().const_get::<_, magnus::RModule>("JSON")?;
|
|
152
|
-
let json_str: String = json_module.funcall("generate", (value,))?;
|
|
153
|
-
serde_json::from_str(&json_str).map_err(|e| {
|
|
154
|
-
Error::new(
|
|
155
|
-
magnus::exception::runtime_error(),
|
|
156
|
-
format!("Failed to parse JSON: {}", e),
|
|
157
|
-
)
|
|
158
|
-
})
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/// Helper to convert JSON to Ruby object
|
|
162
|
-
fn json_to_ruby(ruby: &Ruby, value: &JsonValue) -> Result<Value, Error> {
|
|
163
|
-
match value {
|
|
164
|
-
JsonValue::Null => Ok(ruby.qnil().as_value()),
|
|
165
|
-
JsonValue::Bool(b) => Ok(if *b {
|
|
166
|
-
ruby.qtrue().as_value()
|
|
167
|
-
} else {
|
|
168
|
-
ruby.qfalse().as_value()
|
|
169
|
-
}),
|
|
170
|
-
JsonValue::Number(n) => {
|
|
171
|
-
if let Some(i) = n.as_i64() {
|
|
172
|
-
Ok(ruby.integer_from_i64(i).as_value())
|
|
173
|
-
} else if let Some(u) = n.as_u64() {
|
|
174
|
-
Ok(ruby.integer_from_i64(u as i64).as_value())
|
|
175
|
-
} else if let Some(f) = n.as_f64() {
|
|
176
|
-
Ok(ruby.float_from_f64(f).as_value())
|
|
177
|
-
} else {
|
|
178
|
-
Ok(ruby.qnil().as_value())
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
|
|
182
|
-
JsonValue::Array(arr) => {
|
|
183
|
-
let ruby_arr = ruby.ary_new();
|
|
184
|
-
for item in arr {
|
|
185
|
-
let ruby_val = json_to_ruby(ruby, item)?;
|
|
186
|
-
ruby_arr.push(ruby_val)?;
|
|
187
|
-
}
|
|
188
|
-
Ok(ruby_arr.as_value())
|
|
189
|
-
}
|
|
190
|
-
JsonValue::Object(obj) => {
|
|
191
|
-
let ruby_hash = ruby.hash_new();
|
|
192
|
-
for (key, val) in obj {
|
|
193
|
-
let ruby_val = json_to_ruby(ruby, val)?;
|
|
194
|
-
ruby_hash.aset(ruby.str_new(key), ruby_val)?;
|
|
195
|
-
}
|
|
196
|
-
Ok(ruby_hash.as_value())
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/// Initialize WebSocket test client bindings
|
|
202
|
-
pub fn init(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
|
|
203
|
-
let native_module = module.define_module("Native")?;
|
|
204
|
-
|
|
205
|
-
let ws_conn_class = native_module.define_class("WebSocketTestConnection", ruby.class_object())?;
|
|
206
|
-
ws_conn_class.define_method("send_text", method!(WebSocketTestConnection::send_text, 1))?;
|
|
207
|
-
ws_conn_class.define_method("send_json", method!(WebSocketTestConnection::send_json, 1))?;
|
|
208
|
-
ws_conn_class.define_method("receive_text", method!(WebSocketTestConnection::receive_text, 0))?;
|
|
209
|
-
ws_conn_class.define_method("receive_json", method!(WebSocketTestConnection::receive_json, 0))?;
|
|
210
|
-
ws_conn_class.define_method("receive_bytes", method!(WebSocketTestConnection::receive_bytes, 0))?;
|
|
211
|
-
ws_conn_class.define_method("receive_message", method!(WebSocketTestConnection::receive_message, 0))?;
|
|
212
|
-
ws_conn_class.define_method("close", method!(WebSocketTestConnection::close, 0))?;
|
|
213
|
-
|
|
214
|
-
let ws_msg_class = native_module.define_class("WebSocketMessage", ruby.class_object())?;
|
|
215
|
-
ws_msg_class.define_method("as_text", method!(WebSocketMessage::as_text, 0))?;
|
|
216
|
-
ws_msg_class.define_method("as_json", method!(WebSocketMessage::as_json, 0))?;
|
|
217
|
-
ws_msg_class.define_method("as_binary", method!(WebSocketMessage::as_binary, 0))?;
|
|
218
|
-
ws_msg_class.define_method("is_close", method!(WebSocketMessage::is_close, 0))?;
|
|
219
|
-
|
|
220
|
-
Ok(())
|
|
221
|
-
}
|
|
File without changes
|