Mapping macros

To facilitate code that is generic over all variants of a superstruct, we generate several mapping macros with names like map_foo! and map_foo_into_bar!.

Mapping into Self

For every top-level enum we generate a mapping macro that matches on values of Self and is equipped with a variant constructor for Self.

Consider the following type:

#![allow(unused)]
fn main() {
#[superstruct(variants(First, Second)]
struct Foo {
    x: u8,
    #[only(Second)]
    y: u8
}
}

The mapping macro for Foo will be:

#![allow(unused)]
fn main() {
macro_rules! map_foo {
    ($value:expr, $f:expr) => {
        match $value {
            Foo::First(inner) => f(inner, Foo::First),
            Foo::Second(inner) => f(inner, Foo::Second),
        }
    }
}
}

i.e. map_foo! is a macro taking two arguments:

  • value: an expression which must be of type Foo.
  • f: a function expression, which takes two arguments |inner, constructor| where:
    • inner is an instance of a variant struct, e.g. FooFirst. Note that its type changes between branches!
    • constructor is a function from the selected variant struct type to Foo. Its type also changes between branches, and would be e.g. fn(FooFirst) -> Foo in the case of the First branch.

Example usage looks like this:

#![allow(unused)]
fn main() {
impl Foo {
    fn increase_x(self) -> Self {
        map_foo!(self, |inner, constructor| {
            inner.x += 1;
            constructor(inner)
        })
    }
}
}

Although the type of inner could be FooFirst or FooSecond, both have an x field, so it is legal to increment it. The constructor is then used to re-construct an instance of Foo by injecting the updated inner value. If an invalid closure is provided then the type errors may be quite opaque. On the other hand, if your code type-checks while using map! then you can rest assured that it is valid (superstruct doesn't use any unsafe blocks or do any spicy casting).

Tip: You don't need to use the constructor argument if you are implementing a straight-forward projection on Self. Although in some cases you may need to provide a type hint to the compiler, like let _ = constructor(inner).

Mapping from Ref and RefMut

Mapping macros for Ref and RefMut are also generated. They take an extra lifetime argument (supplied as a reference to _) as their first argument, which must correspond to the lifetime on the Ref/RefMut type.

Example usage for Foo:

#![allow(unused)]
fn main() {
impl Foo {
    fn get_x<'a>(&'a self) -> &'a u64 {
        map_foo_ref!(&'a _, self, |inner, _| {
            &inner.x
        })
    }
}
}

Mapping into other types

Mappings can also be generated between two superstructs with identically named variants.

These mapping macros are available for the top-level enum, Ref and RefMut, and take the same number of arguments. The only difference is that the constructor will be the constructor for the type being mapped into.

The name of the mapping macro is map_X_into_Y! where X is the snake-cased Self type and Y is the snake-cased target type.

Example:

#![allow(unused)]
fn main() {
#[superstruct(
    variants(A, B),
    variant_attributes(derive(Debug, PartialEq, Clone)),
    map_into(Thing2),
    map_ref_into(Thing2Ref),
    map_ref_mut_into(Thing2RefMut)
)]
#[derive(Debug, PartialEq, Clone)]
pub struct Thing1 {
    #[superstruct(only(A), partial_getter(rename = "thing2a"))]
    thing2: Thing2A,
    #[superstruct(only(B), partial_getter(rename = "thing2b"))]
    thing2: Thing2B,
}

#[superstruct(variants(A, B), variant_attributes(derive(Debug, PartialEq, Clone)))]
#[derive(Debug, PartialEq, Clone)]
pub struct Thing2 {
    x: u64,
}

fn thing1_to_thing2(thing1: Thing1) -> Thing2 {
    map_thing1_into_thing2!(thing1, |inner, cons| { cons(inner.thing2) })
}

fn thing1_ref_to_thing2_ref<'a>(thing1: Thing1Ref<'a>) -> Thing2Ref<'a> {
    map_thing1_ref_into_thing2_ref!(&'a _, thing1, |inner, cons| { cons(&inner.thing2) })
}

fn thing1_ref_mut_to_thing2_ref_mut<'a>(thing1: Thing1RefMut<'a>) -> Thing2RefMut<'a> {
    map_thing1_ref_mut_into_thing2_ref_mut!(&'a _, thing1, |inner, cons| {
        cons(&mut inner.thing2)
    })
}
}

Naming

Type names are converted from CamelCase to snake_case on a best-effort basis. E.g.

  • SignedBeaconBlock -> map_signed_beacon_block!
  • NetworkDht -> map_network_dht!

The current algorithm is quite simplistic and may produce strange names if it encounters repeated capital letters. Please open an issue on GitHub if you have suggestions on how to improve this!

Limitations

  • Presently only pure mapping functions are supported. The type-hinting hacks make it hard to support proper closures.
  • Sometimes type-hints are required, e.g. let _ = constructor(inner).
  • Macros are scoped per-module, so you need to be more mindful of name collisions than when defining regular types.