spikard 0.13.0 → 0.15.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 (207) hide show
  1. checksums.yaml +4 -4
  2. data/Steepfile +6 -0
  3. data/ext/spikard_rb/extconf.rb +1 -2
  4. data/ext/spikard_rb/{Cargo.lock → native/Cargo.lock} +819 -424
  5. data/ext/spikard_rb/native/Cargo.toml +24 -0
  6. data/ext/spikard_rb/src/lib.rs +5366 -3
  7. data/lib/spikard/native.rb +86 -0
  8. data/lib/spikard/version.rb +6 -1
  9. data/lib/spikard.rb +8 -52
  10. data/lib/spikard_rb.so +0 -0
  11. data/sig/types.rbs +427 -0
  12. metadata +14 -243
  13. data/LICENSE +0 -1
  14. data/README.md +0 -285
  15. data/ext/spikard_rb/Cargo.toml +0 -17
  16. data/lib/spikard/app.rb +0 -458
  17. data/lib/spikard/background.rb +0 -58
  18. data/lib/spikard/config.rb +0 -506
  19. data/lib/spikard/converters.rb +0 -13
  20. data/lib/spikard/grpc.rb +0 -232
  21. data/lib/spikard/handler_wrapper.rb +0 -113
  22. data/lib/spikard/provide.rb +0 -315
  23. data/lib/spikard/response.rb +0 -198
  24. data/lib/spikard/schema.rb +0 -243
  25. data/lib/spikard/sse.rb +0 -111
  26. data/lib/spikard/streaming_response.rb +0 -44
  27. data/lib/spikard/testing.rb +0 -474
  28. data/lib/spikard/upload_file.rb +0 -131
  29. data/lib/spikard/websocket.rb +0 -59
  30. data/sig/spikard.rbs +0 -739
  31. data/vendor/crates/spikard-bindings-shared/Cargo.toml +0 -75
  32. data/vendor/crates/spikard-bindings-shared/examples/config_extraction.rs +0 -132
  33. data/vendor/crates/spikard-bindings-shared/src/config_extractor.rs +0 -905
  34. data/vendor/crates/spikard-bindings-shared/src/conversion_traits.rs +0 -210
  35. data/vendor/crates/spikard-bindings-shared/src/di_traits.rs +0 -252
  36. data/vendor/crates/spikard-bindings-shared/src/error_response.rs +0 -404
  37. data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +0 -199
  38. data/vendor/crates/spikard-bindings-shared/src/handler_base.rs +0 -252
  39. data/vendor/crates/spikard-bindings-shared/src/json_conversion.rs +0 -829
  40. data/vendor/crates/spikard-bindings-shared/src/lazy_cache.rs +0 -587
  41. data/vendor/crates/spikard-bindings-shared/src/lib.rs +0 -33
  42. data/vendor/crates/spikard-bindings-shared/src/lifecycle_base.rs +0 -298
  43. data/vendor/crates/spikard-bindings-shared/src/lifecycle_executor.rs +0 -594
  44. data/vendor/crates/spikard-bindings-shared/src/response_builder.rs +0 -743
  45. data/vendor/crates/spikard-bindings-shared/src/response_interpreter.rs +0 -944
  46. data/vendor/crates/spikard-bindings-shared/src/test_client_base.rs +0 -260
  47. data/vendor/crates/spikard-bindings-shared/src/validation_helpers.rs +0 -369
  48. data/vendor/crates/spikard-bindings-shared/tests/config_extractor_behavior.rs +0 -192
  49. data/vendor/crates/spikard-bindings-shared/tests/error_response_edge_cases.rs +0 -383
  50. data/vendor/crates/spikard-bindings-shared/tests/full_coverage.rs +0 -459
  51. data/vendor/crates/spikard-bindings-shared/tests/handler_base_integration.rs +0 -280
  52. data/vendor/crates/spikard-bindings-shared/tests/integration_tests.rs +0 -669
  53. data/vendor/crates/spikard-core/Cargo.toml +0 -55
  54. data/vendor/crates/spikard-core/src/bindings/mod.rs +0 -3
  55. data/vendor/crates/spikard-core/src/bindings/response.rs +0 -130
  56. data/vendor/crates/spikard-core/src/debug.rs +0 -127
  57. data/vendor/crates/spikard-core/src/di/container.rs +0 -711
  58. data/vendor/crates/spikard-core/src/di/dependency.rs +0 -273
  59. data/vendor/crates/spikard-core/src/di/error.rs +0 -118
  60. data/vendor/crates/spikard-core/src/di/factory.rs +0 -548
  61. data/vendor/crates/spikard-core/src/di/graph.rs +0 -507
  62. data/vendor/crates/spikard-core/src/di/mod.rs +0 -192
  63. data/vendor/crates/spikard-core/src/di/resolved.rs +0 -428
  64. data/vendor/crates/spikard-core/src/di/value.rs +0 -282
  65. data/vendor/crates/spikard-core/src/errors.rs +0 -72
  66. data/vendor/crates/spikard-core/src/http.rs +0 -492
  67. data/vendor/crates/spikard-core/src/lib.rs +0 -29
  68. data/vendor/crates/spikard-core/src/lifecycle.rs +0 -1273
  69. data/vendor/crates/spikard-core/src/metadata.rs +0 -378
  70. data/vendor/crates/spikard-core/src/parameters.rs +0 -2546
  71. data/vendor/crates/spikard-core/src/problem.rs +0 -358
  72. data/vendor/crates/spikard-core/src/request_data.rs +0 -1146
  73. data/vendor/crates/spikard-core/src/router.rs +0 -530
  74. data/vendor/crates/spikard-core/src/schema_registry.rs +0 -197
  75. data/vendor/crates/spikard-core/src/type_hints.rs +0 -311
  76. data/vendor/crates/spikard-core/src/validation/error_mapper.rs +0 -710
  77. data/vendor/crates/spikard-core/src/validation/mod.rs +0 -470
  78. data/vendor/crates/spikard-core/tests/bindings_response_tests.rs +0 -136
  79. data/vendor/crates/spikard-core/tests/di_dependency_defaults.rs +0 -37
  80. data/vendor/crates/spikard-core/tests/error_mapper.rs +0 -761
  81. data/vendor/crates/spikard-core/tests/parameters_edge_cases.rs +0 -106
  82. data/vendor/crates/spikard-core/tests/parameters_full.rs +0 -701
  83. data/vendor/crates/spikard-core/tests/parameters_schema_and_formats.rs +0 -301
  84. data/vendor/crates/spikard-core/tests/request_data_roundtrip.rs +0 -67
  85. data/vendor/crates/spikard-core/tests/validation_coverage.rs +0 -250
  86. data/vendor/crates/spikard-core/tests/validation_error_paths.rs +0 -45
  87. data/vendor/crates/spikard-http/Cargo.toml +0 -82
  88. data/vendor/crates/spikard-http/examples/sse-notifications.rs +0 -148
  89. data/vendor/crates/spikard-http/examples/websocket-chat.rs +0 -92
  90. data/vendor/crates/spikard-http/src/auth.rs +0 -301
  91. data/vendor/crates/spikard-http/src/background.rs +0 -1859
  92. data/vendor/crates/spikard-http/src/bindings/mod.rs +0 -3
  93. data/vendor/crates/spikard-http/src/bindings/response.rs +0 -1
  94. data/vendor/crates/spikard-http/src/body_metadata.rs +0 -8
  95. data/vendor/crates/spikard-http/src/cors.rs +0 -1026
  96. data/vendor/crates/spikard-http/src/debug.rs +0 -128
  97. data/vendor/crates/spikard-http/src/di_handler.rs +0 -1672
  98. data/vendor/crates/spikard-http/src/grpc/framing.rs +0 -653
  99. data/vendor/crates/spikard-http/src/grpc/handler.rs +0 -1211
  100. data/vendor/crates/spikard-http/src/grpc/mod.rs +0 -556
  101. data/vendor/crates/spikard-http/src/grpc/service.rs +0 -706
  102. data/vendor/crates/spikard-http/src/grpc/streaming.rs +0 -319
  103. data/vendor/crates/spikard-http/src/handler_response.rs +0 -901
  104. data/vendor/crates/spikard-http/src/handler_trait.rs +0 -1015
  105. data/vendor/crates/spikard-http/src/handler_trait_tests.rs +0 -290
  106. data/vendor/crates/spikard-http/src/jsonrpc/http_handler.rs +0 -502
  107. data/vendor/crates/spikard-http/src/jsonrpc/method_registry.rs +0 -648
  108. data/vendor/crates/spikard-http/src/jsonrpc/mod.rs +0 -60
  109. data/vendor/crates/spikard-http/src/jsonrpc/openrpc.rs +0 -325
  110. data/vendor/crates/spikard-http/src/jsonrpc/protocol.rs +0 -1207
  111. data/vendor/crates/spikard-http/src/jsonrpc/router.rs +0 -2262
  112. data/vendor/crates/spikard-http/src/lib.rs +0 -566
  113. data/vendor/crates/spikard-http/src/lifecycle/adapter.rs +0 -230
  114. data/vendor/crates/spikard-http/src/lifecycle.rs +0 -1193
  115. data/vendor/crates/spikard-http/src/middleware/mod.rs +0 -560
  116. data/vendor/crates/spikard-http/src/middleware/multipart.rs +0 -912
  117. data/vendor/crates/spikard-http/src/middleware/urlencoded.rs +0 -513
  118. data/vendor/crates/spikard-http/src/middleware/validation.rs +0 -768
  119. data/vendor/crates/spikard-http/src/openapi/mod.rs +0 -309
  120. data/vendor/crates/spikard-http/src/openapi/parameter_extraction.rs +0 -535
  121. data/vendor/crates/spikard-http/src/openapi/schema_conversion.rs +0 -1363
  122. data/vendor/crates/spikard-http/src/openapi/spec_generation.rs +0 -667
  123. data/vendor/crates/spikard-http/src/query_parser.rs +0 -793
  124. data/vendor/crates/spikard-http/src/response.rs +0 -720
  125. data/vendor/crates/spikard-http/src/server/fast_router.rs +0 -186
  126. data/vendor/crates/spikard-http/src/server/grpc_routing.rs +0 -1243
  127. data/vendor/crates/spikard-http/src/server/handler.rs +0 -1661
  128. data/vendor/crates/spikard-http/src/server/lifecycle_execution.rs +0 -253
  129. data/vendor/crates/spikard-http/src/server/mod.rs +0 -1717
  130. data/vendor/crates/spikard-http/src/server/request_extraction.rs +0 -871
  131. data/vendor/crates/spikard-http/src/server/routing_factory.rs +0 -618
  132. data/vendor/crates/spikard-http/src/sse.rs +0 -1409
  133. data/vendor/crates/spikard-http/src/testing/form.rs +0 -52
  134. data/vendor/crates/spikard-http/src/testing/multipart.rs +0 -64
  135. data/vendor/crates/spikard-http/src/testing/test_client.rs +0 -825
  136. data/vendor/crates/spikard-http/src/testing.rs +0 -617
  137. data/vendor/crates/spikard-http/src/websocket.rs +0 -1477
  138. data/vendor/crates/spikard-http/tests/auth_integration.rs +0 -645
  139. data/vendor/crates/spikard-http/tests/background_behavior.rs +0 -832
  140. data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +0 -1012
  141. data/vendor/crates/spikard-http/tests/common/handlers.rs +0 -309
  142. data/vendor/crates/spikard-http/tests/common/mod.rs +0 -33
  143. data/vendor/crates/spikard-http/tests/common/test_builders.rs +0 -628
  144. data/vendor/crates/spikard-http/tests/di_handler_error_responses.rs +0 -162
  145. data/vendor/crates/spikard-http/tests/di_integration.rs +0 -192
  146. data/vendor/crates/spikard-http/tests/doc_snippets.rs +0 -5
  147. data/vendor/crates/spikard-http/tests/grpc_bidirectional_streaming.rs +0 -430
  148. data/vendor/crates/spikard-http/tests/grpc_client_streaming.rs +0 -738
  149. data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +0 -652
  150. data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +0 -334
  151. data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +0 -532
  152. data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +0 -495
  153. data/vendor/crates/spikard-http/tests/grpc_server_streaming.rs +0 -975
  154. data/vendor/crates/spikard-http/tests/lifecycle_execution.rs +0 -1093
  155. data/vendor/crates/spikard-http/tests/middleware_stack_integration.rs +0 -389
  156. data/vendor/crates/spikard-http/tests/multipart_behavior.rs +0 -656
  157. data/vendor/crates/spikard-http/tests/request_extraction_full.rs +0 -513
  158. data/vendor/crates/spikard-http/tests/server_auth_middleware_behavior.rs +0 -328
  159. data/vendor/crates/spikard-http/tests/server_config_builder.rs +0 -335
  160. data/vendor/crates/spikard-http/tests/server_configured_router_behavior.rs +0 -374
  161. data/vendor/crates/spikard-http/tests/server_cors_preflight.rs +0 -83
  162. data/vendor/crates/spikard-http/tests/server_handler_wrappers.rs +0 -464
  163. data/vendor/crates/spikard-http/tests/server_method_router_additional_behavior.rs +0 -286
  164. data/vendor/crates/spikard-http/tests/server_method_router_coverage.rs +0 -118
  165. data/vendor/crates/spikard-http/tests/server_middleware_behavior.rs +0 -99
  166. data/vendor/crates/spikard-http/tests/server_middleware_branches.rs +0 -204
  167. data/vendor/crates/spikard-http/tests/server_openapi_jsonrpc_static.rs +0 -427
  168. data/vendor/crates/spikard-http/tests/server_router_behavior.rs +0 -121
  169. data/vendor/crates/spikard-http/tests/sse_behavior.rs +0 -620
  170. data/vendor/crates/spikard-http/tests/sse_full_behavior.rs +0 -584
  171. data/vendor/crates/spikard-http/tests/sse_handler_behavior.rs +0 -130
  172. data/vendor/crates/spikard-http/tests/test_client_requests.rs +0 -167
  173. data/vendor/crates/spikard-http/tests/testing_helpers.rs +0 -87
  174. data/vendor/crates/spikard-http/tests/testing_module_coverage.rs +0 -155
  175. data/vendor/crates/spikard-http/tests/urlencoded_content_type.rs +0 -82
  176. data/vendor/crates/spikard-http/tests/websocket_behavior.rs +0 -663
  177. data/vendor/crates/spikard-http/tests/websocket_full_behavior.rs +0 -440
  178. data/vendor/crates/spikard-http/tests/websocket_integration.rs +0 -150
  179. data/vendor/crates/spikard-rb/Cargo.toml +0 -63
  180. data/vendor/crates/spikard-rb/build.rs +0 -200
  181. data/vendor/crates/spikard-rb/src/background.rs +0 -63
  182. data/vendor/crates/spikard-rb/src/config/mod.rs +0 -5
  183. data/vendor/crates/spikard-rb/src/config/server_config.rs +0 -401
  184. data/vendor/crates/spikard-rb/src/conversion.rs +0 -688
  185. data/vendor/crates/spikard-rb/src/di/builder.rs +0 -100
  186. data/vendor/crates/spikard-rb/src/di/mod.rs +0 -410
  187. data/vendor/crates/spikard-rb/src/grpc/handler.rs +0 -875
  188. data/vendor/crates/spikard-rb/src/grpc/mod.rs +0 -13
  189. data/vendor/crates/spikard-rb/src/gvl.rs +0 -80
  190. data/vendor/crates/spikard-rb/src/handler.rs +0 -699
  191. data/vendor/crates/spikard-rb/src/integration/mod.rs +0 -3
  192. data/vendor/crates/spikard-rb/src/lib.rs +0 -2268
  193. data/vendor/crates/spikard-rb/src/lifecycle.rs +0 -334
  194. data/vendor/crates/spikard-rb/src/metadata/mod.rs +0 -5
  195. data/vendor/crates/spikard-rb/src/metadata/route_extraction.rs +0 -507
  196. data/vendor/crates/spikard-rb/src/request.rs +0 -439
  197. data/vendor/crates/spikard-rb/src/runtime/mod.rs +0 -5
  198. data/vendor/crates/spikard-rb/src/runtime/server_runner.rs +0 -368
  199. data/vendor/crates/spikard-rb/src/server.rs +0 -304
  200. data/vendor/crates/spikard-rb/src/sse.rs +0 -231
  201. data/vendor/crates/spikard-rb/src/testing/client.rs +0 -698
  202. data/vendor/crates/spikard-rb/src/testing/mod.rs +0 -7
  203. data/vendor/crates/spikard-rb/src/testing/sse.rs +0 -108
  204. data/vendor/crates/spikard-rb/src/testing/websocket.rs +0 -573
  205. data/vendor/crates/spikard-rb/src/websocket.rs +0 -521
  206. data/vendor/crates/spikard-rb-macros/Cargo.toml +0 -20
  207. data/vendor/crates/spikard-rb-macros/src/lib.rs +0 -51
