I'm trying to wrap my head around how to handle this particular issue in Rust. I've programmed in Rust for a while, but I'm primarily a C# developer, and some of my knowledge in that language might be confusing me with this problem.
I have a web application built in Axum where I'm building a data-access layer to abstract away direct sqlx
connections. I'm attempting to build all my controller objects in a single State
passed around as traits to support dependency injection.
Everything's been working just fine so far - generally I'm wrapping the dyn
traits in Arc
s and requiring them to implement Send + Sync
and Axum is perfectly fine passing them from handler to handler.
Example:
#[async_trait]
pub trait DataLayer : Send + Sync {
async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Option<UserDbModel>;
async fn get_user_by_email<'a>(&self, email: &'a str) -> Option<UserDbModel>;
async fn get_refr_token_by_token<'a>(&self, token: &'a str) -> Option<RefrTokenDbModel>, BoxError>;
async fn get_refr_token_by_id(&self, token: i32) -> Option<RefrTokenDbModel>;
async fn create_refr_token(&self, refr_token: CreateRefrTokenDbModel) -> u64;
async fn revoke_refr_token(&self, token: RevokeRefrTokenDbModel);
}
Then, this DataLayer
trait can be referenced in my other services
#[async_trait]
pub trait AuthService: Send + Sync {
async fn try_accept_creds(&self, info: LoginPayload) -> login_error::Result<TokensModel>;
async fn try_accept_refresh(&self, refr_token: String) -> refresh_error::Result<TokensModel>;
}
#[derive(Clone)]
pub struct CoreAuthService {
data_layer: Arc<dyn DataLayer>,
token_service: Arc<dyn TokenService>,
}
A big problem with the DataLayer
trait, however (as you might be able to see) is I originally set it up to just panic!()
when it hit some kind of database error. I'd like to be able to have each return value in the trait methods to be wrapped in a Result
.
The problem I'm hitting is that I want to ensure this Error
type is generic to whatever the implementation uses. So naturally I tried to create a type
in the trait:
#[async_trait]
pub trait DataLayer : Send + Sync {
type Error : std::error::Error + Send + Sync;
async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> Result<Option<UserDbModel>, Self::Error>;
async fn get_user_by_email<'a>(&self, email: &'a str) -> Result<Option<UserDbModel>, Self::Error>;
async fn get_refr_token_by_token<'a>(&self, token: &'a str) -> Result<Option<RefrTokenDbModel>, Self::Error>;
async fn get_refr_token_by_id(&self, token: i32) -> Result<Option<RefrTokenDbModel>, Self::Error>;
async fn create_refr_token(&self, refr_token: CreateRefrTokenDbModel) -> Result<u64, Self::Error>;
async fn revoke_refr_token(&self, token: RevokeRefrTokenDbModel) -> Result<(), Self::Error>;
}
Then I could define the Error
type however I wanted:
pub struct DbDataLayer {
db: MySqlPool,
settings: TokenSettings
}
#[async_trait]
impl DataLayer for DbDataLayer {
type Error = sqlx::Error;
async fn try_get_user_by_id<'a>(&self, user_id: &'a str) -> sqlx::Result<Option<UserDbModel>> {
let user = sqlx::query_as!(UserDbModel, r"
SELECT id, email, password_hash as pwd_hash, role FROM users
WHERE id = ?
", user_id).fetch_one(&self.db).await;
match user {
Ok(user) => Ok(Some(user)),
Err(sqlx::Error::RowNotFound) => Ok(None)
}
}
...
However, part of dependency-injection is to avoid injecting tight-coupled dependencies into other services. When I try to build the CoreAuthService
from above, the DataLayer
I want to inject now requires a definition of the Error
type. I thought I could maybe just use the same Send + Sync
requirement:
#[derive(Clone)]
pub struct CoreAuthService {
data_layer: Arc<dyn DataLayer<Error = dyn Error + Send + Sync>>,
token_service: Arc<dyn TokenService>,
}
impl CoreAuthService {
pub fn new(
data_layer: Arc<dyn DataLayer<Error = dyn Error + Send + Sync>>,
token_service: Arc<dyn TokenService>,
) -> Self {
Self {
data_layer,
token_service,
}
}
}
However, then I run into the following compiler error whenever I use the DataLayer
methods in the CoreAuthService
methods:
the size for values of type `(dyn StdError + Send + Sync + 'static)` cannot be known at compilation time
the trait `Sized` is not implemented for `(dyn StdError + Send + Sync + 'static)`
I'm not sure how to proceed from here. How could I allow a generic Error
type for the injected DataLayer
but also make the compiler happy?
I'm also wondering if maybe I'm approaching the infrastructure of my codebase badly altogether (as I said, I'm a C# developer, so I'm importing some of the general best practices into Rust).