spikard 0.3.5 → 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 (142) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +674 -659
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +10 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +405 -386
  8. data/lib/spikard/background.rb +27 -27
  9. data/lib/spikard/config.rb +396 -396
  10. data/lib/spikard/converters.rb +13 -13
  11. data/lib/spikard/handler_wrapper.rb +113 -113
  12. data/lib/spikard/provide.rb +214 -214
  13. data/lib/spikard/response.rb +173 -173
  14. data/lib/spikard/schema.rb +243 -243
  15. data/lib/spikard/sse.rb +111 -111
  16. data/lib/spikard/streaming_response.rb +44 -44
  17. data/lib/spikard/testing.rb +256 -221
  18. data/lib/spikard/upload_file.rb +131 -131
  19. data/lib/spikard/version.rb +5 -5
  20. data/lib/spikard/websocket.rb +59 -59
  21. data/lib/spikard.rb +43 -43
  22. data/sig/spikard.rbs +366 -360
  23. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  24. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  25. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  26. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  27. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  28. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  29. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  30. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  31. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  32. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  33. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  34. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  35. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  36. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  37. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  38. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  39. data/vendor/crates/spikard-core/Cargo.toml +40 -40
  40. data/vendor/crates/spikard-core/src/bindings/mod.rs +3 -3
  41. data/vendor/crates/spikard-core/src/bindings/response.rs +133 -133
  42. data/vendor/crates/spikard-core/src/debug.rs +127 -63
  43. data/vendor/crates/spikard-core/src/di/container.rs +702 -726
  44. data/vendor/crates/spikard-core/src/di/dependency.rs +273 -273
  45. data/vendor/crates/spikard-core/src/di/error.rs +118 -118
  46. data/vendor/crates/spikard-core/src/di/factory.rs +534 -538
  47. data/vendor/crates/spikard-core/src/di/graph.rs +506 -545
  48. data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
  49. data/vendor/crates/spikard-core/src/di/resolved.rs +405 -411
  50. data/vendor/crates/spikard-core/src/di/value.rs +281 -283
  51. data/vendor/crates/spikard-core/src/errors.rs +69 -39
  52. data/vendor/crates/spikard-core/src/http.rs +415 -153
  53. data/vendor/crates/spikard-core/src/lib.rs +29 -29
  54. data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -422
  55. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  56. data/vendor/crates/spikard-core/src/parameters.rs +2525 -722
  57. data/vendor/crates/spikard-core/src/problem.rs +344 -310
  58. data/vendor/crates/spikard-core/src/request_data.rs +1154 -189
  59. data/vendor/crates/spikard-core/src/router.rs +510 -249
  60. data/vendor/crates/spikard-core/src/schema_registry.rs +183 -183
  61. data/vendor/crates/spikard-core/src/type_hints.rs +304 -304
  62. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  63. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +457 -699
  64. data/vendor/crates/spikard-http/Cargo.toml +64 -68
  65. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  66. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  67. data/vendor/crates/spikard-http/src/auth.rs +296 -247
  68. data/vendor/crates/spikard-http/src/background.rs +1860 -249
  69. data/vendor/crates/spikard-http/src/bindings/mod.rs +3 -3
  70. data/vendor/crates/spikard-http/src/bindings/response.rs +1 -1
  71. data/vendor/crates/spikard-http/src/body_metadata.rs +8 -8
  72. data/vendor/crates/spikard-http/src/cors.rs +1005 -490
  73. data/vendor/crates/spikard-http/src/debug.rs +128 -63
  74. data/vendor/crates/spikard-http/src/di_handler.rs +1668 -423
  75. data/vendor/crates/spikard-http/src/handler_response.rs +901 -190
  76. data/vendor/crates/spikard-http/src/handler_trait.rs +830 -228
  77. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -284
  78. data/vendor/crates/spikard-http/src/lib.rs +534 -529
  79. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -149
  80. data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -428
  81. data/vendor/crates/spikard-http/src/middleware/mod.rs +540 -285
  82. data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -86
  83. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -147
  84. data/vendor/crates/spikard-http/src/middleware/validation.rs +735 -287
  85. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
  86. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -190
  87. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -308
  88. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -195
  89. data/vendor/crates/spikard-http/src/query_parser.rs +793 -369
  90. data/vendor/crates/spikard-http/src/response.rs +720 -399
  91. data/vendor/crates/spikard-http/src/server/handler.rs +1650 -87
  92. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -98
  93. data/vendor/crates/spikard-http/src/server/mod.rs +1502 -805
  94. data/vendor/crates/spikard-http/src/server/request_extraction.rs +770 -119
  95. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  96. data/vendor/crates/spikard-http/src/sse.rs +1409 -447
  97. data/vendor/crates/spikard-http/src/testing/form.rs +52 -14
  98. data/vendor/crates/spikard-http/src/testing/multipart.rs +60 -60
  99. data/vendor/crates/spikard-http/src/testing/test_client.rs +283 -285
  100. data/vendor/crates/spikard-http/src/testing.rs +377 -377
  101. data/vendor/crates/spikard-http/src/websocket.rs +1375 -324
  102. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  103. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  104. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  105. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  106. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  107. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  108. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  109. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  110. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  111. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  112. data/vendor/crates/spikard-rb/Cargo.toml +48 -42
  113. data/vendor/crates/spikard-rb/build.rs +199 -8
  114. data/vendor/crates/spikard-rb/src/background.rs +63 -63
  115. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  116. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +285 -294
  117. data/vendor/crates/spikard-rb/src/conversion.rs +554 -453
  118. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  119. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +375 -409
  120. data/vendor/crates/spikard-rb/src/handler.rs +618 -625
  121. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  122. data/vendor/crates/spikard-rb/src/lib.rs +1810 -2771
  123. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -274
  124. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  125. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  126. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  127. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  128. data/vendor/crates/spikard-rb/src/server.rs +308 -283
  129. data/vendor/crates/spikard-rb/src/sse.rs +231 -231
  130. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +551 -404
  131. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  132. data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +143 -143
  133. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  134. data/vendor/crates/spikard-rb/src/websocket.rs +374 -233
  135. metadata +46 -13
  136. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  137. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  138. data/vendor/crates/spikard-http/src/router.rs +0 -1
  139. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  140. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  141. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  142. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
