spikard 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
@@ -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: 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()),
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
- 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)?;
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
- 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))?;
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
- if result.is_nil() {
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 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
- })
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 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
- })
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.3.6
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/test_client.rs
148
- - vendor/crates/spikard-rb/src/test_sse.rs
149
- - vendor/crates/spikard-rb/src/test_websocket.rs
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
- }