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.
- checksums.yaml +4 -4
- data/ext/spikard_rb/Cargo.lock +583 -201
- data/ext/spikard_rb/Cargo.toml +1 -1
- data/lib/spikard/grpc.rb +182 -0
- data/lib/spikard/version.rb +1 -1
- data/lib/spikard.rb +1 -1
- data/vendor/crates/spikard-bindings-shared/Cargo.toml +2 -1
- data/vendor/crates/spikard-bindings-shared/src/grpc_metadata.rs +197 -0
- data/vendor/crates/spikard-bindings-shared/src/lib.rs +2 -0
- data/vendor/crates/spikard-core/Cargo.toml +1 -1
- data/vendor/crates/spikard-http/Cargo.toml +5 -1
- data/vendor/crates/spikard-http/src/grpc/handler.rs +260 -0
- data/vendor/crates/spikard-http/src/grpc/mod.rs +342 -0
- data/vendor/crates/spikard-http/src/grpc/service.rs +392 -0
- data/vendor/crates/spikard-http/src/grpc/streaming.rs +237 -0
- data/vendor/crates/spikard-http/src/lib.rs +14 -0
- data/vendor/crates/spikard-http/src/server/grpc_routing.rs +288 -0
- data/vendor/crates/spikard-http/src/server/mod.rs +1 -0
- data/vendor/crates/spikard-http/tests/common/grpc_helpers.rs +1023 -0
- data/vendor/crates/spikard-http/tests/common/mod.rs +8 -0
- data/vendor/crates/spikard-http/tests/grpc_error_handling_test.rs +653 -0
- data/vendor/crates/spikard-http/tests/grpc_integration_test.rs +332 -0
- data/vendor/crates/spikard-http/tests/grpc_metadata_test.rs +518 -0
- data/vendor/crates/spikard-http/tests/grpc_server_integration.rs +476 -0
- data/vendor/crates/spikard-rb/Cargo.toml +2 -1
- data/vendor/crates/spikard-rb/src/config/server_config.rs +1 -0
- data/vendor/crates/spikard-rb/src/grpc/handler.rs +352 -0
- data/vendor/crates/spikard-rb/src/grpc/mod.rs +9 -0
- data/vendor/crates/spikard-rb/src/lib.rs +4 -0
- data/vendor/crates/spikard-rb-macros/Cargo.toml +1 -1
- 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;
|