Can I take a type and generate another with the same-but-optional members?

I have a configuration "server" written in Rust (not in the sense of a public internet server, but a process other processes can talk to via IPC). It makes heavy use of Serde, especially the JSON integration. There's a lot of repeated idioms that I'd like to make a bit less verbose though. I think an example illustrates it well.

Consider eg. a contact info data type:

#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub enum ContactInfo {
    pub name: String,
    pub address: String,
    pub phone: String,
    pub email: String,
}

Any of these fields can be updated. The updates arrive as differences encoded as JSON eg.

{
  "address" : "123 Fake St",
}

I think this naturally lends itself to a type composed of Options:

#[derive(serde::Serialize, serde::Deserialize, Debug, PartialEq, Clone)]
pub enum ContactInfoUpdate {
    pub name: Option<String>,
    pub address: Option<String>,
    pub phone: Option<String>,
    pub email: Option<String>,
}

And then I can implement an update function on ContactInfo that updates the fields from the non-None fields in the update. The JSON messages trivially deserialises to the update type, and the non-optional type trivially serialises to a config file. So far so good.

However, I have dozens of these, and it's a lot of boilerplate. They're not all strings, some are numeric or boolean. Other approaches I've considered:

  • Just make the original type the same as the update type ie. all Options. But then I need a lot of runtime checks to make sure the data in the actual config is all Some(...).
  • Descend into the JSON one more level when I get an update and have an update function for each field. That's probably more boilerplate I suspect, plus then I have JSON code mixed in with my pure-Rust-type code.

Is there a way to (a) just generate the all-Option type from the original type, and (b) have a generic implementation for "applying" it ie. overwriting the existing fields with the non-None values from the update type? Does such a thing already exist?

I'm not even sure what to call this, although I think of it as a difference type (even though it's not strictly symmetric) or a delta type. But searching for that has not been fruitful.

1 Like

So, I don't know if something already exists, but this sounds like a job for macros !:superhero:

(using concat_idents to make ContactInfoUpdate)

macro_rules! make_optional {
    (
        $(#[$outer:meta])*
        pub struct $name:ident {
            $(
                pub $field_name:ident: $field_type:ty
            ),*
            $(,)?
        }
    ) => {
        $(#[$outer])*
        pub struct $name {
            $(
                pub $field_name: $field_type
            ),*
        }

        concat_idents::concat_idents!{
            struct_name = $name, Update {
                $(#[$outer])*
                pub struct struct_name {
                    $(
                        pub $field_name: Option<$field_type>
                    ),*
                }

                impl $name {
                    pub fn update(&mut self, mut update: struct_name) {
                        $(
                            if let Some(field) = update.$field_name.take() {
                                self.$field_name = field
                            }
                        )*
                    }
                }
            }
        }
    };
}

make_optional! {
    #[derive(serde::Serialize, serde::Deserialize, Default, Debug, PartialEq, Clone)]
    pub struct ContactInfo {
        pub name: String,
        pub address: String,
        pub phone: String,
        pub email: String,
    }
}

This implements both (a) and (b) :grin:

1 Like

Here is how it would be used btw:

let mut contact_info = ContactInfo::default();
let contact_info_opt = ContactInfoUpdate::default();
contact_info.update(contact_info_opt)

Wow, thanks for solving my problem! I have avoided macros so far, because I'm still something of a beginner, but maybe I should get over that and start using them. Your code looks perfectly intelligible.

1 Like