I have a Rust application connected to the Postgres database.
Let’s say I have a users
table:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR NOT NULL UNIQUE,
password_hash VARCHAR NOT NULL,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
birth_date TIMESTAMPTZ,
role VARCHAR NOT NULL
)
At some place in the application I want to perform some business logic on some users. For example I want to select all the users whose birthday is today and send them an email. The only data I need for that is a vector of type (let’s assume the birthday check is implemented in the SQL query):
struct User1 {
email: String,
first_name: String, // To use the user's first name in the email - "Dear John, happy birthday!"
}
I also have a trait UserRepo
which has plenty of methods to fetch users based on some conditions. But all these methods return a vector of a type containing all the other database columns:
struct User {
id: i32,
email: String,
password_hash: String,
first_name: String,
last_name: String,
birth_date: DateTime<Utc>,
role: String,
}
The trait looks something like this:
#[async_trait]
trait UserRepo {
async fn get_user_by_id(&self, id: i32) -> anyhow::Result<User>;
async fn get_users(&self, page: i32, per_page: i32) -> anyhow::Result<Vec<User>>;
async fn get_users_born_today(&self) -> anyhow::Result<Vec<User>>;
}
The problem & what I want
I don’t want UserRepo
to return a vector of User1
– or to be more precise I don’t want the trait UserRepo
to cover all the types and nuances that will be used in the business logic.
I just want to specify the fields that I want at the place where I’m calling get_users_born_today()
.
So semantically I need the following:
#[async_trait]
trait UserRepo {
async fn get_user_by_id<T>(&self, id: i32) -> anyhow::Result<T>;
async fn get_users<T>(&self, page: i32, per_page: i32) -> anyhow::Result<Vec<T>>;
async fn get_users_born_today<T>(&self) -> anyhow::Result<Vec<T>>;
}
// Caller
fn some_endpoint() {
struct UserForThisParticularContext {
first_name: String,
email: String
};
// Vec<UserForThisParticularContext>
let users = user_repo.get_users_born_today::<UserForThisParticularContext>()?;
// Use users
}
which, as far as I understand, is not possible in Rust (well, this trait can be used as a constraint, but I can’t create an implementation – “for a trait to be “object safe” it needs to allow building a vtable to allow the call to be resolvable dynamically”).
My temporary solution
The only way I was able to achieve something similar is the following (everything except id
is wrapped in Option
):
struct User {
id: i32,
email: Option<String>,
first_name: Option<String>,
last_name: Option<String>,
password_hash: Option<String>,
birth_date: Option<DateTime<Utc>>,
role: Option<String>,
}
bitflags! {
pub struct UserFields: u32 {
const Email = 1 << 0;
const FirstName = 1 << 1;
const LastName = 1 << 2;
const PasswordHash = 1 << 3;
const BirthDate = 1 << 4;
const Role = 1 << 5;
}
}
impl UserFields {
fn get_fields(&self) -> Vec<String> {
let mut fields: Vec<String> = vec!["id".into()];
if self.contains(UserFields::Email) {
fields.push("email".into());
}
if self.contains(UserFields::FirstName) {
fields.push("first_name".into());
}
if self.contains(UserFields::LastName) {
fields.push("last_name".into());
}
if self.contains(UserFields::PasswordHash) {
fields.push("password_hash".into());
}
if self.contains(UserFields::BirthDate) {
fields.push("birth_date".into());
}
if self.contains(UserFields::Role) {
fields.push("role".into());
}
fields
}
}
#[async_trait]
trait UserRepo {
async fn get_user_by_id(&self, id: i32, fields: UserFields) -> anyhow::Result<User>;
async fn get_users(&self, page: i32, per_page: i32, fields: UserFields) -> anyhow::Result<Vec<User>>;
async fn get_users_born_today(&self, fields: UserFields) -> anyhow::Result<Vec<User>>;
}
But I don’t like that either 🙁
Is there any way to achieve what I described above? I would be glad if someone at least could point me in the right direction. Thanks!