spikard 0.6.2 → 0.7.2

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +90 -508
  3. data/ext/spikard_rb/Cargo.lock +3287 -0
  4. data/ext/spikard_rb/Cargo.toml +1 -1
  5. data/ext/spikard_rb/extconf.rb +3 -3
  6. data/lib/spikard/app.rb +72 -49
  7. data/lib/spikard/background.rb +38 -7
  8. data/lib/spikard/testing.rb +42 -4
  9. data/lib/spikard/version.rb +1 -1
  10. data/sig/spikard.rbs +4 -0
  11. data/vendor/crates/spikard-bindings-shared/Cargo.toml +2 -2
  12. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +191 -0
  13. data/vendor/crates/spikard-core/Cargo.toml +1 -1
  14. data/vendor/crates/spikard-core/src/http.rs +1 -0
  15. data/vendor/crates/spikard-core/src/lifecycle.rs +63 -0
  16. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +136 -0
  17. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +37 -0
  18. data/vendor/crates/spikard-core/tests/error_mapper.rs +761 -0
  19. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +106 -0
  20. data/vendor/crates/spikard-core/tests/parameters_full.rs +701 -0
  21. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +301 -0
  22. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +67 -0
  23. data/vendor/crates/spikard-core/tests/validation_coverage.rs +250 -0
  24. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +45 -0
  25. data/vendor/crates/spikard-http/Cargo.toml +1 -1
  26. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +502 -0
  27. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +648 -0
  28. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +58 -0
  29. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +1207 -0
  30. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2262 -0
  31. data/vendor/crates/spikard-http/src/testing/test_client.rs +155 -2
  32. data/vendor/crates/spikard-http/src/testing.rs +171 -0
  33. data/vendor/crates/spikard-http/src/websocket.rs +79 -6
  34. data/vendor/crates/spikard-http/tests/auth_integration.rs +647 -0
  35. data/vendor/crates/spikard-http/tests/common/test_builders.rs +633 -0
  36. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +162 -0
  37. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +389 -0
  38. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +513 -0
  39. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +244 -0
  40. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +200 -0
  41. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +82 -0
  42. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +464 -0
  43. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +286 -0
  44. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +118 -0
  45. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +99 -0
  46. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +206 -0
  47. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +281 -0
  48. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +121 -0
  49. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +584 -0
  50. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +130 -0
  51. data/vendor/crates/spikard-http/tests/test_client_requests.rs +167 -0
  52. data/vendor/crates/spikard-http/tests/testing_helpers.rs +87 -0
  53. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +156 -0
  54. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +82 -0
  55. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +440 -0
  56. data/vendor/crates/spikard-http/tests/websocket_integration.rs +152 -0
  57. data/vendor/crates/spikard-rb/Cargo.toml +1 -1
  58. data/vendor/crates/spikard-rb/src/gvl.rs +80 -0
  59. data/vendor/crates/spikard-rb/src/handler.rs +12 -9
  60. data/vendor/crates/spikard-rb/src/lib.rs +137 -124
  61. data/vendor/crates/spikard-rb/src/request.rs +342 -0
  62. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +1 -8
  63. data/vendor/crates/spikard-rb/src/server.rs +1 -8
  64. data/vendor/crates/spikard-rb/src/testing/client.rs +168 -9
  65. data/vendor/crates/spikard-rb/src/websocket.rs +119 -30
  66. data/vendor/crates/spikard-rb-macros/Cargo.toml +14 -0
  67. data/vendor/crates/spikard-rb-macros/src/lib.rs +52 -0
  68. metadata +44 -1
@@ -3,7 +3,7 @@
3
3
  //! This module provides the bridge between Ruby blocks/procs and Rust's WebSocket system.
4
4
  //! Uses magnus to safely call Ruby code from Rust async tasks.
5
5
 
6
- use magnus::{RHash, Value, prelude::*, value::Opaque};
6
+ use magnus::{RHash, Ruby, Value, prelude::*, value::Opaque};
7
7
  use serde_json::Value as JsonValue;
8
8
  use spikard_http::WebSocketHandler;
9
9
  use std::sync::mpsc;
