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,392 @@
|
|
|
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::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
|
|
8
|
+
use bytes::Bytes;
|
|
9
|
+
use std::sync::Arc;
|
|
10
|
+
use tonic::{Request, Response, Status};
|
|
11
|
+
|
|
12
|
+
/// Generic gRPC service that routes requests to a GrpcHandler
|
|
13
|
+
///
|
|
14
|
+
/// This service implements Tonic's server traits and routes all requests
|
|
15
|
+
/// to the provided GrpcHandler implementation. It handles serialization
|
|
16
|
+
/// at the boundary between Tonic and our handler trait.
|
|
17
|
+
///
|
|
18
|
+
/// # Example
|
|
19
|
+
///
|
|
20
|
+
/// ```ignore
|
|
21
|
+
/// use spikard_http::grpc::service::GenericGrpcService;
|
|
22
|
+
/// use std::sync::Arc;
|
|
23
|
+
///
|
|
24
|
+
/// let handler = Arc::new(MyGrpcHandler);
|
|
25
|
+
/// let service = GenericGrpcService::new(handler);
|
|
26
|
+
/// ```
|
|
27
|
+
pub struct GenericGrpcService {
|
|
28
|
+
handler: Arc<dyn GrpcHandler>,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
impl GenericGrpcService {
|
|
32
|
+
/// Create a new generic gRPC service with the given handler
|
|
33
|
+
pub fn new(handler: Arc<dyn GrpcHandler>) -> Self {
|
|
34
|
+
Self { handler }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/// Handle a unary RPC call
|
|
38
|
+
///
|
|
39
|
+
/// Converts the Tonic Request into our GrpcRequestData format,
|
|
40
|
+
/// calls the handler, and converts the result back to a Tonic Response.
|
|
41
|
+
///
|
|
42
|
+
/// # Arguments
|
|
43
|
+
///
|
|
44
|
+
/// * `service_name` - Fully qualified service name
|
|
45
|
+
/// * `method_name` - Method name
|
|
46
|
+
/// * `request` - Tonic request containing the serialized protobuf message
|
|
47
|
+
pub async fn handle_unary(
|
|
48
|
+
&self,
|
|
49
|
+
service_name: String,
|
|
50
|
+
method_name: String,
|
|
51
|
+
request: Request<Bytes>,
|
|
52
|
+
) -> Result<Response<Bytes>, Status> {
|
|
53
|
+
// Extract metadata and payload from Tonic request
|
|
54
|
+
let (metadata, _extensions, payload) = request.into_parts();
|
|
55
|
+
|
|
56
|
+
// Create our internal request representation
|
|
57
|
+
let grpc_request = GrpcRequestData {
|
|
58
|
+
service_name,
|
|
59
|
+
method_name,
|
|
60
|
+
payload,
|
|
61
|
+
metadata,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Call the handler
|
|
65
|
+
let result: GrpcHandlerResult = self.handler.call(grpc_request).await;
|
|
66
|
+
|
|
67
|
+
// Convert result to Tonic response
|
|
68
|
+
match result {
|
|
69
|
+
Ok(grpc_response) => {
|
|
70
|
+
let mut response = Response::new(grpc_response.payload);
|
|
71
|
+
copy_metadata(&grpc_response.metadata, response.metadata_mut());
|
|
72
|
+
Ok(response)
|
|
73
|
+
}
|
|
74
|
+
Err(status) => Err(status),
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Get the service name from the handler
|
|
79
|
+
pub fn service_name(&self) -> &str {
|
|
80
|
+
self.handler.service_name()
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Helper function to parse gRPC path into service and method names
|
|
85
|
+
///
|
|
86
|
+
/// gRPC paths follow the format: `/<package>.<service>/<method>`
|
|
87
|
+
///
|
|
88
|
+
/// # Example
|
|
89
|
+
///
|
|
90
|
+
/// ```ignore
|
|
91
|
+
/// use spikard_http::grpc::service::parse_grpc_path;
|
|
92
|
+
///
|
|
93
|
+
/// let (service, method) = parse_grpc_path("/mypackage.UserService/GetUser").unwrap();
|
|
94
|
+
/// assert_eq!(service, "mypackage.UserService");
|
|
95
|
+
/// assert_eq!(method, "GetUser");
|
|
96
|
+
/// ```
|
|
97
|
+
pub fn parse_grpc_path(path: &str) -> Result<(String, String), Status> {
|
|
98
|
+
// gRPC paths are in the format: /<package>.<service>/<method>
|
|
99
|
+
let path = path.trim_start_matches('/');
|
|
100
|
+
let parts: Vec<&str> = path.split('/').collect();
|
|
101
|
+
|
|
102
|
+
if parts.len() != 2 {
|
|
103
|
+
return Err(Status::invalid_argument(format!("Invalid gRPC path: {}", path)));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let service_name = parts[0].to_string();
|
|
107
|
+
let method_name = parts[1].to_string();
|
|
108
|
+
|
|
109
|
+
if service_name.is_empty() || method_name.is_empty() {
|
|
110
|
+
return Err(Status::invalid_argument("Service or method name is empty"));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
Ok((service_name, method_name))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// Check if a request is a gRPC request
|
|
117
|
+
///
|
|
118
|
+
/// Checks the content-type header for "application/grpc" prefix.
|
|
119
|
+
///
|
|
120
|
+
/// # Example
|
|
121
|
+
///
|
|
122
|
+
/// ```ignore
|
|
123
|
+
/// use spikard_http::grpc::service::is_grpc_request;
|
|
124
|
+
/// use axum::http::HeaderMap;
|
|
125
|
+
///
|
|
126
|
+
/// let mut headers = HeaderMap::new();
|
|
127
|
+
/// headers.insert("content-type", "application/grpc".parse().unwrap());
|
|
128
|
+
///
|
|
129
|
+
/// assert!(is_grpc_request(&headers));
|
|
130
|
+
/// ```
|
|
131
|
+
pub fn is_grpc_request(headers: &axum::http::HeaderMap) -> bool {
|
|
132
|
+
headers
|
|
133
|
+
.get(axum::http::header::CONTENT_TYPE)
|
|
134
|
+
.and_then(|v| v.to_str().ok())
|
|
135
|
+
.map(|v| v.starts_with("application/grpc"))
|
|
136
|
+
.unwrap_or(false)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Copy metadata from source to destination MetadataMap
|
|
140
|
+
///
|
|
141
|
+
/// Efficiently copies all metadata entries (both ASCII and binary)
|
|
142
|
+
/// from one MetadataMap to another without unnecessary allocations.
|
|
143
|
+
///
|
|
144
|
+
/// # Arguments
|
|
145
|
+
///
|
|
146
|
+
/// * `source` - Source metadata to copy from
|
|
147
|
+
/// * `dest` - Destination metadata to copy into
|
|
148
|
+
pub fn copy_metadata(source: &tonic::metadata::MetadataMap, dest: &mut tonic::metadata::MetadataMap) {
|
|
149
|
+
for key_value in source.iter() {
|
|
150
|
+
match key_value {
|
|
151
|
+
tonic::metadata::KeyAndValueRef::Ascii(key, value) => {
|
|
152
|
+
dest.insert(key, value.clone());
|
|
153
|
+
}
|
|
154
|
+
tonic::metadata::KeyAndValueRef::Binary(key, value) => {
|
|
155
|
+
dest.insert_bin(key, value.clone());
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/// Convert GrpcResponseData to Tonic Response
|
|
162
|
+
///
|
|
163
|
+
/// Helper function to convert our internal response representation
|
|
164
|
+
/// to a Tonic Response.
|
|
165
|
+
pub fn grpc_response_to_tonic(response: GrpcResponseData) -> Response<Bytes> {
|
|
166
|
+
let mut tonic_response = Response::new(response.payload);
|
|
167
|
+
copy_metadata(&response.metadata, tonic_response.metadata_mut());
|
|
168
|
+
tonic_response
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
#[cfg(test)]
|
|
172
|
+
mod tests {
|
|
173
|
+
use super::*;
|
|
174
|
+
use crate::grpc::handler::GrpcHandler;
|
|
175
|
+
use std::future::Future;
|
|
176
|
+
use std::pin::Pin;
|
|
177
|
+
use tonic::metadata::MetadataMap;
|
|
178
|
+
|
|
179
|
+
struct TestHandler;
|
|
180
|
+
|
|
181
|
+
impl GrpcHandler for TestHandler {
|
|
182
|
+
fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
183
|
+
Box::pin(async move {
|
|
184
|
+
// Echo back the request payload
|
|
185
|
+
Ok(GrpcResponseData {
|
|
186
|
+
payload: request.payload,
|
|
187
|
+
metadata: MetadataMap::new(),
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn service_name(&self) -> &'static str {
|
|
193
|
+
"test.TestService"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
#[tokio::test]
|
|
198
|
+
async fn test_generic_grpc_service_handle_unary() {
|
|
199
|
+
let handler = Arc::new(TestHandler);
|
|
200
|
+
let service = GenericGrpcService::new(handler);
|
|
201
|
+
|
|
202
|
+
let request = Request::new(Bytes::from("test payload"));
|
|
203
|
+
let result = service.handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request).await;
|
|
204
|
+
|
|
205
|
+
assert!(result.is_ok());
|
|
206
|
+
let response = result.unwrap();
|
|
207
|
+
assert_eq!(response.into_inner(), Bytes::from("test payload"));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
#[tokio::test]
|
|
211
|
+
async fn test_generic_grpc_service_with_metadata() {
|
|
212
|
+
let handler = Arc::new(TestHandler);
|
|
213
|
+
let service = GenericGrpcService::new(handler);
|
|
214
|
+
|
|
215
|
+
let mut request = Request::new(Bytes::from("payload"));
|
|
216
|
+
request
|
|
217
|
+
.metadata_mut()
|
|
218
|
+
.insert("custom-header", "custom-value".parse().unwrap());
|
|
219
|
+
|
|
220
|
+
let result = service.handle_unary("test.TestService".to_string(), "TestMethod".to_string(), request).await;
|
|
221
|
+
|
|
222
|
+
assert!(result.is_ok());
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
#[test]
|
|
226
|
+
fn test_parse_grpc_path_valid() {
|
|
227
|
+
let (service, method) = parse_grpc_path("/mypackage.UserService/GetUser").unwrap();
|
|
228
|
+
assert_eq!(service, "mypackage.UserService");
|
|
229
|
+
assert_eq!(method, "GetUser");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
#[test]
|
|
233
|
+
fn test_parse_grpc_path_with_nested_package() {
|
|
234
|
+
let (service, method) = parse_grpc_path("/com.example.api.v1.UserService/GetUser").unwrap();
|
|
235
|
+
assert_eq!(service, "com.example.api.v1.UserService");
|
|
236
|
+
assert_eq!(method, "GetUser");
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
#[test]
|
|
240
|
+
fn test_parse_grpc_path_invalid_format() {
|
|
241
|
+
let result = parse_grpc_path("/invalid");
|
|
242
|
+
assert!(result.is_err());
|
|
243
|
+
let status = result.unwrap_err();
|
|
244
|
+
assert_eq!(status.code(), tonic::Code::InvalidArgument);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
#[test]
|
|
248
|
+
fn test_parse_grpc_path_empty_service() {
|
|
249
|
+
let result = parse_grpc_path("//Method");
|
|
250
|
+
assert!(result.is_err());
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn test_parse_grpc_path_empty_method() {
|
|
255
|
+
let result = parse_grpc_path("/Service/");
|
|
256
|
+
assert!(result.is_err());
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
#[test]
|
|
260
|
+
fn test_parse_grpc_path_no_leading_slash() {
|
|
261
|
+
let (service, method) = parse_grpc_path("package.Service/Method").unwrap();
|
|
262
|
+
assert_eq!(service, "package.Service");
|
|
263
|
+
assert_eq!(method, "Method");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[test]
|
|
267
|
+
fn test_is_grpc_request_valid() {
|
|
268
|
+
let mut headers = axum::http::HeaderMap::new();
|
|
269
|
+
headers.insert(
|
|
270
|
+
axum::http::header::CONTENT_TYPE,
|
|
271
|
+
"application/grpc".parse().unwrap(),
|
|
272
|
+
);
|
|
273
|
+
assert!(is_grpc_request(&headers));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
#[test]
|
|
277
|
+
fn test_is_grpc_request_with_subtype() {
|
|
278
|
+
let mut headers = axum::http::HeaderMap::new();
|
|
279
|
+
headers.insert(
|
|
280
|
+
axum::http::header::CONTENT_TYPE,
|
|
281
|
+
"application/grpc+proto".parse().unwrap(),
|
|
282
|
+
);
|
|
283
|
+
assert!(is_grpc_request(&headers));
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
#[test]
|
|
287
|
+
fn test_is_grpc_request_not_grpc() {
|
|
288
|
+
let mut headers = axum::http::HeaderMap::new();
|
|
289
|
+
headers.insert(
|
|
290
|
+
axum::http::header::CONTENT_TYPE,
|
|
291
|
+
"application/json".parse().unwrap(),
|
|
292
|
+
);
|
|
293
|
+
assert!(!is_grpc_request(&headers));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
#[test]
|
|
297
|
+
fn test_is_grpc_request_no_content_type() {
|
|
298
|
+
let headers = axum::http::HeaderMap::new();
|
|
299
|
+
assert!(!is_grpc_request(&headers));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
#[test]
|
|
303
|
+
fn test_grpc_response_to_tonic_basic() {
|
|
304
|
+
let response = GrpcResponseData {
|
|
305
|
+
payload: Bytes::from("response"),
|
|
306
|
+
metadata: MetadataMap::new(),
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
let tonic_response = grpc_response_to_tonic(response);
|
|
310
|
+
assert_eq!(tonic_response.into_inner(), Bytes::from("response"));
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
#[test]
|
|
314
|
+
fn test_grpc_response_to_tonic_with_metadata() {
|
|
315
|
+
let mut metadata = MetadataMap::new();
|
|
316
|
+
metadata.insert("custom-header", "value".parse().unwrap());
|
|
317
|
+
|
|
318
|
+
let response = GrpcResponseData {
|
|
319
|
+
payload: Bytes::from("data"),
|
|
320
|
+
metadata,
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
let tonic_response = grpc_response_to_tonic(response);
|
|
324
|
+
assert_eq!(tonic_response.get_ref(), &Bytes::from("data"));
|
|
325
|
+
assert!(tonic_response.metadata().get("custom-header").is_some());
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
#[test]
|
|
329
|
+
fn test_generic_grpc_service_service_name() {
|
|
330
|
+
let handler = Arc::new(TestHandler);
|
|
331
|
+
let service = GenericGrpcService::new(handler);
|
|
332
|
+
assert_eq!(service.service_name(), "test.TestService");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#[test]
|
|
336
|
+
fn test_copy_metadata() {
|
|
337
|
+
let mut source = MetadataMap::new();
|
|
338
|
+
source.insert("key1", "value1".parse().unwrap());
|
|
339
|
+
source.insert("key2", "value2".parse().unwrap());
|
|
340
|
+
|
|
341
|
+
let mut dest = MetadataMap::new();
|
|
342
|
+
copy_metadata(&source, &mut dest);
|
|
343
|
+
|
|
344
|
+
assert_eq!(dest.get("key1").unwrap(), "value1");
|
|
345
|
+
assert_eq!(dest.get("key2").unwrap(), "value2");
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
#[test]
|
|
349
|
+
fn test_copy_metadata_empty() {
|
|
350
|
+
let source = MetadataMap::new();
|
|
351
|
+
let mut dest = MetadataMap::new();
|
|
352
|
+
copy_metadata(&source, &mut dest);
|
|
353
|
+
assert!(dest.is_empty());
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#[test]
|
|
357
|
+
fn test_copy_metadata_binary() {
|
|
358
|
+
let mut source = MetadataMap::new();
|
|
359
|
+
source.insert_bin("binary-key-bin", tonic::metadata::MetadataValue::from_bytes(b"binary"));
|
|
360
|
+
|
|
361
|
+
let mut dest = MetadataMap::new();
|
|
362
|
+
copy_metadata(&source, &mut dest);
|
|
363
|
+
|
|
364
|
+
assert!(dest.get_bin("binary-key-bin").is_some());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
#[tokio::test]
|
|
368
|
+
async fn test_generic_grpc_service_error_handling() {
|
|
369
|
+
struct ErrorHandler;
|
|
370
|
+
|
|
371
|
+
impl GrpcHandler for ErrorHandler {
|
|
372
|
+
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
373
|
+
Box::pin(async { Err(Status::not_found("Resource not found")) })
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fn service_name(&self) -> &'static str {
|
|
377
|
+
"test.ErrorService"
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
let handler = Arc::new(ErrorHandler);
|
|
382
|
+
let service = GenericGrpcService::new(handler);
|
|
383
|
+
|
|
384
|
+
let request = Request::new(Bytes::new());
|
|
385
|
+
let result = service.handle_unary("test.ErrorService".to_string(), "ErrorMethod".to_string(), request).await;
|
|
386
|
+
|
|
387
|
+
assert!(result.is_err());
|
|
388
|
+
let status = result.unwrap_err();
|
|
389
|
+
assert_eq!(status.code(), tonic::Code::NotFound);
|
|
390
|
+
assert_eq!(status.message(), "Resource not found");
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
//! Streaming support utilities for gRPC
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides utilities for handling streaming RPCs:
|
|
4
|
+
//! - Client streaming (receiving stream of messages)
|
|
5
|
+
//! - Server streaming (sending stream of messages)
|
|
6
|
+
//! - Bidirectional streaming (both directions)
|
|
7
|
+
|
|
8
|
+
use bytes::Bytes;
|
|
9
|
+
use futures_util::Stream;
|
|
10
|
+
use std::pin::Pin;
|
|
11
|
+
use tonic::Status;
|
|
12
|
+
|
|
13
|
+
/// Type alias for a stream of protobuf message bytes
|
|
14
|
+
///
|
|
15
|
+
/// Used for both client streaming (incoming) and server streaming (outgoing).
|
|
16
|
+
/// Each item in the stream is either:
|
|
17
|
+
/// - Ok(Bytes): A serialized protobuf message
|
|
18
|
+
/// - Err(Status): A gRPC error
|
|
19
|
+
pub type MessageStream = Pin<Box<dyn Stream<Item = Result<Bytes, Status>> + Send>>;
|
|
20
|
+
|
|
21
|
+
/// Request for client streaming RPC
|
|
22
|
+
///
|
|
23
|
+
/// Contains metadata and a stream of incoming messages from the client.
|
|
24
|
+
pub struct StreamingRequest {
|
|
25
|
+
/// Service name
|
|
26
|
+
pub service_name: String,
|
|
27
|
+
/// Method name
|
|
28
|
+
pub method_name: String,
|
|
29
|
+
/// Stream of incoming protobuf messages
|
|
30
|
+
pub message_stream: MessageStream,
|
|
31
|
+
/// Request metadata
|
|
32
|
+
pub metadata: tonic::metadata::MetadataMap,
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Response for server streaming RPC
|
|
36
|
+
///
|
|
37
|
+
/// Contains metadata and a stream of outgoing messages to the client.
|
|
38
|
+
pub struct StreamingResponse {
|
|
39
|
+
/// Stream of outgoing protobuf messages
|
|
40
|
+
pub message_stream: MessageStream,
|
|
41
|
+
/// Response metadata
|
|
42
|
+
pub metadata: tonic::metadata::MetadataMap,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Helper to create a message stream from a vector of bytes
|
|
46
|
+
///
|
|
47
|
+
/// Useful for testing and for handlers that want to create a stream
|
|
48
|
+
/// from a fixed set of messages.
|
|
49
|
+
///
|
|
50
|
+
/// # Example
|
|
51
|
+
///
|
|
52
|
+
/// ```ignore
|
|
53
|
+
/// use spikard_http::grpc::streaming::message_stream_from_vec;
|
|
54
|
+
/// use bytes::Bytes;
|
|
55
|
+
///
|
|
56
|
+
/// let messages = vec![
|
|
57
|
+
/// Bytes::from("message1"),
|
|
58
|
+
/// Bytes::from("message2"),
|
|
59
|
+
/// ];
|
|
60
|
+
///
|
|
61
|
+
/// let stream = message_stream_from_vec(messages);
|
|
62
|
+
/// ```
|
|
63
|
+
pub fn message_stream_from_vec(messages: Vec<Bytes>) -> MessageStream {
|
|
64
|
+
Box::pin(futures_util::stream::iter(messages.into_iter().map(Ok)))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Helper to create an empty message stream
|
|
68
|
+
///
|
|
69
|
+
/// Useful for testing or for handlers that need to return an empty stream.
|
|
70
|
+
pub fn empty_message_stream() -> MessageStream {
|
|
71
|
+
Box::pin(futures_util::stream::empty())
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/// Helper to create a single-message stream
|
|
75
|
+
///
|
|
76
|
+
/// Useful for converting unary responses to streaming responses.
|
|
77
|
+
///
|
|
78
|
+
/// # Example
|
|
79
|
+
///
|
|
80
|
+
/// ```ignore
|
|
81
|
+
/// use spikard_http::grpc::streaming::single_message_stream;
|
|
82
|
+
/// use bytes::Bytes;
|
|
83
|
+
///
|
|
84
|
+
/// let stream = single_message_stream(Bytes::from("response"));
|
|
85
|
+
/// ```
|
|
86
|
+
pub fn single_message_stream(message: Bytes) -> MessageStream {
|
|
87
|
+
Box::pin(futures_util::stream::once(async move { Ok(message) }))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Helper to create an error stream
|
|
91
|
+
///
|
|
92
|
+
/// Returns a stream that immediately yields a gRPC error.
|
|
93
|
+
///
|
|
94
|
+
/// # Example
|
|
95
|
+
///
|
|
96
|
+
/// ```ignore
|
|
97
|
+
/// use spikard_http::grpc::streaming::error_stream;
|
|
98
|
+
/// use tonic::Status;
|
|
99
|
+
///
|
|
100
|
+
/// let stream = error_stream(Status::internal("Something went wrong"));
|
|
101
|
+
/// ```
|
|
102
|
+
pub fn error_stream(status: Status) -> MessageStream {
|
|
103
|
+
Box::pin(futures_util::stream::once(async move { Err(status) }))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Helper to convert a Tonic ReceiverStream to our MessageStream
|
|
107
|
+
///
|
|
108
|
+
/// This is used in the service bridge to convert Tonic's streaming types
|
|
109
|
+
/// to our internal representation.
|
|
110
|
+
pub fn from_tonic_stream<S>(stream: S) -> MessageStream
|
|
111
|
+
where
|
|
112
|
+
S: Stream<Item = Result<Bytes, Status>> + Send + 'static,
|
|
113
|
+
{
|
|
114
|
+
Box::pin(stream)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
#[cfg(test)]
|
|
118
|
+
mod tests {
|
|
119
|
+
use super::*;
|
|
120
|
+
use futures_util::StreamExt;
|
|
121
|
+
|
|
122
|
+
#[tokio::test]
|
|
123
|
+
async fn test_message_stream_from_vec() {
|
|
124
|
+
let messages = vec![Bytes::from("msg1"), Bytes::from("msg2"), Bytes::from("msg3")];
|
|
125
|
+
|
|
126
|
+
let mut stream = message_stream_from_vec(messages.clone());
|
|
127
|
+
|
|
128
|
+
let msg1 = stream.next().await.unwrap().unwrap();
|
|
129
|
+
assert_eq!(msg1, Bytes::from("msg1"));
|
|
130
|
+
|
|
131
|
+
let msg2 = stream.next().await.unwrap().unwrap();
|
|
132
|
+
assert_eq!(msg2, Bytes::from("msg2"));
|
|
133
|
+
|
|
134
|
+
let msg3 = stream.next().await.unwrap().unwrap();
|
|
135
|
+
assert_eq!(msg3, Bytes::from("msg3"));
|
|
136
|
+
|
|
137
|
+
assert!(stream.next().await.is_none());
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[tokio::test]
|
|
141
|
+
async fn test_empty_message_stream() {
|
|
142
|
+
let mut stream = empty_message_stream();
|
|
143
|
+
assert!(stream.next().await.is_none());
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[tokio::test]
|
|
147
|
+
async fn test_single_message_stream() {
|
|
148
|
+
let mut stream = single_message_stream(Bytes::from("single"));
|
|
149
|
+
|
|
150
|
+
let msg = stream.next().await.unwrap().unwrap();
|
|
151
|
+
assert_eq!(msg, Bytes::from("single"));
|
|
152
|
+
|
|
153
|
+
assert!(stream.next().await.is_none());
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#[tokio::test]
|
|
157
|
+
async fn test_error_stream() {
|
|
158
|
+
let mut stream = error_stream(Status::internal("test error"));
|
|
159
|
+
|
|
160
|
+
let result = stream.next().await.unwrap();
|
|
161
|
+
assert!(result.is_err());
|
|
162
|
+
|
|
163
|
+
let error = result.unwrap_err();
|
|
164
|
+
assert_eq!(error.code(), tonic::Code::Internal);
|
|
165
|
+
assert_eq!(error.message(), "test error");
|
|
166
|
+
|
|
167
|
+
assert!(stream.next().await.is_none());
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
#[tokio::test]
|
|
171
|
+
async fn test_message_stream_from_vec_empty() {
|
|
172
|
+
let messages: Vec<Bytes> = vec![];
|
|
173
|
+
let mut stream = message_stream_from_vec(messages);
|
|
174
|
+
assert!(stream.next().await.is_none());
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
#[tokio::test]
|
|
178
|
+
async fn test_message_stream_from_vec_large() {
|
|
179
|
+
let mut messages = vec![];
|
|
180
|
+
for i in 0..100 {
|
|
181
|
+
messages.push(Bytes::from(format!("message{}", i)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let mut stream = message_stream_from_vec(messages);
|
|
185
|
+
|
|
186
|
+
for i in 0..100 {
|
|
187
|
+
let msg = stream.next().await.unwrap().unwrap();
|
|
188
|
+
assert_eq!(msg, Bytes::from(format!("message{}", i)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
assert!(stream.next().await.is_none());
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[tokio::test]
|
|
195
|
+
async fn test_from_tonic_stream() {
|
|
196
|
+
let messages = vec![Ok(Bytes::from("a")), Ok(Bytes::from("b")), Err(Status::cancelled("done"))];
|
|
197
|
+
|
|
198
|
+
let tonic_stream = futures_util::stream::iter(messages);
|
|
199
|
+
let mut stream = from_tonic_stream(tonic_stream);
|
|
200
|
+
|
|
201
|
+
let msg1 = stream.next().await.unwrap().unwrap();
|
|
202
|
+
assert_eq!(msg1, Bytes::from("a"));
|
|
203
|
+
|
|
204
|
+
let msg2 = stream.next().await.unwrap().unwrap();
|
|
205
|
+
assert_eq!(msg2, Bytes::from("b"));
|
|
206
|
+
|
|
207
|
+
let result = stream.next().await.unwrap();
|
|
208
|
+
assert!(result.is_err());
|
|
209
|
+
|
|
210
|
+
assert!(stream.next().await.is_none());
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#[test]
|
|
214
|
+
fn test_streaming_request_creation() {
|
|
215
|
+
let stream = empty_message_stream();
|
|
216
|
+
let request = StreamingRequest {
|
|
217
|
+
service_name: "test.Service".to_string(),
|
|
218
|
+
method_name: "StreamMethod".to_string(),
|
|
219
|
+
message_stream: stream,
|
|
220
|
+
metadata: tonic::metadata::MetadataMap::new(),
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
assert_eq!(request.service_name, "test.Service");
|
|
224
|
+
assert_eq!(request.method_name, "StreamMethod");
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
#[test]
|
|
228
|
+
fn test_streaming_response_creation() {
|
|
229
|
+
let stream = empty_message_stream();
|
|
230
|
+
let response = StreamingResponse {
|
|
231
|
+
message_stream: stream,
|
|
232
|
+
metadata: tonic::metadata::MetadataMap::new(),
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
assert!(response.metadata.is_empty());
|
|
236
|
+
}
|
|
237
|
+
}
|
|
@@ -13,6 +13,7 @@ pub mod cors;
|
|
|
13
13
|
pub mod debug;
|
|
14
14
|
#[cfg(feature = "di")]
|
|
15
15
|
pub mod di_handler;
|
|
16
|
+
pub mod grpc;
|
|
16
17
|
pub mod handler_response;
|
|
17
18
|
pub mod handler_trait;
|
|
18
19
|
pub mod jsonrpc;
|
|
@@ -39,6 +40,10 @@ pub use background::{
|
|
|
39
40
|
pub use body_metadata::ResponseBodySize;
|
|
40
41
|
#[cfg(feature = "di")]
|
|
41
42
|
pub use di_handler::DependencyInjectingHandler;
|
|
43
|
+
pub use grpc::{
|
|
44
|
+
GrpcConfig, GrpcHandler, GrpcHandlerResult, GrpcRegistry, GrpcRequestData, GrpcResponseData, MessageStream,
|
|
45
|
+
StreamingRequest, StreamingResponse,
|
|
46
|
+
};
|
|
42
47
|
pub use handler_response::HandlerResponse;
|
|
43
48
|
pub use handler_trait::{Handler, HandlerResult, RequestData, ValidatedParams};
|
|
44
49
|
pub use jsonrpc::JsonRpcConfig;
|
|
@@ -140,6 +145,8 @@ pub struct ServerConfig {
|
|
|
140
145
|
pub openapi: Option<crate::openapi::OpenApiConfig>,
|
|
141
146
|
/// JSON-RPC configuration
|
|
142
147
|
pub jsonrpc: Option<crate::jsonrpc::JsonRpcConfig>,
|
|
148
|
+
/// gRPC configuration
|
|
149
|
+
pub grpc: Option<crate::grpc::GrpcConfig>,
|
|
143
150
|
/// Lifecycle hooks for request/response processing
|
|
144
151
|
pub lifecycle_hooks: Option<std::sync::Arc<LifecycleHooks>>,
|
|
145
152
|
/// Background task executor configuration
|
|
@@ -169,6 +176,7 @@ impl Default for ServerConfig {
|
|
|
169
176
|
shutdown_timeout: 30,
|
|
170
177
|
openapi: None,
|
|
171
178
|
jsonrpc: None,
|
|
179
|
+
grpc: None,
|
|
172
180
|
lifecycle_hooks: None,
|
|
173
181
|
background_tasks: BackgroundTaskConfig::default(),
|
|
174
182
|
enable_http_trace: false,
|
|
@@ -339,6 +347,12 @@ impl ServerConfigBuilder {
|
|
|
339
347
|
self
|
|
340
348
|
}
|
|
341
349
|
|
|
350
|
+
/// Set gRPC configuration
|
|
351
|
+
pub fn grpc(mut self, grpc: Option<crate::grpc::GrpcConfig>) -> Self {
|
|
352
|
+
self.config.grpc = grpc;
|
|
353
|
+
self
|
|
354
|
+
}
|
|
355
|
+
|
|
342
356
|
/// Set lifecycle hooks for request/response processing
|
|
343
357
|
pub fn lifecycle_hooks(mut self, hooks: Option<std::sync::Arc<LifecycleHooks>>) -> Self {
|
|
344
358
|
self.config.lifecycle_hooks = hooks;
|