@@ -1,706 +0,0 @@
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::framing::encode_grpc_message;
8
- use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
9
- use crate::grpc::streaming::MessageStream;
10
- use axum::http::{HeaderMap, HeaderValue};
11
- use bytes::Bytes;
12
- use futures_util::StreamExt;
13
- use http_body::Frame;
14
- use http_body_util::StreamBody;
15
- use std::convert::Infallible;
16
- use std::sync::Arc;
17
- use tonic::{Request, Response, Status};
18
-
19
- /// Generic gRPC service that routes requests to a GrpcHandler
20
- ///
21
- /// This service implements Tonic's server traits and routes all requests
22
- /// to the provided GrpcHandler implementation. It handles serialization
23
- /// at the boundary between Tonic and our handler trait.
24
- ///
25
- /// # Example
26
- ///
27
- /// ```ignore
28
- /// use spikard_http::grpc::service::GenericGrpcService;
29
- /// use std::sync::Arc;
30
- ///
31
- /// let handler = Arc::new(MyGrpcHandler);
32
- /// let service = GenericGrpcService::new(handler);
33
- /// ```
34
- pub struct GenericGrpcService {
35
- handler: Arc<dyn GrpcHandler>,
36
- }
37
-
38
- impl GenericGrpcService {
39
- /// Create a new generic gRPC service with the given handler
40
- pub fn new(handler: Arc<dyn GrpcHandler>) -> Self {
41
- Self { handler }
42
- }
43
-
44
- /// Handle a unary RPC call
45
- ///
46
- /// Converts the Tonic Request into our GrpcRequestData format,
47
- /// calls the handler, and converts the result back to a Tonic Response.
48
- ///
49
- /// # Arguments
50
- ///
51
- /// * `service_name` - Fully qualified service name
52
- /// * `method_name` - Method name
53
- /// * `request` - Tonic request containing the serialized protobuf message
54
- pub async fn handle_unary(
55
- &self,
56
- service_name: String,
57
- method_name: String,
58
- request: Request<Bytes>,
59
- ) -> Result<Response<Bytes>, Status> {
60
- // Extract metadata and payload from Tonic request
61
- let (metadata, _extensions, payload) = request.into_parts();
62
-
63
- // Create our internal request representation
64
- let grpc_request = GrpcRequestData {
65
- service_name,
66
- method_name,
67
- payload,
68
- metadata,
69
- };
70
-
71
- // Call the handler
72
- let result: GrpcHandlerResult = self.handler.call(grpc_request).await;
73
-
74
- // Convert result to Tonic response
75
- match result {
76
- Ok(grpc_response) => {
77
- let mut response = Response::new(grpc_response.payload);
78
- copy_metadata(&grpc_response.metadata, response.metadata_mut());
79
- Ok(response)
80
- }
81
- Err(status) => Err(status),
82
- }
83
- }
84
-
85
- /// Handle a server streaming RPC call
86
- ///
87
- /// Takes a single request and returns a stream of response messages.
88
- /// Converts the Tonic Request into our GrpcRequestData format, calls the
89
- /// handler's call_server_stream method, and converts the MessageStream
90
- /// into a Tonic streaming response body.
91
- ///
92
- /// # Arguments
93
- ///
94
- /// * `service_name` - Fully qualified service name
95
- /// * `method_name` - Method name
96
- /// * `request` - Tonic request containing the serialized protobuf message
97
- ///
98
- /// # Returns
99
- ///
100
- /// A Response with a streaming body containing the message stream
101
- ///
102
- /// # Error Propagation
103
- ///
104
- /// When a stream returns an error mid-stream (after messages have begun
105
- /// being sent), Spikard preserves the partial messages that were already
106
- /// produced and terminates the stream with gRPC trailers:
107
- ///
108
- /// - **Pre-stream errors** (before any messages): Properly converted to
109
- /// HTTP status codes and returned to the client
110
- /// - **Mid-stream errors** (after messages have begun): The error is converted
111
- /// into terminal `grpc-status` / `grpc-message` trailers
112
- /// - **Successful completion**: The stream ends with `grpc-status: 0`
113
- ///
114
- /// This preserves the gRPC terminal status contract without discarding
115
- /// already-delivered stream data.
116
- pub async fn handle_server_stream(
117
- &self,
118
- service_name: String,
119
- method_name: String,
120
- request: Request<Bytes>,
121
- ) -> Result<Response<axum::body::Body>, Status> {
122
- // Extract metadata and payload from Tonic request
123
- let (metadata, _extensions, payload) = request.into_parts();
124
-
125
- // Create our internal request representation
126
- let grpc_request = GrpcRequestData {
127
- service_name,
128
- method_name,
129
- payload,
130
- metadata,
131
- };
132
-
133
- // Call the handler's server streaming method
134
- let message_stream: MessageStream = self.handler.call_server_stream(grpc_request).await?;
135
-
136
- let body = grpc_stream_body(message_stream);
137
-
138
- // Create response with streaming body
139
- let response = Response::new(body);
140
-
141
- Ok(response)
142
- }
143
-
144
- /// Handle a client streaming RPC call
145
- ///
146
- /// Takes a request body stream of protobuf messages and returns a single response.
147
- /// Parses the HTTP/2 body stream using gRPC frame parser, creates a MessageStream,
148
- /// calls the handler's call_client_stream method, and converts the GrpcResponseData
149
- /// back to a Tonic Response.
150
- ///
151
- /// # Arguments
152
- ///
153
- /// * `service_name` - Fully qualified service name
154
- /// * `method_name` - Method name
155
- /// * `request` - Axum request with streaming body containing HTTP/2 framed protobuf messages
156
- /// * `max_message_size` - Maximum size per message (bytes)
157
- /// * `compression_enabled` - Whether compressed gRPC messages are accepted
158
- ///
159
- /// # Returns
160
- ///
161
- /// A Response with a single message body
162
- ///
163
- /// # Stream Handling
164
- ///
165
- /// The request body stream contains framed protobuf messages. Each frame is parsed
166
- /// and validated for size:
167
- /// - Messages within `max_message_size` are passed to the handler
168
- /// - Messages exceeding the limit result in a ResourceExhausted error
169
- /// - Invalid frames result in InvalidArgument errors
170
- /// - The stream terminates when the client closes the write side
171
- ///
172
- /// # Frame Format
173
- ///
174
- /// Frames follow the gRPC HTTP/2 protocol format:
175
- /// - 1 byte: compression flag (0 = uncompressed)
176
- /// - 4 bytes: message size (big-endian)
177
- /// - N bytes: message payload
178
- ///
179
- /// # Metadata and Trailers
180
- ///
181
- /// - Request metadata (headers) from the Tonic request is passed to the handler
182
- /// - Response metadata from the handler is included in the response headers
183
- /// - gRPC trailers (like grpc-status) should be handled by the caller
184
- pub async fn handle_client_stream(
185
- &self,
186
- service_name: String,
187
- method_name: String,
188
- request: Request<axum::body::Body>,
189
- max_message_size: usize,
190
- compression_enabled: bool,
191
- ) -> Result<Response<Bytes>, Status> {
192
- // Extract metadata and body from Tonic request
193
- let (metadata, _extensions, body) = request.into_parts();
194
- let request_encoding = metadata
195
- .get("grpc-encoding")
196
- .and_then(|value| value.to_str().ok())
197
- .map(str::to_owned);
198
-
199
- // Parse HTTP/2 body into stream of gRPC frames with size validation
200
- let message_stream = crate::grpc::framing::parse_grpc_client_stream(
201
- body,
202
- max_message_size,
203
- request_encoding.as_deref(),
204
- compression_enabled,
205
- )
206
- .await?;
207
-
208
- // Create our internal streaming request representation
209
- let streaming_request = crate::grpc::streaming::StreamingRequest {
210
- service_name,
211
- method_name,
212
- message_stream,
213
- metadata,
214
- };
215
-
216
- // Call the handler's client streaming method
217
- let response: crate::grpc::handler::GrpcHandlerResult =
218
- self.handler.call_client_stream(streaming_request).await;
219
-
220
- // Convert result to Tonic response
221
- match response {
222
- Ok(grpc_response) => {
223
- let mut tonic_response = Response::new(grpc_response.payload);
224
- copy_metadata(&grpc_response.metadata, tonic_response.metadata_mut());
225
- Ok(tonic_response)
226
- }
227
- Err(status) => Err(status),
228
- }
229
- }
230
-
231
- /// Handle a bidirectional streaming RPC call
232
- ///
233
- /// Takes a request body stream and returns a stream of response messages.
234
- /// Parses the HTTP/2 body stream using gRPC frame parser, creates a StreamingRequest,
235
- /// calls the handler's call_bidi_stream method, and converts the MessageStream
236
- /// back to an Axum streaming response body.
237
- ///
238
- /// # Arguments
239
- ///
240
- /// * `service_name` - Fully qualified service name
241
- /// * `method_name` - Method name
242
- /// * `request` - Axum request with streaming body containing HTTP/2 framed protobuf messages
243
- /// * `max_message_size` - Maximum size per message (bytes)
244
- /// * `compression_enabled` - Whether compressed gRPC messages are accepted
245
- ///
246
- /// # Returns
247
- ///
248
- /// A Response with a streaming body containing response messages
249
- ///
250
- /// # Stream Handling
251
- ///
252
- /// - Request stream: Parsed from HTTP/2 body using frame parser
253
- /// - Response stream: Converted from MessageStream to Axum Body
254
- /// - Both streams are independent (full-duplex)
255
- /// - Errors in either stream are propagated appropriately
256
- ///
257
- /// # Error Propagation
258
- ///
259
- /// Bidirectional responses use the same terminal-trailer handling as
260
- /// server-streaming responses. Partial messages are preserved, and the
261
- /// final gRPC status is emitted in trailers.
262
- pub async fn handle_bidi_stream(
263
- &self,
264
- service_name: String,
265
- method_name: String,
266
- request: Request<axum::body::Body>,
267
- max_message_size: usize,
268
- compression_enabled: bool,
269
- ) -> Result<Response<axum::body::Body>, Status> {
270
- // Extract metadata and body from Tonic request
271
- let (metadata, _extensions, body) = request.into_parts();
272
- let request_encoding = metadata
273
- .get("grpc-encoding")
274
- .and_then(|value| value.to_str().ok())
275
- .map(str::to_owned);
276
-
277
- // Parse HTTP/2 body into stream of gRPC frames with size validation
278
- let message_stream = crate::grpc::framing::parse_grpc_client_stream(
279
- body,
280
- max_message_size,
281
- request_encoding.as_deref(),
282
- compression_enabled,
283
- )
284
- .await?;
285
-
286
- // Create our internal streaming request representation
287
- let streaming_request = crate::grpc::streaming::StreamingRequest {
288
- service_name,
289
- method_name,
290
- message_stream,
291
- metadata,
292
- };
293
-
294
- // Call the handler's bidirectional streaming method
295
- let response_stream: MessageStream = self.handler.call_bidi_stream(streaming_request).await?;
296
-
297
- let body = grpc_stream_body(response_stream);
298
- let response = Response::new(body);
299
-
300
- Ok(response)
301
- }
302
-
303
- /// Get the service name from the handler
304
- pub fn service_name(&self) -> &str {
305
- self.handler.service_name()
306
- }
307
- }
308
-
309
- fn grpc_stream_body(message_stream: MessageStream) -> axum::body::Body {
310
- let frame_stream = futures_util::stream::unfold(
311
- GrpcFrameStreamState {
312
- stream: message_stream,
313
- finished: false,
314
- },
315
- |mut state| async move {
316
- if state.finished {
317
- return None;
318
- }
319
-
320
- match state.stream.next().await {
321
- Some(Ok(bytes)) => match encode_grpc_message(bytes) {
322
- Ok(framed) => Some((Ok::<Frame<Bytes>, Infallible>(Frame::data(framed)), state)),
323
- Err(status) => {
324
- state.finished = true;
325
- Some((Ok(Frame::trailers(grpc_status_trailers(&status))), state))
326
- }
327
- },
328
- Some(Err(status)) => {
329
- state.finished = true;
330
- Some((Ok(Frame::trailers(grpc_status_trailers(&status))), state))
331
- }
332
- None => {
333
- state.finished = true;
334
- Some((Ok(Frame::trailers(grpc_success_trailers())), state))
335
- }
336
- }
337
- },
338
- );
339
-
340
- axum::body::Body::new(StreamBody::new(frame_stream))
341
- }
342
-
343
- struct GrpcFrameStreamState {
344
- stream: MessageStream,
345
- finished: bool,
346
- }
347
-
348
- fn grpc_success_trailers() -> HeaderMap {
349
- let mut trailers = HeaderMap::new();
350
- trailers.insert("grpc-status", HeaderValue::from_static("0"));
351
- trailers.insert("grpc-message", HeaderValue::from_static("OK"));
352
- trailers
353
- }
354
-
355
- fn grpc_status_trailers(status: &Status) -> HeaderMap {
356
- let mut trailers = HeaderMap::new();
357
- let code = grpc_code_number(status.code());
358
- trailers.insert(
359
- "grpc-status",
360
- HeaderValue::from_str(code).unwrap_or_else(|_| HeaderValue::from_static("2")),
361
- );
362
-
363
- let encoded_message = if status.message().is_empty() {
364
- "unknown".to_string()
365
- } else {
366
- urlencoding::encode(status.message()).into_owned()
367
- };
368
- trailers.insert(
369
- "grpc-message",
370
- HeaderValue::from_str(&encoded_message).unwrap_or_else(|_| HeaderValue::from_static("unknown")),
371
- );
372
-
373
- trailers
374
- }
375
-
376
- fn grpc_code_number(code: tonic::Code) -> &'static str {
377
- match code {
378
- tonic::Code::Ok => "0",
379
- tonic::Code::Cancelled => "1",
380
- tonic::Code::Unknown => "2",
381
- tonic::Code::InvalidArgument => "3",
382
- tonic::Code::DeadlineExceeded => "4",
383
- tonic::Code::NotFound => "5",
384
- tonic::Code::AlreadyExists => "6",
385
- tonic::Code::PermissionDenied => "7",
386
- tonic::Code::ResourceExhausted => "8",
387
- tonic::Code::FailedPrecondition => "9",
388
- tonic::Code::Aborted => "10",
389
- tonic::Code::OutOfRange => "11",
390
- tonic::Code::Unimplemented => "12",
391
- tonic::Code::Internal => "13",
392
- tonic::Code::Unavailable => "14",
393
- tonic::Code::DataLoss => "15",
394
- tonic::Code::Unauthenticated => "16",
395
- }
396
- }
397
-
398
- /// Helper function to parse gRPC path into service and method names
399
- ///
400
- /// gRPC paths follow the format: `/<package>.<service>/<method>`
401
- ///
402
- /// # Example
403
- ///
404
- /// ```ignore
405
- /// use spikard_http::grpc::service::parse_grpc_path;
406
- ///
407
- /// let (service, method) = parse_grpc_path("/mypackage.UserService/GetUser").unwrap();
408
- /// assert_eq!(service, "mypackage.UserService");
409
- /// assert_eq!(method, "GetUser");
410
- /// ```
411
- pub fn parse_grpc_path(path: &str) -> Result<(String, String), Status> {
412
- // gRPC paths are in the format: /<package>.<service>/<method>
413
- let path = path.trim_start_matches('/');
414
- let parts: Vec<&str> = path.split('/').collect();
415
-
416
- if parts.len() != 2 {
417
- return Err(Status::invalid_argument(format!("Invalid gRPC path: {}", path)));
418
- }
419
-
420
- let service_name = parts[0].to_string();
421
- let method_name = parts[1].to_string();
422
-
423
- if service_name.is_empty() || method_name.is_empty() {
424
- return Err(Status::invalid_argument("Service or method name is empty"));
425
- }
426
-
427
- Ok((service_name, method_name))
428
- }
429
-
430
- /// Check if a request is a gRPC request
431
- ///
432
- /// Checks the content-type header for "application/grpc" prefix.
433
- ///
434
- /// # Example
435
- ///
436
- /// ```ignore
437
- /// use spikard_http::grpc::service::is_grpc_request;
438
- /// use axum::http::HeaderMap;
439
- ///
440
- /// let mut headers = HeaderMap::new();
441
- /// headers.insert("content-type", "application/grpc".parse().unwrap());
442
- ///
443
- /// assert!(is_grpc_request(&headers));
444
- /// ```
445
- pub fn is_grpc_request(headers: &axum::http::HeaderMap) -> bool {
446
- headers
447
- .get(axum::http::header::CONTENT_TYPE)
448
- .and_then(|v| v.to_str().ok())
449
- .map(|v| v.starts_with("application/grpc"))
450
- .unwrap_or(false)
451
- }
452
-
453
- /// Copy metadata from source to destination MetadataMap
454
- ///
455
- /// Efficiently copies all metadata entries (both ASCII and binary)
456
- /// from one MetadataMap to another without unnecessary allocations.
457
- ///
458
- /// # Arguments
459
- ///
460
- /// * `source` - Source metadata to copy from
461
- /// * `dest` - Destination metadata to copy into
462
- pub fn copy_metadata(source: &tonic::metadata::MetadataMap, dest: &mut tonic::metadata::MetadataMap) {
463
- for key_value in source.iter() {
464
- match key_value {
465
- tonic::metadata::KeyAndValueRef::Ascii(key, value) => {
466
- dest.insert(key, value.clone());
467
- }
468
- tonic::metadata::KeyAndValueRef::Binary(key, value) => {
469
- dest.insert_bin(key, value.clone());
470
- }
471
- }
472
- }
473
- }
474
-
475
- /// Convert GrpcResponseData to Tonic Response
476
- ///
477
- /// Helper function to convert our internal response representation
478
- /// to a Tonic Response.
479
- pub fn grpc_response_to_tonic(response: GrpcResponseData) -> Response<Bytes> {
480
- let mut tonic_response = Response::new(response.payload);
481
- copy_metadata(&response.metadata, tonic_response.metadata_mut());
482
- tonic_response
483
- }
484
-
485
- #[cfg(test)]
486
- mod tests {
487
- use super::*;
488
- use crate::grpc::handler::GrpcHandler;
489
- use std::future::Future;
490
- use std::pin::Pin;
491
- use tonic::metadata::MetadataMap;
492
-
493
- struct TestHandler;
494
-
495
- impl GrpcHandler for TestHandler {
496
- fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
497
- Box::pin(async move {
498
- // Echo back the request payload
499
- Ok(GrpcResponseData {
500
- payload: request.payload,
501
- metadata: MetadataMap::new(),
502
- })
503
- })
504
- }
505
-
506
- fn service_name(&self) -> &str {
507
- "test.TestService"
508
- }
509
- }
510
-
511
- #[tokio::test]
512
- async fn test_generic_grpc_service_handle_unary() {
513
- let handler = Arc::new(TestHandler);
514
- let service = GenericGrpcService::new(handler);
515
-
516
- let request = Request::new(Bytes::from("test payload"));
517
- let result = service
518
- .handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request)
519
- .await;
520
-
521
- assert!(result.is_ok());
522
- let response = result.unwrap();
523
- assert_eq!(response.into_inner(), Bytes::from("test payload"));
524
- }
525
-
526
- #[tokio::test]
527
- async fn test_generic_grpc_service_with_metadata() {
528
- let handler = Arc::new(TestHandler);
529
- let service = GenericGrpcService::new(handler);
530
-
531
- let mut request = Request::new(Bytes::from("payload"));
532
- request
533
- .metadata_mut()
534
- .insert("custom-header", "custom-value".parse().unwrap());
535
-
536
- let result = service
537
- .handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request)
538
- .await;
539
-
540
- assert!(result.is_ok());
541
- }
542
-
543
- #[test]
544
- fn test_parse_grpc_path_valid() {
545
- let (service, method) = parse_grpc_path("/mypackage.UserService/GetUser").unwrap();
546
- assert_eq!(service, "mypackage.UserService");
547
- assert_eq!(method, "GetUser");
548
- }
549
-
550
- #[test]
551
- fn test_parse_grpc_path_with_nested_package() {
552
- let (service, method) = parse_grpc_path("/com.example.api.v1.UserService/GetUser").unwrap();
553
- assert_eq!(service, "com.example.api.v1.UserService");
554
- assert_eq!(method, "GetUser");
555
- }
556
-
557
- #[test]
558
- fn test_parse_grpc_path_invalid_format() {
559
- let result = parse_grpc_path("/invalid");
560
- assert!(result.is_err());
561
- let status = result.unwrap_err();
562
- assert_eq!(status.code(), tonic::Code::InvalidArgument);
563
- }
564
-
565
- #[test]
566
- fn test_parse_grpc_path_empty_service() {
567
- let result = parse_grpc_path("//Method");
568
- assert!(result.is_err());
569
- }
570
-
571
- #[test]
572
- fn test_parse_grpc_path_empty_method() {
573
- let result = parse_grpc_path("/Service/");
574
- assert!(result.is_err());
575
- }
576
-
577
- #[test]
578
- fn test_parse_grpc_path_no_leading_slash() {
579
- let (service, method) = parse_grpc_path("package.Service/Method").unwrap();
580
- assert_eq!(service, "package.Service");
581
- assert_eq!(method, "Method");
582
- }
583
-
584
- #[test]
585
- fn test_is_grpc_request_valid() {
586
- let mut headers = axum::http::HeaderMap::new();
587
- headers.insert(axum::http::header::CONTENT_TYPE, "application/grpc".parse().unwrap());
588
- assert!(is_grpc_request(&headers));
589
- }
590
-
591
- #[test]
592
- fn test_is_grpc_request_with_subtype() {
593
- let mut headers = axum::http::HeaderMap::new();
594
- headers.insert(
595
- axum::http::header::CONTENT_TYPE,
596
- "application/grpc+proto".parse().unwrap(),
597
- );
598
- assert!(is_grpc_request(&headers));
599
- }
600
-
601
- #[test]
602
- fn test_is_grpc_request_not_grpc() {
603
- let mut headers = axum::http::HeaderMap::new();
604
- headers.insert(axum::http::header::CONTENT_TYPE, "application/json".parse().unwrap());
605
- assert!(!is_grpc_request(&headers));
606
- }
607
-
608
- #[test]
609
- fn test_is_grpc_request_no_content_type() {
610
- let headers = axum::http::HeaderMap::new();
611
- assert!(!is_grpc_request(&headers));
612
- }
613
-
614
- #[test]
615
- fn test_grpc_response_to_tonic_basic() {
616
- let response = GrpcResponseData {
617
- payload: Bytes::from("response"),
618
- metadata: MetadataMap::new(),
619
- };
620
-
621
- let tonic_response = grpc_response_to_tonic(response);
622
- assert_eq!(tonic_response.into_inner(), Bytes::from("response"));
623
- }
624
-
625
- #[test]
626
- fn test_grpc_response_to_tonic_with_metadata() {
627
- let mut metadata = MetadataMap::new();
628
- metadata.insert("custom-header", "value".parse().unwrap());
629
-
630
- let response = GrpcResponseData {
631
- payload: Bytes::from("data"),
632
- metadata,
633
- };
634
-
635
- let tonic_response = grpc_response_to_tonic(response);
636
- assert_eq!(tonic_response.get_ref(), &Bytes::from("data"));
637
- assert!(tonic_response.metadata().get("custom-header").is_some());
638
- }
639
-
640
- #[test]
641
- fn test_generic_grpc_service_service_name() {
642
- let handler = Arc::new(TestHandler);
643
- let service = GenericGrpcService::new(handler);
644
- assert_eq!(service.service_name(), "test.TestService");
645
- }
646
-
647
- #[test]
648
- fn test_copy_metadata() {
649
- let mut source = MetadataMap::new();
650
- source.insert("key1", "value1".parse().unwrap());
651
- source.insert("key2", "value2".parse().unwrap());
652
-
653
- let mut dest = MetadataMap::new();
654
- copy_metadata(&source, &mut dest);
655
-
656
- assert_eq!(dest.get("key1").unwrap(), "value1");
657
- assert_eq!(dest.get("key2").unwrap(), "value2");
658
- }
659
-
660
- #[test]
661
- fn test_copy_metadata_empty() {
662
- let source = MetadataMap::new();
663
- let mut dest = MetadataMap::new();
664
- copy_metadata(&source, &mut dest);
665
- assert!(dest.is_empty());
666
- }
667
-
668
- #[test]
669
- fn test_copy_metadata_binary() {
670
- let mut source = MetadataMap::new();
671
- source.insert_bin("binary-key-bin", tonic::metadata::MetadataValue::from_bytes(b"binary"));
672
-
673
- let mut dest = MetadataMap::new();
674
- copy_metadata(&source, &mut dest);
675
-
676
- assert!(dest.get_bin("binary-key-bin").is_some());
677
- }
678
-
679
- #[tokio::test]
680
- async fn test_generic_grpc_service_error_handling() {
681
- struct ErrorHandler;
682
-
683
- impl GrpcHandler for ErrorHandler {
684
- fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
685
- Box::pin(async { Err(Status::not_found("Resource not found")) })
686
- }
687
-
688
- fn service_name(&self) -> &str {
689
- "test.ErrorService"
690
- }
691
- }
692
-
693
- let handler = Arc::new(ErrorHandler);
694
- let service = GenericGrpcService::new(handler);
695
-
696
- let request = Request::new(Bytes::new());
697
- let result = service
698
- .handle_unary("test.ErrorService".to_string(), "ErrorMethod".to_string(), request)
699
- .await;
700
-
701
- assert!(result.is_err());
702
- let status = result.unwrap_err();
703
- assert_eq!(status.code(), tonic::Code::NotFound);
704
- assert_eq!(status.message(), "Resource not found");
705
- }
706
- }