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}