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.
Files changed (106) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +19 -10
  3. data/ext/spikard_rb/Cargo.lock +234 -162
  4. data/ext/spikard_rb/Cargo.toml +2 -2
  5. data/ext/spikard_rb/extconf.rb +4 -3
  6. data/lib/spikard/config.rb +88 -12
  7. data/lib/spikard/testing.rb +3 -1
  8. data/lib/spikard/version.rb +1 -1
  9. data/lib/spikard.rb +11 -0
  10. data/vendor/crates/spikard-bindings-shared/Cargo.toml +3 -6
  11. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +8 -8
  12. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +2 -2
  13. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +4 -4
  14. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +10 -4
  15. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +3 -3
  16. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +10 -5
  17. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +829 -0
  18. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +587 -0
  19. data/vendor/crates/spikard-bindings-shared/src/lib.rs +7 -0
  20. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +11 -11
  21. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +9 -37
  22. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +436 -3
  23. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +944 -0
  24. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +4 -4
  25. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +3 -2
  26. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +13 -13
  27. data/vendor/crates/spikard-bindings-shared/tests/{comprehensive_coverage.rs → full_coverage.rs} +10 -5
  28. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +14 -14
  29. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +669 -0
  30. data/vendor/crates/spikard-core/Cargo.toml +3 -3
  31. data/vendor/crates/spikard-core/src/di/container.rs +1 -1
  32. data/vendor/crates/spikard-core/src/di/factory.rs +2 -2
  33. data/vendor/crates/spikard-core/src/di/resolved.rs +2 -2
  34. data/vendor/crates/spikard-core/src/di/value.rs +1 -1
  35. data/vendor/crates/spikard-core/src/http.rs +75 -0
  36. data/vendor/crates/spikard-core/src/lifecycle.rs +43 -43
  37. data/vendor/crates/spikard-core/src/parameters.rs +14 -19
  38. data/vendor/crates/spikard-core/src/problem.rs +1 -1
  39. data/vendor/crates/spikard-core/src/request_data.rs +7 -16
  40. data/vendor/crates/spikard-core/src/router.rs +6 -0
  41. data/vendor/crates/spikard-core/src/schema_registry.rs +2 -3
  42. data/vendor/crates/spikard-core/src/type_hints.rs +3 -2
  43. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +1 -1
  44. data/vendor/crates/spikard-core/src/validation/mod.rs +1 -1
  45. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +1 -1
  46. data/vendor/crates/spikard-core/tests/error_mapper.rs +2 -2
  47. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +1 -1
  48. data/vendor/crates/spikard-core/tests/parameters_full.rs +1 -1
  49. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +1 -1
  50. data/vendor/crates/spikard-core/tests/validation_coverage.rs +4 -4
  51. data/vendor/crates/spikard-http/Cargo.toml +4 -2
  52. data/vendor/crates/spikard-http/src/cors.rs +32 -11
  53. data/vendor/crates/spikard-http/src/di_handler.rs +12 -8
  54. data/vendor/crates/spikard-http/src/grpc/framing.rs +469 -0
  55. data/vendor/crates/spikard-http/src/grpc/handler.rs +887 -25
  56. data/vendor/crates/spikard-http/src/grpc/mod.rs +114 -22
  57. data/vendor/crates/spikard-http/src/grpc/service.rs +232 -2
  58. data/vendor/crates/spikard-http/src/grpc/streaming.rs +80 -2
  59. data/vendor/crates/spikard-http/src/handler_trait.rs +204 -27
  60. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +15 -15
  61. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +2 -2
  62. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +2 -2
  63. data/vendor/crates/spikard-http/src/lib.rs +1 -1
  64. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +2 -2
  65. data/vendor/crates/spikard-http/src/lifecycle.rs +4 -4
  66. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +2 -0
  67. data/vendor/crates/spikard-http/src/server/fast_router.rs +186 -0
  68. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +324 -23
  69. data/vendor/crates/spikard-http/src/server/handler.rs +33 -22
  70. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +21 -2
  71. data/vendor/crates/spikard-http/src/server/mod.rs +125 -20
  72. data/vendor/crates/spikard-http/src/server/request_extraction.rs +126 -44
  73. data/vendor/crates/spikard-http/src/server/routing_factory.rs +80 -69
  74. data/vendor/crates/spikard-http/tests/common/handlers.rs +2 -2
  75. data/vendor/crates/spikard-http/tests/common/test_builders.rs +12 -12
  76. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +2 -2
  77. data/vendor/crates/spikard-http/tests/di_integration.rs +6 -6
  78. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +430 -0
  79. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +738 -0
  80. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +13 -9
  81. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +974 -0
  82. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +2 -2
  83. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +4 -4
  84. data/vendor/crates/spikard-http/tests/server_config_builder.rs +2 -2
  85. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +1 -0
  86. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +140 -0
  87. data/vendor/crates/spikard-rb/Cargo.toml +3 -1
  88. data/vendor/crates/spikard-rb/src/conversion.rs +138 -4
  89. data/vendor/crates/spikard-rb/src/grpc/handler.rs +706 -229
  90. data/vendor/crates/spikard-rb/src/grpc/mod.rs +6 -2
  91. data/vendor/crates/spikard-rb/src/gvl.rs +2 -2
  92. data/vendor/crates/spikard-rb/src/handler.rs +169 -91
  93. data/vendor/crates/spikard-rb/src/lib.rs +444 -62
  94. data/vendor/crates/spikard-rb/src/lifecycle.rs +29 -1
  95. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +108 -43
  96. data/vendor/crates/spikard-rb/src/request.rs +117 -20
  97. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +52 -25
  98. data/vendor/crates/spikard-rb/src/server.rs +23 -14
  99. data/vendor/crates/spikard-rb/src/testing/client.rs +5 -4
  100. data/vendor/crates/spikard-rb/src/testing/sse.rs +1 -36
  101. data/vendor/crates/spikard-rb/src/testing/websocket.rs +3 -38
  102. data/vendor/crates/spikard-rb/src/websocket.rs +32 -23
  103. data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
  104. metadata +14 -4
  105. data/vendor/bundle/ruby/3.4.0/gems/diff-lcs-1.6.2/mise.toml +0 -5
  106. data/vendor/bundle/ruby/3.4.0/gems/rake-compiler-dock-1.10.0/build/buildkitd.toml +0 -2
