spikard 0.3.6 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +21 -6
  3. data/ext/spikard_rb/Cargo.toml +2 -2
  4. data/lib/spikard/app.rb +33 -14
  5. data/lib/spikard/testing.rb +47 -12
  6. data/lib/spikard/version.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +63 -0
  8. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +132 -0
  9. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +752 -0
  10. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +194 -0
  11. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +246 -0
  12. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +401 -0
  13. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +238 -0
  14. data/vendor/crates/spikard-bindings-shared/src/lib.rs +24 -0
  15. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +292 -0
  16. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +616 -0
  17. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +305 -0
  18. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +248 -0
  19. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +351 -0
  20. data/vendor/crates/spikard-bindings-shared/tests/comprehensive_coverage.rs +454 -0
  21. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +383 -0
  22. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +280 -0
  23. data/vendor/crates/spikard-core/Cargo.toml +4 -4
  24. data/vendor/crates/spikard-core/src/debug.rs +64 -0
  25. data/vendor/crates/spikard-core/src/di/container.rs +3 -27
  26. data/vendor/crates/spikard-core/src/di/factory.rs +1 -5
  27. data/vendor/crates/spikard-core/src/di/graph.rs +8 -47
  28. data/vendor/crates/spikard-core/src/di/mod.rs +1 -1
  29. data/vendor/crates/spikard-core/src/di/resolved.rs +1 -7
  30. data/vendor/crates/spikard-core/src/di/value.rs +2 -4
  31. data/vendor/crates/spikard-core/src/errors.rs +30 -0
  32. data/vendor/crates/spikard-core/src/http.rs +262 -0
  33. data/vendor/crates/spikard-core/src/lib.rs +1 -1
  34. data/vendor/crates/spikard-core/src/lifecycle.rs +764 -0
  35. data/vendor/crates/spikard-core/src/metadata.rs +389 -0
  36. data/vendor/crates/spikard-core/src/parameters.rs +1962 -159
  37. data/vendor/crates/spikard-core/src/problem.rs +34 -0
  38. data/vendor/crates/spikard-core/src/request_data.rs +966 -1
  39. data/vendor/crates/spikard-core/src/router.rs +263 -2
  40. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +688 -0
  41. data/vendor/crates/spikard-core/src/{validation.rs → validation/mod.rs} +26 -268
  42. data/vendor/crates/spikard-http/Cargo.toml +12 -16
  43. data/vendor/crates/spikard-http/examples/sse-notifications.rs +148 -0
  44. data/vendor/crates/spikard-http/examples/websocket-chat.rs +92 -0
  45. data/vendor/crates/spikard-http/src/auth.rs +65 -16
  46. data/vendor/crates/spikard-http/src/background.rs +1614 -3
  47. data/vendor/crates/spikard-http/src/cors.rs +515 -0
  48. data/vendor/crates/spikard-http/src/debug.rs +65 -0
  49. data/vendor/crates/spikard-http/src/di_handler.rs +1322 -77
  50. data/vendor/crates/spikard-http/src/handler_response.rs +711 -0
  51. data/vendor/crates/spikard-http/src/handler_trait.rs +607 -5
  52. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +6 -0
  53. data/vendor/crates/spikard-http/src/lib.rs +33 -28
  54. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +81 -0
  55. data/vendor/crates/spikard-http/src/lifecycle.rs +765 -0
  56. data/vendor/crates/spikard-http/src/middleware/mod.rs +372 -117
  57. data/vendor/crates/spikard-http/src/middleware/multipart.rs +836 -10
  58. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +409 -43
  59. data/vendor/crates/spikard-http/src/middleware/validation.rs +513 -65
  60. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +345 -0
  61. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +1055 -0
  62. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +473 -3
  63. data/vendor/crates/spikard-http/src/query_parser.rs +455 -31
  64. data/vendor/crates/spikard-http/src/response.rs +321 -0
  65. data/vendor/crates/spikard-http/src/server/handler.rs +1572 -9
  66. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +136 -0
  67. data/vendor/crates/spikard-http/src/server/mod.rs +875 -178
  68. data/vendor/crates/spikard-http/src/server/request_extraction.rs +674 -23
  69. data/vendor/crates/spikard-http/src/server/routing_factory.rs +599 -0
  70. data/vendor/crates/spikard-http/src/sse.rs +983 -21
  71. data/vendor/crates/spikard-http/src/testing/form.rs +38 -0
  72. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -2
  73. data/vendor/crates/spikard-http/src/testing.rs +7 -7
  74. data/vendor/crates/spikard-http/src/websocket.rs +1055 -4
  75. data/vendor/crates/spikard-http/tests/background_behavior.rs +832 -0
  76. data/vendor/crates/spikard-http/tests/common/handlers.rs +309 -0
  77. data/vendor/crates/spikard-http/tests/common/mod.rs +26 -0
  78. data/vendor/crates/spikard-http/tests/di_integration.rs +192 -0
  79. data/vendor/crates/spikard-http/tests/doc_snippets.rs +5 -0
  80. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +1093 -0
  81. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +656 -0
  82. data/vendor/crates/spikard-http/tests/server_config_builder.rs +314 -0
  83. data/vendor/crates/spikard-http/tests/sse_behavior.rs +620 -0
  84. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +663 -0
  85. data/vendor/crates/spikard-rb/Cargo.toml +10 -4
  86. data/vendor/crates/spikard-rb/build.rs +196 -5
  87. data/vendor/crates/spikard-rb/src/config/mod.rs +5 -0
  88. data/vendor/crates/spikard-rb/src/{config.rs → config/server_config.rs} +100 -109
  89. data/vendor/crates/spikard-rb/src/conversion.rs +121 -20
  90. data/vendor/crates/spikard-rb/src/di/builder.rs +100 -0
  91. data/vendor/crates/spikard-rb/src/{di.rs → di/mod.rs} +12 -46
  92. data/vendor/crates/spikard-rb/src/handler.rs +100 -107
  93. data/vendor/crates/spikard-rb/src/integration/mod.rs +3 -0
  94. data/vendor/crates/spikard-rb/src/lib.rs +467 -1428
  95. data/vendor/crates/spikard-rb/src/lifecycle.rs +1 -0
  96. data/vendor/crates/spikard-rb/src/metadata/mod.rs +5 -0
  97. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +447 -0
  98. data/vendor/crates/spikard-rb/src/runtime/mod.rs +5 -0
  99. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +324 -0
  100. data/vendor/crates/spikard-rb/src/server.rs +47 -22
  101. data/vendor/crates/spikard-rb/src/{test_client.rs → testing/client.rs} +187 -40
  102. data/vendor/crates/spikard-rb/src/testing/mod.rs +7 -0
  103. data/vendor/crates/spikard-rb/src/testing/websocket.rs +635 -0
  104. data/vendor/crates/spikard-rb/src/websocket.rs +178 -37
  105. metadata +46 -13
  106. data/vendor/crates/spikard-http/src/parameters.rs +0 -1
  107. data/vendor/crates/spikard-http/src/problem.rs +0 -1
  108. data/vendor/crates/spikard-http/src/router.rs +0 -1
  109. data/vendor/crates/spikard-http/src/schema_registry.rs +0 -1
  110. data/vendor/crates/spikard-http/src/type_hints.rs +0 -1
  111. data/vendor/crates/spikard-http/src/validation.rs +0 -1
  112. data/vendor/crates/spikard-rb/src/test_websocket.rs +0 -221
  113. /data/vendor/crates/spikard-rb/src/{test_sse.rs → testing/sse.rs} +0 -0
