spikard 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +1 -1
  3. data/README.md +674 -674
  4. data/ext/spikard_rb/Cargo.toml +17 -17
  5. data/ext/spikard_rb/extconf.rb +13 -10
  6. data/ext/spikard_rb/src/lib.rs +6 -6
  7. data/lib/spikard/app.rb +405 -405
  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 -256
  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 -366
  23. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -63
  24. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -132
  25. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -752
  26. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -194
  27. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -246
  28. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -401
  29. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -238
  30. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -24
  31. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -292
  32. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -616
  33. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -305
  34. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -248
  35. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -351
  36. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -454
  37. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -383
  38. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -280
  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 -127
  43. data/vendor/crates/spikard-core/src/di/container.rs +702 -702
  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 -534
  47. data/vendor/crates/spikard-core/src/di/graph.rs +506 -506
  48. data/vendor/crates/spikard-core/src/di/mod.rs +192 -192
  49. data/vendor/crates/spikard-core/src/di/resolved.rs +405 -405
  50. data/vendor/crates/spikard-core/src/di/value.rs +281 -281
  51. data/vendor/crates/spikard-core/src/errors.rs +69 -69
  52. data/vendor/crates/spikard-core/src/http.rs +415 -415
  53. data/vendor/crates/spikard-core/src/lib.rs +29 -29
  54. data/vendor/crates/spikard-core/src/lifecycle.rs +1186 -1186
  55. data/vendor/crates/spikard-core/src/metadata.rs +389 -389
  56. data/vendor/crates/spikard-core/src/parameters.rs +2525 -2525
  57. data/vendor/crates/spikard-core/src/problem.rs +344 -344
  58. data/vendor/crates/spikard-core/src/request_data.rs +1154 -1154
  59. data/vendor/crates/spikard-core/src/router.rs +510 -510
  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 +696 -688
  63. data/vendor/crates/spikard-core/src/validation/mod.rs +457 -457
  64. data/vendor/crates/spikard-http/Cargo.toml +62 -64
  65. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -148
  66. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -92
  67. data/vendor/crates/spikard-http/src/auth.rs +296 -296
  68. data/vendor/crates/spikard-http/src/background.rs +1860 -1860
  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 -1005
  73. data/vendor/crates/spikard-http/src/debug.rs +128 -128
  74. data/vendor/crates/spikard-http/src/di_handler.rs +1668 -1668
  75. data/vendor/crates/spikard-http/src/handler_response.rs +901 -901
  76. data/vendor/crates/spikard-http/src/handler_trait.rs +838 -830
  77. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +290 -290
  78. data/vendor/crates/spikard-http/src/lib.rs +534 -534
  79. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +230 -230
  80. data/vendor/crates/spikard-http/src/lifecycle.rs +1193 -1193
  81. data/vendor/crates/spikard-http/src/middleware/mod.rs +560 -540
  82. data/vendor/crates/spikard-http/src/middleware/multipart.rs +912 -912
  83. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +513 -513
  84. data/vendor/crates/spikard-http/src/middleware/validation.rs +768 -735
  85. data/vendor/crates/spikard-http/src/openapi/mod.rs +309 -309
  86. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +535 -535
  87. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1363 -1363
  88. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +665 -665
  89. data/vendor/crates/spikard-http/src/query_parser.rs +793 -793
  90. data/vendor/crates/spikard-http/src/response.rs +720 -720
  91. data/vendor/crates/spikard-http/src/server/handler.rs +1650 -1650
  92. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +234 -234
  93. data/vendor/crates/spikard-http/src/server/mod.rs +1593 -1502
  94. data/vendor/crates/spikard-http/src/server/request_extraction.rs +789 -770
  95. data/vendor/crates/spikard-http/src/server/routing_factory.rs +629 -599
  96. data/vendor/crates/spikard-http/src/sse.rs +1409 -1409
  97. data/vendor/crates/spikard-http/src/testing/form.rs +52 -52
  98. data/vendor/crates/spikard-http/src/testing/multipart.rs +64 -60
  99. data/vendor/crates/spikard-http/src/testing/test_client.rs +311 -283
  100. data/vendor/crates/spikard-http/src/testing.rs +406 -377
  101. data/vendor/crates/spikard-http/src/websocket.rs +1404 -1375
  102. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -832
  103. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -309
  104. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -26
  105. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -192
  106. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -5
  107. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -1093
  108. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -656
  109. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -314
  110. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -620
  111. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -663
  112. data/vendor/crates/spikard-rb/Cargo.toml +48 -48
  113. data/vendor/crates/spikard-rb/build.rs +199 -199
  114. data/vendor/crates/spikard-rb/src/background.rs +63 -63
  115. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -5
  116. data/vendor/crates/spikard-rb/src/config/server_config.rs +285 -285
  117. data/vendor/crates/spikard-rb/src/conversion.rs +554 -554
  118. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -100
  119. data/vendor/crates/spikard-rb/src/di/mod.rs +375 -375
  120. data/vendor/crates/spikard-rb/src/handler.rs +618 -618
  121. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -3
  122. data/vendor/crates/spikard-rb/src/lib.rs +1806 -1810
  123. data/vendor/crates/spikard-rb/src/lifecycle.rs +275 -275
  124. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -5
  125. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +442 -447
  126. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -5
  127. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -324
  128. data/vendor/crates/spikard-rb/src/server.rs +305 -308
  129. data/vendor/crates/spikard-rb/src/sse.rs +231 -231
  130. data/vendor/crates/spikard-rb/src/testing/client.rs +538 -551
  131. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -7
  132. data/vendor/crates/spikard-rb/src/testing/sse.rs +143 -143
  133. data/vendor/crates/spikard-rb/src/testing/websocket.rs +608 -635
  134. data/vendor/crates/spikard-rb/src/websocket.rs +377 -374
  135. metadata +15 -1
