derived: Automating the boring stuff

Published on Sep 25, 2021 | Updated on Sep 27, 2021

Background

There are structs. Then there are huge structs. And then there are enormous structs. Last week, I was working on simplifying some structures at Skytable when I was really annoyed with a struct.

No, let me not leave you there. It looked like this (module shown for a reason):

mod cfgset {
    struct ConfigKeys {
        tls_cert: String,
        tls_key: String,
        tls_passin_stream: String,
        // ... some large number of other fields ...
    }
}

The problem:

  1. I didn’t want to make the fields public. This means that instantiation like below was not a choice:
    let something = ConfigKeys { ... };
    
  2. I was too lazy to write a constructor (which would have ended up like: pub fn new(tls_cert: String, ...))

Do note that my use of the word constructor is strictly in the Rust context. As in $struct::new()

The solution

The solution to my indolence was a derive macro; and I decided to call it derived. The macro automates the generation of some boring things, for example the constructor generation case illustrated above. With this macro, all you need to do is:

use derived::Ctor;

#[derive(Ctor)]
struct ConfigKeys {
    tls_cert: String,
    tls_key: String,
    tls_passin_stream: String,
    // ... some large number of other fields ...
}

And now, I can simply call:

let mykeys = ConfigKeys::new("/path/to/cert".to_owned(), ...);

Behind the scenes, the Ctor macro will generate a constructor like:

pub fn new(
    tls_cert: String,
    tls_key: String,
    ...
) -> ConfigKeys {
    Self {
        tls_cert,
        tls_key,
        ..
    }
}

Another problem appears

Now, since the fields are private – there are instances when I’d need to access the fields, but again, I did not want to make the fields public! To solve this, we got ourselves another derive macro which I call Gtor (dubbed after getter and ctor).

This suddenly reminded me of the getter and setter days (Java go brrr)

With the Gtor macro, this is all I needed to do:

#[derive(Gtor, Ctor)]
pub struct MyStruct {
    cert: String,
    port: u16,
}

let x = MyStruct::new("cert.pem".to_owned(), 2003);
assert_eq!(x.get_port(), 2003);
assert_eq!(x.get_cert(), "cert.pem");

Woot, the Gtor macro will generate get_* for every field in the struct! Behind the scenes, the macro will generate methods like:

/// Returns the value for the `port` field in struct [`ConfigKeys`]
pub fn get_port(&self) -> u16 {
    self.port
}
/// Returns the value for the `cert` field in struct [`ConfigKeys`]
pub fn get_cert(&self) -> &String {
    &self.cert
}
/// ...

Wait, what?: But wait, what happened when we ran get_port()? Well, the Gtor macro is smart enough to realize that u16 is a Copy type and automatically returns a copy instead of a reference. While for the get_cert(), it will return a reference.

Thinking about setters? Yup, that’s there too! With the Stor derive macro. Simply add #[derive(Stor)] and you’re good to go! Also, the macro will generate doc-comments for you (read more here).

The work ahead

The derived macro crate doesn’t end with ctors, gtors and stors (there are some amazing crates that already do that :D)! I have plans to add several other macros to reduce some of the redundant work in codebases. If you have an idea, just drop it here and we can get to work!