@@ -39,6 +39,12 @@ enum WebSocketWorkItem {
39
39
  Shutdown,
40
40
  }
41
41
 
42
+ enum WebSocketFactoryWorkItem {
43
+ Build {
44
+ reply: mpsc::Sender<Result<RubyWebSocketHandler, String>>,
45
+ },
46
+ }
47
+
42
48
  impl RubyWebSocketHandler {
43
49
  /// Create a new Ruby WebSocket handler
44
50
  #[allow(dead_code)]
@@ -137,6 +143,33 @@ impl RubyWebSocketHandler {
137
143
  }
138
144
  }
139
145
  }
146
+
147
+ fn from_handler(ruby: &Ruby, handler_obj: Value) -> Result<Self, magnus::Error> {
148
+ let handle_message_proc: Value = handler_obj
149
+ .funcall("method", (ruby.to_symbol("handle_message"),))
150
+ .map_err(|e| {
151
+ magnus::Error::new(
152
+ ruby.exception_arg_error(),
153
+ format!("handle_message method not found: {}", e),
154
+ )
155
+ })?;
156
+
157
+ let on_connect_proc = handler_obj
158
+ .funcall::<_, _, Value>("method", (ruby.to_symbol("on_connect"),))
159
+ .ok();
160
+
161
+ let on_disconnect_proc = handler_obj
162
+ .funcall::<_, _, Value>("method", (ruby.to_symbol("on_disconnect"),))
163
+ .ok();
164
+
165
+ Ok(Self::new(
166
+ ruby,
167
+ "WebSocketHandler".to_string(),
168
+ handle_message_proc,
169
+ on_connect_proc,
170
+ on_disconnect_proc,
171
+ ))
172
+ }
140
173
  }
141
174
 
142
175
  impl WebSocketHandler for RubyWebSocketHandler {
@@ -319,26 +352,16 @@ unsafe impl Sync for RubyWebSocketHandler {}
319
352
  #[allow(dead_code)]
320
353
  pub fn create_websocket_state(
321
354
  ruby: &magnus::Ruby,
322
- handler_obj: Value,
355
+ handler_factory: Value,
323
356
  ) -> 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
357
+ let handler_instance: Value = handler_factory.funcall("call", ()).map_err(|e| {
358
+ magnus::Error::new(
359
+ ruby.exception_runtime_error(),
360
+ format!("Failed to create WebSocket handler: {}", e),
361
+ )
362
+ })?;
363
+
364
+ let message_schema = handler_instance
342
365
  .funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_message_schema"),))
343
366
  .ok()
344
367
  .and_then(|v| {
@@ -349,7 +372,7 @@ pub fn create_websocket_state(
349
372
  }
350
373
  });
351
374
 
352
- let response_schema = handler_obj
375
+ let response_schema = handler_instance
353
376
  .funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_response_schema"),))
354
377
  .ok()
355
378
  .and_then(|v| {
@@ -360,18 +383,84 @@ pub fn create_websocket_state(
360
383
  }
361
384
  });
362
385
 
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
- );
386
+ let handler_factory = Opaque::from(handler_factory);
387
+ let (factory_tx, factory_rx) = mpsc::channel();
388
+
389
+ let handler_factory_for_thread = handler_factory;
390
+ ruby.thread_create_from_fn(move |ruby| {
391
+ websocket_factory_worker_loop(ruby, handler_factory_for_thread, factory_rx);
392
+ ruby.qnil()
393
+ });
394
+
395
+ let handler_builder = move || {
396
+ let (reply_tx, reply_rx) = mpsc::channel();
397
+ if factory_tx
398
+ .send(WebSocketFactoryWorkItem::Build { reply: reply_tx })
399
+ .is_err()
400
+ {
401
+ return Err("WebSocket handler factory thread closed".to_string());
402
+ }
403
+ if magnus::Ruby::get().is_ok() {
404
+ let reply_rx_ref = &reply_rx;
405
+ let result = crate::call_without_gvl!(
406
+ recv_factory_reply,
407
+ args: (reply_rx_ref, &mpsc::Receiver<Result<RubyWebSocketHandler, String>>),
408
+ return_type: Option<Result<RubyWebSocketHandler, String>>
409
+ );
410
+ result.ok_or_else(|| "WebSocket handler factory response channel closed".to_string())?
411
+ } else {
412
+ reply_rx
413
+ .recv()
414
+ .map_err(|_| "WebSocket handler factory response channel closed".to_string())?
415
+ }
416
+ };
370
417
 
