aro-core 1.0.0

Core domain layer for the Aro web framework
Documentation
//! Repository port traits for data access.
//!
//! These traits define the interface between the domain layer and the
//! persistence layer. Implementations live in adapter crates (e.g.
//! `aro-fletch`), keeping the core free of framework dependencies.
//!
//! All trait methods return [`BoxFuture`] so that the traits are object-safe
//! and can be used with `dyn Repository<T>` for dynamic dispatch (e.g.
//! via `Dep<dyn Repository<User>>`).
//!
//! # Example
//!
//! ```ignore
//! async fn get_user(
//!     repo: &dyn Repository<User>,
//!     id: u64,
//! ) -> Result<User, RepoError> {
//!     repo.find_by_id(id).await
//! }
//! ```

use std::future::Future;
use std::pin::Pin;

use crate::entity::Entity;
use crate::error::RepoError;
use crate::pagination::{Page, PageRequest};

/// A boxed future that is `Send` and tied to the lifetime of `&self`.
///
/// Used as the return type for object-safe async trait methods.
pub type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

/// Core CRUD repository trait for a given entity type.
///
/// Provides the standard create, read, update, and delete operations.
/// Implementations should map persistence-layer errors to [`RepoError`].
///
/// This trait is object-safe, enabling dynamic dispatch via `dyn Repository<T>`.
pub trait Repository<T: Entity>: Send + Sync {
    /// Find an entity by its unique identifier.
    ///
    /// Returns [`RepoError::NotFound`] if no entity with the given ID exists.
    fn find_by_id(&self, id: T::Id) -> BoxFuture<'_, Result<T, RepoError>>;

    /// Return all entities of this type.
    fn find_all(&self) -> BoxFuture<'_, Result<Vec<T>, RepoError>>;

    /// Persist a new entity and return the created value.
    ///
    /// Returns [`RepoError::Conflict`] if a unique constraint is violated.
    fn create(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>>;

    /// Update an existing entity and return the updated value.
    ///
    /// Returns [`RepoError::NotFound`] if the entity does not exist.
    fn update(&self, entity: T) -> BoxFuture<'_, Result<T, RepoError>>;

    /// Delete an entity by its unique identifier.
    ///
    /// Returns [`RepoError::NotFound`] if the entity does not exist.
    fn delete(&self, id: T::Id) -> BoxFuture<'_, Result<(), RepoError>>;
}

/// Trait for repositories that support filtered and sorted queries.
///
/// The `Filter` associated type allows each implementation to define
/// its own query DSL or filter struct without leaking persistence
/// details into the domain layer.
///
/// **Note:** `Queryable` is *not* object-safe because it has an associated type
/// (`Filter`). Use a concrete type or a trait alias when dynamic dispatch is needed.
pub trait Queryable<T: Entity>: Send + Sync {
    /// A type describing the filter/sort criteria for queries.
    type Filter: Send + Sync;

    /// Find all entities matching the given filter.
    fn find_by(&self, filter: Self::Filter) -> BoxFuture<'_, Result<Vec<T>, RepoError>>;

    /// Return a page of entities matching the given filter.
    ///
    /// The total count and page items both reflect only rows that satisfy
    /// `filter`. Use [`Paginatable::find_page`] when paginating the full table
    /// without a filter.
    fn find_page_by(
        &self,
        filter: Self::Filter,
        request: PageRequest,
    ) -> BoxFuture<'_, Result<Page<T>, RepoError>>;
}

/// Trait for repositories that support paginated queries over all rows.
///
/// Paginates every entity of type `T` without applying a filter. For filtered
/// pagination, use [`Queryable::find_page_by`].
///
/// This trait is object-safe.
pub trait Paginatable<T: Entity>: Send + Sync {
    /// Return a page of all entities according to the given request.
    fn find_page(&self, request: PageRequest) -> BoxFuture<'_, Result<Page<T>, RepoError>>;
}

/// Trait for managing transactional boundaries across repository operations.
///
/// This allows services to coordinate multiple repository calls within
/// a single database transaction without coupling to a specific
/// persistence backend.
///
/// **Note:** `TransactionManager` is *not* object-safe because `transaction`
/// has generic type parameters. Use a concrete type or a trait alias when
/// dynamic dispatch is needed.
///
/// # Example
///
/// ```ignore
/// async fn transfer<TM: TransactionManager>(
///     tm: &TM,
///     amount: u64,
/// ) -> Result<(), RepoError> {
///     tm.transaction(|ctx| async move {
///         // Obtain transaction-scoped repositories from `ctx` (backend-specific).
///         let from_repo = ctx.from_account_repo();
///         let to_repo = ctx.to_account_repo();
///         let mut from = from_repo.find_by_id(1).await?;
///         let mut to = to_repo.find_by_id(2).await?;
///         from.balance -= amount;
///         to.balance += amount;
///         from_repo.update(from).await?;
///         to_repo.update(to).await?;
///         Ok(())
///     }).await
/// }
/// ```
pub trait TransactionManager: Send + Sync {
    /// Opaque handle passed to [`transaction`](Self::transaction) closures.
    ///
    /// Implementations expose transaction-scoped repositories or connections
    /// through this type so writes inside the closure participate in the
    /// open transaction.
    type Context: Send;

    /// Execute the given closure within a transactional boundary.
    ///
    /// The closure receives an owned [`Context`](Self::Context). Use it to
    /// obtain transaction-scoped repositories so all writes share the same
    /// connection and roll back together on error.
    ///
    /// If the closure returns `Ok`, the transaction is committed.
    /// If it returns `Err` or panics, the transaction is rolled back.
    fn transaction<F, Fut, R>(&self, f: F) -> BoxFuture<'_, Result<R, RepoError>>
    where
        F: FnOnce(Self::Context) -> Fut + Send + 'static,
        Fut: Future<Output = Result<R, RepoError>> + Send + 'static,
        R: Send + 'static;
}