spikard 0.7.4 → 0.8.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/ext/spikard_rb/Cargo.lock +583 -201
  3. data/ext/spikard_rb/Cargo.toml +1 -1
  4. data/lib/spikard/grpc.rb +182 -0
  5. data/lib/spikard/version.rb +1 -1
  6. data/lib/spikard.rb +1 -1
  7. data/vendor/crates/spikard-bindings-shared/Cargo.toml +2 -1
  8. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +197 -0
  9. data/vendor/crates/spikard-bindings-shared/src/lib.rs +2 -0
  10. data/vendor/crates/spikard-core/Cargo.toml +1 -1
  11. data/vendor/crates/spikard-http/Cargo.toml +5 -1
  12. data/vendor/crates/spikard-http/src/grpc/handler.rs +260 -0
  13. data/vendor/crates/spikard-http/src/grpc/mod.rs +342 -0
  14. data/vendor/crates/spikard-http/src/grpc/service.rs +392 -0
  15. data/vendor/crates/spikard-http/src/grpc/streaming.rs +237 -0
  16. data/vendor/crates/spikard-http/src/lib.rs +14 -0
  17. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +288 -0
  18. data/vendor/crates/spikard-http/src/server/mod.rs +1 -0
  19. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +1023 -0
  20. data/vendor/crates/spikard-http/tests/common/mod.rs +8 -0
  21. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +653 -0
  22. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +332 -0
  23. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +518 -0
  24. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +476 -0
  25. data/vendor/crates/spikard-rb/Cargo.toml +2 -1
  26. data/vendor/crates/spikard-rb/src/config/server_config.rs +1 -0
  27. data/vendor/crates/spikard-rb/src/grpc/handler.rs +352 -0
  28. data/vendor/crates/spikard-rb/src/grpc/mod.rs +9 -0
  29. data/vendor/crates/spikard-rb/src/lib.rs +4 -0
  30. data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
  31. metadata +15 -1
