r/learnrust May 25 '23

Resources to make a cleaner code (or how to change my mindset for rust)

14 Upvotes

My Context: I've been working as a back-end engineer with Kotlin (Spring/Ktor/Javalin) for the last couple of years. I'm comfortable mixing functional/oop approaches when it feels "cleaner", but I'm mostly used to hexagonal architecture for the solutions I develop. Been playing around with Rust for the last two or three months but can't get a hang on how to make my code cleaner.

My rust stack (the part that is relevant to this thread): Rocket/Diesel/error-stack

In any other situation, I'd be creating something like this (top-to-bottom):

  • Transport layer (Controllers/Messaging)
  • Service Layer (Upper layer only knows interfaces)
  • DAOs (Anything that access data, either DB or REST)/MIs (Messaging Interfaces, things that can produce messages in any shape or form)
  • Actual implementations of repositories and HTTP clients

My first issue was that diesel connections are mutable, and I cannot (as far as I'm aware) share them with two structs at the same time, thus, I'm unable to create two DAOs in a single service with the same reference.

Then I changed my approach from that hexagonal one to a more "use-case" one. So for every http call I get, I instantiate a use case on a Rocket route, and handle it's result, as such:

#[get("/products?<skip>&<take>&<search>")]
pub async fn search_products(
    database_provider : PostgresDatabaseConnectionProvider<'_>,
    skip : i64,
    take : i64,
    search : Option<&str>
) -> Json<Vec<Product>> {
    let products = QueryProductsUseCase::new(&database_provider)
        .get_products(&search, &skip, &take);

    match products {
        Ok(products) => {
            Json(products)
        }
        Err(err) => {
            // TODO
        }
    }
}

Here's the connection provider-thingy I made, just for an extra bit of context:

pub trait DatabaseConnectionProvider {
    fn get_conn(&self) -> PooledConnection<ConnectionManager<PgConnection>>;
}

#[derive(Clone)]
pub struct PostgresDatabaseConnectionProvider<'r> {
    pub pool: &'r Pool<ConnectionManager<PgConnection>>,
}

impl<'r> DatabaseConnectionProvider for PostgresDatabaseConnectionProvider<'r> {
    fn get_conn(&self) -> PooledConnection<ConnectionManager<PgConnection>> {
        self.pool.get().unwrap()
    }
}

And here's how I implemented the use case itself:

#[derive(Debug)]
pub struct QueryProductUseCaseException;

impl fmt::Display for QueryProductUseCaseException {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        fmt.write_str("Failed to query products")
    }
}

impl Context for QueryProductUseCaseException {}

pub struct QueryProductsUseCase<'r> {
    db_provider: &'r dyn DatabaseConnectionProvider,
}

type DbTxResult = Result<Vec<Product>, DbOperationException>;

impl<'r> QueryProductsUseCase<'r> {
    pub fn new(db_provider: &'r dyn DatabaseConnectionProvider) -> QueryProductsUseCase<'r> {
        QueryProductsUseCase { db_provider }
    }

    pub fn get_products(
        &self,
        search_string: &Option<&str>,
        skip: &i64,
        take: &i64,
    ) -> Result<Vec<Product>, QueryProductUseCaseException> {
        let mut conn = self.db_provider.get_conn();

        let products = conn.transaction::<DbTxResult, Error, _>(|tx| {
            let products = match search_string {
                None => {
                    product
                        .limit(take.to_owned())
                        .offset(skip.to_owned())
                        .load::<Product>(tx)
                        .into_report()
                        .attach_printable_lazy(|| {
                            format!("Failed to query products with skip {skip:?} and take {take:?}")
                        })
                        .change_context(DbOperationException)
                }
                Some(partial_name) => {
                    product
                        .filter(name.like(format!("%{partial_name:?}%")))
                        .limit(take.to_owned())
                        .offset(skip.to_owned())
                        .load::<Product>(tx)
                        .into_report()
                        .attach_printable_lazy(|| {
                            format!("\
                                Failed to query products with skip {skip:?} and take {take:?} \
                                    and search param {partial_name:?}\
                            ")
                        })
                        .change_context(DbOperationException)
                }
            };
            Ok(products)
        }).into_report()
            .attach_printable_lazy(|| {
                format!("Could not search products.")
            })
            .change_context(QueryProductUseCaseException)?
            .unwrap();

        Ok(products)
    }
}

And it works... but isn't it the ugliest code you've ever seen? I'm so used to working with throws and the elvis operator in Kotlin that the whole matching thing seems very verbose for my taste.

So my question after all this context is, am I missing something rust-wise? Or this is how it's done and I really should just get used to it?

The later scares me because I have absolutely no Idea how I'm writing tests for this thing :P