I'm trying to rewrite one of my pet projects in Rust, and when I was faced with the problem of filtering a Result<Option<T>, E>
via a Result<bool, E>
, I couldn't figure out what the best approach was.
Since I'll be doing the same thing many times throughout the project I need something that's generic while also being readable, so the solution I came up with was to implement my own traits.
My first case is to write a function that can be used to authenticate a user; by first querying the users based on the e-mail address, and then checking if the stored, encrypted password matches the plain text one retrieved from the request.
For that purpose I have the following functions:
trait AccountRepository {
async fn find_by_email(&self, email: &String) -> Result<Option<AccountModel>, CoreApiError>;
}
trait PasswordEncoder {
fn verify(&self, plain: &String, encoded: &String) -> Result<bool, CoreApiError>;
}
And by combining both, I'd like to return a Result<AccountModel, CoreApiError>
. When the inner Option<>
is empty or the passwords don't match I'd like to return a CoreApiError
that says the account was not found, and when the inner Option<>
is non-empty and the passwords match, I'd like to return the AccoutModel
instance that was found. If either function returns a CoreApiError
I can just return it directly because I'm not really interested in it.
To that end I implemented the following traits:
pub trait FilterInner<T, F: FnOnce(&T) -> Result<bool, CoreApiError>> {
fn filter_inner(self, f: F) -> Result<Option<T>, CoreApiError>;
}
impl<T, F: FnOnce(&T) -> Result<bool, CoreApiError>> FilterInner<T, F> for Result<Option<T>, CoreApiError> {
fn filter_inner(self, f: F) -> Result<Option<T>, CoreApiError> {
let result = self?;
let filtered = match result {
Some(data) => {
let is_ok = f(&data)?;
if is_ok {
Some(data)
} else {
None
}
}
None => None,
};
Ok(filtered)
}
}
pub trait EnsurePresent<T, E>
where
E: Into<CoreApiError>,
{
fn ensure_present(self, message_key: &str) -> Result<T, CoreApiError>;
}
impl<T, E> EnsurePresent<T, E> for Result<Option<T>, E>
where
E: Into<CoreApiError>,
{
fn ensure_present(self, message_key: &str) -> Result<T, CoreApiError> {
let maybe_data = self.map_err(|e| e.into())?;
match maybe_data {
Some(data) => Ok(data),
_ => Err(CoreApiError::not_found(message_key)),
}
}
}
...and used them in the following fashion:
pub async fn initiate_login(&self, command: &InitiateLogin) -> Result<AccountModel, CoreApiError> {
self
.account_repository
.find_by_email(&command.email)
.await
.filter_inner(|account| self.password_encoder_port.verify(&command.password, &account.password))
.ensure_present("account.not.found")
}
At this point I'm pretty happy with the result, because overall I like sweeping complex stuff under the rug as long as I can provide readable and easy to use interfaces. Plus, I'd rather prefer chaining function calls than nesting match
statements, so this is perfect for my needs.
That said I'm not sure if this is the best way of doing it. All throughout the process I couldn't help but think that there should be something I can use to do this easily (probably involving flat_map
), but since I lack the particular Rust experience I couldn't tell what that was.
As such I'm open to any opinions, critiques, and suggestions.
.await?
? \$\endgroup\$.await
to extract aResult<Option<T>, E>
out of the asynchronousfind_by_email(...)
method, so the issue here is more about filtering the value contained in thatResult<>
. \$\endgroup\$.await?
instead of.await
\$\endgroup\$?
at the end. As I mentioned in my post I'm trying to avoid anymatch
statements because I think I should be able to navigate within the same higher-kinded type without explicitly unwrapping it (e.g. using?
to extract the value part of aResult
). The traits I came up with essentially do the same thing, and that's exactly why I'm wondering if there's a better way to achieve the same result, without unwrapping anyResult
s. \$\endgroup\$