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.
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 -0
  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,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
+ }