use crate::Context;
use crate::provider::Provider;
use crate::validator::Validator;
pub trait Repository<Req, Resp> {
type Error: std::error::Error;
fn perform(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error>;
}
pub struct RepositoryFn<F, Resp, E>
where
E: std::error::Error,
{
f: F,
_marker: std::marker::PhantomData<(Resp, E)>,
}
impl<F, Resp, E> RepositoryFn<F, Resp, E>
where
E: std::error::Error,
{
pub fn new<Req>(f: F) -> Self
where
F: Fn(&Context, Req) -> Result<Resp, E>,
{
Self {
f,
_marker: std::marker::PhantomData,
}
}
}
impl<F, Req, Resp, E> Repository<Req, Resp> for RepositoryFn<F, Resp, E>
where
F: Fn(&Context, Req) -> Result<Resp, E>,
E: std::error::Error,
{
type Error = E;
fn perform(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error> {
(self.f)(ctx, req)
}
}
#[derive(Debug)]
pub enum TxError<TE, IE> {
Transaction(TE),
Inner(IE),
}
impl<TE: std::fmt::Display, IE: std::fmt::Display> std::fmt::Display for TxError<TE, IE> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Transaction(e) => write!(f, "transaction error: {e}"),
Self::Inner(e) => write!(f, "{e}"),
}
}
}
impl<TE: std::error::Error + 'static, IE: std::error::Error + 'static> std::error::Error
for TxError<TE, IE>
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Transaction(e) => Some(e),
Self::Inner(e) => Some(e),
}
}
}
pub trait Transaction {
type Error: std::error::Error;
fn in_tx<F, T, E>(&self, ctx: &Context, f: F) -> Result<T, TxError<Self::Error, E>>
where
F: FnOnce(&Context) -> Result<T, E>;
}
#[derive(Debug)]
pub enum ValidatingRepositoryError<VE, PE, TE> {
Validation(VE),
Provider(PE),
Transaction(TE),
}
impl<VE, PE, TE> std::fmt::Display for ValidatingRepositoryError<VE, PE, TE>
where
VE: std::fmt::Display,
PE: std::fmt::Display,
TE: std::fmt::Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Validation(e) => write!(f, "validation error: {e}"),
Self::Provider(e) => write!(f, "provider error: {e}"),
Self::Transaction(e) => write!(f, "transaction error: {e}"),
}
}
}
impl<VE, PE, TE> std::error::Error for ValidatingRepositoryError<VE, PE, TE>
where
VE: std::error::Error + 'static,
PE: std::error::Error + 'static,
TE: std::error::Error + 'static,
{
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Validation(e) => Some(e),
Self::Provider(e) => Some(e),
Self::Transaction(e) => Some(e),
}
}
}
pub struct NoTransaction;
impl Transaction for NoTransaction {
type Error = std::convert::Infallible;
fn in_tx<F, T, E>(&self, ctx: &Context, f: F) -> Result<T, TxError<Self::Error, E>>
where
F: FnOnce(&Context) -> Result<T, E>,
{
f(ctx).map_err(TxError::Inner)
}
}
pub struct ValidatingRepository<V, P, T, Req, Resp>
where
V: Validator<Req>,
P: Provider<Req, Resp>,
T: Transaction,
{
pub validator: V,
pub provider: P,
pub transaction: T,
_marker: std::marker::PhantomData<(Req, Resp)>,
}
impl<V, P, T, Req, Resp> ValidatingRepository<V, P, T, Req, Resp>
where
V: Validator<Req>,
P: Provider<Req, Resp>,
T: Transaction,
{
pub fn new(validator: V, provider: P, transaction: T) -> Self {
Self {
validator,
provider,
transaction,
_marker: std::marker::PhantomData,
}
}
}
impl<V, P, T, Req, Resp> Repository<Req, Resp> for ValidatingRepository<V, P, T, Req, Resp>
where
V: Validator<Req>,
V::Error: 'static,
P: Provider<Req, Resp>,
P::Error: 'static,
T: Transaction,
T::Error: 'static,
{
type Error = ValidatingRepositoryError<V::Error, P::Error, T::Error>;
fn perform(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error> {
self.validator
.validate(ctx, &req)
.map_err(ValidatingRepositoryError::Validation)?;
self.transaction
.in_tx(ctx, |ctx| self.provider.execute(ctx, req))
.map_err(|e| match e {
TxError::Transaction(te) => ValidatingRepositoryError::Transaction(te),
TxError::Inner(pe) => ValidatingRepositoryError::Provider(pe),
})
}
}
#[cfg(feature = "async")]
use async_trait::async_trait;
#[cfg(feature = "async")]
#[async_trait]
pub trait AsyncRepository<Req, Resp>: Send + Sync
where
Req: Send,
Resp: Send,
{
type Error: std::error::Error + Send;
async fn perform(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error>;
}
#[cfg(feature = "async")]
#[async_trait]
impl<R, Req, Resp> AsyncRepository<Req, Resp> for R
where
R: Repository<Req, Resp> + Send + Sync,
R::Error: Send,
Req: Send + 'static,
Resp: Send,
{
type Error = R::Error;
async fn perform(&self, ctx: &Context, req: Req) -> Result<Resp, Self::Error> {
Repository::perform(self, ctx, req)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{CodedError, ProviderFn, ValidatorFn};
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
struct TestTransaction {
entered: Arc<AtomicBool>,
}
impl Transaction for TestTransaction {
type Error = CodedError;
fn in_tx<F, T, E>(&self, ctx: &Context, f: F) -> Result<T, TxError<Self::Error, E>>
where
F: FnOnce(&Context) -> Result<T, E>,
{
self.entered.store(true, Ordering::SeqCst);
f(ctx).map_err(TxError::Inner)
}
}
struct FailingTransaction;
impl Transaction for FailingTransaction {
type Error = CodedError;
fn in_tx<F, T, E>(&self, _ctx: &Context, _f: F) -> Result<T, TxError<Self::Error, E>>
where
F: FnOnce(&Context) -> Result<T, E>,
{
Err(TxError::Transaction(CodedError::new("TX_FAILED")))
}
}
#[test]
fn repository_fn_basic() {
let repo = RepositoryFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> {
Ok(req * 2)
});
let result = Repository::perform(&repo, &Context::new(), 21);
assert_eq!(result.ok(), Some(42));
}
#[test]
fn validating_repository_with_valid_request() {
let validator = ValidatorFn::new(|_ctx: &Context, req: &i32| -> Result<(), CodedError> {
if *req > 0 {
Ok(())
} else {
Err(CodedError::new("INVALID"))
}
});
let provider =
ProviderFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });
let repo = ValidatingRepository::new(validator, provider, NoTransaction);
let result = Repository::perform(&repo, &Context::new(), 21);
assert_eq!(result.ok(), Some(42));
}
#[test]
fn validating_repository_with_invalid_request() {
let validator = ValidatorFn::new(|_ctx: &Context, req: &i32| -> Result<(), CodedError> {
if *req > 0 {
Ok(())
} else {
Err(CodedError::new("INVALID"))
}
});
let provider =
ProviderFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });
let repo = ValidatingRepository::new(validator, provider, NoTransaction);
let result = Repository::perform(&repo, &Context::new(), -1);
assert!(result.is_err());
}
#[test]
fn validating_repository_with_transaction() {
let tx_entered = Arc::new(AtomicBool::new(false));
let tx_entered_clone = tx_entered.clone();
let provider =
ProviderFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });
let tx = TestTransaction {
entered: tx_entered_clone,
};
let repo = ValidatingRepository::new(crate::PassValidator, provider, tx);
let result = Repository::perform(&repo, &Context::new(), 21);
assert_eq!(result.ok(), Some(42));
assert!(
tx_entered.load(Ordering::SeqCst),
"Transaction should have been entered"
);
}
#[test]
fn transaction_error_propagates() {
let provider =
ProviderFn::new(|_ctx: &Context, req: i32| -> Result<i32, CodedError> { Ok(req * 2) });
let repo = ValidatingRepository::new(crate::PassValidator, provider, FailingTransaction);
let result = Repository::perform(&repo, &Context::new(), 21);
match result {
Err(ValidatingRepositoryError::Transaction(ref e)) => {
assert_eq!(e.code(), "TX_FAILED");
}
Err(ValidatingRepositoryError::Validation(e)) => {
panic!("Expected transaction error, got validation error: {e}");
}
Err(ValidatingRepositoryError::Provider(e)) => {
panic!("Expected transaction error, got provider error: {e}");
}
Ok(value) => {
panic!("Expected transaction error, got success value: {value}");
}
}
}
}