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,392 @@
1
+ //! Tonic service bridge
2
+ //!
3
+ //! This module bridges Tonic's service traits with our GrpcHandler trait.
4
+ //! It handles the conversion between Tonic's types and our internal representation,
5
+ //! enabling language-agnostic gRPC handling.
6
+
7
+ use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
8
+ use bytes::Bytes;
9
+ use std::sync::Arc;
10
+ use tonic::{Request, Response, Status};
11
+
12
+ /// Generic gRPC service that routes requests to a GrpcHandler
13
+ ///
14
+ /// This service implements Tonic's server traits and routes all requests
15
+ /// to the provided GrpcHandler implementation. It handles serialization
16
+ /// at the boundary between Tonic and our handler trait.
17
+ ///
18
+ /// # Example
19
+ ///
20
+ /// ```ignore
21
+ /// use spikard_http::grpc::service::GenericGrpcService;
22
+ /// use std::sync::Arc;
23
+ ///
24
+ /// let handler = Arc::new(MyGrpcHandler);
25
+ /// let service = GenericGrpcService::new(handler);
26
+ /// ```
27
+ pub struct GenericGrpcService {
28
+ handler: Arc<dyn GrpcHandler>,
29
+ }
30
+
31
+ impl GenericGrpcService {
32
+ /// Create a new generic gRPC service with the given handler
33
+ pub fn new(handler: Arc<dyn GrpcHandler>) -> Self {
34
+ Self { handler }
35
+ }
36
+
37
+ /// Handle a unary RPC call
38
+ ///
39
+ /// Converts the Tonic Request into our GrpcRequestData format,
40
+ /// calls the handler, and converts the result back to a Tonic Response.
41
+ ///
42
+ /// # Arguments
43
+ ///
44
+ /// * `service_name` - Fully qualified service name
45
+ /// * `method_name` - Method name
46
+ /// * `request` - Tonic request containing the serialized protobuf message
47
+ pub async fn handle_unary(
48
+ &self,
49
+ service_name: String,
50
+ method_name: String,
51
+ request: Request<Bytes>,
52
+ ) -> Result<Response<Bytes>, Status> {
53
+ // Extract metadata and payload from Tonic request
54
+ let (metadata, _extensions, payload) = request.into_parts();
55
+
56
+ // Create our internal request representation
57
+ let grpc_request = GrpcRequestData {
58
+ service_name,
59
+ method_name,
60
+ payload,
61
+ metadata,
62
+ };
63
+
64
+ // Call the handler
65
+ let result: GrpcHandlerResult = self.handler.call(grpc_request).await;
66
+
67
+ // Convert result to Tonic response
68
+ match result {
69
+ Ok(grpc_response) => {
70
+ let mut response = Response::new(grpc_response.payload);
71
+ copy_metadata(&grpc_response.metadata, response.metadata_mut());
72
+ Ok(response)
73
+ }
74
+ Err(status) => Err(status),
75
+ }
76
+ }
77
+
78
+ /// Get the service name from the handler
79
+ pub fn service_name(&self) -> &str {
80
+ self.handler.service_name()
81
+ }
82
+ }
83
+
84
+ /// Helper function to parse gRPC path into service and method names
85
+ ///
86
+ /// gRPC paths follow the format: `/<package>.<service>/<method>`
87
+ ///
88
+ /// # Example
89
+ ///
90
+ /// ```ignore
91
+ /// use spikard_http::grpc::service::parse_grpc_path;
92
+ ///
93
+ /// let (service, method) = parse_grpc_path("/mypackage.UserService/GetUser").unwrap();
94
+ /// assert_eq!(service, "mypackage.UserService");
95
+ /// assert_eq!(method, "GetUser");
96
+ /// ```
97
+ pub fn parse_grpc_path(path: &str) -> Result<(String, String), Status> {
98
+ // gRPC paths are in the format: /<package>.<service>/<method>
99
+ let path = path.trim_start_matches('/');
100
+ let parts: Vec<&str> = path.split('/').collect();
101
+
102
+ if parts.len() != 2 {
103
+ return Err(Status::invalid_argument(format!("Invalid gRPC path: {}", path)));
104
+ }
105
+
106
+ let service_name = parts[0].to_string();
107
+ let method_name = parts[1].to_string();
108
+
109
+ if service_name.is_empty() || method_name.is_empty() {
110
+ return Err(Status::invalid_argument("Service or method name is empty"));
111
+ }
112
+
113
+ Ok((service_name, method_name))
114
+ }
115
+
116
+ /// Check if a request is a gRPC request
117
+ ///
118
+ /// Checks the content-type header for "application/grpc" prefix.
119
+ ///
120
+ /// # Example
121
+ ///
122
+ /// ```ignore
123
+ /// use spikard_http::grpc::service::is_grpc_request;
124
+ /// use axum::http::HeaderMap;
125
+ ///
126
+ /// let mut headers = HeaderMap::new();
127
+ /// headers.insert("content-type", "application/grpc".parse().unwrap());
128
+ ///
129
+ /// assert!(is_grpc_request(&headers));
130
+ /// ```
131
+ pub fn is_grpc_request(headers: &axum::http::HeaderMap) -> bool {
132
+ headers
133
+ .get(axum::http::header::CONTENT_TYPE)
134
+ .and_then(|v| v.to_str().ok())
135
+ .map(|v| v.starts_with("application/grpc"))
136
+ .unwrap_or(false)
137
+ }
138
+
139
+ /// Copy metadata from source to destination MetadataMap
140
+ ///
141
+ /// Efficiently copies all metadata entries (both ASCII and binary)
142
+ /// from one MetadataMap to another without unnecessary allocations.
143
+ ///
144
+ /// # Arguments
145
+ ///
146
+ /// * `source` - Source metadata to copy from
147
+ /// * `dest` - Destination metadata to copy into
148
+ pub fn copy_metadata(source: &tonic::metadata::MetadataMap, dest: &mut tonic::metadata::MetadataMap) {
149
+ for key_value in source.iter() {
150
+ match key_value {
151
+ tonic::metadata::KeyAndValueRef::Ascii(key, value) => {
152
+ dest.insert(key, value.clone());
153
+ }
154
+ tonic::metadata::KeyAndValueRef::Binary(key, value) => {
155
+ dest.insert_bin(key, value.clone());
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ /// Convert GrpcResponseData to Tonic Response
162
+ ///
163
+ /// Helper function to convert our internal response representation
164
+ /// to a Tonic Response.
165
+ pub fn grpc_response_to_tonic(response: GrpcResponseData) -> Response<Bytes> {
166
+ let mut tonic_response = Response::new(response.payload);
167
+ copy_metadata(&response.metadata, tonic_response.metadata_mut());
168
+ tonic_response
169
+ }
170
+
171
+ #[cfg(test)]
172
+ mod tests {
173
+ use super::*;
174
+ use crate::grpc::handler::GrpcHandler;
175
+ use std::future::Future;
176
+ use std::pin::Pin;
177
+ use tonic::metadata::MetadataMap;
178
+
179
+ struct TestHandler;
180
+
181
+ impl GrpcHandler for TestHandler {
182
+ fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
183
+ Box::pin(async move {
184
+ // Echo back the request payload
185
+ Ok(GrpcResponseData {
186
+ payload: request.payload,
187
+ metadata: MetadataMap::new(),
188
+ })
189
+ })
190
+ }
191
+
192
+ fn service_name(&self) -> &'static str {
193
+ "test.TestService"
194
+ }
195
+ }
196
+
197
+ #[tokio::test]
198
+ async fn test_generic_grpc_service_handle_unary() {
199
+ let handler = Arc::new(TestHandler);
200
+ let service = GenericGrpcService::new(handler);
201
+
202
+ let request = Request::new(Bytes::from("test payload"));
203
+ let result = service.handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request).await;
204
+
205
+ assert!(result.is_ok());
206
+ let response = result.unwrap();
207
+ assert_eq!(response.into_inner(), Bytes::from("test payload"));
208
+ }
209
+
210
+ #[tokio::test]
211
+ async fn test_generic_grpc_service_with_metadata() {
212
+ let handler = Arc::new(TestHandler);
213
+ let service = GenericGrpcService::new(handler);
214
+
215
+ let mut request = Request::new(Bytes::from("payload"));
216
+ request
217
+ .metadata_mut()
218
+ .insert("custom-header", "custom-value".parse().unwrap());
219
+
220
+ let result = service.handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request).await;
221
+
222
+ assert!(result.is_ok());
223
+ }
224
+
225
+ #[test]
226
+ fn test_parse_grpc_path_valid() {
227
+ let (service, method) = parse_grpc_path("/mypackage.UserService/GetUser").unwrap();
228
+ assert_eq!(service, "mypackage.UserService");
229
+ assert_eq!(method, "GetUser");
230
+ }
231
+
232
+ #[test]
233
+ fn test_parse_grpc_path_with_nested_package() {
234
+ let (service, method) = parse_grpc_path("/com.example.api.v1.UserService/GetUser").unwrap();
235
+ assert_eq!(service, "com.example.api.v1.UserService");
236
+ assert_eq!(method, "GetUser");
237
+ }
238
+
239
+ #[test]
240
+ fn test_parse_grpc_path_invalid_format() {
241
+ let result = parse_grpc_path("/invalid");
242
+ assert!(result.is_err());
243
+ let status = result.unwrap_err();
244
+ assert_eq!(status.code(), tonic::Code::InvalidArgument);
245
+ }
246
+
247
+ #[test]
248
+ fn test_parse_grpc_path_empty_service() {
249
+ let result = parse_grpc_path("//Method");
250
+ assert!(result.is_err());
251
+ }
252
+
253
+ #[test]
254
+ fn test_parse_grpc_path_empty_method() {
255
+ let result = parse_grpc_path("/Service/");
256
+ assert!(result.is_err());
257
+ }
258
+
259
+ #[test]
260
+ fn test_parse_grpc_path_no_leading_slash() {
261
+ let (service, method) = parse_grpc_path("package.Service/Method").unwrap();
262
+ assert_eq!(service, "package.Service");
263
+ assert_eq!(method, "Method");
264
+ }
265
+
266
+ #[test]
267
+ fn test_is_grpc_request_valid() {
268
+ let mut headers = axum::http::HeaderMap::new();
269
+ headers.insert(
270
+ axum::http::header::CONTENT_TYPE,
271
+ "application/grpc".parse().unwrap(),
272
+ );
273
+ assert!(is_grpc_request(&headers));
274
+ }
275
+
276
+ #[test]
277
+ fn test_is_grpc_request_with_subtype() {
278
+ let mut headers = axum::http::HeaderMap::new();
279
+ headers.insert(
280
+ axum::http::header::CONTENT_TYPE,
281
+ "application/grpc+proto".parse().unwrap(),
282
+ );
283
+ assert!(is_grpc_request(&headers));
284
+ }
285
+
286
+ #[test]
287
+ fn test_is_grpc_request_not_grpc() {
288
+ let mut headers = axum::http::HeaderMap::new();
289
+ headers.insert(
290
+ axum::http::header::CONTENT_TYPE,
291
+ "application/json".parse().unwrap(),
292
+ );
293
+ assert!(!is_grpc_request(&headers));
294
+ }
295
+
296
+ #[test]
297
+ fn test_is_grpc_request_no_content_type() {
298
+ let headers = axum::http::HeaderMap::new();
299
+ assert!(!is_grpc_request(&headers));
300
+ }
301
+
302
+ #[test]
303
+ fn test_grpc_response_to_tonic_basic() {
304
+ let response = GrpcResponseData {
305
+ payload: Bytes::from("response"),
306
+ metadata: MetadataMap::new(),
307
+ };
308
+
309
+ let tonic_response = grpc_response_to_tonic(response);
310
+ assert_eq!(tonic_response.into_inner(), Bytes::from("response"));
311
+ }
312
+
313
+ #[test]
314
+ fn test_grpc_response_to_tonic_with_metadata() {
315
+ let mut metadata = MetadataMap::new();
316
+ metadata.insert("custom-header", "value".parse().unwrap());
317
+
318
+ let response = GrpcResponseData {
319
+ payload: Bytes::from("data"),
320
+ metadata,
321
+ };
322
+
323
+ let tonic_response = grpc_response_to_tonic(response);
324
+ assert_eq!(tonic_response.get_ref(), &Bytes::from("data"));
325
+ assert!(tonic_response.metadata().get("custom-header").is_some());
326
+ }
327
+
328
+ #[test]
329
+ fn test_generic_grpc_service_service_name() {
330
+ let handler = Arc::new(TestHandler);
331
+ let service = GenericGrpcService::new(handler);
332
+ assert_eq!(service.service_name(), "test.TestService");
333
+ }
334
+
335
+ #[test]
336
+ fn test_copy_metadata() {
337
+ let mut source = MetadataMap::new();
338
+ source.insert("key1", "value1".parse().unwrap());
339
+ source.insert("key2", "value2".parse().unwrap());
340
+
341
+ let mut dest = MetadataMap::new();
342
+ copy_metadata(&source, &mut dest);
343
+
344
+ assert_eq!(dest.get("key1").unwrap(), "value1");
345
+ assert_eq!(dest.get("key2").unwrap(), "value2");
346
+ }
347
+
348
+ #[test]
349
+ fn test_copy_metadata_empty() {
350
+ let source = MetadataMap::new();
351
+ let mut dest = MetadataMap::new();
352
+ copy_metadata(&source, &mut dest);
353
+ assert!(dest.is_empty());
354
+ }
355
+
356
+ #[test]
357
+ fn test_copy_metadata_binary() {
358
+ let mut source = MetadataMap::new();
359
+ source.insert_bin("binary-key-bin", tonic::metadata::MetadataValue::from_bytes(b"binary"));
360
+
361
+ let mut dest = MetadataMap::new();
362
+ copy_metadata(&source, &mut dest);
363
+
364
+ assert!(dest.get_bin("binary-key-bin").is_some());
365
+ }
366
+
367
+ #[tokio::test]
368
+ async fn test_generic_grpc_service_error_handling() {
369
+ struct ErrorHandler;
370
+
371
+ impl GrpcHandler for ErrorHandler {
372
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
373
+ Box::pin(async { Err(Status::not_found("Resource not found")) })
374
+ }
375
+
376
+ fn service_name(&self) -> &'static str {
377
+ "test.ErrorService"
378
+ }
379
+ }
380
+
381
+ let handler = Arc::new(ErrorHandler);
382
+ let service = GenericGrpcService::new(handler);
383
+
384
+ let request = Request::new(Bytes::new());
385
+ let result = service.handle_unary("test.ErrorService".to_string(), "ErrorMethod".to_string(), request).await;
386
+
387
+ assert!(result.is_err());
388
+ let status = result.unwrap_err();
389
+ assert_eq!(status.code(), tonic::Code::NotFound);
390
+ assert_eq!(status.message(), "Resource not found");
391
+ }
392
+ }
@@ -0,0 +1,237 @@
1
+ //! Streaming support utilities for gRPC
2
+ //!
3
+ //! This module provides utilities for handling streaming RPCs:
4
+ //! - Client streaming (receiving stream of messages)
5
+ //! - Server streaming (sending stream of messages)
6
+ //! - Bidirectional streaming (both directions)
7
+
8
+ use bytes::Bytes;
9
+ use futures_util::Stream;
10
+ use std::pin::Pin;
11
+ use tonic::Status;
12
+
13
+ /// Type alias for a stream of protobuf message bytes
14
+ ///
15
+ /// Used for both client streaming (incoming) and server streaming (outgoing).
16
+ /// Each item in the stream is either:
17
+ /// - Ok(Bytes): A serialized protobuf message
18
+ /// - Err(Status): A gRPC error
19
+ pub type MessageStream = Pin<Box<dyn Stream<Item = Result<Bytes, Status>> + Send>>;
20
+
21
+ /// Request for client streaming RPC
22
+ ///
23
+ /// Contains metadata and a stream of incoming messages from the client.
24
+ pub struct StreamingRequest {
25
+ /// Service name
26
+ pub service_name: String,
27
+ /// Method name
28
+ pub method_name: String,
29
+ /// Stream of incoming protobuf messages
30
+ pub message_stream: MessageStream,
31
+ /// Request metadata
32
+ pub metadata: tonic::metadata::MetadataMap,
33
+ }
34
+
35
+ /// Response for server streaming RPC
36
+ ///
37
+ /// Contains metadata and a stream of outgoing messages to the client.
38
+ pub struct StreamingResponse {
39
+ /// Stream of outgoing protobuf messages
40
+ pub message_stream: MessageStream,
41
+ /// Response metadata
42
+ pub metadata: tonic::metadata::MetadataMap,
43
+ }
44
+
45
+ /// Helper to create a message stream from a vector of bytes
46
+ ///
47
+ /// Useful for testing and for handlers that want to create a stream
48
+ /// from a fixed set of messages.
49
+ ///
50
+ /// # Example
51
+ ///
52
+ /// ```ignore
53
+ /// use spikard_http::grpc::streaming::message_stream_from_vec;
54
+ /// use bytes::Bytes;
55
+ ///
56
+ /// let messages = vec![
57
+ /// Bytes::from("message1"),
58
+ /// Bytes::from("message2"),
59
+ /// ];
60
+ ///
61
+ /// let stream = message_stream_from_vec(messages);
62
+ /// ```
63
+ pub fn message_stream_from_vec(messages: Vec<Bytes>) -> MessageStream {
64
+ Box::pin(futures_util::stream::iter(messages.into_iter().map(Ok)))
65
+ }
66
+
67
+ /// Helper to create an empty message stream
68
+ ///
69
+ /// Useful for testing or for handlers that need to return an empty stream.
70
+ pub fn empty_message_stream() -> MessageStream {
71
+ Box::pin(futures_util::stream::empty())
72
+ }
73
+
74
+ /// Helper to create a single-message stream
75
+ ///
76
+ /// Useful for converting unary responses to streaming responses.
77
+ ///
78
+ /// # Example
79
+ ///
80
+ /// ```ignore
81
+ /// use spikard_http::grpc::streaming::single_message_stream;
82
+ /// use bytes::Bytes;
83
+ ///
84
+ /// let stream = single_message_stream(Bytes::from("response"));
85
+ /// ```
86
+ pub fn single_message_stream(message: Bytes) -> MessageStream {
87
+ Box::pin(futures_util::stream::once(async move { Ok(message) }))
88
+ }
89
+
90
+ /// Helper to create an error stream
91
+ ///
92
+ /// Returns a stream that immediately yields a gRPC error.
93
+ ///
94
+ /// # Example
95
+ ///
96
+ /// ```ignore
97
+ /// use spikard_http::grpc::streaming::error_stream;
98
+ /// use tonic::Status;
99
+ ///
100
+ /// let stream = error_stream(Status::internal("Something went wrong"));
101
+ /// ```
102
+ pub fn error_stream(status: Status) -> MessageStream {
103
+ Box::pin(futures_util::stream::once(async move { Err(status) }))
104
+ }
105
+
106
+ /// Helper to convert a Tonic ReceiverStream to our MessageStream
107
+ ///
108
+ /// This is used in the service bridge to convert Tonic's streaming types
109
+ /// to our internal representation.
110
+ pub fn from_tonic_stream<S>(stream: S) -> MessageStream
111
+ where
112
+ S: Stream<Item = Result<Bytes, Status>> + Send + 'static,
113
+ {
114
+ Box::pin(stream)
115
+ }
116
+
117
+ #[cfg(test)]
118
+ mod tests {
119
+ use super::*;
120
+ use futures_util::StreamExt;
121
+
122
+ #[tokio::test]
123
+ async fn test_message_stream_from_vec() {
124
+ let messages = vec![Bytes::from("msg1"), Bytes::from("msg2"), Bytes::from("msg3")];
125
+
126
+ let mut stream = message_stream_from_vec(messages.clone());
127
+
128
+ let msg1 = stream.next().await.unwrap().unwrap();
129
+ assert_eq!(msg1, Bytes::from("msg1"));
130
+
131
+ let msg2 = stream.next().await.unwrap().unwrap();
132
+ assert_eq!(msg2, Bytes::from("msg2"));
133
+
134
+ let msg3 = stream.next().await.unwrap().unwrap();
135
+ assert_eq!(msg3, Bytes::from("msg3"));
136
+
137
+ assert!(stream.next().await.is_none());
138
+ }
139
+
140
+ #[tokio::test]
141
+ async fn test_empty_message_stream() {
142
+ let mut stream = empty_message_stream();
143
+ assert!(stream.next().await.is_none());
144
+ }
145
+
146
+ #[tokio::test]
147
+ async fn test_single_message_stream() {
148
+ let mut stream = single_message_stream(Bytes::from("single"));
149
+
150
+ let msg = stream.next().await.unwrap().unwrap();
151
+ assert_eq!(msg, Bytes::from("single"));
152
+
153
+ assert!(stream.next().await.is_none());
154
+ }
155
+
156
+ #[tokio::test]
157
+ async fn test_error_stream() {
158
+ let mut stream = error_stream(Status::internal("test error"));
159
+
160
+ let result = stream.next().await.unwrap();
161
+ assert!(result.is_err());
162
+
163
+ let error = result.unwrap_err();
164
+ assert_eq!(error.code(), tonic::Code::Internal);
165
+ assert_eq!(error.message(), "test error");
166
+
167
+ assert!(stream.next().await.is_none());
168
+ }
169
+
170
+ #[tokio::test]
171
+ async fn test_message_stream_from_vec_empty() {
172
+ let messages: Vec<Bytes> = vec![];
173
+ let mut stream = message_stream_from_vec(messages);
174
+ assert!(stream.next().await.is_none());
175
+ }
176
+
177
+ #[tokio::test]
178
+ async fn test_message_stream_from_vec_large() {
179
+ let mut messages = vec![];
180
+ for i in 0..100 {
181
+ messages.push(Bytes::from(format!("message{}", i)));
182
+ }
183
+
184
+ let mut stream = message_stream_from_vec(messages);
185
+
186
+ for i in 0..100 {
187
+ let msg = stream.next().await.unwrap().unwrap();
188
+ assert_eq!(msg, Bytes::from(format!("message{}", i)));
189
+ }
190
+
191
+ assert!(stream.next().await.is_none());
192
+ }
193
+
194
+ #[tokio::test]
195
+ async fn test_from_tonic_stream() {
196
+ let messages = vec![Ok(Bytes::from("a")), Ok(Bytes::from("b")), Err(Status::cancelled("done"))];
197
+
198
+ let tonic_stream = futures_util::stream::iter(messages);
199
+ let mut stream = from_tonic_stream(tonic_stream);
200
+
201
+ let msg1 = stream.next().await.unwrap().unwrap();
202
+ assert_eq!(msg1, Bytes::from("a"));
203
+
204
+ let msg2 = stream.next().await.unwrap().unwrap();
205
+ assert_eq!(msg2, Bytes::from("b"));
206
+
207
+ let result = stream.next().await.unwrap();
208
+ assert!(result.is_err());
209
+
210
+ assert!(stream.next().await.is_none());
211
+ }
212
+
213
+ #[test]
214
+ fn test_streaming_request_creation() {
215
+ let stream = empty_message_stream();
216
+ let request = StreamingRequest {
217
+ service_name: "test.Service".to_string(),
218
+ method_name: "StreamMethod".to_string(),
219
+ message_stream: stream,
220
+ metadata: tonic::metadata::MetadataMap::new(),
221
+ };
222
+
223
+ assert_eq!(request.service_name, "test.Service");
224
+ assert_eq!(request.method_name, "StreamMethod");
225
+ }
226
+
227
+ #[test]
228
+ fn test_streaming_response_creation() {
229
+ let stream = empty_message_stream();
230
+ let response = StreamingResponse {
231
+ message_stream: stream,
232
+ metadata: tonic::metadata::MetadataMap::new(),
233
+ };
234
+
235
+ assert!(response.metadata.is_empty());
236
+ }
237
+ }
@@ -13,6 +13,7 @@ pub mod cors;
13
13
  pub mod debug;
