Skip to main content

backbone_core/
usecase.rs

1//! Generic use cases — one per CRUD operation, composable via hooks.
2//!
3//! Generated code emits a type alias per entity:
4//!
5//! ```rust,ignore
6//! // Generated:
7//! pub type CreateStoredFileUseCase = CreateUseCase<StoredFile, CreateStoredFileDto, StoredFileService>;
8//! pub type UpdateStoredFileUseCase = UpdateUseCase<StoredFile, UpdateStoredFileDto, StoredFileService>;
9//! // ... etc.
10//! ```
11//!
12//! Custom behaviour is injected through `UseCaseHooks<E, DTO>` — no generated
13//! code modification required.
14//!
15//! # Lifecycle (Create example)
16//!
17//! ```text
18//! before_create(dto) → build entity → validate → after_create(entity) → persist → publish event
19//! ```
20
21use async_trait::async_trait;
22use std::marker::PhantomData;
23use std::sync::Arc;
24
25use crate::validation::ValidationErrors;
26
27// ─── Use case result ─────────────────────────────────────────────────────────
28
29/// The standard error type for all use case operations.
30#[derive(Debug)]
31pub enum UseCaseError {
32    Validation(ValidationErrors),
33    NotFound(String),
34    Forbidden(String),
35    Conflict(String),
36    Internal(String),
37}
38
39impl std::fmt::Display for UseCaseError {
40    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41        match self {
42            UseCaseError::Validation(errs) => write!(f, "validation failed: {errs}"),
43            UseCaseError::NotFound(msg) => write!(f, "not found: {msg}"),
44            UseCaseError::Forbidden(msg) => write!(f, "forbidden: {msg}"),
45            UseCaseError::Conflict(msg) => write!(f, "conflict: {msg}"),
46            UseCaseError::Internal(msg) => write!(f, "internal error: {msg}"),
47        }
48    }
49}
50
51impl std::error::Error for UseCaseError {}
52
53impl From<ValidationErrors> for UseCaseError {
54    fn from(errs: ValidationErrors) -> Self {
55        UseCaseError::Validation(errs)
56    }
57}
58
59pub type UseCaseResult<T> = Result<T, UseCaseError>;
60
61// ─── Hooks ───────────────────────────────────────────────────────────────────
62
63/// Extension points for every CRUD use case.
64///
65/// The default implementation (`DefaultHooks`) does nothing.
66/// Override individual methods in your custom decorator to inject behaviour.
67#[async_trait]
68pub trait UseCaseHooks<E: Send + Sync + 'static, DTO: Send + Sync + 'static>: Send + Sync {
69    /// Called before the entity is built from `dto`.  Mutate the DTO if needed.
70    async fn before_create(&self, dto: &mut DTO) -> UseCaseResult<()> {
71        let _ = dto;
72        Ok(())
73    }
74
75    /// Called after the entity is built, before it is persisted.
76    async fn after_build(&self, entity: &mut E) -> UseCaseResult<()> {
77        let _ = entity;
78        Ok(())
79    }
80
81    /// Called after the entity is persisted.
82    async fn after_create(&self, entity: &E) -> UseCaseResult<()> {
83        let _ = entity;
84        Ok(())
85    }
86
87    /// Called before an update is applied.
88    async fn before_update(&self, entity: &E, dto: &mut DTO) -> UseCaseResult<()> {
89        let _ = (entity, dto);
90        Ok(())
91    }
92
93    /// Called after an update is persisted.
94    async fn after_update(&self, entity: &E) -> UseCaseResult<()> {
95        let _ = entity;
96        Ok(())
97    }
98
99    /// Called before a delete.
100    async fn before_delete(&self, entity: &E) -> UseCaseResult<()> {
101        let _ = entity;
102        Ok(())
103    }
104
105    /// Called after a delete.
106    async fn after_delete(&self, id: &str) -> UseCaseResult<()> {
107        let _ = id;
108        Ok(())
109    }
110}
111
112/// Default no-op hooks implementation.
113pub struct DefaultHooks<E, DTO> {
114    _phantom: PhantomData<(E, DTO)>,
115}
116
117impl<E, DTO> DefaultHooks<E, DTO> {
118    pub fn new() -> Self {
119        Self {
120            _phantom: PhantomData,
121        }
122    }
123}
124
125impl<E, DTO> Default for DefaultHooks<E, DTO> {
126    fn default() -> Self {
127        Self::new()
128    }
129}
130
131#[async_trait]
132impl<E: Send + Sync + 'static, DTO: Send + Sync + 'static> UseCaseHooks<E, DTO>
133    for DefaultHooks<E, DTO>
134{
135    // All methods inherit the default (no-op) implementations.
136}
137
138// ─── Service contract ────────────────────────────────────────────────────────
139
140/// The minimal persistence + lookup contract required by all use cases.
141///
142/// `GenericCrudService` from `service.rs` implements this, as will
143/// any custom service that wraps it.
144#[async_trait]
145pub trait UseCaseService<E: Send + Sync + 'static>: Send + Sync {
146    async fn find_by_id(&self, id: &str) -> UseCaseResult<Option<E>>;
147    async fn persist(&self, entity: E) -> UseCaseResult<E>;
148    async fn remove(&self, id: &str) -> UseCaseResult<()>;
149}
150
151// ─── Entity factory ──────────────────────────────────────────────────────────
152
153/// Builds an entity from a Create DTO.
154///
155/// Generated code emits one impl per entity; custom code can replace it
156/// by wrapping the use case with a `UseCaseHooks` implementation.
157pub trait EntityFactory<E, DTO>: Send + Sync {
158    fn build(&self, dto: DTO) -> UseCaseResult<E>;
159}
160
161/// Applies update fields from an Update DTO to an existing entity.
162pub trait EntityUpdater<E, DTO>: Send + Sync {
163    fn apply(&self, entity: E, dto: DTO) -> UseCaseResult<E>;
164}
165
166// ─── Create use case ─────────────────────────────────────────────────────────
167
168/// Generic "create entity" use case.
169///
170/// Lifecycle: `hooks.before_create(dto)` → `factory.build(dto)` →
171/// `hooks.after_build(entity)` → `service.persist(entity)` →
172/// `hooks.after_create(entity)`
173pub struct CreateUseCase<E, DTO, S> {
174    service: Arc<S>,
175    factory: Arc<dyn EntityFactory<E, DTO>>,
176    hooks: Arc<dyn UseCaseHooks<E, DTO>>,
177    _phantom: PhantomData<(E, DTO)>,
178}
179
180impl<E, DTO, S> CreateUseCase<E, DTO, S>
181where
182    E: Send + Sync + Clone + 'static,
183    DTO: Send + Sync + 'static,
184    S: UseCaseService<E>,
185{
186    pub fn new(
187        service: Arc<S>,
188        factory: Arc<dyn EntityFactory<E, DTO>>,
189        hooks: Arc<dyn UseCaseHooks<E, DTO>>,
190    ) -> Self {
191        Self {
192            service,
193            factory,
194            hooks,
195            _phantom: PhantomData,
196        }
197    }
198
199    pub fn with_default_hooks(
200        service: Arc<S>,
201        factory: Arc<dyn EntityFactory<E, DTO>>,
202    ) -> Self
203    where
204        DTO: 'static,
205    {
206        Self::new(service, factory, Arc::new(DefaultHooks::new()))
207    }
208
209    pub async fn execute(&self, mut dto: DTO) -> UseCaseResult<E> {
210        self.hooks.before_create(&mut dto).await?;
211        let mut entity = self.factory.build(dto)?;
212        self.hooks.after_build(&mut entity).await?;
213        let entity = self.service.persist(entity).await?;
214        self.hooks.after_create(&entity).await?;
215        Ok(entity)
216    }
217}
218
219// ─── Update use case ─────────────────────────────────────────────────────────
220
221/// Generic "update entity" use case.
222pub struct UpdateUseCase<E, DTO, S> {
223    service: Arc<S>,
224    updater: Arc<dyn EntityUpdater<E, DTO>>,
225    hooks: Arc<dyn UseCaseHooks<E, DTO>>,
226    _phantom: PhantomData<(E, DTO)>,
227}
228
229impl<E, DTO, S> UpdateUseCase<E, DTO, S>
230where
231    E: Send + Sync + Clone + 'static,
232    DTO: Send + Sync + Clone + 'static,
233    S: UseCaseService<E>,
234{
235    pub fn new(
236        service: Arc<S>,
237        updater: Arc<dyn EntityUpdater<E, DTO>>,
238        hooks: Arc<dyn UseCaseHooks<E, DTO>>,
239    ) -> Self {
240        Self {
241            service,
242            updater,
243            hooks,
244            _phantom: PhantomData,
245        }
246    }
247
248    pub async fn execute(&self, id: &str, mut dto: DTO) -> UseCaseResult<E> {
249        let entity = self
250            .service
251            .find_by_id(id)
252            .await?
253            .ok_or_else(|| UseCaseError::NotFound(id.to_string()))?;
254
255        self.hooks.before_update(&entity, &mut dto).await?;
256        let updated = self.updater.apply(entity, dto)?;
257        let persisted = self.service.persist(updated).await?;
258        self.hooks.after_update(&persisted).await?;
259        Ok(persisted)
260    }
261}
262
263// ─── Get use case ────────────────────────────────────────────────────────────
264
265/// Generic "get by id" use case.
266pub struct GetUseCase<E, S> {
267    service: Arc<S>,
268    _phantom: PhantomData<E>,
269}
270
271impl<E, S> GetUseCase<E, S>
272where
273    E: Send + Sync + 'static,
274    S: UseCaseService<E>,
275{
276    pub fn new(service: Arc<S>) -> Self {
277        Self {
278            service,
279            _phantom: PhantomData,
280        }
281    }
282
283    pub async fn execute(&self, id: &str) -> UseCaseResult<E> {
284        self.service
285            .find_by_id(id)
286            .await?
287            .ok_or_else(|| UseCaseError::NotFound(id.to_string()))
288    }
289}
290
291// ─── Delete use case ─────────────────────────────────────────────────────────
292
293/// Generic "delete entity" use case (soft-delete by default).
294pub struct DeleteUseCase<E, S> {
295    service: Arc<S>,
296    hooks: Arc<dyn UseCaseHooks<E, ()>>,
297    _phantom: PhantomData<E>,
298}
299
300impl<E, S> DeleteUseCase<E, S>
301where
302    E: Send + Sync + Clone + 'static,
303    S: UseCaseService<E>,
304{
305    pub fn new(service: Arc<S>, hooks: Arc<dyn UseCaseHooks<E, ()>>) -> Self {
306        Self {
307            service,
308            hooks,
309            _phantom: PhantomData,
310        }
311    }
312
313    pub fn with_default_hooks(service: Arc<S>) -> Self {
314        Self::new(service, Arc::new(DefaultHooks::new()))
315    }
316
317    pub async fn execute(&self, id: &str) -> UseCaseResult<()> {
318        let entity = self
319            .service
320            .find_by_id(id)
321            .await?
322            .ok_or_else(|| UseCaseError::NotFound(id.to_string()))?;
323
324        self.hooks.before_delete(&entity).await?;
325        self.service.remove(id).await?;
326        self.hooks.after_delete(id).await?;
327        Ok(())
328    }
329}
330
331// ─── List use case ───────────────────────────────────────────────────────────
332
333/// Paginated list parameters.
334#[derive(Debug, Clone)]
335pub struct ListParams {
336    pub page: u64,
337    pub limit: u64,
338    pub filters: std::collections::HashMap<String, String>,
339}
340
341impl ListParams {
342    pub fn new(page: u64, limit: u64) -> Self {
343        Self {
344            page,
345            limit,
346            filters: Default::default(),
347        }
348    }
349
350    pub fn with_filter(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
351        self.filters.insert(key.into(), value.into());
352        self
353    }
354}
355
356impl Default for ListParams {
357    fn default() -> Self {
358        Self::new(1, 20)
359    }
360}
361
362/// Paginated list result.
363#[derive(Debug, Clone)]
364pub struct ListResult<E> {
365    pub items: Vec<E>,
366    pub total: u64,
367    pub page: u64,
368    pub limit: u64,
369}
370
371impl<E> ListResult<E> {
372    pub fn new(items: Vec<E>, total: u64, page: u64, limit: u64) -> Self {
373        Self {
374            items,
375            total,
376            page,
377            limit,
378        }
379    }
380
381    pub fn total_pages(&self) -> u64 {
382        if self.limit == 0 {
383            return 0;
384        }
385        (self.total + self.limit - 1) / self.limit
386    }
387}
388
389/// Service contract for the list use case (separate because it needs pagination).
390#[async_trait]
391pub trait ListService<E: Send + Sync + 'static>: Send + Sync {
392    async fn list(&self, params: ListParams) -> UseCaseResult<ListResult<E>>;
393}
394
395/// Generic paginated list use case.
396pub struct ListUseCase<E, S> {
397    service: Arc<S>,
398    _phantom: PhantomData<E>,
399}
400
401impl<E, S> ListUseCase<E, S>
402where
403    E: Send + Sync + 'static,
404    S: ListService<E>,
405{
406    pub fn new(service: Arc<S>) -> Self {
407        Self {
408            service,
409            _phantom: PhantomData,
410        }
411    }
412
413    pub async fn execute(&self, params: ListParams) -> UseCaseResult<ListResult<E>> {
414        self.service.list(params).await
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[derive(Debug, Clone, PartialEq)]
423    struct Foo {
424        id: String,
425        name: String,
426    }
427
428    #[derive(Debug, Clone)]
429    struct CreateFooDto {
430        name: String,
431    }
432
433    struct FooFactory;
434    impl EntityFactory<Foo, CreateFooDto> for FooFactory {
435        fn build(&self, dto: CreateFooDto) -> UseCaseResult<Foo> {
436            Ok(Foo {
437                id: "new-id".into(),
438                name: dto.name,
439            })
440        }
441    }
442
443    struct FooService {
444        store: tokio::sync::Mutex<Vec<Foo>>,
445    }
446
447    impl FooService {
448        fn new() -> Self {
449            Self {
450                store: tokio::sync::Mutex::new(Vec::new()),
451            }
452        }
453    }
454
455    #[async_trait]
456    impl UseCaseService<Foo> for FooService {
457        async fn find_by_id(&self, id: &str) -> UseCaseResult<Option<Foo>> {
458            let store = self.store.lock().await;
459            Ok(store.iter().find(|f| f.id == id).cloned())
460        }
461
462        async fn persist(&self, entity: Foo) -> UseCaseResult<Foo> {
463            let mut store = self.store.lock().await;
464            store.retain(|f| f.id != entity.id);
465            store.push(entity.clone());
466            Ok(entity)
467        }
468
469        async fn remove(&self, id: &str) -> UseCaseResult<()> {
470            let mut store = self.store.lock().await;
471            store.retain(|f| f.id != id);
472            Ok(())
473        }
474    }
475
476    #[tokio::test]
477    async fn create_use_case_persists_entity() {
478        let service = Arc::new(FooService::new());
479        let use_case =
480            CreateUseCase::with_default_hooks(service.clone(), Arc::new(FooFactory));
481
482        let result = use_case
483            .execute(CreateFooDto {
484                name: "hello".into(),
485            })
486            .await
487            .unwrap();
488
489        assert_eq!(result.name, "hello");
490
491        let found = service.find_by_id("new-id").await.unwrap();
492        assert!(found.is_some());
493    }
494
495    #[tokio::test]
496    async fn get_use_case_returns_not_found() {
497        let service = Arc::new(FooService::new());
498        let use_case = GetUseCase::new(service);
499        let err = use_case.execute("missing").await.unwrap_err();
500        assert!(matches!(err, UseCaseError::NotFound(_)));
501    }
502
503    #[tokio::test]
504    async fn delete_use_case_removes_entity() {
505        let service = Arc::new(FooService::new());
506        service
507            .persist(Foo {
508                id: "x".into(),
509                name: "X".into(),
510            })
511            .await
512            .unwrap();
513
514        let use_case = DeleteUseCase::with_default_hooks(service.clone());
515        use_case.execute("x").await.unwrap();
516
517        assert!(service.find_by_id("x").await.unwrap().is_none());
518    }
519}