spikard 0.8.3 → 0.10.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +19 -10
- data/ext/spikard_rb/Cargo.lock +234 -162
- data/ext/spikard_rb/Cargo.toml +2 -2
- data/ext/spikard_rb/extconf.rb +4 -3
- data/lib/spikard/config.rb +88 -12
- data/lib/spikard/testing.rb +3 -1
- data/lib/spikard/version.rb +1 -1
- data/lib/spikard.rb +11 -0
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +3 -6
- data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
- data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
- data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
- data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
- data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
- data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
- data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
- data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
- data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
- data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
- data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
- data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
- data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
- data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
- data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
- data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
- data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
- data/vendor/crates/spikard-core/Cargo.toml +3 -3
- data/vendor/crates/spikard-core/src/di/container.rs +1 -1
- data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
- data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
- data/vendor/crates/spikard-core/src/di/value.rs +1 -1
- data/vendor/crates/spikard-core/src/http.rs +75 -0
- data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
- data/vendor/crates/spikard-core/src/parameters.rs +14 -19
- data/vendor/crates/spikard-core/src/problem.rs +1 -1
- data/vendor/crates/spikard-core/src/request_data.rs +7 -16
- data/vendor/crates/spikard-core/src/router.rs +6 -0
- data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
- data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
- data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
- data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
- data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
- data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
- data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
- data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
- data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
- data/vendor/crates/spikard-http/Cargo.toml +4 -2
- data/vendor/crates/spikard-http/src/cors.rs +32 -11
- data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
- data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
- data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
- data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
- data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
- data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
- data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
- data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
- data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
- data/vendor/crates/spikard-http/src/lib.rs +1 -1
- data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
- data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
- data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
- data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
- data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
- data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
- data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
- data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
- data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
- data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
- data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
- data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
- data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
- data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
- data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
- data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
- data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
- data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
- data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
- data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
- data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
- data/vendor/crates/spikard-rb/Cargo.toml +3 -1
- data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
- data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
- data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
- data/vendor/crates/spikard-rb/src/handler.rs +169 -91
- data/vendor/crates/spikard-rb/src/lib.rs +444 -62
- data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
- data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
- data/vendor/crates/spikard-rb/src/request.rs +117 -20
- data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
- data/vendor/crates/spikard-rb/src/server.rs +23 -14
- data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
- data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
- data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
- data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
- data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
- metadata +14 -4
- data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
- data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
//! Ruby gRPC bindings for Spikard
|
|
2
2
|
//!
|
|
3
|
-
//! This module provides
|
|
4
|
-
//!
|
|
3
|
+
//! This module provides Ruby gRPC handler integration with full streaming support:
|
|
4
|
+
//! - Unary RPCs (single request, single response)
|
|
5
|
+
//! - Server streaming RPCs (single request, stream of responses)
|
|
6
|
+
//! - Client streaming RPCs (stream of requests, single response)
|
|
7
|
+
//! - Bidirectional streaming RPCs (stream of requests, stream of responses)
|
|
5
8
|
|
|
6
9
|
pub mod handler;
|
|
7
10
|
|
|
11
|
+
// Re-export types for when the API is integrated
|
|
8
12
|
#[allow(unused_imports)]
|
|
9
13
|
pub use handler::{RubyGrpcHandler, RubyGrpcRequest, RubyGrpcResponse};
|
|
@@ -37,7 +37,7 @@ where
|
|
|
37
37
|
};
|
|
38
38
|
|
|
39
39
|
unsafe {
|
|
40
|
-
rb_sys::rb_thread_call_with_gvl(Some(trampoline::<F, R>), &mut data as *mut
|
|
40
|
+
rb_sys::rb_thread_call_with_gvl(Some(trampoline::<F, R>), &raw mut data as *mut c_void);
|
|
41
41
|
data.result.assume_init()
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -50,7 +50,7 @@ macro_rules! call_without_gvl {
|
|
|
50
50
|
// Box the arguments to ensure they live on the heap for the entire duration of the FFI call
|
|
51
51
|
let data = std::boxed::Box::new((
|
|
52
52
|
($($arg,)+),
|
|
53
|
-
&mut result as *mut std::mem::MaybeUninit<$return_ty>,
|
|
53
|
+
&raw mut result as *mut std::mem::MaybeUninit<$return_ty>,
|
|
54
54
|
));
|
|
55
55
|
let data_ptr = std::boxed::Box::into_raw(data) as *mut std::ffi::c_void;
|
|
56
56
|
|
|
@@ -22,21 +22,13 @@ use std::panic::AssertUnwindSafe;
|
|
|
22
22
|
use std::pin::Pin;
|
|
23
23
|
use std::sync::Arc;
|
|
24
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
|
-
};
|
|
25
|
+
use crate::conversion::ruby_value_to_json;
|
|
28
26
|
use crate::gvl::with_gvl;
|
|
27
|
+
use crate::request::NativeRequest;
|
|
29
28
|
|
|
30
|
-
static KEY_METHOD: LazyId = LazyId::new("method");
|
|
31
|
-
static KEY_PATH: LazyId = LazyId::new("path");
|
|
32
29
|
static KEY_PATH_PARAMS: LazyId = LazyId::new("path_params");
|
|
33
30
|
static KEY_QUERY: LazyId = LazyId::new("query");
|
|
34
|
-
static KEY_RAW_QUERY: LazyId = LazyId::new("raw_query");
|
|
35
|
-
static KEY_HEADERS: LazyId = LazyId::new("headers");
|
|
36
|
-
static KEY_COOKIES: LazyId = LazyId::new("cookies");
|
|
37
31
|
static KEY_BODY: LazyId = LazyId::new("body");
|
|
38
|
-
static KEY_RAW_BODY: LazyId = LazyId::new("raw_body");
|
|
39
|
-
static KEY_PARAMS: LazyId = LazyId::new("params");
|
|
40
32
|
|
|
41
33
|
/// Response payload with status, headers, and body data.
|
|
42
34
|
pub struct HandlerResponsePayload {
|
|
@@ -170,6 +162,7 @@ impl RubyHandler {
|
|
|
170
162
|
"Ruby VM unavailable while creating handler",
|
|
171
163
|
));
|
|
172
164
|
};
|
|
165
|
+
let handler_value = crate::conversion::ensure_callable(&ruby, handler_value, &route.handler_name)?;
|
|
173
166
|
let method_value = Opaque::from(ruby.str_new(&method).as_value());
|
|
174
167
|
let path_value = Opaque::from(ruby.str_new(&path).as_value());
|
|
175
168
|
|
|
@@ -192,7 +185,7 @@ impl RubyHandler {
|
|
|
192
185
|
///
|
|
193
186
|
/// This is used by run_server to create handlers from Ruby Procs
|
|
194
187
|
pub fn new_for_server(
|
|
195
|
-
|
|
188
|
+
ruby: &Ruby,
|
|
196
189
|
handler_value: Value,
|
|
197
190
|
handler_name: String,
|
|
198
191
|
method: String,
|
|
@@ -205,12 +198,7 @@ impl RubyHandler {
|
|
|
205
198
|
} else {
|
|
206
199
|
None
|
|
207
200
|
};
|
|
208
|
-
let
|
|
209
|
-
return Err(Error::new(
|
|
210
|
-
magnus::exception::runtime_error(),
|
|
211
|
-
"Ruby VM unavailable while creating handler",
|
|
212
|
-
));
|
|
213
|
-
};
|
|
201
|
+
let handler_value = crate::conversion::ensure_callable(ruby, handler_value, &handler_name)?;
|
|
214
202
|
let method_value = Opaque::from(ruby.str_new(&method).as_value());
|
|
215
203
|
let path_value = Opaque::from(ruby.str_new(&path).as_value());
|
|
216
204
|
|
|
@@ -255,9 +243,7 @@ impl RubyHandler {
|
|
|
255
243
|
})
|
|
256
244
|
}
|
|
257
245
|
|
|
258
|
-
fn handle_inner(&self, request_data: RequestData) -> HandlerResult {
|
|
259
|
-
let validated_params = request_data.validated_params.clone();
|
|
260
|
-
|
|
246
|
+
fn handle_inner(&self, mut request_data: RequestData) -> HandlerResult {
|
|
261
247
|
let ruby = Ruby::get().map_err(|_| {
|
|
262
248
|
ErrorResponseBuilder::structured_error(
|
|
263
249
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
@@ -266,20 +252,22 @@ impl RubyHandler {
|
|
|
266
252
|
)
|
|
267
253
|
})?;
|
|
268
254
|
|
|
269
|
-
|
|
270
|
-
|
|
255
|
+
// Extract validated_params with Arc::try_unwrap to eliminate clone if possible.
|
|
256
|
+
let validated_params = request_data
|
|
257
|
+
.validated_params
|
|
258
|
+
.take()
|
|
259
|
+
.map(|arc| Arc::try_unwrap(arc).unwrap_or_else(|a| (*a).clone()));
|
|
260
|
+
|
|
261
|
+
// Use NativeRequest for lazy field conversion — fields are only converted
|
|
262
|
+
// to Ruby objects when accessed, avoiding work for unused fields.
|
|
263
|
+
let native_request =
|
|
264
|
+
NativeRequest::from_request_data(request_data, validated_params, self.inner.upload_file_class);
|
|
265
|
+
let request_value = ruby.obj_wrap(native_request).as_value();
|
|
271
266
|
|
|
272
267
|
let handler_proc = self.inner.handler_proc.get_inner_with(&ruby);
|
|
273
|
-
let
|
|
274
|
-
let response_value = match handler_result {
|
|
268
|
+
let response_value = match call_handler_proc(&ruby, handler_proc, request_value) {
|
|
275
269
|
Ok(value) => value,
|
|
276
|
-
Err(err) =>
|
|
277
|
-
return Err(ErrorResponseBuilder::structured_error(
|
|
278
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
279
|
-
"handler_failed",
|
|
280
|
-
format!("Handler '{}' failed: {}", self.inner.handler_name, err),
|
|
281
|
-
));
|
|
282
|
-
}
|
|
270
|
+
Err(err) => return Err(problem_from_ruby_error(&ruby, &self.inner, err)),
|
|
283
271
|
};
|
|
284
272
|
|
|
285
273
|
let handler_result = interpret_handler_response(&ruby, &self.inner, response_value).map_err(|err| {
|
|
@@ -310,7 +298,7 @@ impl RubyHandler {
|
|
|
310
298
|
if let Some(validator) = &self.inner.response_validator {
|
|
311
299
|
let candidate_body = match payload.body.clone() {
|
|
312
300
|
Some(body) => Some(body),
|
|
313
|
-
None => match try_parse_raw_body(
|
|
301
|
+
None => match try_parse_raw_body(payload.raw_body.as_ref()) {
|
|
314
302
|
Ok(parsed) => parsed,
|
|
315
303
|
Err(err) => {
|
|
316
304
|
return Err(ErrorResponseBuilder::structured_error(
|
|
@@ -409,15 +397,16 @@ impl Handler for RubyHandler {
|
|
|
409
397
|
}
|
|
410
398
|
}
|
|
411
399
|
|
|
412
|
-
fn try_parse_raw_body(raw_body:
|
|
400
|
+
fn try_parse_raw_body(raw_body: Option<&Vec<u8>>) -> Result<Option<JsonValue>, String> {
|
|
413
401
|
let Some(bytes) = raw_body else {
|
|
414
402
|
return Ok(None);
|
|
415
403
|
};
|
|
416
|
-
|
|
417
|
-
if text.is_empty() {
|
|
404
|
+
if bytes.is_empty() {
|
|
418
405
|
return Ok(None);
|
|
419
406
|
}
|
|
420
|
-
|
|
407
|
+
// PERFORMANCE: Use from_slice directly to avoid String allocation.
|
|
408
|
+
// serde_json handles UTF-8 validation internally.
|
|
409
|
+
serde_json::from_slice(bytes)
|
|
421
410
|
.map(Some)
|
|
422
411
|
.map_err(|e| format!("Failed to parse response body as JSON: {e}"))
|
|
423
412
|
}
|
|
@@ -432,77 +421,166 @@ fn lookup_upload_file_class() -> Result<Option<Opaque<Value>>, Error> {
|
|
|
432
421
|
Ok(upload_file.map(Opaque::from))
|
|
433
422
|
}
|
|
434
423
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
424
|
+
fn call_handler_proc(ruby: &Ruby, handler_proc: Value, request_value: Value) -> Result<Value, Error> {
|
|
425
|
+
let arity: i64 = handler_proc.funcall("arity", ())?;
|
|
426
|
+
if arity == 0 {
|
|
427
|
+
return handler_proc.funcall("call", ());
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if arity == 1 {
|
|
431
|
+
return handler_proc.funcall("call", (request_value,));
|
|
432
|
+
}
|
|
443
433
|
|
|
444
|
-
|
|
445
|
-
|
|
434
|
+
let (params_value, query_value, body_value) = if let Ok(request) = <&NativeRequest>::try_convert(request_value) {
|
|
435
|
+
(
|
|
436
|
+
NativeRequest::path_params(ruby, request).unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
437
|
+
NativeRequest::query(ruby, request).unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
438
|
+
NativeRequest::body(ruby, request).unwrap_or_else(|_| ruby.qnil().as_value()),
|
|
439
|
+
)
|
|
440
|
+
} else if let Some(hash) = RHash::from_value(request_value) {
|
|
441
|
+
(
|
|
442
|
+
hash.get(*KEY_PATH_PARAMS).unwrap_or_else(|| ruby.qnil().as_value()),
|
|
443
|
+
hash.get(*KEY_QUERY).unwrap_or_else(|| ruby.qnil().as_value()),
|
|
444
|
+
hash.get(*KEY_BODY).unwrap_or_else(|| ruby.qnil().as_value()),
|
|
445
|
+
)
|
|
446
|
+
} else {
|
|
447
|
+
(ruby.qnil().as_value(), ruby.qnil().as_value(), ruby.qnil().as_value())
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
if arity == 2 {
|
|
451
|
+
return handler_proc.funcall("call", (params_value, query_value));
|
|
452
|
+
}
|
|
446
453
|
|
|
447
|
-
|
|
448
|
-
|
|
454
|
+
handler_proc.funcall("call", (params_value, query_value, body_value))
|
|
455
|
+
}
|
|
449
456
|
|
|
450
|
-
|
|
451
|
-
|
|
457
|
+
fn problem_from_ruby_error(ruby: &Ruby, handler: &RubyHandlerInner, err: Error) -> (StatusCode, String) {
|
|
458
|
+
let mut status = StatusCode::INTERNAL_SERVER_ERROR;
|
|
459
|
+
let mut extensions: HashMap<String, JsonValue> = HashMap::new();
|
|
460
|
+
let mut detail = ruby_error_message(ruby, &err);
|
|
452
461
|
|
|
453
|
-
|
|
454
|
-
|
|
462
|
+
if err.is_kind_of(ruby.exception_arg_error()) {
|
|
463
|
+
status = StatusCode::BAD_REQUEST;
|
|
464
|
+
}
|
|
455
465
|
|
|
456
|
-
let
|
|
457
|
-
|
|
466
|
+
if let Some(exception) = err.value() {
|
|
467
|
+
if matches!(exception.respond_to("status", false), Ok(true)) {
|
|
468
|
+
if let Ok(code) = exception.funcall::<_, _, i64>("status", ()) {
|
|
469
|
+
status = StatusCode::from_u16(code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
|
470
|
+
}
|
|
471
|
+
} else if matches!(exception.respond_to("status_code", false), Ok(true))
|
|
472
|
+
&& let Ok(code) = exception.funcall::<_, _, i64>("status_code", ())
|
|
473
|
+
{
|
|
474
|
+
status = StatusCode::from_u16(code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
|
475
|
+
}
|
|
458
476
|
|
|
459
|
-
|
|
460
|
-
|
|
477
|
+
let json_module = handler.json_module.get_inner_with(ruby);
|
|
478
|
+
if matches!(exception.respond_to("code", false), Ok(true))
|
|
479
|
+
&& let Ok(value) = exception.funcall::<_, _, Value>("code", ())
|
|
480
|
+
&& let Ok(json_value) = ruby_value_to_json(ruby, json_module, value)
|
|
481
|
+
{
|
|
482
|
+
extensions.insert("code".to_string(), json_value);
|
|
483
|
+
}
|
|
461
484
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
} else {
|
|
469
|
-
hash.aset(*KEY_RAW_BODY, ruby.qnil())?;
|
|
485
|
+
if matches!(exception.respond_to("details", false), Ok(true))
|
|
486
|
+
&& let Ok(value) = exception.funcall::<_, _, Value>("details", ())
|
|
487
|
+
&& let Ok(json_value) = ruby_value_to_json(ruby, json_module, value)
|
|
488
|
+
{
|
|
489
|
+
extensions.insert("details".to_string(), json_value);
|
|
490
|
+
}
|
|
470
491
|
}
|
|
471
492
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
493
|
+
detail = sanitize_error_detail(&detail);
|
|
494
|
+
|
|
495
|
+
let mut problem = problem_for_status(status, detail);
|
|
496
|
+
for (key, value) in extensions {
|
|
497
|
+
problem = problem.with_extension(key, value);
|
|
498
|
+
}
|
|
478
499
|
|
|
479
|
-
|
|
500
|
+
ErrorResponseBuilder::problem_details_response(&problem)
|
|
480
501
|
}
|
|
481
502
|
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
cookies: Value,
|
|
489
|
-
) -> Result<Value, Error> {
|
|
490
|
-
let params = ruby.hash_new();
|
|
491
|
-
|
|
492
|
-
if let Some(hash) = RHash::from_value(path_params) {
|
|
493
|
-
let _: Value = params.funcall("merge!", (hash,))?;
|
|
494
|
-
}
|
|
495
|
-
if let Some(hash) = RHash::from_value(query) {
|
|
496
|
-
let _: Value = params.funcall("merge!", (hash,))?;
|
|
503
|
+
fn ruby_error_message(_ruby: &Ruby, err: &Error) -> String {
|
|
504
|
+
if let Some(exception) = err.value()
|
|
505
|
+
&& matches!(exception.respond_to("message", false), Ok(true))
|
|
506
|
+
&& let Ok(message) = exception.funcall::<_, _, String>("message", ())
|
|
507
|
+
{
|
|
508
|
+
return message;
|
|
497
509
|
}
|
|
498
|
-
|
|
499
|
-
|
|
510
|
+
err.to_string()
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
fn problem_for_status(status: StatusCode, detail: String) -> ProblemDetails {
|
|
514
|
+
match status {
|
|
515
|
+
StatusCode::BAD_REQUEST => ProblemDetails::bad_request(detail),
|
|
516
|
+
StatusCode::UNAUTHORIZED => {
|
|
517
|
+
ProblemDetails::new("https://spikard.dev/errors/unauthorized", "Unauthorized", status).with_detail(detail)
|
|
518
|
+
}
|
|
519
|
+
StatusCode::FORBIDDEN => {
|
|
520
|
+
ProblemDetails::new("https://spikard.dev/errors/forbidden", "Forbidden", status).with_detail(detail)
|
|
521
|
+
}
|
|
522
|
+
StatusCode::NOT_FOUND => ProblemDetails::not_found(detail),
|
|
523
|
+
StatusCode::UNPROCESSABLE_ENTITY => ProblemDetails::new(
|
|
524
|
+
ProblemDetails::TYPE_VALIDATION_ERROR,
|
|
525
|
+
"Request Validation Failed",
|
|
526
|
+
status,
|
|
527
|
+
)
|
|
528
|
+
.with_detail(detail),
|
|
529
|
+
_ => ProblemDetails::internal_server_error(detail),
|
|
500
530
|
}
|
|
501
|
-
|
|
502
|
-
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
fn sanitize_error_detail(detail: &str) -> String {
|
|
534
|
+
let mut tokens = Vec::new();
|
|
535
|
+
let mut redact_next = false;
|
|
536
|
+
|
|
537
|
+
for token in detail.split_whitespace() {
|
|
538
|
+
let lower = token.to_lowercase();
|
|
539
|
+
if token.starts_with('/') || token.contains(".rb:") {
|
|
540
|
+
tokens.push("[redacted]".to_string());
|
|
541
|
+
redact_next = false;
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if lower.starts_with("password=") {
|
|
546
|
+
tokens.push("password=[redacted]".to_string());
|
|
547
|
+
redact_next = false;
|
|
548
|
+
continue;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
if lower.starts_with("host=") {
|
|
552
|
+
tokens.push("host=[redacted]".to_string());
|
|
553
|
+
redact_next = false;
|
|
554
|
+
continue;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
if lower.starts_with("token=") || lower.starts_with("secret=") {
|
|
558
|
+
tokens.push("[redacted]".to_string());
|
|
559
|
+
redact_next = false;
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if redact_next {
|
|
564
|
+
tokens.push("[redacted]".to_string());
|
|
565
|
+
redact_next = false;
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
if token.eq_ignore_ascii_case("in") {
|
|
570
|
+
tokens.push(token.to_string());
|
|
571
|
+
redact_next = true;
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
tokens.push(token.to_string());
|
|
503
576
|
}
|
|
504
577
|
|
|
505
|
-
|
|
578
|
+
let mut sanitized = tokens.join(" ");
|
|
579
|
+
sanitized = sanitized.replace("SELECT *", "[redacted]");
|
|
580
|
+
sanitized = sanitized.replace("select *", "[redacted]");
|
|
581
|
+
sanitized = sanitized.replace("FROM users", "[redacted]");
|
|
582
|
+
sanitized = sanitized.replace("from users", "[redacted]");
|
|
583
|
+
sanitized
|
|
506
584
|
}
|
|
507
585
|
|
|
508
586
|
/// Interpret a Ruby handler response into our response types.
|