@@ -1,618 +1,618 @@
1
- //! Ruby handler wrapper implementing the Handler trait.
2
- //!
3
- //! This module provides the `RubyHandler` struct that wraps Ruby Proc objects
4
- //! and implements Spikard's `Handler` trait for async request processing.
5
-
6
- #![allow(dead_code)]
7
- #![deny(clippy::unwrap_used)]
8
-
9
- use axum::body::Body;
10
- use axum::http::{HeaderName, HeaderValue, Request, StatusCode};
11
- use magnus::prelude::*;
12
- use magnus::value::LazyId;
13
- use magnus::value::{InnerValue, Opaque};
14
- use magnus::{Error, RHash, RString, Ruby, TryConvert, Value, gc::Marker};
15
- use serde_json::Value as JsonValue;
16
- use spikard_bindings_shared::ErrorResponseBuilder;
17
- use spikard_core::problem::ProblemDetails;
18
- use spikard_http::SchemaValidator;
19
- use spikard_http::{Handler, HandlerResponse, HandlerResult, RequestData};
20
- use std::collections::HashMap;
21
- use std::panic::AssertUnwindSafe;
22
- use std::pin::Pin;
23
- use std::sync::Arc;
24
-
25
- use crate::conversion::{
26
- json_to_ruby, json_to_ruby_with_uploads, map_to_ruby_hash, multimap_to_ruby_hash, ruby_value_to_json,
27
- };
28
-
29
- static KEY_METHOD: LazyId = LazyId::new("method");
30
- static KEY_PATH: LazyId = LazyId::new("path");
31
- static KEY_PATH_PARAMS: LazyId = LazyId::new("path_params");
32
- static KEY_QUERY: LazyId = LazyId::new("query");
33
- static KEY_RAW_QUERY: LazyId = LazyId::new("raw_query");
34
- static KEY_HEADERS: LazyId = LazyId::new("headers");
35
- static KEY_COOKIES: LazyId = LazyId::new("cookies");
36
- static KEY_BODY: LazyId = LazyId::new("body");
37
- static KEY_RAW_BODY: LazyId = LazyId::new("raw_body");
38
- static KEY_PARAMS: LazyId = LazyId::new("params");
39
-
40
- /// Response payload with status, headers, and body data.
41
- pub struct HandlerResponsePayload {
42
- pub status: u16,
43
- pub headers: HashMap<String, String>,
44
- pub body: Option<JsonValue>,
45
- pub raw_body: Option<Vec<u8>>,
46
- }
47
-
48
- /// Streaming response variant containing an enumerator and metadata.
49
- pub struct StreamingResponsePayload {
50
- pub enumerator: Arc<Opaque<Value>>,
51
- pub status: u16,
52
- pub headers: HashMap<String, String>,
53
- }
54
-
55
- /// Handler result: either a payload or a streaming response.
56
- pub enum RubyHandlerResult {
57
- Payload(HandlerResponsePayload),
58
- Streaming(StreamingResponsePayload),
59
- }
60
-
61
- impl StreamingResponsePayload {
62
- /// Convert streaming response into a `HandlerResponse`.
63
- pub fn into_response(self) -> Result<HandlerResponse, Error> {
64
- let ruby = Ruby::get().map_err(|_| {
65
- Error::new(
66
- magnus::exception::runtime_error(),
67
- "Ruby VM became unavailable during streaming response construction",
68
- )
69
- })?;
70
-
71
- let status = StatusCode::from_u16(self.status).map_err(|err| {
72
- Error::new(
73
- ruby.exception_arg_error(),
74
- format!("Invalid streaming status code {}: {}", self.status, err),
75
- )
76
- })?;
77
-
78
- let header_pairs = self
79
- .headers
80
- .into_iter()
81
- .map(|(name, value)| {
82
- let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|err| {
83
- Error::new(
84
- ruby.exception_arg_error(),
85
- format!("Invalid header name '{name}': {err}"),
86
- )
87
- })?;
88
- let header_value = HeaderValue::from_str(&value).map_err(|err| {
89
- Error::new(
90
- ruby.exception_arg_error(),
91
- format!("Invalid header value for '{name}': {err}"),
92
- )
93
- })?;
94
- Ok((header_name, header_value))
95
- })
96
- .collect::<Result<Vec<_>, Error>>()?;
97
-
98
- let enumerator = self.enumerator.clone();
99
- let body_stream = async_stream::stream! {
100
- loop {
101
- match poll_stream_chunk(&enumerator) {
102
- Ok(Some(bytes)) => yield Ok(bytes),
103
- Ok(None) => break,
104
- Err(err) => {
105
- yield Err(Box::new(err));
106
- break;
107
- }
108
- }
109
- }
110
- };
111
-
112
- let mut response = HandlerResponse::stream(body_stream).with_status(status);
113
- for (name, value) in header_pairs {
114
- response = response.with_header(name, value);
115
- }
116
- Ok(response)
117
- }
118
- }
119
-
120
- /// Poll a single chunk from a Ruby enumerator.
121
- fn poll_stream_chunk(enumerator: &Arc<Opaque<Value>>) -> Result<Option<bytes::Bytes>, std::io::Error> {
122
- let ruby = Ruby::get().map_err(|err| std::io::Error::other(err.to_string()))?;
123
- let enum_value = enumerator.get_inner_with(&ruby);
124
- match enum_value.funcall::<_, _, Value>("next", ()) {
125
- Ok(chunk) => crate::conversion::ruby_value_to_bytes(chunk).map(Some),
126
- Err(err) => {
127
- if err.is_kind_of(ruby.exception_stop_iteration()) {
128
- Ok(None)
129
- } else {
130
- Err(std::io::Error::other(err.to_string()))
131
- }
132
- }
133
- }
134
- }
135
-
136
- /// Inner state of a Ruby handler.
137
- pub struct RubyHandlerInner {
138
- pub handler_proc: Opaque<Value>,
139
- pub handler_name: String,
140
- pub method: String,
141
- pub path: String,
142
- method_value: Opaque<Value>,
143
- path_value: Opaque<Value>,
144
- pub json_module: Opaque<Value>,
145
- pub response_validator: Option<Arc<SchemaValidator>>,
146
- pub upload_file_class: Option<Opaque<Value>>,
147
- }
148
-
149
- /// Wrapper around a Ruby Proc that implements the Handler trait.
150
- #[derive(Clone)]
151
- pub struct RubyHandler {
152
- pub inner: Arc<RubyHandlerInner>,
153
- }
154
-
155
- impl RubyHandler {
156
- /// Create a new RubyHandler from a route and handler Proc.
157
- pub fn new(route: &spikard_http::Route, handler_value: Value, json_module: Value) -> Result<Self, Error> {
158
- let upload_file_class = if route.file_params.is_some() {
159
- lookup_upload_file_class()?
160
- } else {
161
- None
162
- };
163
- let method = route.method.as_str().to_string();
164
- let path = route.path.clone();
165
-
166
- let Ok(ruby) = Ruby::get() else {
167
- return Err(Error::new(
168
- magnus::exception::runtime_error(),
169
- "Ruby VM unavailable while creating handler",
170
- ));
171
- };
172
- let method_value = Opaque::from(ruby.str_new(&method).as_value());
173
- let path_value = Opaque::from(ruby.str_new(&path).as_value());
174
-
175
- Ok(Self {
176
- inner: Arc::new(RubyHandlerInner {
177
- handler_proc: Opaque::from(handler_value),
178
- handler_name: route.handler_name.clone(),
179
- method,
180
- path,
181
- method_value,
182
- path_value,
183
- json_module: Opaque::from(json_module),
184
- response_validator: route.response_validator.clone(),
185
- upload_file_class,
186
- }),
187
- })
188
- }
189
-
190
- /// Create a new RubyHandler for server mode
191
- ///
192
- /// This is used by run_server to create handlers from Ruby Procs
193
- pub fn new_for_server(
194
- _ruby: &Ruby,
195
- handler_value: Value,
196
- handler_name: String,
197
- method: String,
198
- path: String,
199
- json_module: Value,
200
- route: &spikard_http::Route,
201
- ) -> Result<Self, Error> {
202
- let upload_file_class = if route.file_params.is_some() {
203
- lookup_upload_file_class()?
204
- } else {
205
- None
206
- };
207
- let Ok(ruby) = Ruby::get() else {
208
- return Err(Error::new(
209
- magnus::exception::runtime_error(),
210
- "Ruby VM unavailable while creating handler",
211
- ));
212
- };
213
- let method_value = Opaque::from(ruby.str_new(&method).as_value());
214
- let path_value = Opaque::from(ruby.str_new(&path).as_value());
215
-
216
- Ok(Self {
217
- inner: Arc::new(RubyHandlerInner {
218
- handler_proc: Opaque::from(handler_value),
219
- handler_name,
220
- method,
221
- path,
222
- method_value,
223
- path_value,
224
- json_module: Opaque::from(json_module),
225
- response_validator: route.response_validator.clone(),
226
- upload_file_class,
227
- }),
228
- })
229
- }
230
-
231
- /// Required by Ruby GC; invoked through the magnus mark hook.
232
- #[allow(dead_code)]
233
- pub fn mark(&self, marker: &Marker) {
234
- if let Ok(ruby) = Ruby::get() {
235
- let proc_val = self.inner.handler_proc.get_inner_with(&ruby);
236
- marker.mark(proc_val);
237
- marker.mark(self.inner.method_value.get_inner_with(&ruby));
238
- marker.mark(self.inner.path_value.get_inner_with(&ruby));
239
- }
240
- }
241
-
242
- /// Handle a request synchronously.
243
- pub fn handle(&self, request_data: RequestData) -> HandlerResult {
244
- let result = std::panic::catch_unwind(AssertUnwindSafe(|| self.handle_inner(request_data)));
245
- match result {
246
- Ok(res) => res,
247
- Err(_) => Err(ErrorResponseBuilder::structured_error(
248
- StatusCode::INTERNAL_SERVER_ERROR,
249
- "panic",
250
- "Unexpected panic while executing Ruby handler",
251
- )),
252
- }
253
- }
254
-
255
- fn handle_inner(&self, request_data: RequestData) -> HandlerResult {
256
- let validated_params = request_data.validated_params.clone();
257
-
258
- let ruby = Ruby::get().map_err(|_| {
259
- ErrorResponseBuilder::structured_error(
260
- StatusCode::INTERNAL_SERVER_ERROR,
261
- "ruby_vm_unavailable",
262
- "Ruby VM unavailable while invoking handler",
263
- )
264
- })?;
265
-
266
- let request_value = build_ruby_request(&ruby, &self.inner, &request_data, validated_params.as_ref())
267
- .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
268
-
269
- let handler_proc = self.inner.handler_proc.get_inner_with(&ruby);
270
- let handler_result = handler_proc.funcall("call", (request_value,));
271
- let response_value = match handler_result {
272
- Ok(value) => value,
273
- Err(err) => {
274
- return Err(ErrorResponseBuilder::structured_error(
275
- StatusCode::INTERNAL_SERVER_ERROR,
276
- "handler_failed",
277
- format!("Handler '{}' failed: {}", self.inner.handler_name, err),
278
- ));
279
- }
280
- };
281
-
282
- let handler_result = interpret_handler_response(&ruby, &self.inner, response_value).map_err(|err| {
283
- ErrorResponseBuilder::structured_error(
284
- StatusCode::INTERNAL_SERVER_ERROR,
285
- "response_interpret_error",
286
- format!(
287
- "Failed to interpret response from '{}': {}",
288
- self.inner.handler_name, err
289
- ),
290
- )
291
- })?;
292
-
293
- let payload = match handler_result {
294
- RubyHandlerResult::Streaming(streaming) => {
295
- let response = streaming.into_response().map_err(|err| {
296
- ErrorResponseBuilder::structured_error(
297
- StatusCode::INTERNAL_SERVER_ERROR,
298
- "streaming_response_error",
299
- format!("Failed to build streaming response: {}", err),
300
- )
301
- })?;
302
- return Ok(response.into_response());
303
- }
304
- RubyHandlerResult::Payload(payload) => payload,
305
- };
306
-
307
- if let Some(validator) = &self.inner.response_validator {
308
- let candidate_body = match payload.body.clone() {
309
- Some(body) => Some(body),
310
- None => match try_parse_raw_body(&payload.raw_body) {
311
- Ok(parsed) => parsed,
312
- Err(err) => {
313
- return Err(ErrorResponseBuilder::structured_error(
314
- StatusCode::INTERNAL_SERVER_ERROR,
315
- "response_body_decode_error",
316
- err,
317
- ));
318
- }
319
- },
320
- };
321
-
322
- match candidate_body {
323
- Some(json_body) => {
324
- if let Err(errors) = validator.validate(&json_body) {
325
- let problem = ProblemDetails::from_validation_error(&errors);
326
- return Err(ErrorResponseBuilder::problem_details_response(&problem));
327
- }
328
- }
329
- None => {
330
- return Err(ErrorResponseBuilder::structured_error(
331
- StatusCode::INTERNAL_SERVER_ERROR,
332
- "response_validation_failed",
333
- "Response validator requires JSON body but handler returned raw bytes",
334
- ));
335
- }
336
- }
337
- }
338
-
339
- let HandlerResponsePayload {
340
- status,
341
- headers,
342
- body,
343
- raw_body,
344
- } = payload;
345
-
346
- let mut response_builder = axum::http::Response::builder().status(status);
347
- let mut has_content_type = false;
348
-
349
- for (name, value) in headers.iter() {
350
- if name.eq_ignore_ascii_case("content-type") {
351
- has_content_type = true;
352
- }
353
- let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|err| {
354
- (
355
- StatusCode::INTERNAL_SERVER_ERROR,
356
- format!("Invalid header name '{name}': {err}"),
357
- )
358
- })?;
359
- let header_value = HeaderValue::from_str(value).map_err(|err| {
360
- (
361
- StatusCode::INTERNAL_SERVER_ERROR,
362
- format!("Invalid header value for '{name}': {err}"),
363
- )
364
- })?;
365
-
366
- response_builder = response_builder.header(header_name, header_value);
367
- }
368
-
369
- if !has_content_type && body.is_some() {
370
- response_builder = response_builder.header(
371
- HeaderName::from_static("content-type"),
372
- HeaderValue::from_static("application/json"),
373
- );
374
- }
375
-
376
- let body_bytes = if let Some(raw) = raw_body {
377
- raw
378
- } else if let Some(json_value) = body {
379
- serde_json::to_vec(&json_value).map_err(|err| {
380
- (
381
- StatusCode::INTERNAL_SERVER_ERROR,
382
- format!("Failed to serialise response body: {err}"),
383
- )
384
- })?
385
- } else {
386
- Vec::new()
387
- };
388
-
389
- response_builder.body(Body::from(body_bytes)).map_err(|err| {
390
- (
391
- StatusCode::INTERNAL_SERVER_ERROR,
392
- format!("Failed to build response: {err}"),
393
- )
394
- })
395
- }
396
- }
397
-
398
- impl Handler for RubyHandler {
399
- fn call(
400
- &self,
401
- _req: Request<Body>,
402
- request_data: RequestData,
403
- ) -> Pin<Box<dyn std::future::Future<Output = HandlerResult> + Send + '_>> {
404
- let handler = self.clone();
405
- Box::pin(async move { handler.handle(request_data) })
406
- }
407
- }
408
-
409
- fn try_parse_raw_body(raw_body: &Option<Vec<u8>>) -> Result<Option<JsonValue>, String> {
410
- let Some(bytes) = raw_body else {
411
- return Ok(None);
412
- };
413
- let text = String::from_utf8(bytes.clone()).map_err(|e| format!("Invalid UTF-8 in response body: {e}"))?;
414
- if text.is_empty() {
415
- return Ok(None);
416
- }
417
- serde_json::from_str(&text)
418
- .map(Some)
419
- .map_err(|e| format!("Failed to parse response body as JSON: {e}"))
420
- }
421
-
422
- fn lookup_upload_file_class() -> Result<Option<Opaque<Value>>, Error> {
423
- let ruby = match Ruby::get() {
424
- Ok(ruby) => ruby,
425
- Err(_) => return Ok(None),
426
- };
427
-
428
- let upload_file = ruby.eval::<Value>("Spikard::UploadFile").ok();
429
- Ok(upload_file.map(Opaque::from))
430
- }
431
-
432
- /// Build a Ruby Hash request object from request data.
433
- fn build_ruby_request(
434
- ruby: &Ruby,
435
- handler: &RubyHandlerInner,
436
- request_data: &RequestData,
437
- validated_params: Option<&JsonValue>,
438
- ) -> Result<Value, Error> {
439
- let hash = ruby.hash_new_capa(9);
440
-
441
- hash.aset(*KEY_METHOD, handler.method_value.get_inner_with(ruby))?;
442
- hash.aset(*KEY_PATH, handler.path_value.get_inner_with(ruby))?;
443
-
444
- let path_params = map_to_ruby_hash(ruby, request_data.path_params.as_ref())?;
445
- hash.aset(*KEY_PATH_PARAMS, path_params)?;
446
-
447
- let query_value = json_to_ruby(ruby, &request_data.query_params)?;
448
- hash.aset(*KEY_QUERY, query_value)?;
449
-
450
- let raw_query = multimap_to_ruby_hash(ruby, request_data.raw_query_params.as_ref())?;
451
- hash.aset(*KEY_RAW_QUERY, raw_query)?;
452
-
453
- let headers = map_to_ruby_hash(ruby, request_data.headers.as_ref())?;
454
- hash.aset(*KEY_HEADERS, headers)?;
455
-
456
- let cookies = map_to_ruby_hash(ruby, request_data.cookies.as_ref())?;
457
- hash.aset(*KEY_COOKIES, cookies)?;
458
-
459
- let upload_class_value = handler.upload_file_class.as_ref().map(|cls| cls.get_inner_with(ruby));
460
- let body_value = json_to_ruby_with_uploads(ruby, &request_data.body, upload_class_value.as_ref())?;
461
- hash.aset(*KEY_BODY, body_value)?;
462
- if let Some(raw) = &request_data.raw_body {
463
- let raw_str = ruby.str_from_slice(raw);
464
- hash.aset(*KEY_RAW_BODY, raw_str)?;
465
- } else {
466
- hash.aset(*KEY_RAW_BODY, ruby.qnil())?;
467
- }
468
-
469
- let params_value = if let Some(validated) = validated_params {
470
- json_to_ruby(ruby, validated)?
471
- } else {
472
- build_default_params_from_converted(ruby, path_params, query_value, headers, cookies)?
473
- };
474
- hash.aset(*KEY_PARAMS, params_value)?;
475
-
476
- Ok(hash.as_value())
477
- }
478
-
479
- /// Build default params from already converted Ruby values, avoiding double conversion.
480
- fn build_default_params_from_converted(
481
- ruby: &Ruby,
482
- path_params: Value,
483
- query: Value,
484
- headers: Value,
485
- cookies: Value,
486
- ) -> Result<Value, Error> {
487
- let params = ruby.hash_new();
488
-
489
- if let Some(hash) = RHash::from_value(path_params) {
490
- let _: Value = params.funcall("merge!", (hash,))?;
491
- }
492
- if let Some(hash) = RHash::from_value(query) {
493
- let _: Value = params.funcall("merge!", (hash,))?;
494
- }
495
- if let Some(hash) = RHash::from_value(headers) {
496
- let _: Value = params.funcall("merge!", (hash,))?;
497
- }
498
- if let Some(hash) = RHash::from_value(cookies) {
499
- let _: Value = params.funcall("merge!", (hash,))?;
500
- }
501
-
502
- Ok(params.as_value())
503
- }
504
-
505
- /// Interpret a Ruby handler response into our response types.
506
- fn interpret_handler_response(
507
- ruby: &Ruby,
508
- handler: &RubyHandlerInner,
509
- value: Value,
510
- ) -> Result<RubyHandlerResult, Error> {
511
- if value.is_nil() {
512
- return Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
513
- status: 200,
514
- headers: HashMap::new(),
515
- body: None,
516
- raw_body: None,
517
- }));
518
- }
519
-
520
- if is_streaming_response(ruby, value)? {
521
- let stream_value: Value = value.funcall("stream", ())?;
522
- let status: i64 = value.funcall("status_code", ())?;
523
- let headers_value: Value = value.funcall("headers", ())?;
524
-
525
- let status_u16 = u16::try_from(status).map_err(|_| {
526
- Error::new(
527
- ruby.exception_arg_error(),
528
- "StreamingResponse status_code must be between 0 and 65535",
529
- )
530
- })?;
531
-
532
- let headers = value_to_string_map(ruby, headers_value)?;
533
-
534
- return Ok(RubyHandlerResult::Streaming(StreamingResponsePayload {
535
- enumerator: Arc::new(Opaque::from(stream_value)),
536
- status: status_u16,
537
- headers,
538
- }));
539
- }
540
-
541
- let status_symbol = ruby.intern("status_code");
542
- if value.respond_to(status_symbol, false)? {
543
- let status: i64 = value.funcall("status_code", ())?;
544
- let status_u16 = u16::try_from(status)
545
- .map_err(|_| Error::new(ruby.exception_arg_error(), "status_code must be between 0 and 65535"))?;
546
-
547
- let headers_value: Value = value.funcall("headers", ())?;
548
- let headers = if headers_value.is_nil() {
549
- HashMap::new()
550
- } else {
551
- let hash = RHash::try_convert(headers_value)?;
552
- hash.to_hash_map::<String, String>()?
553
- };
554
-
555
- let content_value: Value = value.funcall("content", ())?;
556
- let mut raw_body = None;
557
- let body = if content_value.is_nil() {
558
- None
559
- } else if let Ok(str_value) = RString::try_convert(content_value) {
560
- let slice = unsafe { str_value.as_slice() };
561
- raw_body = Some(slice.to_vec());
562
- None
563
- } else {
564
- Some(ruby_value_to_json(
565
- ruby,
566
- handler.json_module.get_inner_with(ruby),
567
- content_value,
568
- )?)
569
- };
570
-
571
- return Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
572
- status: status_u16,
573
- headers,
574
- body,
575
- raw_body,
576
- }));
577
- }
578
-
579
- if let Ok(str_value) = RString::try_convert(value) {
580
- let slice = unsafe { str_value.as_slice() };
581
- return Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
582
- status: 200,
583
- headers: HashMap::new(),
584
- body: None,
585
- raw_body: Some(slice.to_vec()),
586
- }));
587
- }
588
-
589
- let body_json = ruby_value_to_json(ruby, handler.json_module.get_inner_with(ruby), value)?;
590
-
591
- Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
592
- status: 200,
593
- headers: HashMap::new(),
594
- body: Some(body_json),
595
- raw_body: None,
596
- }))
597
- }
598
-
599
- /// Convert a Ruby value to a string HashMap.
600
- fn value_to_string_map(ruby: &Ruby, value: Value) -> Result<HashMap<String, String>, Error> {
601
- if value.is_nil() {
602
- return Ok(HashMap::new());
603
- }
604
- let hash = RHash::try_convert(value)?;
605
- hash.to_hash_map::<String, String>().map_err(|err| {
606
- Error::new(
607
- ruby.exception_arg_error(),
608
- format!("Expected headers hash of strings: {}", err),
609
- )
610
- })
611
- }
612
-
613
- /// Check if a Ruby value is a streaming response.
614
- fn is_streaming_response(ruby: &Ruby, value: Value) -> Result<bool, Error> {
615
- let stream_sym = ruby.intern("stream");
616
- let status_sym = ruby.intern("status_code");
617
- Ok(value.respond_to(stream_sym, false)? && value.respond_to(status_sym, false)?)
618
- }
1
+ //! Ruby handler wrapper implementing the Handler trait.
2
+ //!
3
+ //! This module provides the `RubyHandler` struct that wraps Ruby Proc objects
4
+ //! and implements Spikard's `Handler` trait for async request processing.
5
+
6
+ #![allow(dead_code)]
7
+ #![deny(clippy::unwrap_used)]
8
+
9
+ use axum::body::Body;
10
+ use axum::http::{HeaderName, HeaderValue, Request, StatusCode};
11
+ use magnus::prelude::*;
12
+ use magnus::value::LazyId;
13
+ use magnus::value::{InnerValue, Opaque};
14
+ use magnus::{Error, RHash, RString, Ruby, TryConvert, Value, gc::Marker};
15
+ use serde_json::Value as JsonValue;
16
+ use spikard_bindings_shared::ErrorResponseBuilder;
17
+ use spikard_core::problem::ProblemDetails;
18
+ use spikard_http::SchemaValidator;
19
+ use spikard_http::{Handler, HandlerResponse, HandlerResult, RequestData};
20
+ use std::collections::HashMap;
21
+ use std::panic::AssertUnwindSafe;
22
+ use std::pin::Pin;
23
+ use std::sync::Arc;
24
+
25
+ use crate::conversion::{
26
+ json_to_ruby, json_to_ruby_with_uploads, map_to_ruby_hash, multimap_to_ruby_hash, ruby_value_to_json,
27
+ };
28
+
29
+ static KEY_METHOD: LazyId = LazyId::new("method");
30
+ static KEY_PATH: LazyId = LazyId::new("path");
31
+ static KEY_PATH_PARAMS: LazyId = LazyId::new("path_params");
32
+ static KEY_QUERY: LazyId = LazyId::new("query");
33
+ static KEY_RAW_QUERY: LazyId = LazyId::new("raw_query");
34
+ static KEY_HEADERS: LazyId = LazyId::new("headers");
35
+ static KEY_COOKIES: LazyId = LazyId::new("cookies");
36
+ static KEY_BODY: LazyId = LazyId::new("body");
37
+ static KEY_RAW_BODY: LazyId = LazyId::new("raw_body");
38
+ static KEY_PARAMS: LazyId = LazyId::new("params");
39
+
40
+ /// Response payload with status, headers, and body data.
41
+ pub struct HandlerResponsePayload {
42
+ pub status: u16,
43
+ pub headers: HashMap<String, String>,
44
+ pub body: Option<JsonValue>,
45
+ pub raw_body: Option<Vec<u8>>,
46
+ }
47
+
48
+ /// Streaming response variant containing an enumerator and metadata.
49
+ pub struct StreamingResponsePayload {
50
+ pub enumerator: Arc<Opaque<Value>>,
51
+ pub status: u16,
52
+ pub headers: HashMap<String, String>,
53
+ }
54
+
55
+ /// Handler result: either a payload or a streaming response.
56
+ pub enum RubyHandlerResult {
57
+ Payload(HandlerResponsePayload),
58
+ Streaming(StreamingResponsePayload),
59
+ }
60
+
61
+ impl StreamingResponsePayload {
62
+ /// Convert streaming response into a `HandlerResponse`.
63
+ pub fn into_response(self) -> Result<HandlerResponse, Error> {
64
+ let ruby = Ruby::get().map_err(|_| {
65
+ Error::new(
66
+ magnus::exception::runtime_error(),
67
+ "Ruby VM became unavailable during streaming response construction",
68
+ )
69
+ })?;
70
+
71
+ let status = StatusCode::from_u16(self.status).map_err(|err| {
72
+ Error::new(
73
+ ruby.exception_arg_error(),
74
+ format!("Invalid streaming status code {}: {}", self.status, err),
75
+ )
76
+ })?;
77
+
78
+ let header_pairs = self
79
+ .headers
80
+ .into_iter()
81
+ .map(|(name, value)| {
82
+ let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|err| {
83
+ Error::new(
84
+ ruby.exception_arg_error(),
85
+ format!("Invalid header name '{name}': {err}"),
86
+ )
87
+ })?;
88
+ let header_value = HeaderValue::from_str(&value).map_err(|err| {
89
+ Error::new(
90
+ ruby.exception_arg_error(),
91
+ format!("Invalid header value for '{name}': {err}"),
92
+ )
93
+ })?;
94
+ Ok((header_name, header_value))
95
+ })
96
+ .collect::<Result<Vec<_>, Error>>()?;
97
+
98
+ let enumerator = self.enumerator.clone();
99
+ let body_stream = async_stream::stream! {
100
+ loop {
101
+ match poll_stream_chunk(&enumerator) {
102
+ Ok(Some(bytes)) => yield Ok(bytes),
103
+ Ok(None) => break,
104
+ Err(err) => {
105
+ yield Err(Box::new(err));
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ };
111
+
112
+ let mut response = HandlerResponse::stream(body_stream).with_status(status);
113
+ for (name, value) in header_pairs {
114
+ response = response.with_header(name, value);
115
+ }
116
+ Ok(response)
117
+ }
118
+ }
119
+
120
+ /// Poll a single chunk from a Ruby enumerator.
121
+ fn poll_stream_chunk(enumerator: &Arc<Opaque<Value>>) -> Result<Option<bytes::Bytes>, std::io::Error> {
122
+ let ruby = Ruby::get().map_err(|err| std::io::Error::other(err.to_string()))?;
123
+ let enum_value = enumerator.get_inner_with(&ruby);
124
+ match enum_value.funcall::<_, _, Value>("next", ()) {
125
+ Ok(chunk) => crate::conversion::ruby_value_to_bytes(chunk).map(Some),
126
+ Err(err) => {
127
+ if err.is_kind_of(ruby.exception_stop_iteration()) {
128
+ Ok(None)
129
+ } else {
130
+ Err(std::io::Error::other(err.to_string()))
131
+ }
132
+ }
133
+ }
134
+ }
135
+
136
+ /// Inner state of a Ruby handler.
137
+ pub struct RubyHandlerInner {
138
+ pub handler_proc: Opaque<Value>,
139
+ pub handler_name: String,
140
+ pub method: String,
141
+ pub path: String,
142
+ method_value: Opaque<Value>,
143
+ path_value: Opaque<Value>,
144
+ pub json_module: Opaque<Value>,
145
+ pub response_validator: Option<Arc<SchemaValidator>>,
146
+ pub upload_file_class: Option<Opaque<Value>>,
147
+ }
148
+
149
+ /// Wrapper around a Ruby Proc that implements the Handler trait.
150
+ #[derive(Clone)]
151
+ pub struct RubyHandler {
152
+ pub inner: Arc<RubyHandlerInner>,
153
+ }
154
+
155
+ impl RubyHandler {
156
+ /// Create a new RubyHandler from a route and handler Proc.
157
+ pub fn new(route: &spikard_http::Route, handler_value: Value, json_module: Value) -> Result<Self, Error> {
158
+ let upload_file_class = if route.file_params.is_some() {
159
+ lookup_upload_file_class()?
160
+ } else {
161
+ None
162
+ };
163
+ let method = route.method.as_str().to_string();
164
+ let path = route.path.clone();
165
+
166
+ let Ok(ruby) = Ruby::get() else {
167
+ return Err(Error::new(
168
+ magnus::exception::runtime_error(),
169
+ "Ruby VM unavailable while creating handler",
170
+ ));
171
+ };
172
+ let method_value = Opaque::from(ruby.str_new(&method).as_value());
173
+ let path_value = Opaque::from(ruby.str_new(&path).as_value());
174
+
175
+ Ok(Self {
176
+ inner: Arc::new(RubyHandlerInner {
177
+ handler_proc: Opaque::from(handler_value),
178
+ handler_name: route.handler_name.clone(),
179
+ method,
180
+ path,
181
+ method_value,
182
+ path_value,
183
+ json_module: Opaque::from(json_module),
184
+ response_validator: route.response_validator.clone(),
185
+ upload_file_class,
186
+ }),
187
+ })
188
+ }
189
+
190
+ /// Create a new RubyHandler for server mode
191
+ ///
192
+ /// This is used by run_server to create handlers from Ruby Procs
193
+ pub fn new_for_server(
194
+ _ruby: &Ruby,
195
+ handler_value: Value,
196
+ handler_name: String,
197
+ method: String,
198
+ path: String,
199
+ json_module: Value,
200
+ route: &spikard_http::Route,
201
+ ) -> Result<Self, Error> {
202
+ let upload_file_class = if route.file_params.is_some() {
203
+ lookup_upload_file_class()?
204
+ } else {
205
+ None
206
+ };
207
+ let Ok(ruby) = Ruby::get() else {
208
+ return Err(Error::new(
209
+ magnus::exception::runtime_error(),
210
+ "Ruby VM unavailable while creating handler",
211
+ ));
212
+ };
213
+ let method_value = Opaque::from(ruby.str_new(&method).as_value());
214
+ let path_value = Opaque::from(ruby.str_new(&path).as_value());
215
+
216
+ Ok(Self {
217
+ inner: Arc::new(RubyHandlerInner {
218
+ handler_proc: Opaque::from(handler_value),
219
+ handler_name,
220
+ method,
221
+ path,
222
+ method_value,
223
+ path_value,
224
+ json_module: Opaque::from(json_module),
225
+ response_validator: route.response_validator.clone(),
226
+ upload_file_class,
227
+ }),
228
+ })
229
+ }
230
+
231
+ /// Required by Ruby GC; invoked through the magnus mark hook.
232
+ #[allow(dead_code)]
233
+ pub fn mark(&self, marker: &Marker) {
234
+ if let Ok(ruby) = Ruby::get() {
235
+ let proc_val = self.inner.handler_proc.get_inner_with(&ruby);
236
+ marker.mark(proc_val);
237
+ marker.mark(self.inner.method_value.get_inner_with(&ruby));
238
+ marker.mark(self.inner.path_value.get_inner_with(&ruby));
239
+ }
240
+ }
241
+
242
+ /// Handle a request synchronously.
243
+ pub fn handle(&self, request_data: RequestData) -> HandlerResult {
244
+ let result = std::panic::catch_unwind(AssertUnwindSafe(|| self.handle_inner(request_data)));
245
+ match result {
246
+ Ok(res) => res,
247
+ Err(_) => Err(ErrorResponseBuilder::structured_error(
248
+ StatusCode::INTERNAL_SERVER_ERROR,
249
+ "panic",
250
+ "Unexpected panic while executing Ruby handler",
251
+ )),
252
+ }
253
+ }
254
+
255
+ fn handle_inner(&self, request_data: RequestData) -> HandlerResult {
256
+ let validated_params = request_data.validated_params.clone();
257
+
258
+ let ruby = Ruby::get().map_err(|_| {
259
+ ErrorResponseBuilder::structured_error(
260
+ StatusCode::INTERNAL_SERVER_ERROR,
261
+ "ruby_vm_unavailable",
262
+ "Ruby VM unavailable while invoking handler",
263
+ )
264
+ })?;
265
+
266
+ let request_value = build_ruby_request(&ruby, &self.inner, &request_data, validated_params.as_ref())
267
+ .map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
268
+
269
+ let handler_proc = self.inner.handler_proc.get_inner_with(&ruby);
270
+ let handler_result = handler_proc.funcall("call", (request_value,));
271
+ let response_value = match handler_result {
272
+ Ok(value) => value,
273
+ Err(err) => {
274
+ return Err(ErrorResponseBuilder::structured_error(
275
+ StatusCode::INTERNAL_SERVER_ERROR,
276
+ "handler_failed",
277
+ format!("Handler '{}' failed: {}", self.inner.handler_name, err),
278
+ ));
279
+ }
280
+ };
281
+
282
+ let handler_result = interpret_handler_response(&ruby, &self.inner, response_value).map_err(|err| {
283
+ ErrorResponseBuilder::structured_error(
284
+ StatusCode::INTERNAL_SERVER_ERROR,
285
+ "response_interpret_error",
286
+ format!(
287
+ "Failed to interpret response from '{}': {}",
288
+ self.inner.handler_name, err
289
+ ),
290
+ )
291
+ })?;
292
+
293
+ let payload = match handler_result {
294
+ RubyHandlerResult::Streaming(streaming) => {
295
+ let response = streaming.into_response().map_err(|err| {
296
+ ErrorResponseBuilder::structured_error(
297
+ StatusCode::INTERNAL_SERVER_ERROR,
298
+ "streaming_response_error",
299
+ format!("Failed to build streaming response: {}", err),
300
+ )
301
+ })?;
302
+ return Ok(response.into_response());
303
+ }
304
+ RubyHandlerResult::Payload(payload) => payload,
305
+ };
306
+
307
+ if let Some(validator) = &self.inner.response_validator {
308
+ let candidate_body = match payload.body.clone() {
309
+ Some(body) => Some(body),
310
+ None => match try_parse_raw_body(&payload.raw_body) {
311
+ Ok(parsed) => parsed,
312
+ Err(err) => {
313
+ return Err(ErrorResponseBuilder::structured_error(
314
+ StatusCode::INTERNAL_SERVER_ERROR,
315
+ "response_body_decode_error",
316
+ err,
317
+ ));
318
+ }
319
+ },
320
+ };
321
+
322
+ match candidate_body {
323
+ Some(json_body) => {
324
+ if let Err(errors) = validator.validate(&json_body) {
325
+ let problem = ProblemDetails::from_validation_error(&errors);
326
+ return Err(ErrorResponseBuilder::problem_details_response(&problem));
327
+ }
328
+ }
329
+ None => {
330
+ return Err(ErrorResponseBuilder::structured_error(
331
+ StatusCode::INTERNAL_SERVER_ERROR,
332
+ "response_validation_failed",
333
+ "Response validator requires JSON body but handler returned raw bytes",
334
+ ));
335
+ }
336
+ }
337
+ }
338
+
339
+ let HandlerResponsePayload {
340
+ status,
341
+ headers,
342
+ body,
343
+ raw_body,
344
+ } = payload;
345
+
346
+ let mut response_builder = axum::http::Response::builder().status(status);
347
+ let mut has_content_type = false;
348
+
349
+ for (name, value) in headers.iter() {
350
+ if name.eq_ignore_ascii_case("content-type") {
351
+ has_content_type = true;
352
+ }
353
+ let header_name = HeaderName::from_bytes(name.as_bytes()).map_err(|err| {
354
+ (
355
+ StatusCode::INTERNAL_SERVER_ERROR,
356
+ format!("Invalid header name '{name}': {err}"),
357
+ )
358
+ })?;
359
+ let header_value = HeaderValue::from_str(value).map_err(|err| {
360
+ (
361
+ StatusCode::INTERNAL_SERVER_ERROR,
362
+ format!("Invalid header value for '{name}': {err}"),
363
+ )
364
+ })?;
365
+
366
+ response_builder = response_builder.header(header_name, header_value);
367
+ }
368
+
369
+ if !has_content_type && body.is_some() {
370
+ response_builder = response_builder.header(
371
+ HeaderName::from_static("content-type"),
372
+ HeaderValue::from_static("application/json"),
373
+ );
374
+ }
375
+
376
+ let body_bytes = if let Some(raw) = raw_body {
377
+ raw
378
+ } else if let Some(json_value) = body {
379
+ serde_json::to_vec(&json_value).map_err(|err| {
380
+ (
381
+ StatusCode::INTERNAL_SERVER_ERROR,
382
+ format!("Failed to serialise response body: {err}"),
383
+ )
384
+ })?
385
+ } else {
386
+ Vec::new()
387
+ };
388
+
389
+ response_builder.body(Body::from(body_bytes)).map_err(|err| {
390
+ (
391
+ StatusCode::INTERNAL_SERVER_ERROR,
392
+ format!("Failed to build response: {err}"),
393
+ )
394
+ })
395
+ }
396
+ }
397
+
398
+ impl Handler for RubyHandler {
399
+ fn call(
400
+ &self,
401
+ _req: Request<Body>,
402
+ request_data: RequestData,
403
+ ) -> Pin<Box<dyn std::future::Future<Output = HandlerResult> + Send + '_>> {
404
+ let handler = self.clone();
405
+ Box::pin(async move { handler.handle(request_data) })
406
+ }
407
+ }
408
+
409
+ fn try_parse_raw_body(raw_body: &Option<Vec<u8>>) -> Result<Option<JsonValue>, String> {
410
+ let Some(bytes) = raw_body else {
411
+ return Ok(None);
412
+ };
413
+ let text = String::from_utf8(bytes.clone()).map_err(|e| format!("Invalid UTF-8 in response body: {e}"))?;
414
+ if text.is_empty() {
415
+ return Ok(None);
416
+ }
417
+ serde_json::from_str(&text)
418
+ .map(Some)
419
+ .map_err(|e| format!("Failed to parse response body as JSON: {e}"))
420
+ }
421
+
422
+ fn lookup_upload_file_class() -> Result<Option<Opaque<Value>>, Error> {
423
+ let ruby = match Ruby::get() {
424
+ Ok(ruby) => ruby,
425
+ Err(_) => return Ok(None),
426
+ };
427
+
428
+ let upload_file = ruby.eval::<Value>("Spikard::UploadFile").ok();
429
+ Ok(upload_file.map(Opaque::from))
430
+ }
431
+
432
+ /// Build a Ruby Hash request object from request data.
433
+ fn build_ruby_request(
434
+ ruby: &Ruby,
435
+ handler: &RubyHandlerInner,
436
+ request_data: &RequestData,
437
+ validated_params: Option<&JsonValue>,
438
+ ) -> Result<Value, Error> {
439
+ let hash = ruby.hash_new_capa(9);
440
+
441
+ hash.aset(*KEY_METHOD, handler.method_value.get_inner_with(ruby))?;
442
+ hash.aset(*KEY_PATH, handler.path_value.get_inner_with(ruby))?;
443
+
444
+ let path_params = map_to_ruby_hash(ruby, request_data.path_params.as_ref())?;
445
+ hash.aset(*KEY_PATH_PARAMS, path_params)?;
446
+
447
+ let query_value = json_to_ruby(ruby, &request_data.query_params)?;
448
+ hash.aset(*KEY_QUERY, query_value)?;
449
+
450
+ let raw_query = multimap_to_ruby_hash(ruby, request_data.raw_query_params.as_ref())?;
451
+ hash.aset(*KEY_RAW_QUERY, raw_query)?;
452
+
453
+ let headers = map_to_ruby_hash(ruby, request_data.headers.as_ref())?;
454
+ hash.aset(*KEY_HEADERS, headers)?;
455
+
456
+ let cookies = map_to_ruby_hash(ruby, request_data.cookies.as_ref())?;
457
+ hash.aset(*KEY_COOKIES, cookies)?;
458
+
459
+ let upload_class_value = handler.upload_file_class.as_ref().map(|cls| cls.get_inner_with(ruby));
460
+ let body_value = json_to_ruby_with_uploads(ruby, &request_data.body, upload_class_value.as_ref())?;
461
+ hash.aset(*KEY_BODY, body_value)?;
462
+ if let Some(raw) = &request_data.raw_body {
463
+ let raw_str = ruby.str_from_slice(raw);
464
+ hash.aset(*KEY_RAW_BODY, raw_str)?;
465
+ } else {
466
+ hash.aset(*KEY_RAW_BODY, ruby.qnil())?;
467
+ }
468
+
469
+ let params_value = if let Some(validated) = validated_params {
470
+ json_to_ruby(ruby, validated)?
471
+ } else {
472
+ build_default_params_from_converted(ruby, path_params, query_value, headers, cookies)?
473
+ };
474
+ hash.aset(*KEY_PARAMS, params_value)?;
475
+
476
+ Ok(hash.as_value())
477
+ }
478
+
479
+ /// Build default params from already converted Ruby values, avoiding double conversion.
480
+ fn build_default_params_from_converted(
481
+ ruby: &Ruby,
482
+ path_params: Value,
483
+ query: Value,
484
+ headers: Value,
485
+ cookies: Value,
486
+ ) -> Result<Value, Error> {
487
+ let params = ruby.hash_new();
488
+
489
+ if let Some(hash) = RHash::from_value(path_params) {
490
+ let _: Value = params.funcall("merge!", (hash,))?;
491
+ }
492
+ if let Some(hash) = RHash::from_value(query) {
493
+ let _: Value = params.funcall("merge!", (hash,))?;
494
+ }
495
+ if let Some(hash) = RHash::from_value(headers) {
496
+ let _: Value = params.funcall("merge!", (hash,))?;
497
+ }
498
+ if let Some(hash) = RHash::from_value(cookies) {
499
+ let _: Value = params.funcall("merge!", (hash,))?;
500
+ }
501
+
502
+ Ok(params.as_value())
503
+ }
504
+
505
+ /// Interpret a Ruby handler response into our response types.
506
+ fn interpret_handler_response(
507
+ ruby: &Ruby,
508
+ handler: &RubyHandlerInner,
509
+ value: Value,
510
+ ) -> Result<RubyHandlerResult, Error> {
511
+ if value.is_nil() {
512
+ return Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
513
+ status: 200,
514
+ headers: HashMap::new(),
515
+ body: None,
516
+ raw_body: None,
517
+ }));
518
+ }
519
+
520
+ if is_streaming_response(ruby, value)? {
521
+ let stream_value: Value = value.funcall("stream", ())?;
522
+ let status: i64 = value.funcall("status_code", ())?;
523
+ let headers_value: Value = value.funcall("headers", ())?;
524
+
525
+ let status_u16 = u16::try_from(status).map_err(|_| {
526
+ Error::new(
527
+ ruby.exception_arg_error(),
528
+ "StreamingResponse status_code must be between 0 and 65535",
529
+ )
530
+ })?;
531
+
532
+ let headers = value_to_string_map(ruby, headers_value)?;
533
+
534
+ return Ok(RubyHandlerResult::Streaming(StreamingResponsePayload {
535
+ enumerator: Arc::new(Opaque::from(stream_value)),
536
+ status: status_u16,
537
+ headers,
538
+ }));
539
+ }
540
+
541
+ let status_symbol = ruby.intern("status_code");
542
+ if value.respond_to(status_symbol, false)? {
543
+ let status: i64 = value.funcall("status_code", ())?;
544
+ let status_u16 = u16::try_from(status)
545
+ .map_err(|_| Error::new(ruby.exception_arg_error(), "status_code must be between 0 and 65535"))?;
546
+
547
+ let headers_value: Value = value.funcall("headers", ())?;
548
+ let headers = if headers_value.is_nil() {
549
+ HashMap::new()
550
+ } else {
551
+ let hash = RHash::try_convert(headers_value)?;
552
+ hash.to_hash_map::<String, String>()?
553
+ };
554
+
555
+ let content_value: Value = value.funcall("content", ())?;
556
+ let mut raw_body = None;
557
+ let body = if content_value.is_nil() {
558
+ None
559
+ } else if let Ok(str_value) = RString::try_convert(content_value) {
560
+ let slice = unsafe { str_value.as_slice() };
561
+ raw_body = Some(slice.to_vec());
562
+ None
563
+ } else {
564
+ Some(ruby_value_to_json(
565
+ ruby,
566
+ handler.json_module.get_inner_with(ruby),
567
+ content_value,
568
+ )?)
569
+ };
570
+
571
+ return Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
572
+ status: status_u16,
573
+ headers,
574
+ body,
575
+ raw_body,
576
+ }));
577
+ }
578
+
579
+ if let Ok(str_value) = RString::try_convert(value) {
580
+ let slice = unsafe { str_value.as_slice() };
581
+ return Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
582
+ status: 200,
583
+ headers: HashMap::new(),
584
+ body: None,
585
+ raw_body: Some(slice.to_vec()),
586
+ }));
587
+ }
588
+
589
+ let body_json = ruby_value_to_json(ruby, handler.json_module.get_inner_with(ruby), value)?;
590
+
591
+ Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
592
+ status: 200,
593
+ headers: HashMap::new(),
594
+ body: Some(body_json),
595
+ raw_body: None,
596
+ }))
597
+ }
598
+
599
+ /// Convert a Ruby value to a string HashMap.
600
+ fn value_to_string_map(ruby: &Ruby, value: Value) -> Result<HashMap<String, String>, Error> {
601
+ if value.is_nil() {
602
+ return Ok(HashMap::new());
603
+ }
604
+ let hash = RHash::try_convert(value)?;
605
+ hash.to_hash_map::<String, String>().map_err(|err| {
606
+ Error::new(
607
+ ruby.exception_arg_error(),
608
+ format!("Expected headers hash of strings: {}", err),
609
+ )
610
+ })
611
+ }
612
+
613
+ /// Check if a Ruby value is a streaming response.
614
+ fn is_streaming_response(ruby: &Ruby, value: Value) -> Result<bool, Error> {
615
+ let stream_sym = ruby.intern("stream");
616
+ let status_sym = ruby.intern("status_code");
617
+ Ok(value.respond_to(stream_sym, false)? && value.respond_to(status_sym, false)?)
618
+ }