@@ -1,233 +1,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 tracing::{debug, error};
10
-
11
- /// Ruby implementation of WebSocketHandler
12
- pub struct RubyWebSocketHandler {
13
- /// Handler name for debugging
14
- name: String,
15
- /// Ruby proc/callable for handle_message (Opaque for Send safety)
16
- handle_message_proc: Opaque<Value>,
17
- /// Ruby proc/callable for on_connect (Opaque for Send safety)
18
- on_connect_proc: Option<Opaque<Value>>,
19
- /// Ruby proc/callable for on_disconnect (Opaque for Send safety)
20
- on_disconnect_proc: Option<Opaque<Value>>,
21
- }
22
-
23
- impl RubyWebSocketHandler {
24
- /// Create a new Ruby WebSocket handler
25
- #[allow(dead_code)]
26
- pub fn new(
27
- name: String,
28
- handle_message_proc: Value,
29
- on_connect_proc: Option<Value>,
30
- on_disconnect_proc: Option<Value>,
31
- ) -> Self {
32
- Self {
33
- name,
34
- handle_message_proc: handle_message_proc.into(),
35
- on_connect_proc: on_connect_proc.map(|v| v.into()),
36
- on_disconnect_proc: on_disconnect_proc.map(|v| v.into()),
37
- }
38
- }
39
-
40
- /// Convert Ruby value to JSON
41
- fn ruby_to_json(ruby: &magnus::Ruby, value: Value) -> Result<JsonValue, String> {
42
- if value.is_nil() {
43
- return Ok(JsonValue::Null);
44
- }
45
-
46
- let json_module: Value = ruby
47
- .class_object()
48
- .const_get("JSON")
49
- .map_err(|e| format!("JSON module not available: {}", e))?;
50
-
51
- let json_str: String = json_module
52
- .funcall("generate", (value,))
53
- .map_err(|e| format!("Failed to generate JSON: {}", e))?;
54
-
55
- serde_json::from_str(&json_str).map_err(|e| format!("Failed to parse JSON: {}", e))
56
- }
57
-
58
- /// Convert JSON value to Ruby value
59
- fn json_to_ruby(ruby: &magnus::Ruby, value: &JsonValue) -> Result<Value, String> {
60
- match value {
61
- JsonValue::Null => Ok(ruby.qnil().as_value()),
62
- JsonValue::Bool(b) => Ok(if *b {
63
- ruby.qtrue().as_value()
64
- } else {
65
- ruby.qfalse().as_value()
66
- }),
67
- JsonValue::Number(num) => {
68
- if let Some(i) = num.as_i64() {
69
- Ok(ruby.integer_from_i64(i).as_value())
70
- } else if let Some(f) = num.as_f64() {
71
- Ok(ruby.float_from_f64(f).as_value())
72
- } else {
73
- Ok(ruby.qnil().as_value())
74
- }
75
- }
76
- JsonValue::String(s) => Ok(ruby.str_new(s).as_value()),
77
- JsonValue::Array(arr) => {
78
- let ruby_array = ruby.ary_new();
79
- for item in arr {
80
- ruby_array
81
- .push(Self::json_to_ruby(ruby, item)?)
82
- .map_err(|e| format!("Failed to push to array: {}", e))?;
83
- }
84
- Ok(ruby_array.as_value())
85
- }
86
- JsonValue::Object(obj) => {
87
- let ruby_hash = RHash::new();
88
- for (key, val) in obj {
89
- ruby_hash
90
- .aset(ruby.str_new(key), Self::json_to_ruby(ruby, val)?)
91
- .map_err(|e| format!("Failed to set hash value: {}", e))?;
92
- }
93
- Ok(ruby_hash.as_value())
94
- }
95
- }
96
- }
97
- }
98
-
99
- impl WebSocketHandler for RubyWebSocketHandler {
100
- async fn handle_message(&self, message: JsonValue) -> Option<JsonValue> {
101
- debug!("Ruby WebSocket handler '{}': handle_message", self.name);
102
-
103
- match magnus::Ruby::get()
104
- .map_err(|e| format!("Failed to get Ruby: {}", e))
105
- .and_then(|ruby| {
106
- let message_ruby = Self::json_to_ruby(&ruby, &message)?;
107
-
108
- let proc_value = ruby.get_inner(self.handle_message_proc);
109
- let result: Value = proc_value
110
- .funcall("call", (message_ruby,))
111
- .map_err(|e| format!("Handler '{}' call failed: {}", self.name, e))?;
112
-
113
- if result.is_nil() {
114
- Ok(None)
115
- } else {
116
- Self::ruby_to_json(&ruby, result).map(Some)
117
- }
118
- }) {
119
- Ok(value) => value,
120
- Err(e) => {
121
- error!("Ruby error in handle_message: {}", e);
122
- None
123
- }
124
- }
125
- }
126
-
127
- async fn on_connect(&self) {
128
- debug!("Ruby WebSocket handler '{}': on_connect", self.name);
129
-
130
- if let Some(on_connect_proc) = self.on_connect_proc {
131
- if let Err(e) = magnus::Ruby::get()
132
- .map_err(|e| format!("Failed to get Ruby: {}", e))
133
- .and_then(|ruby| {
134
- let proc_value = ruby.get_inner(on_connect_proc);
135
- proc_value
136
- .funcall::<_, _, Value>("call", ())
137
- .map_err(|e| format!("on_connect '{}' call failed: {}", self.name, e))?;
138
- Ok(())
139
- })
140
- {
141
- error!("on_connect error: {}", e);
142
- }
143
-
144
- debug!("Ruby WebSocket handler '{}': on_connect completed", self.name);
145
- }
146
- }
147
-
148
- async fn on_disconnect(&self) {
149
- debug!("Ruby WebSocket handler '{}': on_disconnect", self.name);
150
-
151
- if let Some(on_disconnect_proc) = self.on_disconnect_proc {
152
- if let Err(e) = magnus::Ruby::get()
153
- .map_err(|e| format!("Failed to get Ruby: {}", e))
154
- .and_then(|ruby| {
155
- let proc_value = ruby.get_inner(on_disconnect_proc);
156
- proc_value
157
- .funcall::<_, _, Value>("call", ())
158
- .map_err(|e| format!("on_disconnect '{}' call failed: {}", self.name, e))?;
159
- Ok(())
160
- })
161
- {
162
- error!("on_disconnect error: {}", e);
163
- }
164
-
165
- debug!("Ruby WebSocket handler '{}': on_disconnect completed", self.name);
166
- }
167
- }
168
- }
169
-
170
- unsafe impl Send for RubyWebSocketHandler {}
171
- unsafe impl Sync for RubyWebSocketHandler {}
172
-
173
- /// Create WebSocketState from Ruby handler object
174
- ///
175
- /// This function is designed to be called from Ruby to register WebSocket handlers.
176
- #[allow(dead_code)]
177
- pub fn create_websocket_state(
178
- ruby: &magnus::Ruby,
179
- handler_obj: Value,
180
- ) -> Result<spikard_http::WebSocketState<RubyWebSocketHandler>, magnus::Error> {
181
- let handle_message_proc: Value = handler_obj
182
- .funcall("method", (ruby.to_symbol("handle_message"),))
183
- .map_err(|e| {
184
- magnus::Error::new(
185
- ruby.exception_arg_error(),
186
- format!("handle_message method not found: {}", e),
187
- )
188
- })?;
189
-
190
- let on_connect_proc = handler_obj
191
- .funcall::<_, _, Value>("method", (ruby.to_symbol("on_connect"),))
192
- .ok();
193
-
194
- let on_disconnect_proc = handler_obj
195
- .funcall::<_, _, Value>("method", (ruby.to_symbol("on_disconnect"),))
196
- .ok();
197
-
198
- let message_schema = handler_obj
199
- .funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_message_schema"),))
200
- .ok()
201
- .and_then(|v| {
202
- if v.is_nil() {
203
- None
204
- } else {
205
- RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
206
- }
207
- });
208
-
209
- let response_schema = handler_obj
210
- .funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_response_schema"),))
211
- .ok()
212
- .and_then(|v| {
213
- if v.is_nil() {
214
- None
215
- } else {
216
- RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
217
- }
218
- });
219
-
220
- let ruby_handler = RubyWebSocketHandler::new(
221
- "WebSocketHandler".to_string(),
222
- handle_message_proc,
223
- on_connect_proc,
224
- on_disconnect_proc,
225
- );
226
-
227
- if message_schema.is_some() || response_schema.is_some() {
228
- spikard_http::WebSocketState::with_schemas(ruby_handler, message_schema, response_schema)
229
- .map_err(|e| magnus::Error::new(ruby.exception_runtime_error(), e))
230
- } else {
231
- Ok(spikard_http::WebSocketState::new(ruby_handler))
232
- }
233
- }
1
+ //! Ruby WebSocket handler bindings
2
+ //!
3
+ //! This module provides the bridge between Ruby blocks/procs and Rust's WebSocket system.
4
+ //! Uses magnus to safely call Ruby code from Rust async tasks.
5
+
6
+ use magnus::{RHash, Value, prelude::*, value::Opaque};
7
+ use serde_json::Value as JsonValue;
8
+ use spikard_http::WebSocketHandler;
9
+ use 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 { message, reply: reply_tx })
150
+ .is_err()
151
+ {
152
+ error!("Ruby WebSocket handler '{}' worker thread closed", self.name);
153
+ return None;
154
+ }
155
+
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
+ };
163
+
164
+ match result {
165
+ Ok(value) => value,
166
+ Err(e) => {
167
+ error!("Ruby error in handle_message: {}", e);
168
+ None
169
+ }
170
+ }
171
+ }
172
+
173
+ async fn on_connect(&self) {
174
+ debug!("Ruby WebSocket handler '{}': on_connect", self.name);
175
+
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()
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 {
196
+ error!("on_connect error: {}", e);
197
+ }
198
+
199
+ debug!("Ruby WebSocket handler '{}': on_connect completed", self.name);
200
+ }
201
+ }
202
+
203
+ async fn on_disconnect(&self) {
204
+ debug!("Ruby WebSocket handler '{}': on_disconnect", self.name);
205
+
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()
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 {
226
+ error!("on_disconnect error: {}", e);
227
+ }
228
+
229
+ debug!("Ruby WebSocket handler '{}': on_disconnect completed", self.name);
230
+ }
231
+ }
232
+ }
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
+
310
+ unsafe impl Send for RubyWebSocketHandler {}
311
+ unsafe impl Sync for RubyWebSocketHandler {}
312
+
313
+ /// Create WebSocketState from Ruby handler object
314
+ ///
315
+ /// This function is designed to be called from Ruby to register WebSocket handlers.
316
+ #[allow(dead_code)]
317
+ pub fn create_websocket_state(
318
+ ruby: &magnus::Ruby,
319
+ handler_obj: Value,
320
+ ) -> Result<spikard_http::WebSocketState<RubyWebSocketHandler>, magnus::Error> {
321
+ let handle_message_proc: Value = handler_obj
322
+ .funcall("method", (ruby.to_symbol("handle_message"),))
323
+ .map_err(|e| {
324
+ magnus::Error::new(
325
+ ruby.exception_arg_error(),
326
+ format!("handle_message method not found: {}", e),
327
+ )
328
+ })?;
329
+
330
+ let on_connect_proc = handler_obj
331
+ .funcall::<_, _, Value>("method", (ruby.to_symbol("on_connect"),))
332
+ .ok();
333
+
334
+ let on_disconnect_proc = handler_obj
335
+ .funcall::<_, _, Value>("method", (ruby.to_symbol("on_disconnect"),))
336
+ .ok();
337
+
338
+ let message_schema = handler_obj
339
+ .funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_message_schema"),))
340
+ .ok()
341
+ .and_then(|v| {
342
+ if v.is_nil() {
343
+ None
344
+ } else {
345
+ RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
346
+ }
347
+ });
348
+
349
+ let response_schema = handler_obj
350
+ .funcall::<_, _, Value>("instance_variable_get", (ruby.to_symbol("@_response_schema"),))
351
+ .ok()
352
+ .and_then(|v| {
353
+ if v.is_nil() {
354
+ None
355
+ } else {
356
+ RubyWebSocketHandler::ruby_to_json(ruby, v).ok()
357
+ }
358
+ });
359
+
360
+ let ruby_handler = RubyWebSocketHandler::new(
361
+ ruby,
362
+ "WebSocketHandler".to_string(),
363
+ handle_message_proc,
364
+ on_connect_proc,
365
+ on_disconnect_proc,
366
+ );
367
+
368
+ if message_schema.is_some() || response_schema.is_some() {
369
+ spikard_http::WebSocketState::with_schemas(ruby_handler, message_schema, response_schema)
370
+ .map_err(|e| magnus::Error::new(ruby.exception_runtime_error(), e))
371
+ } else {
372
+ Ok(spikard_http::WebSocketState::new(ruby_handler))
373
+ }
374
+ }