Skip to main content

backbone_core/
grpc.rs

1//! gRPC layer and service implementations
2
3use anyhow::Result;
4use async_trait::async_trait;
5use serde::{Serialize, Deserialize};
6use uuid::Uuid;
7use std::collections::HashMap;
8use std::marker::PhantomData;
9use std::sync::Arc;
10
11use crate::service::{ServiceError, ServiceResult};
12
13/// gRPC request/response wrapper
14#[derive(Debug, Serialize, Deserialize)]
15pub struct GrpcResponse<T> {
16    pub success: bool,
17    pub data: Option<T>,
18    pub error: Option<String>,
19}
20
21impl<T> GrpcResponse<T> {
22    pub fn success(data: T) -> Self {
23        Self {
24            success: true,
25            data: Some(data),
26            error: None,
27        }
28    }
29
30    pub fn error(error: String) -> Self {
31        Self {
32            success: false,
33            data: None,
34            error: Some(error),
35        }
36    }
37}
38
39/// gRPC list request with pagination
40#[derive(Debug, Serialize, Deserialize)]
41pub struct GrpcListRequest {
42    pub page: u32,
43    pub limit: u32,
44    pub sort_by: Option<String>,
45    pub sort_order: Option<String>,
46    pub filters: Option<HashMap<String, String>>,
47}
48
49/// gRPC list response with metadata
50#[derive(Debug, Serialize, Deserialize)]
51pub struct GrpcListResponse<T> {
52    pub items: Vec<T>,
53    pub total: u64,
54    pub page: u32,
55    pub limit: u32,
56    pub total_pages: u32,
57}
58
59/// gRPC bulk create request
60#[derive(Debug, Serialize, Deserialize)]
61pub struct GrpcBulkCreateRequest<T> {
62    pub items: Vec<T>,
63}
64
65/// gRPC bulk response
66#[derive(Debug, Serialize, Deserialize)]
67pub struct GrpcBulkResponse<T> {
68    pub items: Vec<T>,
69    pub total: usize,
70    pub failed: usize,
71    pub errors: Vec<String>,
72}
73
74/// gRPC upsert request
75#[derive(Debug, Serialize, Deserialize)]
76pub struct GrpcUpsertRequest<T> {
77    pub entity: T,
78    pub create_if_not_exists: bool,
79}
80
81/// gRPC partial update request
82#[derive(Debug, Serialize, Deserialize)]
83pub struct GrpcPartialUpdateRequest {
84    pub id: Uuid,
85    pub fields: HashMap<String, String>,
86}
87
88/// Complete gRPC service trait for all 11 Backbone endpoints
89pub trait BackboneGrpcService<T> {
90    /// 1. List (paginated, filtered, sorted)
91    fn list(&self, request: GrpcListRequest) -> Result<GrpcResponse<GrpcListResponse<T>>, anyhow::Error>;
92
93    /// 2. Create
94    fn create(&self, request: T) -> Result<GrpcResponse<T>, anyhow::Error>;
95
96    /// 3. Get by ID
97    fn get_by_id(&self, request: Uuid) -> Result<GrpcResponse<T>, anyhow::Error>;
98
99    /// 4. Full update
100    fn update(&self, request: T) -> Result<GrpcResponse<T>, anyhow::Error>;
101
102    /// 5. Partial update
103    fn partial_update(&self, request: GrpcPartialUpdateRequest) -> Result<GrpcResponse<T>, anyhow::Error>;
104
105    /// 6. Soft delete
106    fn soft_delete(&self, request: Uuid) -> Result<GrpcResponse<()>, anyhow::Error>;
107
108    /// 7. Bulk create
109    fn bulk_create(&self, request: GrpcBulkCreateRequest<T>) -> Result<GrpcResponse<GrpcBulkResponse<T>>, anyhow::Error>;
110
111    /// 8. Upsert
112    fn upsert(&self, request: GrpcUpsertRequest<T>) -> Result<GrpcResponse<T>, anyhow::Error>;
113
114    /// 9. List deleted
115    fn list_deleted(&self, request: GrpcListRequest) -> Result<GrpcResponse<GrpcListResponse<T>>, anyhow::Error>;
116
117    /// 10. Restore
118    fn restore(&self, request: Uuid) -> Result<GrpcResponse<T>, anyhow::Error>;
119
120    /// 11. Empty trash
121    fn empty_trash(&self, request: ()) -> Result<GrpcResponse<()>, anyhow::Error>;
122}
123
124/// Legacy gRPC service trait for backwards compatibility
125pub trait GrpcService<T> {
126    fn create(&self, request: T) -> Result<T, anyhow::Error>;
127    fn get(&self, request: T) -> Result<T, anyhow::Error>;
128    fn update(&self, request: T) -> Result<T, anyhow::Error>;
129    fn delete(&self, request: T) -> Result<T, anyhow::Error>;
130    fn list(&self, request: T) -> Result<T, anyhow::Error>;
131}
132
133// ─── GenericGrpcService ───────────────────────────────────────────────────────
134
135/// Service contract required by `GenericGrpcService`.
136///
137/// `GenericCrudService<E,C,U,R>` satisfies this contract.
138/// Any custom service wrapper that exposes the same 8 methods will work too.
139#[async_trait]
140pub trait GrpcCapableService<E, C, U>: Send + Sync + 'static
141where
142    E: Clone + Send + Sync + 'static,
143    C: Send + Sync + 'static,
144    U: Send + Sync + 'static,
145{
146    async fn list(&self, page: u32, limit: u32, filters: HashMap<String, String>) -> ServiceResult<(Vec<E>, u64)>;
147    async fn create(&self, dto: C) -> ServiceResult<E>;
148    async fn get_by_id(&self, id: &str) -> ServiceResult<Option<E>>;
149    async fn update(&self, id: &str, dto: U) -> ServiceResult<Option<E>>;
150    async fn soft_delete(&self, id: &str) -> ServiceResult<bool>;
151    async fn restore(&self, id: &str) -> ServiceResult<Option<E>>;
152    async fn list_deleted(&self, page: u32, limit: u32) -> ServiceResult<(Vec<E>, u64)>;
153    async fn empty_trash(&self) -> ServiceResult<u64>;
154}
155
156/// Generic gRPC service — mirrors `BackboneCrudHandler` for the gRPC transport.
157///
158/// Generated code emits a type alias:
159///
160/// ```rust,ignore
161/// // Generated (was ~300 lines of proto dispatch boilerplate):
162/// pub type OrderGrpcService = GenericGrpcService<
163///     Order, CreateOrderDto, UpdateOrderDto, OrderService
164/// >;
165/// ```
166///
167/// Custom extensions go in the `// <<< CUSTOM` decorator wrapper.
168pub struct GenericGrpcService<E, C, U, S>
169where
170    E: Clone + Send + Sync + 'static,
171    C: Send + Sync + 'static,
172    U: Send + Sync + 'static,
173    S: GrpcCapableService<E, C, U>,
174{
175    service: Arc<S>,
176    _phantom: PhantomData<(E, C, U)>,
177}
178
179impl<E, C, U, S> GenericGrpcService<E, C, U, S>
180where
181    E: Clone + Send + Sync + 'static,
182    C: Send + Sync + 'static,
183    U: Send + Sync + 'static,
184    S: GrpcCapableService<E, C, U>,
185{
186    pub fn new(service: Arc<S>) -> Self {
187        Self {
188            service,
189            _phantom: PhantomData,
190        }
191    }
192
193    pub fn service(&self) -> &Arc<S> {
194        &self.service
195    }
196
197    // ── Delegate methods — each maps directly to one gRPC RPC ────────────
198
199    pub async fn rpc_list(
200        &self,
201        page: u32,
202        limit: u32,
203        filters: HashMap<String, String>,
204    ) -> GrpcResponse<GrpcListResponse<E>>
205    where
206        E: Serialize + for<'de> serde::Deserialize<'de>,
207    {
208        match self.service.list(page, limit, filters).await {
209            Ok((items, total)) => {
210                let total_pages = if limit == 0 { 0 } else { ((total as f64) / (limit as f64)).ceil() as u32 };
211                GrpcResponse::success(GrpcListResponse {
212                    items,
213                    total,
214                    page,
215                    limit,
216                    total_pages,
217                })
218            }
219            Err(e) => GrpcResponse::error(e.to_string()),
220        }
221    }
222
223    pub async fn rpc_create(&self, dto: C) -> GrpcResponse<E>
224    where
225        E: Serialize + for<'de> serde::Deserialize<'de>,
226    {
227        match self.service.create(dto).await {
228            Ok(entity) => GrpcResponse::success(entity),
229            Err(e) => GrpcResponse::error(e.to_string()),
230        }
231    }
232
233    pub async fn rpc_get_by_id(&self, id: &str) -> GrpcResponse<Option<E>>
234    where
235        E: Serialize + for<'de> serde::Deserialize<'de>,
236    {
237        match self.service.get_by_id(id).await {
238            Ok(entity) => GrpcResponse::success(entity),
239            Err(e) => GrpcResponse::error(e.to_string()),
240        }
241    }
242
243    pub async fn rpc_update(&self, id: &str, dto: U) -> GrpcResponse<Option<E>>
244    where
245        E: Serialize + for<'de> serde::Deserialize<'de>,
246    {
247        match self.service.update(id, dto).await {
248            Ok(entity) => GrpcResponse::success(entity),
249            Err(e) => GrpcResponse::error(e.to_string()),
250        }
251    }
252
253    pub async fn rpc_soft_delete(&self, id: &str) -> GrpcResponse<bool> {
254        match self.service.soft_delete(id).await {
255            Ok(deleted) => GrpcResponse::success(deleted),
256            Err(e) => GrpcResponse::error(e.to_string()),
257        }
258    }
259
260    pub async fn rpc_restore(&self, id: &str) -> GrpcResponse<Option<E>>
261    where
262        E: Serialize + for<'de> serde::Deserialize<'de>,
263    {
264        match self.service.restore(id).await {
265            Ok(entity) => GrpcResponse::success(entity),
266            Err(e) => GrpcResponse::error(e.to_string()),
267        }
268    }
269
270    pub async fn rpc_list_deleted(
271        &self,
272        page: u32,
273        limit: u32,
274    ) -> GrpcResponse<GrpcListResponse<E>>
275    where
276        E: Serialize + for<'de> serde::Deserialize<'de>,
277    {
278        match self.service.list_deleted(page, limit).await {
279            Ok((items, total)) => {
280                let total_pages = if limit == 0 { 0 } else { ((total as f64) / (limit as f64)).ceil() as u32 };
281                GrpcResponse::success(GrpcListResponse {
282                    items,
283                    total,
284                    page,
285                    limit,
286                    total_pages,
287                })
288            }
289            Err(e) => GrpcResponse::error(e.to_string()),
290        }
291    }
292
293    pub async fn rpc_empty_trash(&self) -> GrpcResponse<u64> {
294        match self.service.empty_trash().await {
295            Ok(count) => GrpcResponse::success(count),
296            Err(e) => GrpcResponse::error(e.to_string()),
297        }
298    }
299}
300
301/// gRPC service configuration
302#[derive(Debug, Clone)]
303pub struct GrpcConfig {
304    pub host: String,
305    pub port: u16,
306    pub max_message_size: usize,
307}
308
309impl Default for GrpcConfig {
310    fn default() -> Self {
311        Self {
312            host: "127.0.0.1".to_string(),
313            port: 50051,
314            max_message_size: 4 * 1024 * 1024, // 4MB
315        }
316    }
317}