14
14
  #[cfg(feature = "di")]
15
15
  pub mod di_handler;
16
+ pub mod grpc;
16
17
  pub mod handler_response;
17
18
  pub mod handler_trait;
18
19
  pub mod jsonrpc;
@@ -39,6 +40,10 @@ pub use background::{
39
40
  pub use body_metadata::ResponseBodySize;
40
41
  #[cfg(feature = "di")]
41
42
  pub use di_handler::DependencyInjectingHandler;
43
+ pub use grpc::{
44
+ GrpcConfig, GrpcHandler, GrpcHandlerResult, GrpcRegistry, GrpcRequestData, GrpcResponseData, MessageStream,
45
+ StreamingRequest, StreamingResponse,
46
+ };
42
47
  pub use handler_response::HandlerResponse;
43
48
  pub use handler_trait::{Handler, HandlerResult, RequestData, ValidatedParams};
44
49
  pub use jsonrpc::JsonRpcConfig;
@@ -140,6 +145,8 @@ pub struct ServerConfig {
140
145
  pub openapi: Option<crate::openapi::OpenApiConfig>,
141
146
  /// JSON-RPC configuration
142
147
  pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
148
+ /// gRPC configuration
149
+ pub grpc: Option<crate::grpc::GrpcConfig>,
143
150
  /// Lifecycle hooks for request/response processing
144
151
  pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
145
152
  /// Background task executor configuration
@@ -169,6 +176,7 @@ impl Default for ServerConfig {
169
176
  shutdown_timeout: 30,
170
177
  openapi: None,
171
178
  jsonrpc: None,
179
+ grpc: None,
172
180
  lifecycle_hooks: None,
173
181
  background_tasks: BackgroundTaskConfig::default(),
174
182
  enable_http_trace: false,
@@ -339,6 +347,12 @@ impl ServerConfigBuilder {
339
347
  self
340
348
  }
341
349
 
350
+ /// Set gRPC configuration
351
+ pub fn grpc(mut self, grpc: Option<crate::grpc::GrpcConfig>) -> Self {
352
+ self.config.grpc = grpc;
353
+ self
354
+ }
355
+
342
356
  /// Set lifecycle hooks for request/response processing
343
357
  pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
344
358
  self.config.lifecycle_hooks = hooks;