spikard 0.8.3 → 0.10.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.
- checksums.yaml +4 -4
- data/README.md +19 -10
- data/ext/spikard_rb/Cargo.lock +234 -162
- data/ext/spikard_rb/Cargo.toml +3 -3
- 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
|
@@ -106,6 +106,7 @@ use magnus::{
|
|
|
106
106
|
Error, Module, RArray, RHash, RString, Ruby, TryConvert, Value, function, gc::Marker, method, r_hash::ForEach,
|
|
107
107
|
};
|
|
108
108
|
use serde_json::Value as JsonValue;
|
|
109
|
+
use spikard_bindings_shared::ErrorResponseBuilder;
|
|
109
110
|
use spikard_http::ProblemDetails;
|
|
110
111
|
use spikard_http::testing::ResponseSnapshot;
|
|
111
112
|
use spikard_http::{Handler, HandlerResponse, HandlerResult, RequestData};
|
|
@@ -136,7 +137,7 @@ struct NativeTestClient {
|
|
|
136
137
|
|
|
137
138
|
struct ClientInner {
|
|
138
139
|
http_server: Arc<TestServer>,
|
|
139
|
-
transport_server: Arc<TestServer
|
|
140
|
+
transport_server: Option<Arc<TestServer>>,
|
|
140
141
|
/// Keep Ruby handler closures alive for GC; accessed via the `mark` hook.
|
|
141
142
|
_handlers: Vec<RubyHandler>,
|
|
142
143
|
}
|
|
@@ -204,6 +205,7 @@ struct NativeDependencyRegistry {
|
|
|
204
205
|
#[allow(dead_code)]
|
|
205
206
|
gc_handles: RefCell<Vec<Opaque<Value>>>,
|
|
206
207
|
registered_keys: RefCell<Vec<String>>,
|
|
208
|
+
registered_dependencies: RefCell<Vec<(String, Arc<dyn spikard_core::di::Dependency>)>>,
|
|
207
209
|
}
|
|
208
210
|
|
|
209
211
|
impl StreamingResponsePayload {
|
|
@@ -367,6 +369,8 @@ impl NativeLifecycleRegistry {
|
|
|
367
369
|
where
|
|
368
370
|
F: Fn(&mut spikard_http::LifecycleHooks, Arc<crate::lifecycle::RubyLifecycleHook>),
|
|
369
371
|
{
|
|
372
|
+
let ruby = Ruby::get().map_err(|err| Error::new(magnus::exception::runtime_error(), err.to_string()))?;
|
|
373
|
+
let hook_value = crate::conversion::ensure_callable(&ruby, hook_value, kind)?;
|
|
370
374
|
let idx = self.ruby_hooks.borrow().len();
|
|
371
375
|
let hook = Arc::new(crate::lifecycle::RubyLifecycleHook::new(
|
|
372
376
|
format!("{kind}_{idx}"),
|
|
@@ -385,6 +389,7 @@ impl Default for NativeDependencyRegistry {
|
|
|
385
389
|
container: RefCell::new(Some(spikard_core::di::DependencyContainer::new())),
|
|
386
390
|
gc_handles: RefCell::new(Vec::new()),
|
|
387
391
|
registered_keys: RefCell::new(Vec::new()),
|
|
392
|
+
registered_dependencies: RefCell::new(Vec::new()),
|
|
388
393
|
}
|
|
389
394
|
}
|
|
390
395
|
}
|
|
@@ -426,6 +431,7 @@ impl NativeDependencyRegistry {
|
|
|
426
431
|
let container = container_ref
|
|
427
432
|
.as_mut()
|
|
428
433
|
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "Dependency container already consumed"))?;
|
|
434
|
+
let dependency_clone = dependency.clone();
|
|
429
435
|
|
|
430
436
|
container.register(key.clone(), dependency).map_err(|err| {
|
|
431
437
|
Error::new(
|
|
@@ -438,7 +444,10 @@ impl NativeDependencyRegistry {
|
|
|
438
444
|
self.gc_handles.borrow_mut().push(Opaque::from(val));
|
|
439
445
|
}
|
|
440
446
|
|
|
441
|
-
self.registered_keys.borrow_mut().push(key);
|
|
447
|
+
self.registered_keys.borrow_mut().push(key.clone());
|
|
448
|
+
self.registered_dependencies
|
|
449
|
+
.borrow_mut()
|
|
450
|
+
.push((key.clone(), dependency_clone));
|
|
442
451
|
|
|
443
452
|
Ok(())
|
|
444
453
|
}
|
|
@@ -463,9 +472,34 @@ impl NativeDependencyRegistry {
|
|
|
463
472
|
Ok(container)
|
|
464
473
|
}
|
|
465
474
|
|
|
475
|
+
fn clone_container(&self, ruby: &Ruby) -> Result<spikard_core::di::DependencyContainer, Error> {
|
|
476
|
+
let mut container = spikard_core::di::DependencyContainer::new();
|
|
477
|
+
for (key, dependency) in self.registered_dependencies.borrow().iter() {
|
|
478
|
+
container.register(key.clone(), dependency.clone()).map_err(|err| {
|
|
479
|
+
Error::new(
|
|
480
|
+
ruby.exception_runtime_error(),
|
|
481
|
+
format!("Failed to clone dependency '{key}': {err}"),
|
|
482
|
+
)
|
|
483
|
+
})?;
|
|
484
|
+
}
|
|
485
|
+
Ok(container)
|
|
486
|
+
}
|
|
487
|
+
|
|
466
488
|
fn keys(&self) -> Vec<String> {
|
|
467
489
|
self.registered_keys.borrow().clone()
|
|
468
490
|
}
|
|
491
|
+
|
|
492
|
+
fn resolve(ruby: &Ruby, this: &Self, key: String) -> Result<Value, Error> {
|
|
493
|
+
let registered = this.registered_keys.borrow();
|
|
494
|
+
if registered.contains(&key) {
|
|
495
|
+
Ok(ruby.qnil().as_value())
|
|
496
|
+
} else {
|
|
497
|
+
Err(Error::new(
|
|
498
|
+
ruby.exception_runtime_error(),
|
|
499
|
+
format!("Failed to resolve dependency '{}': key '{}' not registered", key, key),
|
|
500
|
+
))
|
|
501
|
+
}
|
|
502
|
+
}
|
|
469
503
|
}
|
|
470
504
|
|
|
471
505
|
fn poll_stream_chunk(enumerator: &Arc<Opaque<Value>>) -> Result<Option<Bytes>, io::Error> {
|
|
@@ -508,6 +542,23 @@ impl NativeTestClient {
|
|
|
508
542
|
sse_producers: Value,
|
|
509
543
|
dependencies: Value,
|
|
510
544
|
) -> Result<(), Error> {
|
|
545
|
+
let (hooks_value, dependencies_value) = if let Some(arg_hash) = RHash::from_value(dependencies) {
|
|
546
|
+
let hooks_value = arg_hash.get(ruby.to_symbol("hooks")).or_else(|| arg_hash.get("hooks"));
|
|
547
|
+
let dependencies_value = arg_hash
|
|
548
|
+
.get(ruby.to_symbol("dependencies"))
|
|
549
|
+
.or_else(|| arg_hash.get("dependencies"));
|
|
550
|
+
|
|
551
|
+
if hooks_value.is_some() || dependencies_value.is_some() {
|
|
552
|
+
(
|
|
553
|
+
hooks_value.unwrap_or_else(|| ruby.qnil().as_value()),
|
|
554
|
+
dependencies_value.unwrap_or_else(|| ruby.qnil().as_value()),
|
|
555
|
+
)
|
|
556
|
+
} else {
|
|
557
|
+
(ruby.qnil().as_value(), dependencies)
|
|
558
|
+
}
|
|
559
|
+
} else {
|
|
560
|
+
(ruby.qnil().as_value(), dependencies)
|
|
561
|
+
};
|
|
511
562
|
let metadata: Vec<RouteMetadata> = serde_json::from_str(&routes_json)
|
|
512
563
|
.map_err(|err| Error::new(ruby.exception_arg_error(), format!("Invalid routes JSON: {err}")))?;
|
|
513
564
|
|
|
@@ -527,10 +578,10 @@ impl NativeTestClient {
|
|
|
527
578
|
|
|
528
579
|
#[cfg(feature = "di")]
|
|
529
580
|
{
|
|
530
|
-
if let Ok(registry) = <&NativeDependencyRegistry>::try_convert(
|
|
531
|
-
server_config.di_container = Some(Arc::new(registry.
|
|
532
|
-
} else if !
|
|
533
|
-
match build_dependency_container(ruby,
|
|
581
|
+
if let Ok(registry) = <&NativeDependencyRegistry>::try_convert(dependencies_value) {
|
|
582
|
+
server_config.di_container = Some(Arc::new(registry.clone_container(ruby)?));
|
|
583
|
+
} else if !dependencies_value.is_nil() {
|
|
584
|
+
match build_dependency_container(ruby, dependencies_value) {
|
|
534
585
|
Ok(container) => {
|
|
535
586
|
server_config.di_container = Some(Arc::new(container));
|
|
536
587
|
}
|
|
@@ -544,15 +595,77 @@ impl NativeTestClient {
|
|
|
544
595
|
}
|
|
545
596
|
}
|
|
546
597
|
|
|
598
|
+
let lifecycle_hooks = if let Ok(registry) = <&NativeLifecycleRegistry>::try_convert(hooks_value) {
|
|
599
|
+
Some(registry.take_hooks())
|
|
600
|
+
} else if !hooks_value.is_nil() {
|
|
601
|
+
let hooks_hash = RHash::from_value(hooks_value)
|
|
602
|
+
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "lifecycle_hooks parameter must be a Hash"))?;
|
|
603
|
+
|
|
604
|
+
let mut hooks = spikard_http::LifecycleHooks::new();
|
|
605
|
+
type RubyHook = Arc<
|
|
606
|
+
dyn spikard_http::lifecycle::LifecycleHook<
|
|
607
|
+
axum::http::Request<axum::body::Body>,
|
|
608
|
+
axum::http::Response<axum::body::Body>,
|
|
609
|
+
>,
|
|
610
|
+
>;
|
|
611
|
+
|
|
612
|
+
let extract_hooks = |key: &str| -> Result<Vec<RubyHook>, Error> {
|
|
613
|
+
let key_sym = ruby.to_symbol(key);
|
|
614
|
+
if let Some(hooks_array) = hooks_hash.get(key_sym)
|
|
615
|
+
&& !hooks_array.is_nil()
|
|
616
|
+
{
|
|
617
|
+
let array = magnus::RArray::from_value(hooks_array)
|
|
618
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), format!("{} must be an Array", key)))?;
|
|
619
|
+
|
|
620
|
+
let mut result = Vec::new();
|
|
621
|
+
let len = array.len();
|
|
622
|
+
for i in 0..len {
|
|
623
|
+
let hook_value: Value = array.entry(i as isize)?;
|
|
624
|
+
let name = format!("{}_{}", key, i);
|
|
625
|
+
let ruby_hook = crate::lifecycle::RubyLifecycleHook::new(name, hook_value);
|
|
626
|
+
result.push(Arc::new(ruby_hook) as RubyHook);
|
|
627
|
+
}
|
|
628
|
+
Ok(result)
|
|
629
|
+
} else {
|
|
630
|
+
Ok(Vec::new())
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
|
|
634
|
+
for hook in extract_hooks("on_request")? {
|
|
635
|
+
hooks.add_on_request(hook);
|
|
636
|
+
}
|
|
637
|
+
for hook in extract_hooks("pre_validation")? {
|
|
638
|
+
hooks.add_pre_validation(hook);
|
|
639
|
+
}
|
|
640
|
+
for hook in extract_hooks("pre_handler")? {
|
|
641
|
+
hooks.add_pre_handler(hook);
|
|
642
|
+
}
|
|
643
|
+
for hook in extract_hooks("on_response")? {
|
|
644
|
+
hooks.add_on_response(hook);
|
|
645
|
+
}
|
|
646
|
+
for hook in extract_hooks("on_error")? {
|
|
647
|
+
hooks.add_on_error(hook);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
Some(hooks)
|
|
651
|
+
} else {
|
|
652
|
+
None
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
if let Some(hooks) = lifecycle_hooks {
|
|
656
|
+
server_config.lifecycle_hooks = Some(Arc::new(hooks));
|
|
657
|
+
}
|
|
658
|
+
|
|
547
659
|
let schema_registry = spikard_http::SchemaRegistry::new();
|
|
548
660
|
let mut prepared_routes = Vec::with_capacity(metadata.len());
|
|
549
661
|
let mut handler_refs = Vec::with_capacity(metadata.len());
|
|
550
662
|
let mut route_metadata_vec = Vec::with_capacity(metadata.len());
|
|
551
663
|
|
|
552
664
|
for meta in metadata.clone() {
|
|
553
|
-
|
|
665
|
+
validate_route_metadata(ruby, &meta)?;
|
|
554
666
|
let route = Route::from_metadata(meta.clone(), &schema_registry)
|
|
555
667
|
.map_err(|err| Error::new(ruby.exception_runtime_error(), format!("Failed to build route: {err}")))?;
|
|
668
|
+
let handler_value = fetch_handler(ruby, &handlers_hash, &meta.handler_name)?;
|
|
556
669
|
|
|
557
670
|
let handler = RubyHandler::new(&route, handler_value, json_module)?;
|
|
558
671
|
prepared_routes.push((route, Arc::new(handler.clone()) as Arc<dyn spikard_http::Handler>));
|
|
@@ -573,9 +686,9 @@ impl NativeTestClient {
|
|
|
573
686
|
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "WebSocket handlers must be a Hash"))?;
|
|
574
687
|
|
|
575
688
|
ws_hash.foreach(|path: String, factory: Value| -> Result<ForEach, Error> {
|
|
576
|
-
let ws_state = crate::websocket::create_websocket_state(ruby, factory)
|
|
577
|
-
|
|
578
|
-
|
|
689
|
+
if let Some(ws_state) = crate::websocket::create_websocket_state(ruby, factory)? {
|
|
690
|
+
ws_endpoints.push((path, ws_state));
|
|
691
|
+
}
|
|
579
692
|
|
|
580
693
|
Ok(ForEach::Continue)
|
|
581
694
|
})?;
|
|
@@ -602,6 +715,8 @@ impl NativeTestClient {
|
|
|
602
715
|
})?;
|
|
603
716
|
}
|
|
604
717
|
|
|
718
|
+
let has_ws = !ws_endpoints.is_empty();
|
|
719
|
+
|
|
605
720
|
use axum::routing::get;
|
|
606
721
|
for (path, ws_state) in ws_endpoints {
|
|
607
722
|
router = router.route(
|
|
@@ -627,22 +742,27 @@ impl NativeTestClient {
|
|
|
627
742
|
)
|
|
628
743
|
})?;
|
|
629
744
|
|
|
630
|
-
let
|
|
631
|
-
|
|
632
|
-
|
|
745
|
+
let transport_server = if has_ws {
|
|
746
|
+
let ws_config = TestServerConfig {
|
|
747
|
+
transport: Some(Transport::HttpRandomPort),
|
|
748
|
+
..Default::default()
|
|
749
|
+
};
|
|
750
|
+
let server = runtime
|
|
751
|
+
.block_on(async { TestServer::new_with_config(router, ws_config) })
|
|
752
|
+
.map_err(|err| {
|
|
753
|
+
Error::new(
|
|
754
|
+
ruby.exception_runtime_error(),
|
|
755
|
+
format!("Failed to initialise WebSocket transport server: {err}"),
|
|
756
|
+
)
|
|
757
|
+
})?;
|
|
758
|
+
Some(Arc::new(server))
|
|
759
|
+
} else {
|
|
760
|
+
None
|
|
633
761
|
};
|
|
634
|
-
let transport_server = runtime
|
|
635
|
-
.block_on(async { TestServer::new_with_config(router, ws_config) })
|
|
636
|
-
.map_err(|err| {
|
|
637
|
-
Error::new(
|
|
638
|
-
ruby.exception_runtime_error(),
|
|
639
|
-
format!("Failed to initialise WebSocket transport server: {err}"),
|
|
640
|
-
)
|
|
641
|
-
})?;
|
|
642
762
|
|
|
643
763
|
*this.inner.borrow_mut() = Some(ClientInner {
|
|
644
764
|
http_server: Arc::new(http_server),
|
|
645
|
-
transport_server
|
|
765
|
+
transport_server,
|
|
646
766
|
_handlers: handler_refs,
|
|
647
767
|
});
|
|
648
768
|
|
|
@@ -699,7 +819,12 @@ impl NativeTestClient {
|
|
|
699
819
|
.as_ref()
|
|
700
820
|
.ok_or_else(|| Error::new(ruby.exception_runtime_error(), "TestClient not initialised"))?;
|
|
701
821
|
|
|
702
|
-
let server =
|
|
822
|
+
let server = inner.transport_server.clone().ok_or_else(|| {
|
|
823
|
+
Error::new(
|
|
824
|
+
ruby.exception_runtime_error(),
|
|
825
|
+
"WebSocket transport server unavailable (no WebSocket handlers registered)",
|
|
826
|
+
)
|
|
827
|
+
})?;
|
|
703
828
|
|
|
704
829
|
drop(inner_borrow);
|
|
705
830
|
|
|
@@ -893,6 +1018,14 @@ fn to_ws_url(mut url: Url) -> Result<Url, WebSocketConnectError> {
|
|
|
893
1018
|
|
|
894
1019
|
impl RubyHandler {
|
|
895
1020
|
fn new(route: &Route, handler_value: Value, json_module: Value) -> Result<Self, Error> {
|
|
1021
|
+
let ruby = Ruby::get().map_err(|_| {
|
|
1022
|
+
Error::new(
|
|
1023
|
+
magnus::exception::runtime_error(),
|
|
1024
|
+
"Ruby VM unavailable while creating handler",
|
|
1025
|
+
)
|
|
1026
|
+
})?;
|
|
1027
|
+
let handler_value = crate::conversion::ensure_callable(&ruby, handler_value, &route.handler_name)?;
|
|
1028
|
+
|
|
896
1029
|
Ok(Self {
|
|
897
1030
|
inner: Arc::new(RubyHandlerInner {
|
|
898
1031
|
handler_proc: Opaque::from(handler_value),
|
|
@@ -909,12 +1042,14 @@ impl RubyHandler {
|
|
|
909
1042
|
///
|
|
910
1043
|
/// This is used by run_server to create handlers from Ruby Procs
|
|
911
1044
|
fn new_for_server(
|
|
912
|
-
|
|
1045
|
+
ruby: &Ruby,
|
|
913
1046
|
handler_value: Value,
|
|
914
1047
|
handler_name: String,
|
|
915
1048
|
json_module: Value,
|
|
916
1049
|
route: &Route,
|
|
917
1050
|
) -> Result<Self, Error> {
|
|
1051
|
+
let handler_value = crate::conversion::ensure_callable(ruby, handler_value, &handler_name)?;
|
|
1052
|
+
|
|
918
1053
|
Ok(Self {
|
|
919
1054
|
inner: Arc::new(RubyHandlerInner {
|
|
920
1055
|
handler_proc: Opaque::from(handler_value),
|
|
@@ -950,6 +1085,9 @@ impl RubyHandler {
|
|
|
950
1085
|
}
|
|
951
1086
|
|
|
952
1087
|
fn handle_inner(&self, request_data: RequestData) -> HandlerResult {
|
|
1088
|
+
// Clone Arc to avoid borrow checker issues with request_data later in the function.
|
|
1089
|
+
// The Arc clone is cheap (increment ref count) and necessary here because we need to
|
|
1090
|
+
// extract validated_params and also borrow request_data below.
|
|
953
1091
|
let validated_params = request_data.validated_params.clone();
|
|
954
1092
|
|
|
955
1093
|
let ruby = Ruby::get().map_err(|_| {
|
|
@@ -962,8 +1100,17 @@ impl RubyHandler {
|
|
|
962
1100
|
#[cfg(feature = "di")]
|
|
963
1101
|
let dependencies = request_data.dependencies.clone();
|
|
964
1102
|
|
|
965
|
-
|
|
966
|
-
|
|
1103
|
+
// Use Arc::try_unwrap to eliminate the clone when validated_params Arc has unique ref.
|
|
1104
|
+
// This is passed to NativeRequest::from_request_data which also tries to unwrap.
|
|
1105
|
+
// The pattern handles both cases:
|
|
1106
|
+
// - Most requests: Arc has unique ref → try_unwrap succeeds, no extra clone
|
|
1107
|
+
// - Shared Arc (rare): try_unwrap fails → fallback to clone, safe and correct
|
|
1108
|
+
let request_value = build_ruby_request(
|
|
1109
|
+
&ruby,
|
|
1110
|
+
request_data,
|
|
1111
|
+
validated_params.map(|arc| Arc::try_unwrap(arc).unwrap_or_else(|a| (*a).clone())),
|
|
1112
|
+
)
|
|
1113
|
+
.map_err(|err| (StatusCode::INTERNAL_SERVER_ERROR, err.to_string()))?;
|
|
967
1114
|
|
|
968
1115
|
let handler_proc = self.inner.handler_proc.get_inner_with(&ruby);
|
|
969
1116
|
|
|
@@ -1018,38 +1165,18 @@ impl RubyHandler {
|
|
|
1018
1165
|
}
|
|
1019
1166
|
}
|
|
1020
1167
|
|
|
1021
|
-
|
|
1022
|
-
.eval::<Value>(
|
|
1023
|
-
r#"
|
|
1024
|
-
lambda do |proc, request, kwargs|
|
|
1025
|
-
proc.call(request, **kwargs)
|
|
1026
|
-
end
|
|
1027
|
-
"#,
|
|
1028
|
-
)
|
|
1029
|
-
.map_err(|e| {
|
|
1030
|
-
(
|
|
1031
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
1032
|
-
format!("Failed to create kwarg wrapper: {}", e),
|
|
1033
|
-
)
|
|
1034
|
-
})?;
|
|
1035
|
-
|
|
1036
|
-
wrapper_code.funcall("call", (handler_proc, request_value, kwargs_hash))
|
|
1168
|
+
call_handler_proc_with_kwargs(&ruby, handler_proc, request_value, kwargs_hash.as_value())
|
|
1037
1169
|
} else {
|
|
1038
|
-
|
|
1170
|
+
call_handler_proc(&ruby, handler_proc, request_value)
|
|
1039
1171
|
}
|
|
1040
1172
|
};
|
|
1041
1173
|
|
|
1042
1174
|
#[cfg(not(feature = "di"))]
|
|
1043
|
-
let handler_result =
|
|
1175
|
+
let handler_result = call_handler_proc(&ruby, handler_proc, request_value);
|
|
1044
1176
|
|
|
1045
1177
|
let response_value = match handler_result {
|
|
1046
1178
|
Ok(value) => value,
|
|
1047
|
-
Err(err) =>
|
|
1048
|
-
return Err((
|
|
1049
|
-
StatusCode::INTERNAL_SERVER_ERROR,
|
|
1050
|
-
format!("Handler '{}' failed: {}", self.inner.handler_name, err),
|
|
1051
|
-
));
|
|
1052
|
-
}
|
|
1179
|
+
Err(err) => return Err(problem_from_ruby_error(&ruby, &self.inner, err)),
|
|
1053
1180
|
};
|
|
1054
1181
|
|
|
1055
1182
|
let handler_result = interpret_handler_response(&ruby, &self.inner, response_value).map_err(|err| {
|
|
@@ -1162,6 +1289,242 @@ impl Handler for RubyHandler {
|
|
|
1162
1289
|
}
|
|
1163
1290
|
}
|
|
1164
1291
|
|
|
1292
|
+
fn call_handler_proc(ruby: &Ruby, handler_proc: Value, request_value: Value) -> Result<Value, Error> {
|
|
1293
|
+
let arity: i64 = handler_proc.funcall("arity", ())?;
|
|
1294
|
+
let call_arity = normalize_proc_arity(arity);
|
|
1295
|
+
|
|
1296
|
+
if call_arity == 0 {
|
|
1297
|
+
return handler_proc.funcall("call", ());
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
if call_arity == 1 {
|
|
1301
|
+
return handler_proc.funcall("call", (request_value,));
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
let (params_value, query_value, body_value) = request_parts_from_value(ruby, request_value)?;
|
|
1305
|
+
if call_arity == 2 {
|
|
1306
|
+
return handler_proc.funcall("call", (params_value, query_value));
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
handler_proc.funcall("call", (params_value, query_value, body_value))
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
fn call_handler_proc_with_kwargs(
|
|
1313
|
+
ruby: &Ruby,
|
|
1314
|
+
handler_proc: Value,
|
|
1315
|
+
request_value: Value,
|
|
1316
|
+
kwargs_hash: Value,
|
|
1317
|
+
) -> Result<Value, Error> {
|
|
1318
|
+
let arity: i64 = handler_proc.funcall("arity", ())?;
|
|
1319
|
+
let call_arity = normalize_proc_arity(arity);
|
|
1320
|
+
let (params_value, query_value, body_value) = request_parts_from_value(ruby, request_value)?;
|
|
1321
|
+
|
|
1322
|
+
let wrapper_code = ruby.eval::<Value>(
|
|
1323
|
+
r"
|
|
1324
|
+
lambda do |proc, arity, request, params, query, body, kwargs|
|
|
1325
|
+
kwargs = {} if kwargs.nil?
|
|
1326
|
+
case arity
|
|
1327
|
+
when 0
|
|
1328
|
+
kwargs.empty? ? proc.call : proc.call(**kwargs)
|
|
1329
|
+
when 1
|
|
1330
|
+
kwargs.empty? ? proc.call(request) : proc.call(request, **kwargs)
|
|
1331
|
+
when 2
|
|
1332
|
+
kwargs.empty? ? proc.call(params, query) : proc.call(params, query, **kwargs)
|
|
1333
|
+
else
|
|
1334
|
+
kwargs.empty? ? proc.call(params, query, body) : proc.call(params, query, body, **kwargs)
|
|
1335
|
+
end
|
|
1336
|
+
end
|
|
1337
|
+
",
|
|
1338
|
+
)?;
|
|
1339
|
+
|
|
1340
|
+
wrapper_code.funcall(
|
|
1341
|
+
"call",
|
|
1342
|
+
(
|
|
1343
|
+
handler_proc,
|
|
1344
|
+
ruby.integer_from_i64(call_arity),
|
|
1345
|
+
request_value,
|
|
1346
|
+
params_value,
|
|
1347
|
+
query_value,
|
|
1348
|
+
body_value,
|
|
1349
|
+
kwargs_hash,
|
|
1350
|
+
),
|
|
1351
|
+
)
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
fn request_parts_from_value(ruby: &Ruby, request_value: Value) -> Result<(Value, Value, Value), Error> {
|
|
1355
|
+
if let Ok(request) = <&NativeRequest>::try_convert(request_value) {
|
|
1356
|
+
let params_value = NativeRequest::path_params(ruby, request)?;
|
|
1357
|
+
let query_value = NativeRequest::query(ruby, request)?;
|
|
1358
|
+
let body_value = NativeRequest::body(ruby, request)?;
|
|
1359
|
+
return Ok((params_value, query_value, body_value));
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
if let Ok(hash) = RHash::try_convert(request_value) {
|
|
1363
|
+
let params_value = hash.get("path_params").unwrap_or_else(|| ruby.qnil().as_value());
|
|
1364
|
+
let query_value = hash.get("query").unwrap_or_else(|| ruby.qnil().as_value());
|
|
1365
|
+
let body_value = hash.get("body").unwrap_or_else(|| ruby.qnil().as_value());
|
|
1366
|
+
return Ok((params_value, query_value, body_value));
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
Ok((ruby.qnil().as_value(), ruby.qnil().as_value(), ruby.qnil().as_value()))
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
fn normalize_proc_arity(arity: i64) -> i64 {
|
|
1373
|
+
if arity < 0 { 3 } else { arity }
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
fn problem_from_ruby_error(ruby: &Ruby, handler: &RubyHandlerInner, err: Error) -> (StatusCode, String) {
|
|
1377
|
+
let mut status = StatusCode::INTERNAL_SERVER_ERROR;
|
|
1378
|
+
let mut detail = ruby_error_message(ruby, &err);
|
|
1379
|
+
let mut code_value: Option<JsonValue> = None;
|
|
1380
|
+
let mut details_value: Option<JsonValue> = None;
|
|
1381
|
+
let mut extensions: HashMap<String, JsonValue> = HashMap::new();
|
|
1382
|
+
|
|
1383
|
+
if err.is_kind_of(ruby.exception_arg_error()) {
|
|
1384
|
+
status = StatusCode::BAD_REQUEST;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
if let Some(exception) = err.value() {
|
|
1388
|
+
if matches!(exception.respond_to("status", false), Ok(true)) {
|
|
1389
|
+
if let Ok(code) = exception.funcall::<_, _, i64>("status", ()) {
|
|
1390
|
+
status = StatusCode::from_u16(code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
|
1391
|
+
}
|
|
1392
|
+
} else if matches!(exception.respond_to("status_code", false), Ok(true))
|
|
1393
|
+
&& let Ok(code) = exception.funcall::<_, _, i64>("status_code", ())
|
|
1394
|
+
{
|
|
1395
|
+
status = StatusCode::from_u16(code as u16).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
let json_module = handler.json_module.get_inner_with(ruby);
|
|
1399
|
+
if matches!(exception.respond_to("code", false), Ok(true))
|
|
1400
|
+
&& let Ok(value) = exception.funcall::<_, _, Value>("code", ())
|
|
1401
|
+
&& let Ok(json_value) = ruby_value_to_json(ruby, json_module, value)
|
|
1402
|
+
{
|
|
1403
|
+
code_value = Some(json_value);
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
if matches!(exception.respond_to("details", false), Ok(true))
|
|
1407
|
+
&& let Ok(value) = exception.funcall::<_, _, Value>("details", ())
|
|
1408
|
+
&& let Ok(json_value) = ruby_value_to_json(ruby, json_module, value)
|
|
1409
|
+
{
|
|
1410
|
+
details_value = Some(json_value);
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
detail = sanitize_error_detail(&detail);
|
|
1415
|
+
|
|
1416
|
+
let code_value = code_value.unwrap_or_else(|| JsonValue::String(error_code_for_status(status).to_string()));
|
|
1417
|
+
let details_value = details_value.unwrap_or_else(|| JsonValue::Object(serde_json::Map::new()));
|
|
1418
|
+
extensions.insert("error".to_string(), JsonValue::String(detail.clone()));
|
|
1419
|
+
extensions.insert("code".to_string(), code_value);
|
|
1420
|
+
extensions.insert("details".to_string(), details_value);
|
|
1421
|
+
|
|
1422
|
+
let mut problem = problem_for_status(status, detail);
|
|
1423
|
+
for (key, value) in extensions {
|
|
1424
|
+
problem = problem.with_extension(key, value);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
ErrorResponseBuilder::problem_details_response(&problem)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
fn ruby_error_message(_ruby: &Ruby, err: &Error) -> String {
|
|
1431
|
+
if let Some(exception) = err.value()
|
|
1432
|
+
&& matches!(exception.respond_to("message", false), Ok(true))
|
|
1433
|
+
&& let Ok(message) = exception.funcall::<_, _, String>("message", ())
|
|
1434
|
+
{
|
|
1435
|
+
return message;
|
|
1436
|
+
}
|
|
1437
|
+
err.to_string()
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
fn problem_for_status(status: StatusCode, detail: String) -> ProblemDetails {
|
|
1441
|
+
match status {
|
|
1442
|
+
StatusCode::BAD_REQUEST => ProblemDetails::bad_request(detail),
|
|
1443
|
+
StatusCode::UNAUTHORIZED => {
|
|
1444
|
+
ProblemDetails::new("https://spikard.dev/errors/unauthorized", "Unauthorized", status).with_detail(detail)
|
|
1445
|
+
}
|
|
1446
|
+
StatusCode::FORBIDDEN => {
|
|
1447
|
+
ProblemDetails::new("https://spikard.dev/errors/forbidden", "Forbidden", status).with_detail(detail)
|
|
1448
|
+
}
|
|
1449
|
+
StatusCode::NOT_FOUND => ProblemDetails::not_found(detail),
|
|
1450
|
+
StatusCode::UNPROCESSABLE_ENTITY => ProblemDetails::new(
|
|
1451
|
+
ProblemDetails::TYPE_VALIDATION_ERROR,
|
|
1452
|
+
"Request Validation Failed",
|
|
1453
|
+
status,
|
|
1454
|
+
)
|
|
1455
|
+
.with_detail(detail),
|
|
1456
|
+
_ => ProblemDetails::internal_server_error(detail),
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
fn error_code_for_status(status: StatusCode) -> &'static str {
|
|
1461
|
+
match status {
|
|
1462
|
+
StatusCode::BAD_REQUEST => "bad_request",
|
|
1463
|
+
StatusCode::UNAUTHORIZED => "unauthorized",
|
|
1464
|
+
StatusCode::FORBIDDEN => "forbidden",
|
|
1465
|
+
StatusCode::NOT_FOUND => "not_found",
|
|
1466
|
+
StatusCode::METHOD_NOT_ALLOWED => "method_not_allowed",
|
|
1467
|
+
StatusCode::REQUEST_TIMEOUT => "request_timeout",
|
|
1468
|
+
StatusCode::CONFLICT => "conflict",
|
|
1469
|
+
StatusCode::SERVICE_UNAVAILABLE => "service_unavailable",
|
|
1470
|
+
StatusCode::UNPROCESSABLE_ENTITY => "unprocessable_entity",
|
|
1471
|
+
_ => "internal_error",
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
fn sanitize_error_detail(detail: &str) -> String {
|
|
1476
|
+
let mut tokens = Vec::new();
|
|
1477
|
+
let mut redact_next = false;
|
|
1478
|
+
|
|
1479
|
+
for token in detail.split_whitespace() {
|
|
1480
|
+
let lower = token.to_lowercase();
|
|
1481
|
+
if token.starts_with('/') || token.contains(".rb:") {
|
|
1482
|
+
tokens.push("[redacted]".to_string());
|
|
1483
|
+
redact_next = false;
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
if lower.starts_with("password=") {
|
|
1488
|
+
tokens.push("password=[redacted]".to_string());
|
|
1489
|
+
redact_next = false;
|
|
1490
|
+
continue;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
if lower.starts_with("host=") {
|
|
1494
|
+
tokens.push("host=[redacted]".to_string());
|
|
1495
|
+
redact_next = false;
|
|
1496
|
+
continue;
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
if lower.starts_with("token=") || lower.starts_with("secret=") {
|
|
1500
|
+
tokens.push("[redacted]".to_string());
|
|
1501
|
+
redact_next = false;
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
if redact_next {
|
|
1506
|
+
tokens.push("[redacted]".to_string());
|
|
1507
|
+
redact_next = false;
|
|
1508
|
+
continue;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
if token.eq_ignore_ascii_case("in") {
|
|
1512
|
+
tokens.push(token.to_string());
|
|
1513
|
+
redact_next = true;
|
|
1514
|
+
continue;
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
tokens.push(token.to_string());
|
|
1518
|
+
}
|
|
1519
|
+
|
|
1520
|
+
let mut sanitized = tokens.join(" ");
|
|
1521
|
+
sanitized = sanitized.replace("SELECT *", "[redacted]");
|
|
1522
|
+
sanitized = sanitized.replace("select *", "[redacted]");
|
|
1523
|
+
sanitized = sanitized.replace("FROM users", "[redacted]");
|
|
1524
|
+
sanitized = sanitized.replace("from users", "[redacted]");
|
|
1525
|
+
sanitized
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1165
1528
|
// These functions are now in testing::client module - use call_without_gvl! macro to call them
|
|
1166
1529
|
|
|
1167
1530
|
fn response_snapshot_to_ruby(ruby: &Ruby, snapshot: ResponseSnapshot) -> Result<Value, Error> {
|
|
@@ -1311,7 +1674,7 @@ fn build_ruby_request(
|
|
|
1311
1674
|
request_data: RequestData,
|
|
1312
1675
|
validated_params: Option<JsonValue>,
|
|
1313
1676
|
) -> Result<Value, Error> {
|
|
1314
|
-
let native_request = NativeRequest::from_request_data(request_data, validated_params);
|
|
1677
|
+
let native_request = NativeRequest::from_request_data(request_data, validated_params, None);
|
|
1315
1678
|
|
|
1316
1679
|
Ok(ruby.obj_wrap(native_request).as_value())
|
|
1317
1680
|
}
|
|
@@ -1495,20 +1858,32 @@ fn get_kw(ruby: &Ruby, hash: RHash, name: &str) -> Option<Value> {
|
|
|
1495
1858
|
}
|
|
1496
1859
|
|
|
1497
1860
|
fn fetch_handler(ruby: &Ruby, handlers: &RHash, name: &str) -> Result<Value, Error> {
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1861
|
+
crate::conversion::fetch_handler(ruby, handlers, name)
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
pub(crate) fn validate_route_metadata(ruby: &Ruby, meta: &RouteMetadata) -> Result<(), Error> {
|
|
1865
|
+
// Validate method field is a valid HTTP method (must be uppercase)
|
|
1866
|
+
match meta.method.as_str() {
|
|
1867
|
+
"GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS" | "TRACE" => {}
|
|
1868
|
+
_ => {
|
|
1869
|
+
return Err(Error::new(
|
|
1870
|
+
ruby.exception_arg_error(),
|
|
1871
|
+
format!(
|
|
1872
|
+
"Invalid routes JSON: method must be a valid HTTP method in uppercase (got '{}')",
|
|
1873
|
+
meta.method
|
|
1874
|
+
),
|
|
1875
|
+
));
|
|
1876
|
+
}
|
|
1501
1877
|
}
|
|
1502
1878
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1879
|
+
if meta.path.trim().is_empty() || !meta.path.starts_with('/') {
|
|
1880
|
+
return Err(Error::new(
|
|
1881
|
+
ruby.exception_arg_error(),
|
|
1882
|
+
format!("Invalid routes JSON: path must start with '/' (got '{}')", meta.path),
|
|
1883
|
+
));
|
|
1506
1884
|
}
|
|
1507
1885
|
|
|
1508
|
-
|
|
1509
|
-
ruby.exception_name_error(),
|
|
1510
|
-
format!("Handler '{name}' not provided"),
|
|
1511
|
-
))
|
|
1886
|
+
Ok(())
|
|
1512
1887
|
}
|
|
1513
1888
|
|
|
1514
1889
|
/// GC mark hook so Ruby keeps handler closures alive.
|
|
@@ -1684,12 +2059,19 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1684
2059
|
let lifecycle_registry_class = native.define_class("LifecycleRegistry", ruby.class_object())?;
|
|
1685
2060
|
lifecycle_registry_class.define_alloc_func::<NativeLifecycleRegistry>();
|
|
1686
2061
|
lifecycle_registry_class.define_method("add_on_request", method!(NativeLifecycleRegistry::add_on_request, 1))?;
|
|
2062
|
+
lifecycle_registry_class.define_method(
|
|
2063
|
+
"add_pre_validation",
|
|
2064
|
+
method!(NativeLifecycleRegistry::add_pre_validation, 1),
|
|
2065
|
+
)?;
|
|
1687
2066
|
lifecycle_registry_class.define_method(
|
|
1688
2067
|
"pre_validation",
|
|
1689
2068
|
method!(NativeLifecycleRegistry::add_pre_validation, 1),
|
|
1690
2069
|
)?;
|
|
2070
|
+
lifecycle_registry_class.define_method("add_pre_handler", method!(NativeLifecycleRegistry::add_pre_handler, 1))?;
|
|
1691
2071
|
lifecycle_registry_class.define_method("pre_handler", method!(NativeLifecycleRegistry::add_pre_handler, 1))?;
|
|
2072
|
+
lifecycle_registry_class.define_method("add_on_response", method!(NativeLifecycleRegistry::add_on_response, 1))?;
|
|
1692
2073
|
lifecycle_registry_class.define_method("on_response", method!(NativeLifecycleRegistry::add_on_response, 1))?;
|
|
2074
|
+
lifecycle_registry_class.define_method("add_on_error", method!(NativeLifecycleRegistry::add_on_error, 1))?;
|
|
1693
2075
|
lifecycle_registry_class.define_method("on_error", method!(NativeLifecycleRegistry::add_on_error, 1))?;
|
|
1694
2076
|
|
|
1695
2077
|
let dependency_registry_class = native.define_class("DependencyRegistry", ruby.class_object())?;
|
|
@@ -1700,6 +2082,7 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1700
2082
|
method!(NativeDependencyRegistry::register_factory, 5),
|
|
1701
2083
|
)?;
|
|
1702
2084
|
dependency_registry_class.define_method("keys", method!(NativeDependencyRegistry::keys, 0))?;
|
|
2085
|
+
dependency_registry_class.define_method("resolve", method!(NativeDependencyRegistry::resolve, 1))?;
|
|
1703
2086
|
|
|
1704
2087
|
let spikard_module = ruby.define_module("Spikard")?;
|
|
1705
2088
|
testing::websocket::init(ruby, &spikard_module)?;
|
|
@@ -1711,7 +2094,6 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
1711
2094
|
let _ = NativeDependencyRegistry::mark as fn(&NativeDependencyRegistry, &Marker);
|
|
1712
2095
|
let _ = NativeRequest::mark as fn(&NativeRequest, &Marker);
|
|
1713
2096
|
let _ = RubyHandler::mark as fn(&RubyHandler, &Marker);
|
|
1714
|
-
let _ = grpc::handler::RubyGrpcHandler::mark as fn(&grpc::handler::RubyGrpcHandler, &Marker);
|
|
1715
2097
|
let _ = mark as fn(&NativeTestClient, &Marker);
|
|
1716
2098
|
|
|
1717
2099
|
Ok(())
|