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 typeFoo
.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 toFoo
. Its type also changes between branches, and would be e.g.fn(FooFirst) -> Foo
in the case of theFirst
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, likelet _ = 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 superstruct
s 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.