@@ -0,0 +1,352 @@
1
+ //! Ruby gRPC handler implementation using Magnus FFI
2
+ //!
3
+ //! This module provides a bridge between Ruby code implementing gRPC handlers
4
+ //! and Spikard's Rust-based gRPC runtime. It handles serialization/deserialization
5
+ //! of protobuf messages as binary strings.
6
+
7
+ use bytes::Bytes;
8
+ use magnus::prelude::*;
9
+ use magnus::value::{InnerValue, Opaque};
10
+ use magnus::{Error, RHash, RString, Ruby, Symbol, TryConvert, Value, gc::Marker};
11
+ use spikard_bindings_shared::grpc_metadata::{extract_metadata_to_hashmap, hashmap_to_metadata};
12
+ use spikard_http::grpc::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
13
+ use std::cell::RefCell;
14
+ use std::collections::HashMap;
15
+ use std::future::Future;
16
+ use std::panic::AssertUnwindSafe;
17
+ use std::pin::Pin;
18
+ use std::sync::Arc;
19
+
20
+ use crate::gvl::with_gvl;
21
+
22
+ /// Ruby-facing gRPC request object
23
+ ///
24
+ /// This struct is exposed to Ruby code and contains the parsed components
25
+ /// of a gRPC request. The payload is provided as a binary string that Ruby
26
+ /// code can deserialize using the google-protobuf gem.
27
+ #[derive(Debug, Clone)]
28
+ #[magnus::wrap(class = "Spikard::Grpc::Request", free_immediately)]
29
+ pub struct RubyGrpcRequest {
30
+ service_name: String,
31
+ method_name: String,
32
+ payload: Vec<u8>,
33
+ metadata: HashMap<String, String>,
34
+ }
35
+
36
+ impl RubyGrpcRequest {
37
+ /// Create a new RubyGrpcRequest from GrpcRequestData
38
+ fn from_grpc_request(request: GrpcRequestData) -> Self {
39
+ let metadata = extract_metadata_to_hashmap(&request.metadata, true);
40
+ Self {
41
+ service_name: request.service_name,
42
+ method_name: request.method_name,
43
+ payload: request.payload.to_vec(),
44
+ metadata,
45
+ }
46
+ }
47
+
48
+ /// Get the service name
49
+ fn service_name(&self) -> &str {
50
+ &self.service_name
51
+ }
52
+
53
+ /// Get the method name
54
+ fn method_name(&self) -> &str {
55
+ &self.method_name
56
+ }
57
+
58
+ /// Get the payload as a binary string
59
+ fn payload(ruby: &Ruby, rb_self: &Self) -> Value {
60
+ ruby.str_from_slice(&rb_self.payload).as_value()
61
+ }
62
+
63
+ /// Get metadata as a Ruby hash
64
+ fn metadata(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
65
+ let hash = ruby.hash_new();
66
+ for (key, value) in &rb_self.metadata {
67
+ hash.aset(ruby.str_new(key), ruby.str_new(value))?;
68
+ }
69
+ Ok(hash.as_value())
70
+ }
71
+ }
72
+
73
+ /// Ruby-facing gRPC response object
74
+ ///
75
+ /// Ruby code creates instances of this class to return gRPC responses.
76
+ /// The payload should be a binary string containing the serialized protobuf message.
77
+ #[derive(Debug, Clone, Default)]
78
+ #[magnus::wrap(class = "Spikard::Grpc::Response", free_immediately)]
79
+ pub struct RubyGrpcResponse {
80
+ payload: RefCell<Vec<u8>>,
81
+ metadata: RefCell<HashMap<String, String>>,
82
+ }
83
+
84
+ impl RubyGrpcResponse {
85
+ /// Initialize the response with a payload (called by Ruby's new)
86
+ fn initialize(&self, args: &[Value]) -> Result<(), Error> {
87
+ // Handle both positional and keyword arguments
88
+ let payload_value = if args.is_empty() {
89
+ return Err(Error::new(magnus::exception::arg_error(), "missing keyword: payload"));
90
+ } else if args.len() == 1 {
91
+ // Check if it's a hash (keyword args) or a string (positional arg)
92
+ if let Ok(hash) = RHash::try_convert(args[0]) {
93
+ // Keyword arguments: { payload: "data" }
94
+ hash.get(Symbol::new("payload"))
95
+ .ok_or_else(|| Error::new(magnus::exception::arg_error(), "missing keyword: payload"))?
96
+ } else {
97
+ // Positional argument: "data"
98
+ args[0]
99
+ }
100
+ } else {
101
+ return Err(Error::new(magnus::exception::arg_error(), "wrong number of arguments"));
102
+ };
103
+
104
+ let payload_str = RString::try_convert(payload_value).map_err(|_| {
105
+ Error::new(magnus::exception::arg_error(), "payload must be a String (binary)")
106
+ })?;
107
+
108
+ let payload_bytes = unsafe { payload_str.as_slice() }.to_vec();
109
+
110
+ *self.payload.borrow_mut() = payload_bytes;
111
+ *self.metadata.borrow_mut() = HashMap::new();
112
+ Ok(())
113
+ }
114
+
115
+ /// Set metadata on the response
116
+ fn set_metadata(&self, metadata: Value) -> Result<(), Error> {
117
+ if metadata.is_nil() {
118
+ return Ok(());
119
+ }
120
+
121
+ let hash = RHash::try_convert(metadata)?;
122
+ let metadata_map = hash.to_hash_map::<String, String>()?;
123
+ *self.metadata.borrow_mut() = metadata_map;
124
+ Ok(())
125
+ }
126
+
127
+ /// Get the payload
128
+ fn payload(ruby: &Ruby, rb_self: &Self) -> Value {
129
+ ruby.str_from_slice(&rb_self.payload.borrow()).as_value()
130
+ }
131
+
132
+ /// Get metadata as a Ruby hash
133
+ fn get_metadata(ruby: &Ruby, rb_self: &Self) -> Result<Value, Error> {
134
+ let hash = ruby.hash_new();
135
+ for (key, value) in rb_self.metadata.borrow().iter() {
136
+ hash.aset(ruby.str_new(key), ruby.str_new(value))?;
137
+ }
138
+ Ok(hash.as_value())
139
+ }
140
+
141
+ /// Convert to GrpcResponseData
142
+ fn into_grpc_response(self) -> Result<GrpcResponseData, String> {
143
+ let metadata = hashmap_to_metadata(&self.metadata.borrow())?;
144
+
145
+ Ok(GrpcResponseData {
146
+ payload: Bytes::from(self.payload.borrow().clone()),
147
+ metadata,
148
+ })
149
+ }
150
+ }
151
+
152
+ /// Ruby gRPC handler wrapper
153
+ ///
154
+ /// Wraps a Ruby handler object and implements the GrpcHandler trait,
155
+ /// allowing Ruby code to handle gRPC requests.
156
+ #[derive(Clone)]
157
+ pub struct RubyGrpcHandler {
158
+ inner: Arc<RubyGrpcHandlerInner>,
159
+ }
160
+
161
+ struct RubyGrpcHandlerInner {
162
+ handler: Opaque<Value>,
163
+ service_name: String,
164
+ }
165
+
166
+ impl RubyGrpcHandler {
167
+ /// Create a new RubyGrpcHandler
168
+ ///
169
+ /// # Arguments
170
+ ///
171
+ /// * `handler` - A Ruby object that responds to `handle_request(request)`
172
+ /// * `service_name` - The fully qualified service name (e.g., "mypackage.MyService")
173
+ #[allow(dead_code)]
174
+ pub fn new(handler: Value, service_name: String) -> Self {
175
+ Self {
176
+ inner: Arc::new(RubyGrpcHandlerInner {
177
+ handler: Opaque::from(handler),
178
+ service_name,
179
+ }),
180
+ }
181
+ }
182
+
183
+ /// Required by Ruby GC; invoked through the magnus mark hook.
184
+ #[allow(dead_code)]
185
+ pub fn mark(&self, marker: &Marker) {
186
+ if let Ok(ruby) = Ruby::get() {
187
+ let handler_val = self.inner.handler.get_inner_with(&ruby);
188
+ marker.mark(handler_val);
189
+ }
190
+ }
191
+
192
+ /// Handle a gRPC request by calling into Ruby
193
+ fn handle_request(&self, request: GrpcRequestData) -> GrpcHandlerResult {
194
+ with_gvl(|| {
195
+ let result = std::panic::catch_unwind(AssertUnwindSafe(|| self.handle_request_inner(request)));
196
+ match result {
197
+ Ok(res) => res,
198
+ Err(_) => Err(tonic::Status::internal(
199
+ "Unexpected panic while executing Ruby gRPC handler",
200
+ )),
201
+ }
202
+ })
203
+ }
204
+
205
+ fn handle_request_inner(&self, request: GrpcRequestData) -> GrpcHandlerResult {
206
+ let ruby = Ruby::get().map_err(|_| {
207
+ tonic::Status::internal("Ruby VM unavailable while invoking gRPC handler")
208
+ })?;
209
+
210
+ // Convert request to Ruby object
211
+ let ruby_request = RubyGrpcRequest::from_grpc_request(request);
212
+ let request_value = ruby
213
+ .obj_wrap(ruby_request)
214
+ .as_value();
215
+
216
+ // Call Ruby handler
217
+ let handler_value = self.inner.handler.get_inner_with(&ruby);
218
+ let response_value = handler_value
219
+ .funcall::<_, _, Value>("handle_request", (request_value,))
220
+ .map_err(|err| {
221
+ tonic::Status::internal(format!("Ruby gRPC handler failed: {}", err))
222
+ })?;
223
+
224
+ // Convert Ruby response to GrpcResponseData
225
+ let ruby_response = <&RubyGrpcResponse>::try_convert(response_value)
226
+ .map_err(|err| {
227
+ tonic::Status::internal(format!(
228
+ "Handler must return Spikard::Grpc::Response, got error: {}",
229
+ err
230
+ ))
231
+ })?;
232
+
233
+ ruby_response
234
+ .clone()
235
+ .into_grpc_response()
236
+ .map_err(|err| tonic::Status::internal(format!("Failed to build gRPC response: {}", err)))
237
+ }
238
+ }
239
+
240
+ impl GrpcHandler for RubyGrpcHandler {
241
+ fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
242
+ let handler = self.clone();
243
+ Box::pin(async move { handler.handle_request(request) })
244
+ }
245
+
246
+ fn service_name(&self) -> &'static str {
247
+ // We need to return a 'static str, but we have a String.
248
+ // For now, we'll leak the string to get a 'static reference.
249
+ // This is acceptable because service names are registered once at startup.
250
+ Box::leak(self.inner.service_name.clone().into_boxed_str())
251
+ }
252
+ }
253
+
254
+ /// Initialize the gRPC module in Ruby
255
+ pub fn init(ruby: &Ruby, spikard_module: &magnus::RModule) -> Result<(), Error> {
256
+ let grpc_module = spikard_module.define_module("Grpc")?;
257
+
258
+ // Define Spikard::Grpc::Request class
259
+ let request_class = grpc_module.define_class("Request", ruby.class_object())?;
260
+ request_class.define_method("service_name", magnus::method!(RubyGrpcRequest::service_name, 0))?;
261
+ request_class.define_method("method_name", magnus::method!(RubyGrpcRequest::method_name, 0))?;
262
+ request_class.define_method("payload", magnus::method!(RubyGrpcRequest::payload, 0))?;
263
+ request_class.define_method("metadata", magnus::method!(RubyGrpcRequest::metadata, 0))?;
264
+
265
+ // Define Spikard::Grpc::Response class
266
+ let response_class = grpc_module.define_class("Response", ruby.class_object())?;
267
+ response_class.define_alloc_func::<RubyGrpcResponse>();
268
+ response_class.define_method("initialize", magnus::method!(RubyGrpcResponse::initialize, -1))?;
269
+ response_class.define_method("metadata=", magnus::method!(RubyGrpcResponse::set_metadata, 1))?;
270
+ response_class.define_method("metadata", magnus::method!(RubyGrpcResponse::get_metadata, 0))?;
271
+ response_class.define_method("payload", magnus::method!(RubyGrpcResponse::payload, 0))?;
272
+
273
+ Ok(())
274
+ }
275
+
276
+ #[cfg(test)]
277
+ mod tests {
278
+ use super::*;
279
+ use bytes::Bytes;
280
+ use tonic::metadata::MetadataMap;
281
+
282
+ #[test]
283
+ fn test_ruby_grpc_request_creation() {
284
+ let request = GrpcRequestData {
285
+ service_name: "test.TestService".to_string(),
286
+ method_name: "TestMethod".to_string(),
287
+ payload: Bytes::from("test payload"),
288
+ metadata: MetadataMap::new(),
289
+ };
290
+
291
+ let ruby_request = RubyGrpcRequest::from_grpc_request(request);
292
+ assert_eq!(ruby_request.service_name, "test.TestService");
293
+ assert_eq!(ruby_request.method_name, "TestMethod");
294
+ assert_eq!(ruby_request.payload, b"test payload");
295
+ }
296
+
297
+ #[test]
298
+ fn test_metadata_extraction() {
299
+ use spikard_bindings_shared::grpc_metadata::extract_metadata_to_hashmap;
300
+
301
+ let mut metadata = MetadataMap::new();
302
+ metadata.insert("content-type", "application/grpc".parse().unwrap());
303
+ metadata.insert("authorization", "Bearer token123".parse().unwrap());
304
+
305
+ let extracted = extract_metadata_to_hashmap(&metadata, false);
306
+ assert_eq!(extracted.get("content-type").unwrap(), "application/grpc");
307
+ assert_eq!(extracted.get("authorization").unwrap(), "Bearer token123");
308
+ }
309
+
310
+ #[test]
311
+ fn test_grpc_response_conversion() {
312
+ let response = RubyGrpcResponse {
313
+ payload: RefCell::new(b"test response".to_vec()),
314
+ metadata: RefCell::new(HashMap::new()),
315
+ };
316
+
317
+ let grpc_response = response.into_grpc_response();
318
+ assert!(grpc_response.is_ok());
319
+ let grpc_response = grpc_response.unwrap();
320
+ assert_eq!(grpc_response.payload, Bytes::from("test response"));
321
+ }
322
+
323
+ #[test]
324
+ fn test_grpc_response_with_metadata() {
325
+ let mut metadata = HashMap::new();
326
+ metadata.insert("x-custom-header".to_string(), "custom-value".to_string());
327
+
328
+ let response = RubyGrpcResponse {
329
+ payload: RefCell::new(b"test".to_vec()),
330
+ metadata: RefCell::new(metadata),
331
+ };
332
+
333
+ let grpc_response = response.into_grpc_response();
334
+ assert!(grpc_response.is_ok());
335
+ let grpc_response = grpc_response.unwrap();
336
+ assert!(!grpc_response.metadata.is_empty());
337
+ }
338
+
339
+ #[test]
340
+ fn test_invalid_metadata_key() {
341
+ let mut metadata = HashMap::new();
342
+ metadata.insert("invalid\nkey".to_string(), "value".to_string());
343
+
344
+ let response = RubyGrpcResponse {
345
+ payload: RefCell::new(b"test".to_vec()),
346
+ metadata: RefCell::new(metadata),
347
+ };
348
+
349
+ let result = response.into_grpc_response();
350
+ assert!(result.is_err());
351
+ }
352
+ }
@@ -0,0 +1,9 @@
1
+ //! Ruby gRPC bindings for Spikard
2
+ //!
3
+ //! This module provides a bridge between Ruby code and Spikard's gRPC runtime,
4
+ //! allowing Ruby handlers to process gRPC requests using protobuf serialization.
5
+
6
+ pub mod handler;
7
+
8
+ #[allow(unused_imports)]
9
+ pub use handler::{RubyGrpcHandler, RubyGrpcRequest, RubyGrpcResponse};
@@ -18,11 +18,13 @@
18
18
  //! - `lifecycle`: Lifecycle hook implementations
