Skip to main content

aro_core/
repository.rs

1//! Repository port traits for data access.
2//!
3//! These traits define the interface between the domain layer and the
4//! persistence layer. Implementations live in adapter crates (e.g.
5//! `aro-fletch`), keeping the core free of framework dependencies.
6//!
7//! All trait methods return [`BoxFuture`] so that the traits are object-safe
8//! and can be used with `dyn Repository<T>` for dynamic dispatch (e.g.
9//! via `Dep<dyn Repository<User>>`).
10//!
11//! # Example
12//!
13//! ```ignore
14//! async fn get_user(
15//!     repo: &dyn Repository<User>,
16//!     id: u64,
17//! ) -> Result<User, RepoError> {
18//!     repo.find_by_id(id).await
19//! }
20//! ```
21
22use std::future::Future;
23use std::pin::Pin;
24
25use crate::entity::Entity;
26use crate::error::RepoError;
27use crate::pagination::{Page, PageRequest};
28
29/// A boxed future that is `Send` and tied to the lifetime of `&self`.
30///
31/// Used as the return type for object-safe async trait methods.
32pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
33
34/// Core CRUD repository trait for a given entity type.
35///
36/// Provides the standard create, read, update, and delete operations.
37/// Implementations should map persistence-layer errors to [`RepoError`].
38///
39/// This trait is object-safe, enabling dynamic dispatch via `dyn Repository<T>`.
40pub trait Repository<T: Entity>: Send + Sync {
41    /// Find an entity by its unique identifier.
42    ///
43    /// Returns [`RepoError::NotFound`] if no entity with the given ID exists.
44    fn find_by_id(&self, id: T::Id) -> BoxFuture<'_, Result<T, RepoError>>;
45
46    /// Return all entities of this type.
47    fn find_all(&self) -> BoxFuture<'_, Result<Vec<T>, RepoError>>;
48
49    /// Persist a new entity and return the created value.
50    ///
51    /// Returns [`RepoError::Conflict`] if a unique constraint is violated.
52    fn create(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>>;
53
54    /// Update an existing entity and return the updated value.
55    ///
56    /// Returns [`RepoError::NotFound`] if the entity does not exist.
57    fn update(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>>;
58
59    /// Delete an entity by its unique identifier.
60    ///
61    /// Returns [`RepoError::NotFound`] if the entity does not exist.
62    fn delete(&self, id: T::Id) -> BoxFuture<'_, Result<(), RepoError>>;
63}
64
65/// Trait for repositories that support filtered and sorted queries.
66///
67/// The `Filter` associated type allows each implementation to define
68/// its own query DSL or filter struct without leaking persistence
69/// details into the domain layer.
70///
71/// **Note:** `Queryable` is *not* object-safe because it has an associated type
72/// (`Filter`). Use a concrete type or a trait alias when dynamic dispatch is needed.
73pub trait Queryable<T: Entity>: Send + Sync {
74    /// A type describing the filter/sort criteria for queries.
75    type Filter: Send + Sync;
76
77    /// Find all entities matching the given filter.
78    fn find_by(&self, filter: Self::Filter) -> BoxFuture<'_, Result<Vec<T>, RepoError>>;
79
80    /// Return a page of entities matching the given filter.
81    ///
82    /// The total count and page items both reflect only rows that satisfy
83    /// `filter`. Use [`Paginatable::find_page`] when paginating the full table
84    /// without a filter.
85    fn find_page_by(
86        &self,
87        filter: Self::Filter,
88        request: PageRequest,
89    ) -> BoxFuture<'_, Result<Page<T>, RepoError>>;
90}
91
92/// Trait for repositories that support paginated queries over all rows.
93///
94/// Paginates every entity of type `T` without applying a filter. For filtered
95/// pagination, use [`Queryable::find_page_by`].
96///
97/// This trait is object-safe.
98pub trait Paginatable<T: Entity>: Send + Sync {
99    /// Return a page of all entities according to the given request.
100    fn find_page(&self, request: PageRequest) -> BoxFuture<'_, Result<Page<T>, RepoError>>;
101}
102
103/// Trait for managing transactional boundaries across repository operations.
104///
105/// This allows services to coordinate multiple repository calls within
106/// a single database transaction without coupling to a specific
107/// persistence backend.
108///
109/// **Note:** `TransactionManager` is *not* object-safe because `transaction`
110/// has generic type parameters. Use a concrete type or a trait alias when
111/// dynamic dispatch is needed.
112///
113/// # Example
114///
115/// ```ignore
116/// async fn transfer<TM: TransactionManager>(
117///     tm: &TM,
118///     amount: u64,
119/// ) -> Result<(), RepoError> {
120///     tm.transaction(|ctx| async move {
121///         // Obtain transaction-scoped repositories from `ctx` (backend-specific).
122///         let from_repo = ctx.from_account_repo();
123///         let to_repo = ctx.to_account_repo();
124///         let mut from = from_repo.find_by_id(1).await?;
125///         let mut to = to_repo.find_by_id(2).await?;
126///         from.balance -= amount;
127///         to.balance += amount;
128///         from_repo.update(from).await?;
129///         to_repo.update(to).await?;
130///         Ok(())
131///     }).await
132/// }
133/// ```
134pub trait TransactionManager: Send + Sync {
135    /// Opaque handle passed to [`transaction`](Self::transaction) closures.
136    ///
137    /// Implementations expose transaction-scoped repositories or connections
138    /// through this type so writes inside the closure participate in the
139    /// open transaction.
140    type Context: Send;
141
142    /// Execute the given closure within a transactional boundary.
143    ///
144    /// The closure receives an owned [`Context`](Self::Context). Use it to
145    /// obtain transaction-scoped repositories so all writes share the same
146    /// connection and roll back together on error.
147    ///
148    /// If the closure returns `Ok`, the transaction is committed.
149    /// If it returns `Err` or panics, the transaction is rolled back.
150    fn transaction<F, Fut, R>(&self, f: F) -> BoxFuture<'_, Result<R, RepoError>>
151    where
152        F: FnOnce(Self::Context) -> Fut + Send + 'static,
153        Fut: Future<Output = Result<R, RepoError>> + Send + 'static,
154        R: Send + 'static;
155}