Derive traits for a struct defined in another file?

I'd like to define a module with the models my application use are defined, and where there is no dependency on any third party crate. I'd like to keep all implementations of third party crates in separate files. The idea is to keep the coupling between my own logic and the libraries low so the hassle of migrating from one third party crate to another is kept to a minimum when for instance changing the database-ORM library or API framework. This seems quite straight forward if implementing traits manually, like:

// models.rs
pub struct MyModel {
  data: String
}

// db.rs - database orm implementations & db methods
use crate::models::MyModel;
impl Insertable for MyModel {
  // some implementation 
}

// mod.rs
pub mod models;
pub mod db;

Alot of the libraries I use provide handy derives that I can use to implement most traits - But I only know how to do this in the same file where the model is defined (i.e. models.rs above), which would then break the project-structure I wish to keep and polluting my "core logic" with library specific code.

Is there a way to derive traits for a structure subsequent to the definition of the structure and within another file - In a similar way as manual implementations, but without having to write out all of the boilerplate code?

Obviously this doesn't work, but just to give an idea:

// models.rs
pub struct MyModel {
  data: String
}

//db.rs
use crate::models::MyModel;
#[derive(Insertable)]
MyModel

// mod.rs
pub mod models;
pub mod db;

Can this be achieved without implementing all traits manually somehow? If not - What options exist to keep business logic and framework implementations nicely separated?

Derive macros have to be placed on the actual type definition. They actually take the type definition as their input before any identifiers are resolved, so that's unlikely to change any time soon.

Since you're typically only adding a single identifier (or maybe a couple) per dependency I don't really think you're creating a tight coupling just by adding the derives to your definition.

2 Likes

I'll echo semicoleon's sentiment that the coupling from having the derive is not that tight.

That said, If you really had a concrete use case where the models module needed to be compilable without relying on the db module, then a relatively ugly solution would be to use cfg_attr like so:

// models.rs
#[cfg(feature = "db")]
pub use crate::db::*;

#[cfg_attr(feature = "db", derive(Insertable))]
pub struct MyModel {
  data: String
}

//db.rs
/// This cannot rely on `models`, but must expose all the db related functionality
// ...

// mod.rs
pub mod models;
pub mod db;

You would also have to set up the feature in Cargo.toml, and in a real use of this technique probably actually separate db and models into separate crates, and make the db crate only depended upon conditionally which I have not explained here.

Another relatively ugly option, that preserves that models does not depend on db is to use separate structs which have the derives placed on them, and then to provide conversions back and forth. So like this:

// models.rs
pub struct MyModel {
  data: String
}

//db.rs
use crate::models::MyModel;
#[derive(Insertable)]
pub struct MyDbModel {
  data: String
}

impl From<MyModel> for MyDbModel {
   // ...
}
  
impl From<MyDbModel> for MyModel {
   // ...
}

// mod.rs
pub mod models;
pub mod db;

This does keep models entirely ignorant of db, but introduces a lot of boilerplate. A macro like this could help alleviate some of the boilerplate if you actually have several modules like db that you want to keep separate:

// models.rs
pub struct MyModel {
  data: String
}

macro_rules! impl_from_into_my_model {
    ($other_struct: ident) => {
        impl From<MyModel> for $other_struct {
            fn from(model: MyModel) -> Self {
                 Self {
                     data: model.data,
                 }
            }
        }
        
        impl From<$other_struct> for MyModel {
           fn from(model: $other_struct) -> Self {
                 Self {
                     data: model.data,
                 }
            }
        }
    }
}

//db.rs
use crate::models::MyModel;
#[derive(Insertable)]
pub struct MyDbModel {
  data: String
}

impl_from_into_my_model!{MyDbModel}

// mod.rs
pub mod models;
pub mod db;

Again, I think that most of the time one should prefer just using the derive as opposed to either of the two above options, but if you really have hard requirements for decoupling here, then those are the best options I can think of at the moment.

2 Likes

Thanks @Ryan1729!

I'm trying to implement a "ports and adapters" /hexagonal-architecture for a project I'm working on. While the coupling may not be that tight, the way I understand the idea of that pattern is that each layer need to keep it's integrity and not bleed into it's adjacencies.

If that pattern is kept, then the "core/domain/business logic" and it's implementation in the http-api or database model-classes may all be within different crates. The core may remain wholly untouched by a change of ORM or web-framework.

I'm leaning towards defining separate structs between the domain-layer and it's implementation since it feels like the other option might still cause some bleeding between the layers.

I'm a beginner at rust and haven't really wrapped my head around macros - but I'm thinking the structs could be a good candidate for a macro that generates them and derives the required traits for them? The shape of the struct will more or less always be identical to the structs defined in the domain-layer, so there would be a "template" for them available. Any pointers or tips for taking first steps with macros would be much appreciated :slight_smile:

Pushing further on things like decoupling than you might in a purely practical scenario, as a learning exercise makes sense. That way when you run into a real scenario you can consider how much benefit pushing hard on that aspect of the code provided you, and make an informed decision based on the particular real scenario.

If you haven’t seen it already, the little book of rust macros is a good resource on the topic.

1 Like

@Ryan1729 great tip! Thanks :slight_smile: haven't come across that one before

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.