19
19
  //! - `sse`: Server-Sent Events support
20
20
  //! - `websocket`: WebSocket support
21
+ //! - `grpc`: gRPC handler support
21
22
 
22
23
  mod background;
23
24
  mod config;
24
25
  mod conversion;
25
26
  mod di;
27
+ mod grpc;
26
28
  mod gvl;
27
29
  mod handler;
28
30
  mod integration;
@@ -1645,12 +1647,14 @@ pub fn init(ruby: &Ruby) -> Result<(), Error> {
1645
1647
  let spikard_module = ruby.define_module("Spikard")?;
1646
1648
  testing::websocket::init(ruby, &spikard_module)?;
1647
1649
  testing::sse::init(ruby, &spikard_module)?;
1650
+ grpc::handler::init(ruby, &spikard_module)?;
1648
1651
 
1649
1652
  let _ = NativeBuiltResponse::mark as fn(&NativeBuiltResponse, &Marker);
1650
1653
  let _ = NativeLifecycleRegistry::mark as fn(&NativeLifecycleRegistry, &Marker);
1651
1654
  let _ = NativeDependencyRegistry::mark as fn(&NativeDependencyRegistry, &Marker);
1652
1655
  let _ = NativeRequest::mark as fn(&NativeRequest, &Marker);
1653
1656
  let _ = RubyHandler::mark as fn(&RubyHandler, &Marker);
1657
+ let _ = grpc::handler::RubyGrpcHandler::mark as fn(&grpc::handler::RubyGrpcHandler, &Marker);
1654
1658
  let _ = mark as fn(&NativeTestClient, &Marker);
1655
1659
 
1656
1660
  Ok(())
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb-macros"
3
- version = "0.7.4"
3
+ version = "0.8.0"
4
4
  edition = "2024"
5
5
  license = "MIT"
6
6
  publish = false
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spikard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Na'aman Hirschfeld
@@ -71,6 +71,7 @@ files:
71
71
  - lib/spikard/background.rb
72
72
  - lib/spikard/config.rb
73
73
  - lib/spikard/converters.rb
74
+ - lib/spikard/grpc.rb
74
75
  - lib/spikard/handler_wrapper.rb
75
76
  - lib/spikard/provide.rb
76
77
  - lib/spikard/response.rb
@@ -90,6 +91,7 @@ files:
90
91
  - vendor/crates/spikard-bindings-shared/src/conversion_traits.rs
91
92
  - vendor/crates/spikard-bindings-shared/src/di_traits.rs
92
93
  - vendor/crates/spikard-bindings-shared/src/error_response.rs
94
+ - vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs
93
95
  - vendor/crates/spikard-bindings-shared/src/handler_base.rs
94
96
  - vendor/crates/spikard-bindings-shared/src/lib.rs
95
97
  - vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs
@@ -146,6 +148,10 @@ files:
146
148
  - vendor/crates/spikard-http/src/cors.rs
147
149
  - vendor/crates/spikard-http/src/debug.rs
148
150
  - vendor/crates/spikard-http/src/di_handler.rs
151
+ - vendor/crates/spikard-http/src/grpc/handler.rs
152
+ - vendor/crates/spikard-http/src/grpc/mod.rs
153
+ - vendor/crates/spikard-http/src/grpc/service.rs
154
+ - vendor/crates/spikard-http/src/grpc/streaming.rs
149
155
  - vendor/crates/spikard-http/src/handler_response.rs
150
156
  - vendor/crates/spikard-http/src/handler_trait.rs
151
157
  - vendor/crates/spikard-http/src/handler_trait_tests.rs
@@ -167,6 +173,7 @@ files:
167
173
  - vendor/crates/spikard-http/src/openapi/spec_generation.rs
168
174
  - vendor/crates/spikard-http/src/query_parser.rs
169
175
  - vendor/crates/spikard-http/src/response.rs
176
+ - vendor/crates/spikard-http/src/server/grpc_routing.rs
170
177
  - vendor/crates/spikard-http/src/server/handler.rs
171
178
  - vendor/crates/spikard-http/src/server/lifecycle_execution.rs
172
179
  - vendor/crates/spikard-http/src/server/mod.rs
@@ -180,12 +187,17 @@ files:
180
187
  - vendor/crates/spikard-http/src/websocket.rs
181
188
  - vendor/crates/spikard-http/tests/auth_integration.rs
182
189
  - vendor/crates/spikard-http/tests/background_behavior.rs
190
+ - vendor/crates/spikard-http/tests/common/grpc_helpers.rs
183
191
  - vendor/crates/spikard-http/tests/common/handlers.rs
184
192
  - vendor/crates/spikard-http/tests/common/mod.rs
185
193
  - vendor/crates/spikard-http/tests/common/test_builders.rs
186
194
  - vendor/crates/spikard-http/tests/di_handler_error_responses.rs
187
195
  - vendor/crates/spikard-http/tests/di_integration.rs
188
196
  - vendor/crates/spikard-http/tests/doc_snippets.rs
197
+ - vendor/crates/spikard-http/tests/grpc_error_handling_test.rs
198
+ - vendor/crates/spikard-http/tests/grpc_integration_test.rs
199
+ - vendor/crates/spikard-http/tests/grpc_metadata_test.rs
200
+ - vendor/crates/spikard-http/tests/grpc_server_integration.rs
189
201
  - vendor/crates/spikard-http/tests/lifecycle_execution.rs
190
202
  - vendor/crates/spikard-http/tests/middleware_stack_integration.rs
191
203
  - vendor/crates/spikard-http/tests/multipart_behavior.rs
@@ -221,6 +233,8 @@ files:
221
233
  - vendor/crates/spikard-rb/src/conversion.rs
222
234
  - vendor/crates/spikard-rb/src/di/builder.rs
223
235
  - vendor/crates/spikard-rb/src/di/mod.rs
236
+ - vendor/crates/spikard-rb/src/grpc/handler.rs
237
+ - vendor/crates/spikard-rb/src/grpc/mod.rs
224
238
  - vendor/crates/spikard-rb/src/gvl.rs
225
239
  - vendor/crates/spikard-rb/src/handler.rs
226
240
  - vendor/crates/spikard-rb/src/integration/mod.rs