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,288 @@
1
+ //! gRPC request routing and multiplexing
2
+ //!
3
+ //! This module handles routing gRPC requests to the appropriate handlers
4
+ //! and multiplexing between HTTP/1.1 REST and HTTP/2 gRPC traffic.
5
+
6
+ use crate::grpc::{GrpcRegistry, parse_grpc_path};
7
+ use axum::body::Body;
8
+ use axum::http::{Request, Response, StatusCode};
9
+ use std::sync::Arc;
10
+
11
+ /// Convert gRPC status code to HTTP status code
12
+ ///
13
+ /// Maps all gRPC status codes to appropriate HTTP status codes
14
+ /// following the gRPC-HTTP status code mapping specification.
15
+ fn grpc_status_to_http(code: tonic::Code) -> StatusCode {
16
+ match code {
17
+ tonic::Code::Ok => StatusCode::OK,
18
+ tonic::Code::Cancelled => StatusCode::from_u16(499).unwrap(), // Client Closed Request
19
+ tonic::Code::Unknown => StatusCode::INTERNAL_SERVER_ERROR,
20
+ tonic::Code::InvalidArgument => StatusCode::BAD_REQUEST,
21
+ tonic::Code::DeadlineExceeded => StatusCode::GATEWAY_TIMEOUT,
22
+ tonic::Code::NotFound => StatusCode::NOT_FOUND,
23
+ tonic::Code::AlreadyExists => StatusCode::CONFLICT,
24
+ tonic::Code::PermissionDenied => StatusCode::FORBIDDEN,
25
+ tonic::Code::ResourceExhausted => StatusCode::TOO_MANY_REQUESTS,
26
+ tonic::Code::FailedPrecondition => StatusCode::BAD_REQUEST,
27
+ tonic::Code::Aborted => StatusCode::CONFLICT,
28
+ tonic::Code::OutOfRange => StatusCode::BAD_REQUEST,
29
+ tonic::Code::Unimplemented => StatusCode::NOT_IMPLEMENTED,
30
+ tonic::Code::Internal => StatusCode::INTERNAL_SERVER_ERROR,
31
+ tonic::Code::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
32
+ tonic::Code::DataLoss => StatusCode::INTERNAL_SERVER_ERROR,
33
+ tonic::Code::Unauthenticated => StatusCode::UNAUTHORIZED,
34
+ }
35
+ }
36
+
37
+ /// Route a gRPC request to the appropriate handler
38
+ ///
39
+ /// Parses the request path to extract service and method names,
40
+ /// looks up the handler in the registry, and invokes it.
41
+ ///
42
+ /// # Arguments
43
+ ///
44
+ /// * `registry` - gRPC handler registry
45
+ /// * `request` - The incoming gRPC request
46
+ ///
47
+ /// # Returns
48
+ ///
49
+ /// A future that resolves to a gRPC response or error
50
+ pub async fn route_grpc_request(
51
+ registry: Arc<GrpcRegistry>,
52
+ request: Request<Body>,
53
+ ) -> Result<Response<Body>, (StatusCode, String)> {
54
+ // Extract the request path
55
+ let path = request.uri().path();
56
+
57
+ // Parse service and method names from the path
58
+ let (service_name, method_name) = match parse_grpc_path(path) {
59
+ Ok(names) => names,
60
+ Err(status) => {
61
+ return Err((
62
+ StatusCode::BAD_REQUEST,
63
+ format!("Invalid gRPC path: {}", status.message()),
64
+ ));
65
+ }
66
+ };
67
+
68
+ // Look up the handler for this service
69
+ let handler = match registry.get(&service_name) {
70
+ Some(h) => h,
71
+ None => {
72
+ return Err((
73
+ StatusCode::NOT_FOUND,
74
+ format!("Service not found: {}", service_name),
75
+ ));
76
+ }
77
+ };
78
+
79
+ // Convert the Axum request to bytes
80
+ let (parts, body) = request.into_parts();
81
+ let body_bytes = match axum::body::to_bytes(body, usize::MAX).await {
82
+ Ok(bytes) => bytes,
83
+ Err(e) => {
84
+ return Err((StatusCode::BAD_REQUEST, format!("Failed to read request body: {}", e)));
85
+ }
86
+ };
87
+
88
+ // Create a Tonic request
89
+ let mut tonic_request = tonic::Request::new(body_bytes);
90
+
91
+ // Copy headers to Tonic metadata
92
+ for (key, value) in parts.headers.iter() {
93
+ if let Ok(value_str) = value.to_str() {
94
+ // Try to parse as ASCII metadata
95
+ if let Ok(metadata_value) = value_str.parse::<tonic::metadata::MetadataValue<tonic::metadata::Ascii>>() {
96
+ // Use key.as_str() directly instead of creating String
97
+ if let Ok(metadata_key) = key.as_str().parse::<tonic::metadata::MetadataKey<tonic::metadata::Ascii>>() {
98
+ tonic_request.metadata_mut().insert(metadata_key, metadata_value);
99
+ }
100
+ }
101
+ }
102
+ }
103
+
104
+ // Use the service bridge to handle the request
105
+ let service = crate::grpc::GenericGrpcService::new(handler);
106
+ let tonic_response = match service.handle_unary(service_name, method_name, tonic_request).await {
107
+ Ok(resp) => resp,
108
+ Err(status) => {
109
+ let status_code = grpc_status_to_http(status.code());
110
+ return Err((status_code, status.message().to_string()));
111
+ }
112
+ };
113
+
114
+ // Convert Tonic response to Axum response
115
+ let payload = tonic_response.get_ref().clone();
116
+ let metadata = tonic_response.metadata();
117
+
118
+ let mut response = Response::builder()
119
+ .status(StatusCode::OK)
120
+ .header("content-type", "application/grpc+proto");
121
+
122
+ // Copy metadata to response headers
123
+ for key_value in metadata.iter() {
124
+ if let tonic::metadata::KeyAndValueRef::Ascii(key, value) = key_value
125
+ && let Ok(header_value) = axum::http::HeaderValue::from_str(value.to_str().unwrap_or(""))
126
+ {
127
+ response = response.header(key.as_str(), header_value);
128
+ }
129
+ }
130
+
131
+ // Add gRPC status trailer (success)
132
+ response = response.header("grpc-status", "0");
133
+
134
+ // Convert bytes::Bytes to Body
135
+ let response = response
136
+ .body(Body::from(payload))
137
+ .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to build response: {}", e)))?;
138
+
139
+ Ok(response)
140
+ }
141
+
142
+ /// Check if an incoming request is a gRPC request
143
+ ///
144
+ /// Returns true if the request has a content-type starting with "application/grpc"
145
+ pub fn is_grpc_request(request: &Request<Body>) -> bool {
146
+ request
147
+ .headers()
148
+ .get(axum::http::header::CONTENT_TYPE)
149
+ .and_then(|v| v.to_str().ok())
150
+ .map(|v| v.starts_with("application/grpc"))
151
+ .unwrap_or(false)
152
+ }
153
+
154
+ #[cfg(test)]
155
+ mod tests {
156
+ use super::*;
157
+ use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
158
+ use bytes::Bytes;
159
+ use std::future::Future;
160
+ use std::pin::Pin;
161
+
162
+ struct EchoHandler;
163
+
164
+ impl GrpcHandler for EchoHandler {
165
+ fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
166
+ Box::pin(async move {
167
+ Ok(GrpcResponseData {
168
+ payload: request.payload,
169
+ metadata: tonic::metadata::MetadataMap::new(),
170
+ })
171
+ })
172
+ }
173
+
174
+ fn service_name(&self) -> &'static str {
175
+ "test.EchoService"
176
+ }
177
+ }
178
+
179
+ #[tokio::test]
180
+ async fn test_route_grpc_request_success() {
181
+ let mut registry = GrpcRegistry::new();
182
+ registry.register("test.EchoService", Arc::new(EchoHandler));
183
+ let registry = Arc::new(registry);
184
+
185
+ let request = Request::builder()
186
+ .uri("/test.EchoService/Echo")
187
+ .header("content-type", "application/grpc")
188
+ .body(Body::from(Bytes::from("test payload")))
189
+ .unwrap();
190
+
191
+ let result = route_grpc_request(registry, request).await;
192
+ assert!(result.is_ok());
193
+ }
194
+
195
+ #[tokio::test]
196
+ async fn test_route_grpc_request_service_not_found() {
197
+ let registry = Arc::new(GrpcRegistry::new());
198
+
199
+ let request = Request::builder()
200
+ .uri("/nonexistent.Service/Method")
201
+ .header("content-type", "application/grpc")
202
+ .body(Body::from(Bytes::new()))
203
+ .unwrap();
204
+
205
+ let result = route_grpc_request(registry, request).await;
206
+ assert!(result.is_err());
207
+
208
+ let (status, message) = result.unwrap_err();
209
+ assert_eq!(status, StatusCode::NOT_FOUND);
210
+ assert!(message.contains("Service not found"));
211
+ }
212
+
213
+ #[tokio::test]
214
+ async fn test_route_grpc_request_invalid_path() {
215
+ let registry = Arc::new(GrpcRegistry::new());
216
+
217
+ let request = Request::builder()
218
+ .uri("/invalid")
219
+ .header("content-type", "application/grpc")
220
+ .body(Body::from(Bytes::new()))
221
+ .unwrap();
222
+
223
+ let result = route_grpc_request(registry, request).await;
224
+ assert!(result.is_err());
225
+
226
+ let (status, _message) = result.unwrap_err();
227
+ assert_eq!(status, StatusCode::BAD_REQUEST);
228
+ }
229
+
230
+ #[test]
231
+ fn test_is_grpc_request_true() {
232
+ let request = Request::builder()
233
+ .header("content-type", "application/grpc")
234
+ .body(Body::empty())
235
+ .unwrap();
236
+
237
+ assert!(is_grpc_request(&request));
238
+ }
239
+
240
+ #[test]
241
+ fn test_is_grpc_request_with_subtype() {
242
+ let request = Request::builder()
243
+ .header("content-type", "application/grpc+proto")
244
+ .body(Body::empty())
245
+ .unwrap();
246
+
247
+ assert!(is_grpc_request(&request));
248
+ }
249
+
250
+ #[test]
251
+ fn test_is_grpc_request_false() {
252
+ let request = Request::builder()
253
+ .header("content-type", "application/json")
254
+ .body(Body::empty())
255
+ .unwrap();
256
+
257
+ assert!(!is_grpc_request(&request));
258
+ }
259
+
260
+ #[test]
261
+ fn test_is_grpc_request_no_content_type() {
262
+ let request = Request::builder().body(Body::empty()).unwrap();
263
+
264
+ assert!(!is_grpc_request(&request));
265
+ }
266
+
267
+ #[test]
268
+ fn test_grpc_status_to_http_mappings() {
269
+ // Test all gRPC status codes map correctly
270
+ assert_eq!(grpc_status_to_http(tonic::Code::Ok), StatusCode::OK);
271
+ assert_eq!(grpc_status_to_http(tonic::Code::Cancelled), StatusCode::from_u16(499).unwrap());
272
+ assert_eq!(grpc_status_to_http(tonic::Code::Unknown), StatusCode::INTERNAL_SERVER_ERROR);
273
+ assert_eq!(grpc_status_to_http(tonic::Code::InvalidArgument), StatusCode::BAD_REQUEST);
274
+ assert_eq!(grpc_status_to_http(tonic::Code::DeadlineExceeded), StatusCode::GATEWAY_TIMEOUT);
275
+ assert_eq!(grpc_status_to_http(tonic::Code::NotFound), StatusCode::NOT_FOUND);
276
+ assert_eq!(grpc_status_to_http(tonic::Code::AlreadyExists), StatusCode::CONFLICT);
277
+ assert_eq!(grpc_status_to_http(tonic::Code::PermissionDenied), StatusCode::FORBIDDEN);
278
+ assert_eq!(grpc_status_to_http(tonic::Code::ResourceExhausted), StatusCode::TOO_MANY_REQUESTS);
279
+ assert_eq!(grpc_status_to_http(tonic::Code::FailedPrecondition), StatusCode::BAD_REQUEST);
280
+ assert_eq!(grpc_status_to_http(tonic::Code::Aborted), StatusCode::CONFLICT);
281
+ assert_eq!(grpc_status_to_http(tonic::Code::OutOfRange), StatusCode::BAD_REQUEST);
282
+ assert_eq!(grpc_status_to_http(tonic::Code::Unimplemented), StatusCode::NOT_IMPLEMENTED);
283
+ assert_eq!(grpc_status_to_http(tonic::Code::Internal), StatusCode::INTERNAL_SERVER_ERROR);
284
+ assert_eq!(grpc_status_to_http(tonic::Code::Unavailable), StatusCode::SERVICE_UNAVAILABLE);
285
+ assert_eq!(grpc_status_to_http(tonic::Code::DataLoss), StatusCode::INTERNAL_SERVER_ERROR);
286
+ assert_eq!(grpc_status_to_http(tonic::Code::Unauthenticated), StatusCode::UNAUTHORIZED);
287
+ }
288
+ }
@@ -3,6 +3,7 @@
3
3
  //! This module provides the main server builder and routing infrastructure, with
4
4
  //! focused submodules for handler validation, request extraction, and lifecycle execution.
5
5
 
6
+ pub mod grpc_routing;
6
7
  pub mod handler;
7
8
  pub mod lifecycle_execution;
8
9
  pub mod request_extraction;