@@ -4,17 +4,18 @@
4
4
  //! and implements Spikard's `Handler` trait for async request processing.
5
5
 
6
6
  #![allow(dead_code)]
7
+ #![deny(clippy::unwrap_used)]
7
8
 
8
9
  use axum::body::Body;
9
10
  use axum::http::{HeaderName, HeaderValue, Request, StatusCode};
10
11
  use magnus::prelude::*;
12
+ use magnus::value::LazyId;
11
13
  use magnus::value::{InnerValue, Opaque};
12
14
  use magnus::{Error, RHash, RString, Ruby, TryConvert, Value, gc::Marker};
13
- use serde_json::{Map as JsonMap, Value as JsonValue};
14
- use spikard_core::errors::StructuredError;
15
- use spikard_http::ParameterValidator;
15
+ use serde_json::Value as JsonValue;
16
+ use spikard_bindings_shared::ErrorResponseBuilder;
17
+ use spikard_core::problem::ProblemDetails;
16
18
  use spikard_http::SchemaValidator;
17
- use spikard_http::problem::ProblemDetails;
18
19
  use spikard_http::{Handler, HandlerResponse, HandlerResult, RequestData};
19
20
  use std::collections::HashMap;
20
21
  use std::panic::AssertUnwindSafe;
@@ -25,6 +26,17 @@ use crate::conversion::{
25
26
  json_to_ruby, json_to_ruby_with_uploads, map_to_ruby_hash, multimap_to_ruby_hash, ruby_value_to_json,
26
27
  };
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
+
28
40
  /// Response payload with status, headers, and body data.