371
418
  if message_schema.is_some() || response_schema.is_some() {
372
- spikard_http::WebSocketState::with_schemas(ruby_handler, message_schema, response_schema)
419
+ spikard_http::WebSocketState::with_factory(handler_builder, message_schema, response_schema)
373
420
  .map_err(|e| magnus::Error::new(ruby.exception_runtime_error(), e))
374
421
  } else {
375
- Ok(spikard_http::WebSocketState::new(ruby_handler))
422
+ spikard_http::WebSocketState::with_factory(handler_builder, None, None)
423
+ .map_err(|e| magnus::Error::new(ruby.exception_runtime_error(), e))
376
424
  }
377
425
  }
426
+
427
+ fn websocket_factory_worker_loop(
428
+ ruby: &magnus::Ruby,
429
+ handler_factory: Opaque<Value>,
430
+ work_rx: mpsc::Receiver<WebSocketFactoryWorkItem>,
431
+ ) {
432
+ let work_rx_ref = &work_rx;
433
+ loop {
434
+ let work = crate::call_without_gvl!(
435
+ recv_factory_item,
436
+ args: (work_rx_ref, &mpsc::Receiver<WebSocketFactoryWorkItem>),
437
+ return_type: Option<WebSocketFactoryWorkItem>
438
+ );
439
+ let Some(work) = work else {
440
+ break;
441
+ };
442
+
443
+ match work {
444
+ WebSocketFactoryWorkItem::Build { reply } => {
445
+ let result = (|| {
446
+ let factory_value = ruby.get_inner(handler_factory);
447
+ let handler_instance: Value = factory_value
448
+ .funcall("call", ())
449
+ .map_err(|e| format!("Failed to create WebSocket handler: {}", e))?;
450
+ RubyWebSocketHandler::from_handler(ruby, handler_instance).map_err(|e| e.to_string())
451
+ })();
452
+ let _ = reply.send(result);
453
+ }
454
+ }
455
+ }
456
+ }
457
+
458
+ fn recv_factory_item(receiver: &mpsc::Receiver<WebSocketFactoryWorkItem>) -> Option<WebSocketFactoryWorkItem> {
459
+ receiver.recv().ok()
460
+ }
461
+
462
+ fn recv_factory_reply(
463
+ receiver: &mpsc::Receiver<Result<RubyWebSocketHandler, String>>,
464
+ ) -> Option<Result<RubyWebSocketHandler, String>> {
465
+ receiver.recv().ok()
466
+ }
@@ -0,0 +1,14 @@
1
+ [package]
2
+ name = "spikard-rb-macros"
3
+ version = "0.7.2"
4
+ edition = "2024"
5
+ license = "MIT"
6
+ publish = false
7
+
8
+ [lib]
9
+ proc-macro = true
10
+
11
+ [dependencies]
12
+ proc-macro2 = "1"
13
+ quote = "1"
14
+ syn = { version = "2", features = ["full"] }
@@ -0,0 +1,52 @@
1
+ use proc_macro::TokenStream;
2
+ use quote::quote;
3
+ use syn::{FnArg, ItemFn, parse_macro_input};
4
+
5
+ #[proc_macro_attribute]
6
+ pub fn without_gvl(_attr: TokenStream, item: TokenStream) -> TokenStream {
7
+ let input = parse_macro_input!(item as ItemFn);
8
+ let attrs = &input.attrs;
9
+ let vis = &input.vis;
10
+ let sig = &input.sig;
11
+ let mut anon_sig = sig.clone();
12
+ anon_sig.ident = syn::Ident::new("__anon_wrapper", sig.ident.span());
13
+
14
+ let params = sig.inputs.iter().map(|arg| match arg {
15
+ FnArg::Typed(pat) => {
16
+ let arg = &pat.pat;
17
+ let ty = &pat.ty;
18
+ quote!(#arg, #ty)
19
+ }
20
+ FnArg::Receiver(recv) => {
21
+ let ty = if let Some((_and, lifetime)) = &recv.reference {
22
+ let mutability = recv.mutability;
23
+ if let Some(lifetime) = lifetime {
24
+ quote!(&#lifetime #mutability Self)
25
+ } else {
26
+ quote!(& #mutability Self)
27
+ }
28
+ } else {
29
+ quote!(Self)
30
+ };
31
+ quote!(self, #ty)
32
+ }
33
+ });
34
+
35
+ let return_ty = match &sig.output {
36
+ syn::ReturnType::Default => quote!(()),
37
+ syn::ReturnType::Type(_, ty) => quote!(#ty),
38
+ };
39
+
40
+ let block = &input.block;
41
+
42
+ quote!(
43
+ #(#attrs)*
44
+ #vis #sig {
45
+ #anon_sig {
46
+ #block
47
+ }
48
+ crate::call_without_gvl!(__anon_wrapper, args: (#(#params),*), return_type: #return_ty)
49
+ }
50
+ )
51
+ .into()
52
+ }
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.6.2
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Na'aman Hirschfeld
@@ -62,6 +62,7 @@ extra_rdoc_files: []
62
62
  files:
63
63
  - LICENSE
64
64
  - README.md
65
+ - ext/spikard_rb/Cargo.lock
65
66
  - ext/spikard_rb/Cargo.toml
66
67
  - ext/spikard_rb/extconf.rb
67
68
  - ext/spikard_rb/src/lib.rs
@@ -97,6 +98,7 @@ files:
97
98
  - vendor/crates/spikard-bindings-shared/src/test_client_base.rs
98
99
  - vendor/crates/spikard-bindings-shared/src/validation_helpers.rs
99
100
  - vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs
101
+ - vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs
100
102
  - vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs
101
103
  - vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs
102
104
  - vendor/crates/spikard-core/Cargo.toml
@@ -124,6 +126,15 @@ files:
124
126
  - vendor/crates/spikard-core/src/type_hints.rs
125
127
  - vendor/crates/spikard-core/src/validation/error_mapper.rs
126
128
  - vendor/crates/spikard-core/src/validation/mod.rs
129
+ - vendor/crates/spikard-core/tests/bindings_response_tests.rs
130
+ - vendor/crates/spikard-core/tests/di_dependency_defaults.rs
131
+ - vendor/crates/spikard-core/tests/error_mapper.rs
132
+ - vendor/crates/spikard-core/tests/parameters_edge_cases.rs
133
+ - vendor/crates/spikard-core/tests/parameters_full.rs
134
+ - vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs
135
+ - vendor/crates/spikard-core/tests/request_data_roundtrip.rs
136
+ - vendor/crates/spikard-core/tests/validation_coverage.rs
137
+ - vendor/crates/spikard-core/tests/validation_error_paths.rs
127
138
  - vendor/crates/spikard-http/Cargo.toml
128
139
  - vendor/crates/spikard-http/examples/sse-notifications.rs
129
140
  - vendor/crates/spikard-http/examples/websocket-chat.rs
@@ -138,6 +149,11 @@ files:
138
149
  - vendor/crates/spikard-http/src/handler_response.rs
139
150
  - vendor/crates/spikard-http/src/handler_trait.rs
140
151
  - vendor/crates/spikard-http/src/handler_trait_tests.rs
152
+ - vendor/crates/spikard-http/src/jsonrpc/http_handler.rs
153
+ - vendor/crates/spikard-http/src/jsonrpc/method_registry.rs
154
+ - vendor/crates/spikard-http/src/jsonrpc/mod.rs
155
+ - vendor/crates/spikard-http/src/jsonrpc/protocol.rs
156
+ - vendor/crates/spikard-http/src/jsonrpc/router.rs
141
157
  - vendor/crates/spikard-http/src/lib.rs
142
158
  - vendor/crates/spikard-http/src/lifecycle.rs
143
159
  - vendor/crates/spikard-http/src/lifecycle/adapter.rs
@@ -162,16 +178,41 @@ files:
162
178
  - vendor/crates/spikard-http/src/testing/multipart.rs
163
179
  - vendor/crates/spikard-http/src/testing/test_client.rs
164
180
  - vendor/crates/spikard-http/src/websocket.rs
181
+ - vendor/crates/spikard-http/tests/auth_integration.rs
165
182
  - vendor/crates/spikard-http/tests/background_behavior.rs
166
183
  - vendor/crates/spikard-http/tests/common/handlers.rs
167
184
  - vendor/crates/spikard-http/tests/common/mod.rs
185
+ - vendor/crates/spikard-http/tests/common/test_builders.rs
186
+ - vendor/crates/spikard-http/tests/di_handler_error_responses.rs
168
187
  - vendor/crates/spikard-http/tests/di_integration.rs
169
188
  - vendor/crates/spikard-http/tests/doc_snippets.rs
170
189
  - vendor/crates/spikard-http/tests/lifecycle_execution.rs
190
+ - vendor/crates/spikard-http/tests/middleware_stack_integration.rs
171
191
  - vendor/crates/spikard-http/tests/multipart_behavior.rs
192
+ - vendor/crates/spikard-http/tests/request_extraction_full.rs
193
+ - vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs
172
194
  - vendor/crates/spikard-http/tests/server_config_builder.rs
195
+ - vendor/crates/spikard-http/tests/server_configured_router_behavior.rs
196
+ - vendor/crates/spikard-http/tests/server_cors_preflight.rs
197
+ - vendor/crates/spikard-http/tests/server_handler_wrappers.rs
198
+ - vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs
199
+ - vendor/crates/spikard-http/tests/server_method_router_coverage.rs
200
+ - vendor/crates/spikard-http/tests/server_middleware_behavior.rs
201
+ - vendor/crates/spikard-http/tests/server_middleware_branches.rs
202
+ - vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs
203
+ - vendor/crates/spikard-http/tests/server_router_behavior.rs
173
204
  - vendor/crates/spikard-http/tests/sse_behavior.rs
205
+ - vendor/crates/spikard-http/tests/sse_full_behavior.rs
206
+ - vendor/crates/spikard-http/tests/sse_handler_behavior.rs
207
+ - vendor/crates/spikard-http/tests/test_client_requests.rs
208
+ - vendor/crates/spikard-http/tests/testing_helpers.rs
209
+ - vendor/crates/spikard-http/tests/testing_module_coverage.rs
210
+ - vendor/crates/spikard-http/tests/urlencoded_content_type.rs
174
211
  - vendor/crates/spikard-http/tests/websocket_behavior.rs
212
+ - vendor/crates/spikard-http/tests/websocket_full_behavior.rs
213
+ - vendor/crates/spikard-http/tests/websocket_integration.rs
214
+ - vendor/crates/spikard-rb-macros/Cargo.toml
215
+ - vendor/crates/spikard-rb-macros/src/lib.rs
175
216
  - vendor/crates/spikard-rb/Cargo.toml
176
217
  - vendor/crates/spikard-rb/build.rs
177
218
  - vendor/crates/spikard-rb/src/background.rs
@@ -180,12 +221,14 @@ files:
180
221
  - vendor/crates/spikard-rb/src/conversion.rs
181
222
  - vendor/crates/spikard-rb/src/di/builder.rs
182
223
  - vendor/crates/spikard-rb/src/di/mod.rs
224
+ - vendor/crates/spikard-rb/src/gvl.rs
183
225
  - vendor/crates/spikard-rb/src/handler.rs
184
226
  - vendor/crates/spikard-rb/src/integration/mod.rs
185
227
  - vendor/crates/spikard-rb/src/lib.rs
186
228
  - vendor/crates/spikard-rb/src/lifecycle.rs
187
229
  - vendor/crates/spikard-rb/src/metadata/mod.rs
188
230
  - vendor/crates/spikard-rb/src/metadata/route_extraction.rs
231
+ - vendor/crates/spikard-rb/src/request.rs
189
232
  - vendor/crates/spikard-rb/src/runtime/mod.rs
190
233
  - vendor/crates/spikard-rb/src/runtime/server_runner.rs
191
234
  - vendor/crates/spikard-rb/src/server.rs