spikard_http/grpc/handler.rs
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
7use bytes::Bytes;
8use std::future::Future;
9use std::pin::Pin;
10use 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)]
19pub 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)]
35pub 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
47pub 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/// ```
79pub 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)]
130mod 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}