spikard 0.7.5 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 -0
- 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,260 @@
|
|
|
1
|
+
//! Core GrpcHandler trait for language-agnostic gRPC request handling
|
|
2
|
+
//!
|
|
3
|
+
//! This module defines the handler trait that language bindings implement
|
|
4
|
+
//! to handle gRPC requests. Similar to the HttpHandler pattern but designed
|
|
5
|
+
//! specifically for gRPC's protobuf-based message format.
|
|
6
|
+
|
|
7
|
+
use bytes::Bytes;
|
|
8
|
+
use std::future::Future;
|
|
9
|
+
use std::pin::Pin;
|
|
10
|
+
use tonic::metadata::MetadataMap;
|
|
11
|
+
|
|
12
|
+
/// gRPC request data passed to handlers
|
|
13
|
+
///
|
|
14
|
+
/// Contains the parsed components of a gRPC request:
|
|
15
|
+
/// - Service and method names from the request path
|
|
16
|
+
/// - Serialized protobuf payload as bytes
|
|
17
|
+
/// - Request metadata (headers)
|
|
18
|
+
#[derive(Debug, Clone)]
|
|
19
|
+
pub struct GrpcRequestData {
|
|
20
|
+
/// Fully qualified service name (e.g., "mypackage.MyService")
|
|
21
|
+
pub service_name: String,
|
|
22
|
+
/// Method name (e.g., "GetUser")
|
|
23
|
+
pub method_name: String,
|
|
24
|
+
/// Serialized protobuf message bytes
|
|
25
|
+
pub payload: Bytes,
|
|
26
|
+
/// gRPC metadata (similar to HTTP headers)
|
|
27
|
+
pub metadata: MetadataMap,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// gRPC response data returned by handlers
|
|
31
|
+
///
|
|
32
|
+
/// Contains the serialized protobuf response and any metadata to include
|
|
33
|
+
/// in the response headers.
|
|
34
|
+
#[derive(Debug, Clone)]
|
|
35
|
+
pub struct GrpcResponseData {
|
|
36
|
+
/// Serialized protobuf message bytes
|
|
37
|
+
pub payload: Bytes,
|
|
38
|
+
/// gRPC metadata to include in response (similar to HTTP headers)
|
|
39
|
+
pub metadata: MetadataMap,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Result type for gRPC handlers
|
|
43
|
+
///
|
|
44
|
+
/// Returns either:
|
|
45
|
+
/// - Ok(GrpcResponseData): A successful response with payload and metadata
|
|
46
|
+
/// - Err(tonic::Status): A gRPC error status with code and message
|
|
47
|
+
pub type GrpcHandlerResult = Result<GrpcResponseData, tonic::Status>;
|
|
48
|
+
|
|
49
|
+
/// Handler trait for gRPC requests
|
|
50
|
+
///
|
|
51
|
+
/// This is the language-agnostic interface that all gRPC handler implementations
|
|
52
|
+
/// must satisfy. Language bindings (Python, TypeScript, Ruby, PHP) will implement
|
|
53
|
+
/// this trait to bridge their runtime to Spikard's gRPC server.
|
|
54
|
+
///
|
|
55
|
+
/// # Example
|
|
56
|
+
///
|
|
57
|
+
/// ```ignore
|
|
58
|
+
/// use spikard_http::grpc::{GrpcHandler, GrpcRequestData, GrpcHandlerResult};
|
|
59
|
+
/// use std::pin::Pin;
|
|
60
|
+
/// use std::future::Future;
|
|
61
|
+
///
|
|
62
|
+
/// struct MyGrpcHandler;
|
|
63
|
+
///
|
|
64
|
+
/// impl GrpcHandler for MyGrpcHandler {
|
|
65
|
+
/// fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
66
|
+
/// Box::pin(async move {
|
|
67
|
+
/// // Deserialize request.payload using protobuf
|
|
68
|
+
/// // Process the request
|
|
69
|
+
/// // Serialize response using protobuf
|
|
70
|
+
/// // Return GrpcResponseData
|
|
71
|
+
/// Ok(GrpcResponseData {
|
|
72
|
+
/// payload: bytes::Bytes::from("serialized response"),
|
|
73
|
+
/// metadata: tonic::metadata::MetadataMap::new(),
|
|
74
|
+
/// })
|
|
75
|
+
/// })
|
|
76
|
+
/// }
|
|
77
|
+
/// }
|
|
78
|
+
/// ```
|
|
79
|
+
pub trait GrpcHandler: Send + Sync {
|
|
80
|
+
/// Handle a gRPC request
|
|
81
|
+
///
|
|
82
|
+
/// Takes the parsed request data and returns a future that resolves to either:
|
|
83
|
+
/// - Ok(GrpcResponseData): A successful response
|
|
84
|
+
/// - Err(tonic::Status): An error with appropriate gRPC status code
|
|
85
|
+
///
|
|
86
|
+
/// # Arguments
|
|
87
|
+
///
|
|
88
|
+
/// * `request` - The parsed gRPC request containing service/method names,
|
|
89
|
+
/// serialized payload, and metadata
|
|
90
|
+
///
|
|
91
|
+
/// # Returns
|
|
92
|
+
///
|
|
93
|
+
/// A future that resolves to a GrpcHandlerResult
|
|
94
|
+
fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>>;
|
|
95
|
+
|
|
96
|
+
/// Get the fully qualified service name this handler serves
|
|
97
|
+
///
|
|
98
|
+
/// This is used for routing requests to the appropriate handler.
|
|
99
|
+
/// Should return the fully qualified service name as defined in the .proto file.
|
|
100
|
+
///
|
|
101
|
+
/// # Example
|
|
102
|
+
///
|
|
103
|
+
/// For a service defined as:
|
|
104
|
+
/// ```proto
|
|
105
|
+
/// package mypackage;
|
|
106
|
+
/// service UserService { ... }
|
|
107
|
+
/// ```
|
|
108
|
+
///
|
|
109
|
+
/// This should return "mypackage.UserService"
|
|
110
|
+
fn service_name(&self) -> &'static str;
|
|
111
|
+
|
|
112
|
+
/// Whether this handler supports streaming requests
|
|
113
|
+
///
|
|
114
|
+
/// If true, the handler can receive multiple request messages in sequence.
|
|
115
|
+
/// Default implementation returns false (unary requests only).
|
|
116
|
+
fn supports_streaming_requests(&self) -> bool {
|
|
117
|
+
false
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/// Whether this handler supports streaming responses
|
|
121
|
+
///
|
|
122
|
+
/// If true, the handler can send multiple response messages in sequence.
|
|
123
|
+
/// Default implementation returns false (unary responses only).
|
|
124
|
+
fn supports_streaming_responses(&self) -> bool {
|
|
125
|
+
false
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
#[cfg(test)]
|
|
130
|
+
mod tests {
|
|
131
|
+
use super::*;
|
|
132
|
+
|
|
133
|
+
struct TestGrpcHandler;
|
|
134
|
+
|
|
135
|
+
impl GrpcHandler for TestGrpcHandler {
|
|
136
|
+
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
137
|
+
Box::pin(async {
|
|
138
|
+
Ok(GrpcResponseData {
|
|
139
|
+
payload: Bytes::from("test response"),
|
|
140
|
+
metadata: MetadataMap::new(),
|
|
141
|
+
})
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
fn service_name(&self) -> &'static str {
|
|
146
|
+
"test.TestService"
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[tokio::test]
|
|
151
|
+
async fn test_grpc_handler_basic_call() {
|
|
152
|
+
let handler = TestGrpcHandler;
|
|
153
|
+
let request = GrpcRequestData {
|
|
154
|
+
service_name: "test.TestService".to_string(),
|
|
155
|
+
method_name: "TestMethod".to_string(),
|
|
156
|
+
payload: Bytes::from("test payload"),
|
|
157
|
+
metadata: MetadataMap::new(),
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
let result = handler.call(request).await;
|
|
161
|
+
assert!(result.is_ok());
|
|
162
|
+
|
|
163
|
+
let response = result.unwrap();
|
|
164
|
+
assert_eq!(response.payload, Bytes::from("test response"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#[test]
|
|
168
|
+
fn test_grpc_handler_service_name() {
|
|
169
|
+
let handler = TestGrpcHandler;
|
|
170
|
+
assert_eq!(handler.service_name(), "test.TestService");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
#[test]
|
|
174
|
+
fn test_grpc_handler_default_streaming_support() {
|
|
175
|
+
let handler = TestGrpcHandler;
|
|
176
|
+
assert!(!handler.supports_streaming_requests());
|
|
177
|
+
assert!(!handler.supports_streaming_responses());
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn test_grpc_request_data_creation() {
|
|
182
|
+
let request = GrpcRequestData {
|
|
183
|
+
service_name: "mypackage.MyService".to_string(),
|
|
184
|
+
method_name: "GetUser".to_string(),
|
|
185
|
+
payload: Bytes::from("payload"),
|
|
186
|
+
metadata: MetadataMap::new(),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
assert_eq!(request.service_name, "mypackage.MyService");
|
|
190
|
+
assert_eq!(request.method_name, "GetUser");
|
|
191
|
+
assert_eq!(request.payload, Bytes::from("payload"));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
#[test]
|
|
195
|
+
fn test_grpc_response_data_creation() {
|
|
196
|
+
let response = GrpcResponseData {
|
|
197
|
+
payload: Bytes::from("response"),
|
|
198
|
+
metadata: MetadataMap::new(),
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
assert_eq!(response.payload, Bytes::from("response"));
|
|
202
|
+
assert!(response.metadata.is_empty());
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
#[test]
|
|
206
|
+
fn test_grpc_request_data_clone() {
|
|
207
|
+
let original = GrpcRequestData {
|
|
208
|
+
service_name: "test.Service".to_string(),
|
|
209
|
+
method_name: "Method".to_string(),
|
|
210
|
+
payload: Bytes::from("data"),
|
|
211
|
+
metadata: MetadataMap::new(),
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let cloned = original.clone();
|
|
215
|
+
assert_eq!(original.service_name, cloned.service_name);
|
|
216
|
+
assert_eq!(original.method_name, cloned.method_name);
|
|
217
|
+
assert_eq!(original.payload, cloned.payload);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
#[test]
|
|
221
|
+
fn test_grpc_response_data_clone() {
|
|
222
|
+
let original = GrpcResponseData {
|
|
223
|
+
payload: Bytes::from("response data"),
|
|
224
|
+
metadata: MetadataMap::new(),
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
let cloned = original.clone();
|
|
228
|
+
assert_eq!(original.payload, cloned.payload);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
#[tokio::test]
|
|
232
|
+
async fn test_grpc_handler_error_response() {
|
|
233
|
+
struct ErrorHandler;
|
|
234
|
+
|
|
235
|
+
impl GrpcHandler for ErrorHandler {
|
|
236
|
+
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
237
|
+
Box::pin(async { Err(tonic::Status::not_found("Resource not found")) })
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
fn service_name(&self) -> &'static str {
|
|
241
|
+
"test.ErrorService"
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
let handler = ErrorHandler;
|
|
246
|
+
let request = GrpcRequestData {
|
|
247
|
+
service_name: "test.ErrorService".to_string(),
|
|
248
|
+
method_name: "ErrorMethod".to_string(),
|
|
249
|
+
payload: Bytes::new(),
|
|
250
|
+
metadata: MetadataMap::new(),
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
let result = handler.call(request).await;
|
|
254
|
+
assert!(result.is_err());
|
|
255
|
+
|
|
256
|
+
let error = result.unwrap_err();
|
|
257
|
+
assert_eq!(error.code(), tonic::Code::NotFound);
|
|
258
|
+
assert_eq!(error.message(), "Resource not found");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
//! gRPC runtime support for Spikard
|
|
2
|
+
//!
|
|
3
|
+
//! This module provides gRPC server infrastructure using Tonic, enabling
|
|
4
|
+
//! Spikard to handle both HTTP/1.1 REST requests and HTTP/2 gRPC requests.
|
|
5
|
+
//!
|
|
6
|
+
//! # Architecture
|
|
7
|
+
//!
|
|
8
|
+
//! The gRPC support follows the same language-agnostic pattern as the HTTP handler:
|
|
9
|
+
//!
|
|
10
|
+
//! 1. **GrpcHandler trait**: Language-agnostic interface for handling gRPC requests
|
|
11
|
+
//! 2. **Service bridge**: Converts between Tonic's types and our internal representation
|
|
12
|
+
//! 3. **Streaming support**: Utilities for handling streaming RPCs
|
|
13
|
+
//! 4. **Server integration**: Multiplexes HTTP/1.1 and HTTP/2 traffic
|
|
14
|
+
//!
|
|
15
|
+
//! # Example
|
|
16
|
+
//!
|
|
17
|
+
//! ```ignore
|
|
18
|
+
//! use spikard_http::grpc::{GrpcHandler, GrpcRequestData, GrpcResponseData};
|
|
19
|
+
//! use std::sync::Arc;
|
|
20
|
+
//!
|
|
21
|
+
//! // Implement GrpcHandler for your language binding
|
|
22
|
+
//! struct MyGrpcHandler;
|
|
23
|
+
//!
|
|
24
|
+
//! impl GrpcHandler for MyGrpcHandler {
|
|
25
|
+
//! fn call(&self, request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
26
|
+
//! Box::pin(async move {
|
|
27
|
+
//! // Handle the gRPC request
|
|
28
|
+
//! Ok(GrpcResponseData {
|
|
29
|
+
//! payload: bytes::Bytes::from("response"),
|
|
30
|
+
//! metadata: tonic::metadata::MetadataMap::new(),
|
|
31
|
+
//! })
|
|
32
|
+
//! })
|
|
33
|
+
//! }
|
|
34
|
+
//!
|
|
35
|
+
//! fn service_name(&self) -> &str {
|
|
36
|
+
//! "mypackage.MyService"
|
|
37
|
+
//! }
|
|
38
|
+
//! }
|
|
39
|
+
//!
|
|
40
|
+
//! // Register with the server
|
|
41
|
+
//! let handler = Arc::new(MyGrpcHandler);
|
|
42
|
+
//! let config = GrpcConfig::default();
|
|
43
|
+
//! ```
|
|
44
|
+
|
|
45
|
+
pub mod handler;
|
|
46
|
+
pub mod service;
|
|
47
|
+
pub mod streaming;
|
|
48
|
+
|
|
49
|
+
// Re-export main types
|
|
50
|
+
pub use handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData, GrpcResponseData};
|
|
51
|
+
pub use service::{copy_metadata, GenericGrpcService, is_grpc_request, parse_grpc_path};
|
|
52
|
+
pub use streaming::{MessageStream, StreamingRequest, StreamingResponse};
|
|
53
|
+
|
|
54
|
+
use serde::{Deserialize, Serialize};
|
|
55
|
+
use std::collections::HashMap;
|
|
56
|
+
use std::sync::Arc;
|
|
57
|
+
|
|
58
|
+
/// Configuration for gRPC support
|
|
59
|
+
///
|
|
60
|
+
/// Controls how the server handles gRPC requests, including compression,
|
|
61
|
+
/// timeouts, and protocol settings.
|
|
62
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
63
|
+
pub struct GrpcConfig {
|
|
64
|
+
/// Enable gRPC support
|
|
65
|
+
#[serde(default = "default_true")]
|
|
66
|
+
pub enabled: bool,
|
|
67
|
+
|
|
68
|
+
/// Maximum message size in bytes (for both sending and receiving)
|
|
69
|
+
#[serde(default = "default_max_message_size")]
|
|
70
|
+
pub max_message_size: usize,
|
|
71
|
+
|
|
72
|
+
/// Enable gzip compression for gRPC messages
|
|
73
|
+
#[serde(default = "default_true")]
|
|
74
|
+
pub enable_compression: bool,
|
|
75
|
+
|
|
76
|
+
/// Timeout for gRPC requests in seconds (None = no timeout)
|
|
77
|
+
#[serde(default)]
|
|
78
|
+
pub request_timeout: Option<u64>,
|
|
79
|
+
|
|
80
|
+
/// Maximum number of concurrent streams per connection
|
|
81
|
+
#[serde(default = "default_max_concurrent_streams")]
|
|
82
|
+
pub max_concurrent_streams: u32,
|
|
83
|
+
|
|
84
|
+
/// Enable HTTP/2 keepalive
|
|
85
|
+
#[serde(default = "default_true")]
|
|
86
|
+
pub enable_keepalive: bool,
|
|
87
|
+
|
|
88
|
+
/// HTTP/2 keepalive interval in seconds
|
|
89
|
+
#[serde(default = "default_keepalive_interval")]
|
|
90
|
+
pub keepalive_interval: u64,
|
|
91
|
+
|
|
92
|
+
/// HTTP/2 keepalive timeout in seconds
|
|
93
|
+
#[serde(default = "default_keepalive_timeout")]
|
|
94
|
+
pub keepalive_timeout: u64,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
impl Default for GrpcConfig {
|
|
98
|
+
fn default() -> Self {
|
|
99
|
+
Self {
|
|
100
|
+
enabled: true,
|
|
101
|
+
max_message_size: default_max_message_size(),
|
|
102
|
+
enable_compression: true,
|
|
103
|
+
request_timeout: None,
|
|
104
|
+
max_concurrent_streams: default_max_concurrent_streams(),
|
|
105
|
+
enable_keepalive: true,
|
|
106
|
+
keepalive_interval: default_keepalive_interval(),
|
|
107
|
+
keepalive_timeout: default_keepalive_timeout(),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const fn default_true() -> bool {
|
|
113
|
+
true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fn default_max_message_size() -> usize {
|
|
117
|
+
4 * 1024 * 1024 // 4MB
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const fn default_max_concurrent_streams() -> u32 {
|
|
121
|
+
100
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const fn default_keepalive_interval() -> u64 {
|
|
125
|
+
75 // seconds
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const fn default_keepalive_timeout() -> u64 {
|
|
129
|
+
20 // seconds
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// Registry for gRPC handlers
|
|
133
|
+
///
|
|
134
|
+
/// Maps service names to their handlers. Used by the server to route
|
|
135
|
+
/// incoming gRPC requests to the appropriate handler.
|
|
136
|
+
///
|
|
137
|
+
/// # Example
|
|
138
|
+
///
|
|
139
|
+
/// ```ignore
|
|
140
|
+
/// use spikard_http::grpc::GrpcRegistry;
|
|
141
|
+
/// use std::sync::Arc;
|
|
142
|
+
///
|
|
143
|
+
/// let mut registry = GrpcRegistry::new();
|
|
144
|
+
/// registry.register("mypackage.UserService", Arc::new(user_handler));
|
|
145
|
+
/// registry.register("mypackage.PostService", Arc::new(post_handler));
|
|
146
|
+
/// ```
|
|
147
|
+
#[derive(Clone)]
|
|
148
|
+
pub struct GrpcRegistry {
|
|
149
|
+
handlers: Arc<HashMap<String, Arc<dyn GrpcHandler>>>,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
impl GrpcRegistry {
|
|
153
|
+
/// Create a new empty gRPC handler registry
|
|
154
|
+
pub fn new() -> Self {
|
|
155
|
+
Self {
|
|
156
|
+
handlers: Arc::new(HashMap::new()),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Register a gRPC handler for a service
|
|
161
|
+
///
|
|
162
|
+
/// # Arguments
|
|
163
|
+
///
|
|
164
|
+
/// * `service_name` - Fully qualified service name (e.g., "mypackage.MyService")
|
|
165
|
+
/// * `handler` - Handler implementation for this service
|
|
166
|
+
pub fn register(&mut self, service_name: impl Into<String>, handler: Arc<dyn GrpcHandler>) {
|
|
167
|
+
let handlers = Arc::make_mut(&mut self.handlers);
|
|
168
|
+
handlers.insert(service_name.into(), handler);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/// Get a handler by service name
|
|
172
|
+
pub fn get(&self, service_name: &str) -> Option<Arc<dyn GrpcHandler>> {
|
|
173
|
+
self.handlers.get(service_name).cloned()
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/// Get all registered service names
|
|
177
|
+
pub fn service_names(&self) -> Vec<String> {
|
|
178
|
+
self.handlers.keys().cloned().collect()
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Check if a service is registered
|
|
182
|
+
pub fn contains(&self, service_name: &str) -> bool {
|
|
183
|
+
self.handlers.contains_key(service_name)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Get the number of registered services
|
|
187
|
+
pub fn len(&self) -> usize {
|
|
188
|
+
self.handlers.len()
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/// Check if the registry is empty
|
|
192
|
+
pub fn is_empty(&self) -> bool {
|
|
193
|
+
self.handlers.is_empty()
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
impl Default for GrpcRegistry {
|
|
198
|
+
fn default() -> Self {
|
|
199
|
+
Self::new()
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
#[cfg(test)]
|
|
204
|
+
mod tests {
|
|
205
|
+
use super::*;
|
|
206
|
+
use crate::grpc::handler::{GrpcHandler, GrpcHandlerResult, GrpcRequestData};
|
|
207
|
+
use std::future::Future;
|
|
208
|
+
use std::pin::Pin;
|
|
209
|
+
|
|
210
|
+
struct TestHandler;
|
|
211
|
+
|
|
212
|
+
impl GrpcHandler for TestHandler {
|
|
213
|
+
fn call(&self, _request: GrpcRequestData) -> Pin<Box<dyn Future<Output = GrpcHandlerResult> + Send>> {
|
|
214
|
+
Box::pin(async {
|
|
215
|
+
Ok(GrpcResponseData {
|
|
216
|
+
payload: bytes::Bytes::new(),
|
|
217
|
+
metadata: tonic::metadata::MetadataMap::new(),
|
|
218
|
+
})
|
|
219
|
+
})
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
fn service_name(&self) -> &'static str {
|
|
223
|
+
// Since we can't return a reference to self.0 with 'static lifetime,
|
|
224
|
+
// we need to use a workaround. In real usage, service names should be static.
|
|
225
|
+
"test.Service"
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
#[test]
|
|
230
|
+
fn test_grpc_config_default() {
|
|
231
|
+
let config = GrpcConfig::default();
|
|
232
|
+
assert!(config.enabled);
|
|
233
|
+
assert_eq!(config.max_message_size, 4 * 1024 * 1024);
|
|
234
|
+
assert!(config.enable_compression);
|
|
235
|
+
assert!(config.request_timeout.is_none());
|
|
236
|
+
assert_eq!(config.max_concurrent_streams, 100);
|
|
237
|
+
assert!(config.enable_keepalive);
|
|
238
|
+
assert_eq!(config.keepalive_interval, 75);
|
|
239
|
+
assert_eq!(config.keepalive_timeout, 20);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#[test]
|
|
243
|
+
fn test_grpc_config_serialization() {
|
|
244
|
+
let config = GrpcConfig::default();
|
|
245
|
+
let json = serde_json::to_string(&config).unwrap();
|
|
246
|
+
let deserialized: GrpcConfig = serde_json::from_str(&json).unwrap();
|
|
247
|
+
|
|
248
|
+
assert_eq!(config.enabled, deserialized.enabled);
|
|
249
|
+
assert_eq!(config.max_message_size, deserialized.max_message_size);
|
|
250
|
+
assert_eq!(config.enable_compression, deserialized.enable_compression);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
#[test]
|
|
254
|
+
fn test_grpc_registry_new() {
|
|
255
|
+
let registry = GrpcRegistry::new();
|
|
256
|
+
assert!(registry.is_empty());
|
|
257
|
+
assert_eq!(registry.len(), 0);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
#[test]
|
|
261
|
+
fn test_grpc_registry_register() {
|
|
262
|
+
let mut registry = GrpcRegistry::new();
|
|
263
|
+
let handler = Arc::new(TestHandler);
|
|
264
|
+
|
|
265
|
+
registry.register("test.Service", handler);
|
|
266
|
+
|
|
267
|
+
assert!(!registry.is_empty());
|
|
268
|
+
assert_eq!(registry.len(), 1);
|
|
269
|
+
assert!(registry.contains("test.Service"));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
#[test]
|
|
273
|
+
fn test_grpc_registry_get() {
|
|
274
|
+
let mut registry = GrpcRegistry::new();
|
|
275
|
+
let handler = Arc::new(TestHandler);
|
|
276
|
+
|
|
277
|
+
registry.register("test.Service", handler);
|
|
278
|
+
|
|
279
|
+
let retrieved = registry.get("test.Service");
|
|
280
|
+
assert!(retrieved.is_some());
|
|
281
|
+
assert_eq!(retrieved.unwrap().service_name(), "test.Service");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
#[test]
|
|
285
|
+
fn test_grpc_registry_get_nonexistent() {
|
|
286
|
+
let registry = GrpcRegistry::new();
|
|
287
|
+
let result = registry.get("nonexistent.Service");
|
|
288
|
+
assert!(result.is_none());
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
#[test]
|
|
292
|
+
fn test_grpc_registry_service_names() {
|
|
293
|
+
let mut registry = GrpcRegistry::new();
|
|
294
|
+
|
|
295
|
+
registry.register("service1", Arc::new(TestHandler));
|
|
296
|
+
registry.register("service2", Arc::new(TestHandler));
|
|
297
|
+
registry.register("service3", Arc::new(TestHandler));
|
|
298
|
+
|
|
299
|
+
let mut names = registry.service_names();
|
|
300
|
+
names.sort();
|
|
301
|
+
|
|
302
|
+
assert_eq!(names, vec!["service1", "service2", "service3"]);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
#[test]
|
|
306
|
+
fn test_grpc_registry_contains() {
|
|
307
|
+
let mut registry = GrpcRegistry::new();
|
|
308
|
+
registry.register("test.Service", Arc::new(TestHandler));
|
|
309
|
+
|
|
310
|
+
assert!(registry.contains("test.Service"));
|
|
311
|
+
assert!(!registry.contains("other.Service"));
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
#[test]
|
|
315
|
+
fn test_grpc_registry_multiple_services() {
|
|
316
|
+
let mut registry = GrpcRegistry::new();
|
|
317
|
+
|
|
318
|
+
registry.register("user.Service", Arc::new(TestHandler));
|
|
319
|
+
registry.register("post.Service", Arc::new(TestHandler));
|
|
320
|
+
|
|
321
|
+
assert_eq!(registry.len(), 2);
|
|
322
|
+
assert!(registry.contains("user.Service"));
|
|
323
|
+
assert!(registry.contains("post.Service"));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
#[test]
|
|
327
|
+
fn test_grpc_registry_clone() {
|
|
328
|
+
let mut registry = GrpcRegistry::new();
|
|
329
|
+
registry.register("test.Service", Arc::new(TestHandler));
|
|
330
|
+
|
|
331
|
+
let cloned = registry.clone();
|
|
332
|
+
|
|
333
|
+
assert_eq!(cloned.len(), 1);
|
|
334
|
+
assert!(cloned.contains("test.Service"));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
#[test]
|
|
338
|
+
fn test_grpc_registry_default() {
|
|
339
|
+
let registry = GrpcRegistry::default();
|
|
340
|
+
assert!(registry.is_empty());
|
|
341
|
+
}
|
|
342
|
+
}
|