Announcing `attrsets`, proc macro for defining variants of a struct/enum with different field attributes e.g. for serde

Have you ever wanted to, say, define both a human-readable and a compact Serde serializations for the same struct/enum?
You can define multiple structs with the same fields but different serde attributes on these fields and then transmute between the struct (reference) types to pick a serialization.
But manually writing copies of the same struct is basically unmaintainable and looks bad in the code!

Here's a proc macro to automate that:

https://crates.io/crates/attrsets | https://github.com/myfreeweb/attrsets

#[attrsets::attrsets(Compact)]
#[derive(Deserialize, Serialize)]
pub struct Thing {
    #[attrset(Compact, serde(rename = "f"))]
    pub flag: bool,

    #[attrset(Compact, serde(with = "ts_seconds"))]
    #[attrset(Compact, serde(rename = "t"))]
    pub time: DateTime<Utc>,
}

basically expands into

#[derive(Deserialize, Serialize)]
pub struct Thing {
    pub flag: bool,
    pub time: DateTime<Utc>,
}

#[derive(Deserialize, Serialize)]
pub struct ThingCompact {
    #[serde(rename = "f")]
    pub flag: bool,

    #[serde(with = "ts_seconds")]
    #[serde(rename = "t")]
    pub time: DateTime<Utc>,
}

and now you can


let thing = ThingCompact { flag, time };
// the Compact variant here is intended for a non-readable format like CBOR, but let's view JSON
println!("{}", serde_json::to_string(&thing).unwrap());
// {"f":true,"t":1594561453}
println!("{}", serde_json::to_string(unsafe { mem::transmute::<_, &Thing>(&thing) }).unwrap());
// {"flag":true,"time":"2020-07-12T13:44:13Z"}

Transmuting a reference always seems weird to me, I would use pointer casting.
I'm pretty sure it's UB to go from &ThingCompact to &Thing if they are repr(Rust).

1 Like

Yes, the transmute is unsound unless the structs are #[repr(C)].

2 Likes

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.