spikard 0.7.5 → 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 -0
  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,476 @@
1
+ //! Comprehensive gRPC server integration tests
2
+ //!
3
+ //! Tests the gRPC runtime infrastructure including:
4
+ //! - Server routing with multiple services
5
+ //! - Unary RPC handling
6
+ //! - Request/response payload handling
7
+ //! - Error responses with different gRPC status codes
8
+
9
+ use crate::common::grpc_helpers::*;
10
+ use bytes::Bytes;
11
+ use serde_json::json;
12
+ use std::sync::Arc;
13
+ use tonic::Code;
14
+
15
+ mod common;
16
+
17
+ /// Test successful unary RPC with JSON payload
18
+ #[tokio::test]
19
+ async fn test_unary_rpc_success_with_json_payload() {
20
+ // Arrange: Create test server and handler
21
+ let mut server = GrpcTestServer::new();
22
+
23
+ // Create a custom handler with proper service name
24
+ struct UserServiceHandler;
25
+ impl spikard_http::grpc::GrpcHandler for UserServiceHandler {
26
+ fn call(&self, _request: spikard_http::grpc::GrpcRequestData)
27
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
28
+ {
29
+ let payload = serde_json::to_vec(&json!({
30
+ "id": 123,
31
+ "name": "Alice Johnson",
32
+ "email": "alice@example.com"
33
+ })).unwrap();
34
+ Box::pin(async {
35
+ Ok(spikard_http::grpc::GrpcResponseData {
36
+ payload: Bytes::from(payload),
37
+ metadata: tonic::metadata::MetadataMap::new(),
38
+ })
39
+ })
40
+ }
41
+ fn service_name(&self) -> &'static str {
42
+ "com.example.api.v1.UserService"
43
+ }
44
+ }
45
+
46
+ server.register_service(Arc::new(UserServiceHandler));
47
+
48
+ // Act: Create and send request
49
+ let mut request_builder = ProtobufMessageBuilder::new();
50
+ let request_payload = request_builder
51
+ .add_int_field("user_id", 123)
52
+ .build()
53
+ .unwrap();
54
+
55
+ let response = send_unary_request(
56
+ &server,
57
+ "com.example.api.v1.UserService",
58
+ "GetUser",
59
+ request_payload,
60
+ create_test_metadata(),
61
+ )
62
+ .await
63
+ .expect("Failed to send unary request");
64
+
65
+ // Assert
66
+ assert_grpc_response(response, json!({
67
+ "id": 123,
68
+ "name": "Alice Johnson",
69
+ "email": "alice@example.com"
70
+ }));
71
+ }
72
+
73
+ /// Test server routing with multiple services
74
+ #[tokio::test]
75
+ async fn test_server_routes_to_correct_service() {
76
+ // Arrange: Register multiple services
77
+ let mut server = GrpcTestServer::new();
78
+
79
+ struct UserServiceHandler;
80
+ impl spikard_http::grpc::GrpcHandler for UserServiceHandler {
81
+ fn call(&self, _request: spikard_http::grpc::GrpcRequestData)
82
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
83
+ {
84
+ Box::pin(async {
85
+ Ok(spikard_http::grpc::GrpcResponseData {
86
+ payload: Bytes::from(r#"{"service": "UserService"}"#),
87
+ metadata: tonic::metadata::MetadataMap::new(),
88
+ })
89
+ })
90
+ }
91
+ fn service_name(&self) -> &'static str {
92
+ "api.UserService"
93
+ }
94
+ }
95
+
96
+ struct OrderServiceHandler;
97
+ impl spikard_http::grpc::GrpcHandler for OrderServiceHandler {
98
+ fn call(&self, _request: spikard_http::grpc::GrpcRequestData)
99
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
100
+ {
101
+ Box::pin(async {
102
+ Ok(spikard_http::grpc::GrpcResponseData {
103
+ payload: Bytes::from(r#"{"service": "OrderService"}"#),
104
+ metadata: tonic::metadata::MetadataMap::new(),
105
+ })
106
+ })
107
+ }
108
+ fn service_name(&self) -> &'static str {
109
+ "api.OrderService"
110
+ }
111
+ }
112
+
113
+ server.register_service(Arc::new(UserServiceHandler));
114
+ server.register_service(Arc::new(OrderServiceHandler));
115
+
116
+ // Act & Assert: Route to UserService
117
+ let user_response = send_unary_request(
118
+ &server,
119
+ "api.UserService",
120
+ "GetUser",
121
+ Bytes::from("{}"),
122
+ create_test_metadata(),
123
+ )
124
+ .await
125
+ .expect("UserService request failed");
126
+
127
+ assert_grpc_response(user_response, json!({"service": "UserService"}));
128
+
129
+ // Act & Assert: Route to OrderService
130
+ let order_response = send_unary_request(
131
+ &server,
132
+ "api.OrderService",
133
+ "GetOrder",
134
+ Bytes::from("{}"),
135
+ create_test_metadata(),
136
+ )
137
+ .await
138
+ .expect("OrderService request failed");
139
+
140
+ assert_grpc_response(order_response, json!({"service": "OrderService"}));
141
+ }
142
+
143
+ /// Test server correctly counts registered services
144
+ #[tokio::test]
145
+ async fn test_server_service_registration_count() {
146
+ let mut server = GrpcTestServer::new();
147
+
148
+ assert_eq!(server.service_count(), 0);
149
+
150
+ // Register first service
151
+ let handler1 = Arc::new(MockGrpcHandler::with_json("service.One", json!({"id": 1})));
152
+ server.register_service(handler1);
153
+ assert_eq!(server.service_count(), 1);
154
+
155
+ // Register second service
156
+ let handler2 = Arc::new(MockGrpcHandler::with_json("service.Two", json!({"id": 2})));
157
+ server.register_service(handler2);
158
+ assert_eq!(server.service_count(), 2);
159
+
160
+ // Register third service
161
+ let handler3 = Arc::new(MockGrpcHandler::with_json("service.Three", json!({"id": 3})));
162
+ server.register_service(handler3);
163
+ assert_eq!(server.service_count(), 3);
164
+ }
165
+
166
+ /// Test unary RPC with complex nested JSON payload
167
+ #[tokio::test]
168
+ async fn test_unary_rpc_with_nested_json_payload() {
169
+ let mut server = GrpcTestServer::new();
170
+
171
+ struct ProductServiceHandler;
172
+ impl spikard_http::grpc::GrpcHandler for ProductServiceHandler {
173
+ fn call(&self, _request: spikard_http::grpc::GrpcRequestData)
174
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
175
+ {
176
+ Box::pin(async {
177
+ Ok(spikard_http::grpc::GrpcResponseData {
178
+ payload: Bytes::from(r#"{
179
+ "id": 42,
180
+ "name": "Laptop",
181
+ "price": 999.99,
182
+ "inventory": {
183
+ "quantity": 100,
184
+ "warehouse": "US-WEST"
185
+ },
186
+ "tags": ["electronics", "computers", "portable"]
187
+ }"#),
188
+ metadata: tonic::metadata::MetadataMap::new(),
189
+ })
190
+ })
191
+ }
192
+ fn service_name(&self) -> &'static str {
193
+ "shop.ProductService"
194
+ }
195
+ }
196
+
197
+ server.register_service(Arc::new(ProductServiceHandler));
198
+
199
+ let response = send_unary_request(
200
+ &server,
201
+ "shop.ProductService",
202
+ "GetProduct",
203
+ Bytes::from("{}"),
204
+ create_test_metadata(),
205
+ )
206
+ .await
207
+ .expect("Failed to get product");
208
+
209
+ // Verify nested structure
210
+ let payload = serde_json::from_slice::<serde_json::Value>(&response.payload).unwrap();
211
+ assert_eq!(payload["id"], 42);
212
+ assert_eq!(payload["name"], "Laptop");
213
+ assert_eq!(payload["price"], 999.99);
214
+ assert_eq!(payload["inventory"]["quantity"], 100);
215
+ assert_eq!(payload["inventory"]["warehouse"], "US-WEST");
216
+ assert_eq!(payload["tags"].as_array().unwrap().len(), 3);
217
+ }
218
+
219
+ /// Test unary RPC with binary payload (raw bytes)
220
+ #[tokio::test]
221
+ async fn test_unary_rpc_with_binary_payload() {
222
+ let mut server = GrpcTestServer::new();
223
+
224
+ struct EchoServiceHandler;
225
+ impl spikard_http::grpc::GrpcHandler for EchoServiceHandler {
226
+ fn call(&self, request: spikard_http::grpc::GrpcRequestData)
227
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
228
+ {
229
+ let payload = request.payload;
230
+ Box::pin(async {
231
+ Ok(spikard_http::grpc::GrpcResponseData {
232
+ payload,
233
+ metadata: tonic::metadata::MetadataMap::new(),
234
+ })
235
+ })
236
+ }
237
+ fn service_name(&self) -> &'static str {
238
+ "echo.EchoService"
239
+ }
240
+ }
241
+
242
+ server.register_service(Arc::new(EchoServiceHandler));
243
+
244
+ // Send binary payload
245
+ let binary_data = vec![0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD];
246
+ let response = send_unary_request(
247
+ &server,
248
+ "echo.EchoService",
249
+ "Echo",
250
+ Bytes::from(binary_data.clone()),
251
+ create_test_metadata(),
252
+ )
253
+ .await
254
+ .expect("Failed to echo");
255
+
256
+ // Verify binary data is unchanged
257
+ assert_eq!(response.payload.to_vec(), binary_data);
258
+ }
259
+
260
+ /// Test request with custom metadata is preserved in response
261
+ #[tokio::test]
262
+ async fn test_request_metadata_handling() {
263
+ let mut server = GrpcTestServer::new();
264
+
265
+ struct MetadataAwareHandler;
266
+ impl spikard_http::grpc::GrpcHandler for MetadataAwareHandler {
267
+ fn call(&self, request: spikard_http::grpc::GrpcRequestData)
268
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
269
+ {
270
+ // Check that metadata was received
271
+ let has_custom_header = request.metadata.get("x-custom-header").is_some();
272
+ let response_payload = if has_custom_header {
273
+ Bytes::from(r#"{"status": "metadata_received"}"#)
274
+ } else {
275
+ Bytes::from(r#"{"status": "no_metadata"}"#)
276
+ };
277
+
278
+ Box::pin(async move {
279
+ Ok(spikard_http::grpc::GrpcResponseData {
280
+ payload: response_payload,
281
+ metadata: tonic::metadata::MetadataMap::new(),
282
+ })
283
+ })
284
+ }
285
+ fn service_name(&self) -> &'static str {
286
+ "test.MetadataService"
287
+ }
288
+ }
289
+
290
+ server.register_service(Arc::new(MetadataAwareHandler));
291
+
292
+ let mut metadata = create_test_metadata();
293
+ add_metadata_header(&mut metadata, "x-custom-header", "test-value").unwrap();
294
+
295
+ let response = send_unary_request(
296
+ &server,
297
+ "test.MetadataService",
298
+ "TestMetadata",
299
+ Bytes::from("{}"),
300
+ metadata,
301
+ )
302
+ .await
303
+ .expect("Failed to send request with metadata");
304
+
305
+ assert_grpc_response(response, json!({"status": "metadata_received"}));
306
+ }
307
+
308
+ /// Test error response with NOT_FOUND status
309
+ #[tokio::test]
310
+ async fn test_error_response_not_found() {
311
+ let mut server = GrpcTestServer::new();
312
+
313
+ let handler = Arc::new(ErrorMockHandler::new(
314
+ "api.NotFoundService",
315
+ Code::NotFound,
316
+ "User with ID 999 not found",
317
+ ));
318
+ server.register_service(handler);
319
+
320
+ let result = send_unary_request(
321
+ &server,
322
+ "api.NotFoundService",
323
+ "GetUser",
324
+ Bytes::from("{}"),
325
+ create_test_metadata(),
326
+ )
327
+ .await;
328
+
329
+ assert!(result.is_err());
330
+ }
331
+
332
+ /// Test error response with INVALID_ARGUMENT status
333
+ #[tokio::test]
334
+ async fn test_error_response_invalid_argument() {
335
+ let mut server = GrpcTestServer::new();
336
+
337
+ let handler = Arc::new(ErrorMockHandler::new(
338
+ "api.ValidationService",
339
+ Code::InvalidArgument,
340
+ "Email address is invalid",
341
+ ));
342
+ server.register_service(handler);
343
+
344
+ let result = send_unary_request(
345
+ &server,
346
+ "api.ValidationService",
347
+ "ValidateEmail",
348
+ Bytes::from("{}"),
349
+ create_test_metadata(),
350
+ )
351
+ .await;
352
+
353
+ assert!(result.is_err());
354
+ }
355
+
356
+ /// Test error response with INTERNAL status
357
+ #[tokio::test]
358
+ async fn test_error_response_internal_server_error() {
359
+ let mut server = GrpcTestServer::new();
360
+
361
+ let handler = Arc::new(ErrorMockHandler::new(
362
+ "api.DatabaseService",
363
+ Code::Internal,
364
+ "Database connection failed",
365
+ ));
366
+ server.register_service(handler);
367
+
368
+ let result = send_unary_request(
369
+ &server,
370
+ "api.DatabaseService",
371
+ "QueryData",
372
+ Bytes::from("{}"),
373
+ create_test_metadata(),
374
+ )
375
+ .await;
376
+
377
+ assert!(result.is_err());
378
+ }
379
+
380
+ /// Test server with no services registered
381
+ #[tokio::test]
382
+ async fn test_server_no_services_registered() {
383
+ let server = GrpcTestServer::new();
384
+
385
+ assert_eq!(server.service_count(), 0);
386
+ assert!(server.handlers().is_empty());
387
+
388
+ // Attempting to call a non-existent service should fail
389
+ let result = send_unary_request(
390
+ &server,
391
+ "nonexistent.Service",
392
+ "Method",
393
+ Bytes::from("{}"),
394
+ create_test_metadata(),
395
+ )
396
+ .await;
397
+
398
+ assert!(result.is_err());
399
+ }
400
+
401
+ /// Test unary RPC with empty request payload
402
+ #[tokio::test]
403
+ async fn test_unary_rpc_empty_request_payload() {
404
+ let mut server = GrpcTestServer::new();
405
+
406
+ struct EmptyRequestHandler;
407
+ impl spikard_http::grpc::GrpcHandler for EmptyRequestHandler {
408
+ fn call(&self, _request: spikard_http::grpc::GrpcRequestData)
409
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
410
+ {
411
+ Box::pin(async {
412
+ Ok(spikard_http::grpc::GrpcResponseData {
413
+ payload: Bytes::from(r#"{"status": "ok"}"#),
414
+ metadata: tonic::metadata::MetadataMap::new(),
415
+ })
416
+ })
417
+ }
418
+ fn service_name(&self) -> &'static str {
419
+ "test.EmptyRequestService"
420
+ }
421
+ }
422
+
423
+ server.register_service(Arc::new(EmptyRequestHandler));
424
+
425
+ let response = send_unary_request(
426
+ &server,
427
+ "test.EmptyRequestService",
428
+ "HealthCheck",
429
+ Bytes::new(),
430
+ create_test_metadata(),
431
+ )
432
+ .await
433
+ .expect("Failed to send empty request");
434
+
435
+ assert_grpc_response(response, json!({"status": "ok"}));
436
+ }
437
+
438
+ /// Test echo handler preserves exact request payload
439
+ #[tokio::test]
440
+ async fn test_echo_handler_payload_preservation() {
441
+ let mut server = GrpcTestServer::new();
442
+
443
+ struct TestEchoHandler;
444
+ impl spikard_http::grpc::GrpcHandler for TestEchoHandler {
445
+ fn call(&self, request: spikard_http::grpc::GrpcRequestData)
446
+ -> std::pin::Pin<Box<dyn std::future::Future<Output = spikard_http::grpc::GrpcHandlerResult> + Send>>
447
+ {
448
+ let payload = request.payload;
449
+ Box::pin(async move {
450
+ Ok(spikard_http::grpc::GrpcResponseData {
451
+ payload,
452
+ metadata: tonic::metadata::MetadataMap::new(),
453
+ })
454
+ })
455
+ }
456
+ fn service_name(&self) -> &'static str {
457
+ "test.EchoService"
458
+ }
459
+ }
460
+
461
+ server.register_service(Arc::new(TestEchoHandler));
462
+
463
+ let request_payload = Bytes::from(r#"{"echo": "test message"}"#);
464
+
465
+ let response = send_unary_request(
466
+ &server,
467
+ "test.EchoService",
468
+ "Echo",
469
+ request_payload.clone(),
470
+ create_test_metadata(),
471
+ )
472
+ .await
473
+ .expect("Failed to echo");
474
+
475
+ assert_eq!(response.payload, request_payload);
476
+ }
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "spikard-rb"
3
- version = "0.7.5"
3
+ version = "0.8.0"
4
4
  edition = "2024"
5
5
  authors = ["Na'aman Hirschfeld <nhirschfeld@gmail.com>"]
6
6
  license = "MIT"
@@ -29,6 +29,7 @@ axum-test = "18"
29
29
  bytes = "1.11"
30
30
  cookie = "0.18"
31
31
  tokio = { version = "1", features = ["full"] }
32
+ tonic = { version = "0.14", features = ["transport", "codegen", "gzip"] }
32
33
  tungstenite = "0.28"
33
34
  tracing = "0.1"
34
35
  serde_qs = "0.15"
@@ -257,6 +257,7 @@ pub fn extract_server_config(ruby: &Ruby, config_value: Value) -> Result<spikard
257
257
  enable_http_trace: false,
258
258
  openapi,
259
259
  jsonrpc: None,
260
+ grpc: None,
260
261
  lifecycle_hooks: None,
261
262
  di_container: None,
262
263
  })