29
41
  pub struct HandlerResponsePayload {
30
42
  pub status: u16,
@@ -51,8 +63,8 @@ impl StreamingResponsePayload {
51
63
  pub fn into_response(self) -> Result<HandlerResponse, Error> {
52
64
  let ruby = Ruby::get().map_err(|_| {
53
65
  Error::new(
54
- Ruby::get().unwrap().exception_runtime_error(),
55
- "Ruby VM unavailable while building streaming response",
66
+ magnus::exception::runtime_error(),
67
+ "Ruby VM became unavailable during streaming response construction",
56
68
  )
57
69
  })?;
58
70
 
@@ -127,10 +139,10 @@ pub struct RubyHandlerInner {
127
139
  pub handler_name: String,
128
140
  pub method: String,
129
141
  pub path: String,
142
+ method_value: Opaque<Value>,
143
+ path_value: Opaque<Value>,
130
144
  pub json_module: Opaque<Value>,
131
- pub request_validator: Option<Arc<SchemaValidator>>,
132
145
  pub response_validator: Option<Arc<SchemaValidator>>,
133
- pub parameter_validator: Option<ParameterValidator>,
134
146
  pub upload_file_class: Option<Opaque<Value>>,
135
147
  }
136
148
 
@@ -143,17 +155,33 @@ pub struct RubyHandler {
143
155
  impl RubyHandler {
144
156
  /// Create a new RubyHandler from a route and handler Proc.
145
157
  pub fn new(route: &spikard_http::Route, handler_value: Value, json_module: Value) -> Result<Self, Error> {
146
- let upload_file_class = lookup_upload_file_class()?;
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
+
147
175
  Ok(Self {
148
176
  inner: Arc::new(RubyHandlerInner {
149
177
  handler_proc: Opaque::from(handler_value),
150
178
  handler_name: route.handler_name.clone(),
151
- method: route.method.as_str().to_string(),
152
- path: route.path.clone(),
179
+ method,
180
+ path,
181
+ method_value,
182
+ path_value,
153
183
  json_module: Opaque::from(json_module),
154
- request_validator: route.request_validator.clone(),
155
184
  response_validator: route.response_validator.clone(),
156
- parameter_validator: route.parameter_validator.clone(),
157
185
  upload_file_class,
158
186
  }),
159
187
  })
@@ -171,17 +199,30 @@ impl RubyHandler {
171
199
  json_module: Value,
172
200
  route: &spikard_http::Route,
173
201
  ) -> Result<Self, Error> {
174
- let upload_file_class = lookup_upload_file_class()?;
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
+
175
216
  Ok(Self {
176
217
  inner: Arc::new(RubyHandlerInner {
177
218
  handler_proc: Opaque::from(handler_value),
178
219
  handler_name,
179
220
  method,
180
221
  path,
222
+ method_value,
223
+ path_value,
181
224
  json_module: Opaque::from(json_module),
182
- request_validator: route.request_validator.clone(),
183
225
  response_validator: route.response_validator.clone(),
184
- parameter_validator: route.parameter_validator.clone(),
185
226
  upload_file_class,
186
227
  }),
187
228
  })
@@ -193,16 +234,17 @@ impl RubyHandler {
193
234
  if let Ok(ruby) = Ruby::get() {
194
235
  let proc_val = self.inner.handler_proc.get_inner_with(&ruby);
195
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));
196
239
  }
197
240
  }
198
241
 
199
242
  /// Handle a request synchronously.
200
243
  pub fn handle(&self, request_data: RequestData) -> HandlerResult {
201
- let cloned = request_data.clone();
202
- let result = std::panic::catch_unwind(AssertUnwindSafe(|| self.handle_inner(cloned)));
244
+ let result = std::panic::catch_unwind(AssertUnwindSafe(|| self.handle_inner(request_data)));
203
245
  match result {
204
246
  Ok(res) => res,
205
- Err(_) => Err(structured_error(
247
+ Err(_) => Err(ErrorResponseBuilder::structured_error(
206
248
  StatusCode::INTERNAL_SERVER_ERROR,
207
249
  "panic",
208
250
  "Unexpected panic while executing Ruby handler",
@@ -211,33 +253,10 @@ impl RubyHandler {
211
253
  }
212
254
 
213
255
  fn handle_inner(&self, request_data: RequestData) -> HandlerResult {
214
- if let Some(validator) = &self.inner.request_validator
215
- && let Err(errors) = validator.validate(&request_data.body)
216
- {
217
- let problem = ProblemDetails::from_validation_error(&errors);
218
- return Err(validation_error_response(&problem));
219
- }
220
-
221
- let validated_params = if let Some(validator) = &self.inner.parameter_validator {
222
- match validator.validate_and_extract(
223
- &request_data.query_params,
224
- request_data.raw_query_params.as_ref(),
225
- request_data.path_params.as_ref(),
226
- request_data.headers.as_ref(),
227
- request_data.cookies.as_ref(),
228
- ) {
229
- Ok(value) => Some(value),
230
- Err(errors) => {
231
- let problem = ProblemDetails::from_validation_error(&errors);
232
- return Err(validation_error_response(&problem));
233
- }
234
- }
235
- } else {
236
- None
237
- };
256
+ let validated_params = request_data.validated_params.clone();
238
257
 
239
258
  let ruby = Ruby::get().map_err(|_| {
240
- structured_error(
259
+ ErrorResponseBuilder::structured_error(
241
260
  StatusCode::INTERNAL_SERVER_ERROR,
242
261
  "ruby_vm_unavailable",
243
262
  "Ruby VM unavailable while invoking handler",
@@ -252,7 +271,7 @@ impl RubyHandler {
252
271
  let response_value = match handler_result {
253
272
  Ok(value) => value,
254
273
  Err(err) => {
255
- return Err(structured_error(
274
+ return Err(ErrorResponseBuilder::structured_error(
256
275
  StatusCode::INTERNAL_SERVER_ERROR,
257
276
  "handler_failed",
258
277
  format!("Handler '{}' failed: {}", self.inner.handler_name, err),
@@ -261,7 +280,7 @@ impl RubyHandler {
261
280
  };
262
281
 
263
282
  let handler_result = interpret_handler_response(&ruby, &self.inner, response_value).map_err(|err| {
264
- structured_error(
283
+ ErrorResponseBuilder::structured_error(
265
284
  StatusCode::INTERNAL_SERVER_ERROR,
266
285
  "response_interpret_error",
267
286
  format!(
@@ -274,7 +293,7 @@ impl RubyHandler {
274
293
  let payload = match handler_result {
275
294
  RubyHandlerResult::Streaming(streaming) => {
276
295
  let response = streaming.into_response().map_err(|err| {
277
- structured_error(
296
+ ErrorResponseBuilder::structured_error(
278
297
  StatusCode::INTERNAL_SERVER_ERROR,
279
298
  "streaming_response_error",
280
299
  format!("Failed to build streaming response: {}", err),
@@ -291,7 +310,7 @@ impl RubyHandler {
291
310
  None => match try_parse_raw_body(&payload.raw_body) {
292
311
  Ok(parsed) => parsed,
293
312
  Err(err) => {
294
- return Err(structured_error(
313
+ return Err(ErrorResponseBuilder::structured_error(
295
314
  StatusCode::INTERNAL_SERVER_ERROR,
296
315
  "response_body_decode_error",
297
316
  err,
@@ -304,11 +323,11 @@ impl RubyHandler {
304
323
  Some(json_body) => {
305
324
  if let Err(errors) = validator.validate(&json_body) {
306
325
  let problem = ProblemDetails::from_validation_error(&errors);
307
- return Err(validation_error_response(&problem));
326
+ return Err(ErrorResponseBuilder::problem_details_response(&problem));
308
327
  }
309
328
  }
310
329
  None => {
311
- return Err(structured_error(
330
+ return Err(ErrorResponseBuilder::structured_error(
312
331
  StatusCode::INTERNAL_SERVER_ERROR,
313
332
  "response_validation_failed",
314
333
  "Response validator requires JSON body but handler returned raw bytes",
@@ -387,24 +406,6 @@ impl Handler for RubyHandler {
387
406
  }
388
407
  }
389
408
 
390
- fn structured_error(status: StatusCode, code: &str, message: impl Into<String>) -> (StatusCode, String) {
391
- let payload = StructuredError::simple(code.to_string(), message.into());
392
- let body = serde_json::to_string(&payload)
393
- .unwrap_or_else(|_| r#"{"error":"internal_error","code":"internal_error","details":{}}"#.to_string());
394
- (status, body)
395
- }
396
-
397
- fn validation_error_response(problem: &ProblemDetails) -> (StatusCode, String) {
398
- let payload = StructuredError::new(
399
- "validation_error".to_string(),
400
- problem.title.clone(),
401
- serde_json::to_value(problem).unwrap_or_else(|_| serde_json::json!({})),
402
- );
403
- let body = serde_json::to_string(&payload)
404
- .unwrap_or_else(|_| r#"{"error":"validation_error","code":"validation_error","details":{}}"#.to_string());
405
- (problem.status_code(), body)
406
- }
407
-
408
409
  fn try_parse_raw_body(raw_body: &Option<Vec<u8>>) -> Result<Option<JsonValue>, String> {
409
410
  let Some(bytes) = raw_body else {
410
411
  return Ok(None);
@@ -435,69 +436,70 @@ fn build_ruby_request(
435
436
  request_data: &RequestData,
436
437
  validated_params: Option<&JsonValue>,
437
438
  ) -> Result<Value, Error> {
438
- let hash = ruby.hash_new();
439
+ let hash = ruby.hash_new_capa(9);
439
440
 
440
- hash.aset(ruby.intern("method"), ruby.str_new(&handler.method))?;
441
- hash.aset(ruby.intern("path"), ruby.str_new(&handler.path))?;
441
+ hash.aset(*KEY_METHOD, handler.method_value.get_inner_with(ruby))?;
442
+ hash.aset(*KEY_PATH, handler.path_value.get_inner_with(ruby))?;
442
443
 
443
444
  let path_params = map_to_ruby_hash(ruby, request_data.path_params.as_ref())?;
444
- hash.aset(ruby.intern("path_params"), path_params)?;
445
+ hash.aset(*KEY_PATH_PARAMS, path_params)?;
445
446
 
446
447
  let query_value = json_to_ruby(ruby, &request_data.query_params)?;
447
- hash.aset(ruby.intern("query"), query_value)?;
448
+ hash.aset(*KEY_QUERY, query_value)?;
448
449
 
449
450
  let raw_query = multimap_to_ruby_hash(ruby, request_data.raw_query_params.as_ref())?;
450
- hash.aset(ruby.intern("raw_query"), raw_query)?;
451
+ hash.aset(*KEY_RAW_QUERY, raw_query)?;
451
452
 
452
453
  let headers = map_to_ruby_hash(ruby, request_data.headers.as_ref())?;
453
- hash.aset(ruby.intern("headers"), headers)?;
454
+ hash.aset(*KEY_HEADERS, headers)?;
454
455
 
455
456
  let cookies = map_to_ruby_hash(ruby, request_data.cookies.as_ref())?;
456
- hash.aset(ruby.intern("cookies"), cookies)?;
457
+ hash.aset(*KEY_COOKIES, cookies)?;
457
458
 
458
459
  let upload_class_value = handler.upload_file_class.as_ref().map(|cls| cls.get_inner_with(ruby));
459
460
  let body_value = json_to_ruby_with_uploads(ruby, &request_data.body, upload_class_value.as_ref())?;
460
- hash.aset(ruby.intern("body"), body_value)?;
461
+ hash.aset(*KEY_BODY, body_value)?;
461
462
  if let Some(raw) = &request_data.raw_body {
462
463
  let raw_str = ruby.str_from_slice(raw);
463
- hash.aset(ruby.intern("raw_body"), raw_str)?;
464
+ hash.aset(*KEY_RAW_BODY, raw_str)?;
464
465
  } else {
465
- hash.aset(ruby.intern("raw_body"), ruby.qnil())?;
466
+ hash.aset(*KEY_RAW_BODY, ruby.qnil())?;
466
467
  }
467
468
 
468
469
  let params_value = if let Some(validated) = validated_params {
469
470
  json_to_ruby(ruby, validated)?
470
471
  } else {
471
- build_default_params(ruby, request_data)?
472
+ build_default_params_from_converted(ruby, path_params, query_value, headers, cookies)?
472
473
  };
473
- hash.aset(ruby.intern("params"), params_value)?;
474
+ hash.aset(*KEY_PARAMS, params_value)?;
474
475
 
475
476
  Ok(hash.as_value())
476
477
  }
477
478
 
478
- /// Build default params from request data path/query/headers/cookies.
479
- fn build_default_params(ruby: &Ruby, request_data: &RequestData) -> Result<Value, Error> {
480
- let mut map = JsonMap::new();
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();
481
488
 
482
- for (key, value) in request_data.path_params.as_ref() {
483
- map.insert(key.clone(), JsonValue::String(value.clone()));
489
+ if let Some(hash) = RHash::from_value(path_params) {
490
+ let _: Value = params.funcall("merge!", (hash,))?;
484
491
  }
485
-
486
- if let JsonValue::Object(obj) = &request_data.query_params {
487
- for (key, value) in obj {
488
- map.insert(key.clone(), value.clone());
489
- }
492
+ if let Some(hash) = RHash::from_value(query) {
493
+ let _: Value = params.funcall("merge!", (hash,))?;
490
494
  }
491
-
492
- for (key, value) in request_data.headers.as_ref() {
493
- map.insert(key.clone(), JsonValue::String(value.clone()));
495
+ if let Some(hash) = RHash::from_value(headers) {
496
+ let _: Value = params.funcall("merge!", (hash,))?;
494
497
  }
495
-
496
- for (key, value) in request_data.cookies.as_ref() {
497
- map.insert(key.clone(), JsonValue::String(value.clone()));
498
+ if let Some(hash) = RHash::from_value(cookies) {
499
+ let _: Value = params.funcall("merge!", (hash,))?;
498
500
  }
499
501
 
500
- json_to_ruby(ruby, &JsonValue::Object(map))
502
+ Ok(params.as_value())
501
503
  }
502
504
 
503
505
  /// Interpret a Ruby handler response into our response types.
@@ -555,11 +557,6 @@ fn interpret_handler_response(
555
557
  let body = if content_value.is_nil() {
556
558
  None
557
559
  } else if let Ok(str_value) = RString::try_convert(content_value) {
558
- // SAFETY: Magnus RString::as_slice() yields a valid byte slice for the
559
- // lifetime of the RString value. We immediately call .to_vec() which copies
560
- // the bytes into owned memory, so the result does not retain any references
561
- // to the underlying RString. This is safe because the copy happens before
562
- // the RString reference is released.
563
560
  let slice = unsafe { str_value.as_slice() };
564
561
  raw_body = Some(slice.to_vec());
565
562
  None
@@ -580,10 +577,6 @@ fn interpret_handler_response(
580
577
  }
581
578
 
582
579
  if let Ok(str_value) = RString::try_convert(value) {
583
- // SAFETY: Magnus RString::as_slice() returns a valid byte slice that remains
584
- // valid for the lifetime of the RString. We call .to_vec() to copy the bytes
585
- // into owned storage, ensuring the returned HandlerResponsePayload does not
586
- // hold any references back to the RString. This copy-then-own pattern is safe.
587
580
  let slice = unsafe { str_value.as_slice() };
588
581
  return Ok(RubyHandlerResult::Payload(HandlerResponsePayload {
589
582
  status: 200,
@@ -0,0 +1,3 @@
1
+ //! Integration-level utilities for testing and dependency injection.
2
+
3
+ pub use crate::di::build_dependency_container;