@@ -282,10 +282,10 @@ impl RequestBuilder {
282
282
 
283
283
  let request_data = RequestData {
284
284
  path_params: Arc::new(HashMap::new()),
285
- query_params: build_query_json(&self.query_params),
285
+ query_params: Arc::new(build_query_json(&self.query_params)),
286
286
  validated_params: None,
287
287
  raw_query_params: Arc::new(self.query_params),
288
- body: self.body,
288
+ body: Arc::new(self.body),
289
289
  raw_body: None,
290
290
  headers: Arc::new(self.headers),
291
291
  cookies: Arc::new(self.cookies),
@@ -409,10 +409,10 @@ mod tests {
409
409
  let request = Request::builder().body(Body::empty()).unwrap();
410
410
  let request_data = RequestData {
411
411
  path_params: Arc::new(HashMap::new()),
412
- query_params: json!({}),
412
+ query_params: Arc::new(json!({})),
413
413
  validated_params: None,
414
414
  raw_query_params: Arc::new(HashMap::new()),
415
- body: json!(null),
415
+ body: Arc::new(json!(null)),
416
416
  raw_body: None,
417
417
  headers: Arc::new(HashMap::new()),
418
418
  cookies: Arc::new(HashMap::new()),
@@ -435,10 +435,10 @@ mod tests {
435
435
  let request = Request::builder().body(Body::empty()).unwrap();
436
436
  let request_data = RequestData {
437
437
  path_params: Arc::new(HashMap::new()),
438
- query_params: json!({}),
438
+ query_params: Arc::new(json!({})),
439
439
  validated_params: None,
440
440
  raw_query_params: Arc::new(HashMap::new()),
441
- body: json!(null),
441
+ body: Arc::new(json!(null)),
442
442
  raw_body: None,
443
443
  headers: Arc::new(HashMap::new()),
444
444
  cookies: Arc::new(HashMap::new()),
@@ -463,10 +463,10 @@ mod tests {
463
463
  let request = Request::builder().body(Body::empty()).unwrap();
464
464
  let request_data = RequestData {
465
465
  path_params: Arc::new(HashMap::new()),
466
- query_params: json!({}),
466
+ query_params: Arc::new(json!({})),
467
467
  validated_params: None,
468
468
  raw_query_params: Arc::new(HashMap::new()),
469
- body: json!(null),
469
+ body: Arc::new(json!(null)),
470
470
  raw_body: None,
471
471
  headers: Arc::new(HashMap::new()),
472
472
  cookies: Arc::new(HashMap::new()),
@@ -488,10 +488,10 @@ mod tests {
488
488
  let request = Request::builder().body(Body::empty()).unwrap();
489
489
  let request_data = RequestData {
490
490
  path_params: Arc::new(HashMap::new()),
491
- query_params: json!({}),
491
+ query_params: Arc::new(json!({})),
492
492
  validated_params: None,
493
493
  raw_query_params: Arc::new(HashMap::new()),
494
- body: json!(null),
494
+ body: Arc::new(json!(null)),
495
495
  raw_body: None,
496
496
  headers: Arc::new(HashMap::new()),
497
497
  cookies: Arc::new(HashMap::new()),
@@ -527,7 +527,7 @@ mod tests {
527
527
 
528
528
  assert_eq!(request.method(), &Method::POST);
529
529
  assert_eq!(request_data.path, "/users");
530
- assert_eq!(request_data.body, body);
530
+ assert_eq!(*request_data.body, body);
531
531
  }
532
532
 
533
533
  #[test]
@@ -609,7 +609,7 @@ mod tests {
609
609
 
610
610
  assert_eq!(request_data.method, "PUT");
611
611
  assert_eq!(request_data.path, "/users/42");
612
- assert_eq!(request_data.body, body);
612
+ assert_eq!(*request_data.body, body);
613
613
  assert_eq!(
614
614
  request_data.headers.get("authorization"),
615
615
  Some(&"Bearer abc123".to_string())
@@ -26,10 +26,10 @@ impl Handler for OkHandler {
26
26
  fn minimal_request_data() -> RequestData {
27
27
  RequestData {
28
28
  path_params: Arc::new(HashMap::new()),
29
- query_params: serde_json::json!({}),
29
+ query_params: Arc::new(serde_json::json!({})),
30
30
  validated_params: None,
31
31
  raw_query_params: Arc::new(HashMap::new()),
32
- body: serde_json::Value::Null,
32
+ body: Arc::new(serde_json::Value::Null),
33
33
  raw_body: None,
34
34
  headers: Arc::new(HashMap::new()),
35
35
  cookies: Arc::new(HashMap::new()),
@@ -73,10 +73,10 @@ async fn test_di_value_injection() {
73
73
  let request = Request::builder().body(Body::empty()).unwrap();
74
74
  let request_data = RequestData {
75
75
  path_params: Arc::new(HashMap::new()),
76
- query_params: serde_json::Value::Null,
76
+ query_params: Arc::new(serde_json::Value::Null),
77
77
  validated_params: None,
78
78
  raw_query_params: Arc::new(HashMap::new()),
79
- body: serde_json::Value::Null,
79
+ body: Arc::new(serde_json::Value::Null),
80
80
  raw_body: None,
81
81
  headers: Arc::new(HashMap::new()),
82
82
  cookies: Arc::new(HashMap::new()),
@@ -115,10 +115,10 @@ async fn test_di_missing_dependency_error() {
115
115
  let request = Request::builder().body(Body::empty()).unwrap();
116
116
  let request_data = RequestData {
117
117
  path_params: Arc::new(HashMap::new()),
118
- query_params: serde_json::Value::Null,
118
+ query_params: Arc::new(serde_json::Value::Null),
119
119
  validated_params: None,
120
120
  raw_query_params: Arc::new(HashMap::new()),
121
- body: serde_json::Value::Null,
121
+ body: Arc::new(serde_json::Value::Null),
122
122
  raw_body: None,
123
123
  headers: Arc::new(HashMap::new()),
124
124
  cookies: Arc::new(HashMap::new()),
@@ -162,10 +162,10 @@ async fn test_di_multiple_value_dependencies() {
162
162
  let request = Request::builder().body(Body::empty()).unwrap();
163
163
  let request_data = RequestData {
164
164
  path_params: Arc::new(HashMap::new()),
165
- query_params: serde_json::Value::Null,
165
+ query_params: Arc::new(serde_json::Value::Null),
166
166
  validated_params: None,
167
167
  raw_query_params: Arc::new(HashMap::new()),
168
- body: serde_json::Value::Null,
168
+ body: Arc::new(serde_json::Value::Null),
169
169
  raw_body: None,
170
170
  headers: Arc::new(HashMap::new()),
171
171
  cookies: Arc::new(HashMap::new()),
@@ -0,0 +1,430 @@
1
+ //! Integration tests for gRPC bidirectional streaming
2
+ //!
3
+ //! Tests end-to-end bidirectional streaming functionality through GenericGrpcService
4
+ //! and grpc_routing, including:
5
+ //! - Echo service with bidirectional messages
6
+ //! - Message transformation during streaming
7
+ //! - Error handling in request and response streams
8
+ //! - Empty streams
9
+ //! - Large payloads (100+ messages in both directions)
10
+ //! - Message ordering preservation
11
+ //! - Concurrent bidirectional streaming requests
12
+ #![allow(
13
+ clippy::doc_markdown,
14
+ clippy::uninlined_format_args,
15
+ clippy::redundant_closure_for_method_calls,
16
+ reason = "Integration test for streaming with many test cases"
17
+ )]
18
+
19
+ use bytes::Bytes;
20
+ use futures_util::StreamExt;
21
+ use spikard_http::grpc::streaming::StreamingRequest;
22
+ use spikard_http::grpc::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, RpcMode};
23
+ use std::future::Future;
24
+ use std::pin::Pin;
25
+ use tonic::metadata::MetadataMap;
26
+
27
+ // ============================================================================
28
+ // Test Handlers
29
+ // ============================================================================
30
+
31
+ /// Handler that echoes back messages from bidirectional stream
32
+ struct EchoBidiHandler;
33
+
34
+ impl GrpcHandler for EchoBidiHandler {
35
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
36
+ Box::pin(async { Err(tonic::Status::unimplemented("Use call_bidi_stream for streaming")) })
37
+ }
38
+
39
+ fn service_name(&self) -> &'static str {
40
+ "integration.EchoService"
41
+ }
42
+
43
+ fn rpc_mode(&self) -> RpcMode {
44
+ RpcMode::BidirectionalStreaming
45
+ }
46
+
47
+ fn call_bidi_stream(
48
+ &self,
49
+ request: StreamingRequest,
50
+ ) -> Pin<Box<dyn Future<Output = Result<spikard_http::grpc::streaming::MessageStream, tonic::Status>> + Send>> {
51
+ Box::pin(async move {
52
+ let mut messages = Vec::new();
53
+ let mut stream = request.message_stream;
54
+
55
+ while let Some(msg_result) = stream.next().await {
56
+ match msg_result {
57
+ Ok(msg) => messages.push(msg),
58
+ Err(e) => return Err(e),
59
+ }
60
+ }
61
+
62
+ Ok(spikard_http::grpc::streaming::message_stream_from_vec(messages))
63
+ })
64
+ }
65
+ }
66
+
67
+ /// Handler that transforms messages (uppercase) in bidirectional stream
68
+ struct TransformBidiHandler;
69
+
70
+ impl GrpcHandler for TransformBidiHandler {
71
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
72
+ Box::pin(async { Err(tonic::Status::unimplemented("Use call_bidi_stream for streaming")) })
73
+ }
74
+
75
+ fn service_name(&self) -> &'static str {
76
+ "integration.TransformService"
77
+ }
78
+
79
+ fn rpc_mode(&self) -> RpcMode {
80
+ RpcMode::BidirectionalStreaming
81
+ }
82
+
83
+ fn call_bidi_stream(
84
+ &self,
85
+ request: StreamingRequest,
86
+ ) -> Pin<Box<dyn Future<Output = Result<spikard_http::grpc::streaming::MessageStream, tonic::Status>> + Send>> {
87
+ Box::pin(async move {
88
+ let mut messages = Vec::new();
89
+ let mut stream = request.message_stream;
90
+
91
+ while let Some(msg_result) = stream.next().await {
92
+ match msg_result {
93
+ Ok(msg) => {
94
+ let text = String::from_utf8_lossy(&msg).to_uppercase();
95
+ messages.push(Bytes::from(text));
96
+ }
97
+ Err(e) => return Err(e),
98
+ }
99
+ }
100
+
101
+ Ok(spikard_http::grpc::streaming::message_stream_from_vec(messages))
102
+ })
103
+ }
104
+ }
105
+
106
+ // ============================================================================
107
+ // Integration Tests
108
+ // ============================================================================
109
+
110
+ #[tokio::test]
111
+ async fn test_integration_bidi_echo() {
112
+ let handler = EchoBidiHandler;
113
+
114
+ let messages = vec![Bytes::from("hello"), Bytes::from("world"), Bytes::from("test")];
115
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages.clone());
116
+ let request = StreamingRequest {
117
+ service_name: "integration.EchoService".to_string(),
118
+ method_name: "Echo".to_string(),
119
+ message_stream: stream,
120
+ metadata: MetadataMap::new(),
121
+ };
122
+
123
+ let result = handler.call_bidi_stream(request).await;
124
+ assert!(result.is_ok());
125
+
126
+ let mut response_stream = result.unwrap();
127
+ let mut responses = Vec::new();
128
+
129
+ while let Some(msg_result) = response_stream.next().await {
130
+ match msg_result {
131
+ Ok(msg) => responses.push(msg),
132
+ Err(_) => break,
133
+ }
134
+ }
135
+
136
+ assert_eq!(responses.len(), 3);
137
+ assert_eq!(responses, messages);
138
+ }
139
+
140
+ #[tokio::test]
141
+ async fn test_integration_bidi_transform() {
142
+ let handler = TransformBidiHandler;
143
+
144
+ let messages = vec![Bytes::from("hello"), Bytes::from("world")];
145
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages);
146
+ let request = StreamingRequest {
147
+ service_name: "integration.TransformService".to_string(),
148
+ method_name: "Transform".to_string(),
149
+ message_stream: stream,
150
+ metadata: MetadataMap::new(),
151
+ };
152
+
153
+ let result = handler.call_bidi_stream(request).await;
154
+ assert!(result.is_ok());
155
+
156
+ let mut response_stream = result.unwrap();
157
+ let mut responses = Vec::new();
158
+
159
+ while let Some(msg_result) = response_stream.next().await {
160
+ match msg_result {
161
+ Ok(msg) => responses.push(String::from_utf8_lossy(&msg).to_string()),
162
+ Err(_) => break,
163
+ }
164
+ }
165
+
166
+ assert_eq!(responses.len(), 2);
167
+ assert_eq!(responses[0], "HELLO");
168
+ assert_eq!(responses[1], "WORLD");
169
+ }
170
+
171
+ #[tokio::test]
172
+ async fn test_integration_bidi_empty_stream() {
173
+ let handler = EchoBidiHandler;
174
+
175
+ let messages: Vec<Bytes> = vec![];
176
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages);
177
+ let request = StreamingRequest {
178
+ service_name: "integration.EchoService".to_string(),
179
+ method_name: "Echo".to_string(),
180
+ message_stream: stream,
181
+ metadata: MetadataMap::new(),
182
+ };
183
+
184
+ let result = handler.call_bidi_stream(request).await;
185
+ assert!(result.is_ok());
186
+
187
+ let mut response_stream = result.unwrap();
188
+ let mut responses = Vec::new();
189
+
190
+ while let Some(msg_result) = response_stream.next().await {
191
+ match msg_result {
192
+ Ok(msg) => responses.push(msg),
193
+ Err(_) => break,
194
+ }
195
+ }
196
+
197
+ assert_eq!(responses.len(), 0);
198
+ }
199
+
200
+ #[tokio::test]
201
+ async fn test_integration_bidi_large_stream() {
202
+ let handler = EchoBidiHandler;
203
+
204
+ // Create a large stream of messages (100+)
205
+ let messages: Vec<Bytes> = (0..150).map(|i| Bytes::from(format!("msg_{}", i))).collect();
206
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages);
207
+ let request = StreamingRequest {
208
+ service_name: "integration.EchoService".to_string(),
209
+ method_name: "Echo".to_string(),
210
+ message_stream: stream,
211
+ metadata: MetadataMap::new(),
212
+ };
213
+
214
+ let result = handler.call_bidi_stream(request).await;
215
+ assert!(result.is_ok());
216
+
217
+ let mut response_stream = result.unwrap();
218
+ let mut responses = Vec::new();
219
+
220
+ while let Some(msg_result) = response_stream.next().await {
221
+ match msg_result {
222
+ Ok(msg) => responses.push(msg),
223
+ Err(_) => break,
224
+ }
225
+ }
226
+
227
+ assert_eq!(responses.len(), 150);
228
+ }
229
+
230
+ #[tokio::test]
231
+ async fn test_integration_bidi_metadata_propagation() {
232
+ let handler = EchoBidiHandler;
233
+
234
+ let mut metadata = MetadataMap::new();
235
+ metadata.insert("x-request-id", "bidi-001".parse().unwrap());
236
+ metadata.insert("x-custom", "value".parse().unwrap());
237
+
238
+ let messages = vec![Bytes::from("test")];
239
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages);
240
+ let request = StreamingRequest {
241
+ service_name: "integration.EchoService".to_string(),
242
+ method_name: "Echo".to_string(),
243
+ message_stream: stream,
244
+ metadata,
245
+ };
246
+
247
+ let result = handler.call_bidi_stream(request).await;
248
+ assert!(result.is_ok());
249
+
250
+ let mut response_stream = result.unwrap();
251
+ let mut count = 0;
252
+
253
+ while let Some(msg_result) = response_stream.next().await {
254
+ match msg_result {
255
+ Ok(_msg) => count += 1,
256
+ Err(_) => break,
257
+ }
258
+ }
259
+
260
+ assert_eq!(count, 1);
261
+ }
262
+
263
+ #[tokio::test]
264
+ async fn test_integration_bidi_concurrent_requests() {
265
+ /// Handler with request-specific ID
266
+ struct ConcurrentBidiHandler {
267
+ request_id: usize,
268
+ }
269
+
270
+ impl GrpcHandler for ConcurrentBidiHandler {
271
+ fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
272
+ Box::pin(async { Err(tonic::Status::unimplemented("Use call_bidi_stream for streaming")) })
273
+ }
274
+
275
+ fn service_name(&self) -> &'static str {
276
+ "integration.ConcurrentService"
277
+ }
278
+
279
+ fn rpc_mode(&self) -> RpcMode {
280
+ RpcMode::BidirectionalStreaming
281
+ }
282
+
283
+ fn call_bidi_stream(
284
+ &self,
285
+ request: StreamingRequest,
286
+ ) -> Pin<Box<dyn Future<Output = Result<spikard_http::grpc::streaming::MessageStream, tonic::Status>> + Send>>
287
+ {
288
+ let request_id = self.request_id;
289
+ Box::pin(async move {
290
+ let mut stream = request.message_stream;
291
+ let mut count = 0;
292
+ let mut responses = Vec::new();
293
+
294
+ while let Some(msg_result) = stream.next().await {
295
+ match msg_result {
296
+ Ok(_msg) => {
297
+ count += 1;
298
+ responses.push(Bytes::from(format!("req_{}:{}", request_id, count)));
299
+ }
300
+ Err(e) => return Err(e),
301
+ }
302
+ }
303
+
304
+ Ok(spikard_http::grpc::streaming::message_stream_from_vec(responses))
305
+ })
306
+ }
307
+ }
308
+
309
+ let handler1 = ConcurrentBidiHandler { request_id: 1 };
310
+ let handler2 = ConcurrentBidiHandler { request_id: 2 };
311
+ let handler3 = ConcurrentBidiHandler { request_id: 3 };
312
+
313
+ let task1 = tokio::spawn(async move {
314
+ let messages = vec![Bytes::from("a"), Bytes::from("b")];
315
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages);
316
+ let request = StreamingRequest {
317
+ service_name: "integration.ConcurrentService".to_string(),
318
+ method_name: "Concurrent".to_string(),
319
+ message_stream: stream,
320
+ metadata: MetadataMap::new(),
321
+ };
322
+ handler1.call_bidi_stream(request).await
323
+ });
324
+
325
+ let task2 = tokio::spawn(async move {
326
+ let messages = vec![Bytes::from("x"), Bytes::from("y"), Bytes::from("z")];
327
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages);
328
+ let request = StreamingRequest {
329
+ service_name: "integration.ConcurrentService".to_string(),
330
+ method_name: "Concurrent".to_string(),
331
+ message_stream: stream,
332
+ metadata: MetadataMap::new(),
333
+ };
334
+ handler2.call_bidi_stream(request).await
335
+ });
336
+
337
+ let task3 = tokio::spawn(async move {
338
+ let messages = vec![Bytes::from("single")];
339
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages);
340
+ let request = StreamingRequest {
341
+ service_name: "integration.ConcurrentService".to_string(),
342
+ method_name: "Concurrent".to_string(),
343
+ message_stream: stream,
344
+ metadata: MetadataMap::new(),
345
+ };
346
+ handler3.call_bidi_stream(request).await
347
+ });
348
+
349
+ let result1 = task1.await.unwrap();
350
+ let result2 = task2.await.unwrap();
351
+ let result3 = task3.await.unwrap();
352
+
353
+ assert!(result1.is_ok());
354
+ assert!(result2.is_ok());
355
+ assert!(result3.is_ok());
356
+
357
+ // Verify response counts
358
+ let mut responses1 = Vec::new();
359
+ if let Ok(mut stream) = result1 {
360
+ while let Some(msg_result) = stream.next().await {
361
+ if let Ok(msg) = msg_result {
362
+ responses1.push(msg);
363
+ }
364
+ }
365
+ }
366
+ assert_eq!(responses1.len(), 2);
367
+ }
368
+
369
+ #[tokio::test]
370
+ async fn test_integration_bidi_ordering_preserved() {
371
+ let handler = EchoBidiHandler;
372
+
373
+ // Verify that message ordering is preserved through bidi stream
374
+ let messages: Vec<Bytes> = (0..10).map(|i| Bytes::from(format!("msg{:02}", i))).collect();
375
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages.clone());
376
+ let request = StreamingRequest {
377
+ service_name: "integration.EchoService".to_string(),
378
+ method_name: "Echo".to_string(),
379
+ message_stream: stream,
380
+ metadata: MetadataMap::new(),
381
+ };
382
+
383
+ let result = handler.call_bidi_stream(request).await;
384
+ assert!(result.is_ok());
385
+
386
+ let mut response_stream = result.unwrap();
387
+ let mut responses = Vec::new();
388
+
389
+ while let Some(msg_result) = response_stream.next().await {
390
+ match msg_result {
391
+ Ok(msg) => responses.push(msg),
392
+ Err(_) => break,
393
+ }
394
+ }
395
+
396
+ assert_eq!(responses.len(), 10);
397
+ for (i, resp) in responses.iter().enumerate() {
398
+ assert_eq!(resp, &Bytes::from(format!("msg{:02}", i)));
399
+ }
400
+ }
401
+
402
+ #[tokio::test]
403
+ async fn test_integration_bidi_single_message() {
404
+ let handler = EchoBidiHandler;
405
+
406
+ let messages = vec![Bytes::from("single_message")];
407
+ let stream = spikard_http::grpc::streaming::message_stream_from_vec(messages.clone());
408
+ let request = StreamingRequest {
409
+ service_name: "integration.EchoService".to_string(),
410
+ method_name: "Echo".to_string(),
411
+ message_stream: stream,
412
+ metadata: MetadataMap::new(),
413
+ };
414
+
415
+ let result = handler.call_bidi_stream(request).await;
416
+ assert!(result.is_ok());
417
+
418
+ let mut response_stream = result.unwrap();
419
+ let mut responses = Vec::new();
420
+
421
+ while let Some(msg_result) = response_stream.next().await {
422
+ match msg_result {
423
+ Ok(msg) => responses.push(msg),
424
+ Err(_) => break,
425
+ }
426
+ }
427
+
428
+ assert_eq!(responses.len(), 1);
429
+ assert_eq!(responses[0], messages[0]);
430
+ }