How to implement a decorator or similar?

I would like to call a decorator function before any call to methods on a struct, and modify the input arguments to that function. I've used decorators in dynamically typed languages like python and javascript before, but I can't wrap my head around how to do this in rust.

This is my usecase
I'm implementing a repository struct for an API im writing. The repository struct has methods to persist some data in an SQL database, and for that it creates a pooled connection on it's creation. That pooled connection is available at self.pool in the methods.

In order to execute an SQL query I need to get a connection from the pool by calling self.pool.get() which returns a Result<connection, Error>.

I would like to create a decorator that runs before any method-call on this struct, calls self.pool.get(), handles any errors and gives the resulting connection to the method that's beeing called.

This is what my code (without decorators) look like right now - calling self.pool.get() in each method will create alot of code duplication

impl IDriven<OwnerEntity> for Repository {
    type Representation = Owner;
    type Error = DomainError;

    fn create(&self, new_owner: OwnerEntity) -> Result<Owner, Self::Error> {

        let c = self.pool.get().expect("Could not get connection");

        let owner: Result<Owner, Error> = diesel::insert_into(schema::owner::table)
            .values(OwnerNew::from(new_owner))
            .get_result(&c);

        match owner {
            Ok(owner) => Ok(owner),
            Err(e) => Err(DomainError::Unknown),
        }
    }
}

If you want to be faithful to the decorator pattern, you could create an attribute macro that you apply to the impl block. The macro could re-emit the body of each function with the initial pool.get() etc. part added.

Otherwise, I think a simple closure/callback-based API could work for refactoring that particular part.

Thanks for you reply @H2CO3 - Any chance you could share a basic sketch of what a simple closure/callback-based API could look like in this case..?

I thought I'd perhaps try and separate the query execution from the struct methods, so something like (pseudo):

impl Repository {
  fn exectute(&self, statement: SQLStatement) -> Result<Owner, SQLError> {
    let c = self.pool.get()?;
    SQLStatement.execute(&c)
  }
}

impl IDriven<OwnerEntity> for Repository {
  ...

  fn create(&self, new_owner: OwnerEntity) -> Result<Owner, Self::Error> {
   self.execute(diesel::insert_into(schema::owner::table).values(OwnerNew::from(new_owner)))
   ...
  }
}

What I mean is the closure passed in should stand in for the variable parts of the method body:

impl Repository {
    fn with_conn<T, F>(&self, callback: F) -> T
    where
        F: FnOnce(Connection) -> T
    {
        let c = self.pool.get().expect("Could not get connection");
        callback(c)
    }
}

impl IDriven<OwnerEntity> for Repository {
    type Representation = Owner;
    type Error = DomainError;

    fn create(&self, new_owner: OwnerEntity) -> Result<Owner, Self::Error> {
        self.with_conn(|c| {
            diesel::insert_into(schema::owner::table)
                .values(OwnerNew::from(new_owner))
                .get_result(&c)
                .map_err(|_| DomainError::Unknown)
        })
    }
}
3 Likes

thanks for the clarification and example :slight_smile: I mostly get it now.
So if I would like to handle errors better, could I perhaps do that directly in with_conn, i.e returning a DomainError::Conflict for diesel conflicts, and DomainError::NotFound for not found etc.?

I'm not so familiar with map_err - can it be used to map many diffrent error-types at once, or will it just always capture any error and translate it to one other error (in this case DomainError::Unknown)?

You should definitely handle errors better than just crashing; I reproduced the expect() only to keep the code simple.

Sorry I don't understand what you are asking here. Maybe you should look at its signature for clarification.

What I'm trying to say is I want a common way to handle SQL-errors, and I was asking if map_err() could be used to do that.
If i understand the signature correct I guess i could do something like this?

impl Repository {
    fn with_conn<T, F>(&self, callback: F) -> Result<T, DomainError>
    where
        F: FnOnce(Connection) -> T
    {
        let c = self.pool.get().expect("Could not get connection");
        callback(c).map_err(|SomeSQLError| match SomeSQLError {
          SQLError::Conflict => DomainError::Conflict,
          SQLError::NotFound = DomainError::NotFound,
          _ => DomainError::Unknown
        } )
    }    

Yes, you can. You can map any error to any other error, since map_err is generic and it's implemented on Result no matter the error type.

Fantastic! Thanks for all the help @H2CO3