Introduction
SuperStruct is a Rust library for working with versioned data. It allows you to define and operate
on variants of a struct
which share some fields in common.
As an example, imagine you're working on a program that accepts a Request
struct from the user.
In the first version of the program you only allow users to specify a start: u16
field:
#![allow(unused)] fn main() { pub struct Request { start: u16, } }
After a while you realise that it would be nice if users could also specify an end: u16
in their
requests, so you would like to change the definition of Request
to:
#![allow(unused)] fn main() { pub struct Request { start: u16, end: u16, } }
Now imagine that your program needs to work with old versions of Request
as well as new, i.e.
it needs to be backwards-compatible. This is reasonably common when databases are involved and
you need to write schema migrations, or when working with network protocols.
SuperStruct allows you to define both versions of the Request
with a single definition, and
also generates an enum to unify them:
use superstruct::superstruct;
#[superstruct(variants(V1, V2))]
pub struct Request {
pub start: u16,
#[superstruct(only(V2))]
pub end: u16,
}
#[cfg_attr(test, test)]
fn main() {
let r1 = Request::V1(RequestV1 { start: 0 });
let r2 = Request::V2(RequestV2 { start: 0, end: 10 });
assert_eq!(r1.start(), r2.start());
assert_eq!(r1.end(), Err(()));
assert_eq!(r2.end(), Ok(&10));
}
The superstruct
definition generates:
- Two structs
RequestV1
andRequestV2
where theend
field is only present inRequestV2
. - An enum
Request
with variantsV1
andV2
wrappingRequestV1
andRequestV2
respectively. - A getter function on
Request
for the sharedstart
field, e.g.r1.start()
. - A partial getter function returning
Result<&u16, ()>
forend
, e.g.r2.end()
. - Lots of other useful goodies that are covered in the Codegen section of the book.
When should you use SuperStruct?
- If you want to avoid duplication when defining multiple related structs.
- If you are considering manually writing getters to extract common fields from an enum.
- If you are considering writing traits to unify types with fields in common.
When should you not use SuperStruct?
- If you can get away with just using an
Option
field. In our example,Request
could defineend: Option<u16>
. - If you can achieve backwards compatible (de)serialization through clever use of
serde
macros.
What next?
- Check out the Code Generation docs.
- Check out the Configuration docs for information on how to
control
superstruct
's behaviour, including renaming getters, working withCopy
types, etc.
Setup
To use SuperStruct in your project add superstruct
as a dependency in your Cargo.toml
:
superstruct = "0.4.0"
For the latest published version please consult crates.io
.
To use SuperStruct, import the superstruct
procedural macro with use superstruct::superstruct
,
like so:
use superstruct::superstruct;
#[superstruct(variants(V1, V2))]
pub struct Request {
pub start: u16,
#[superstruct(only(V2))]
pub end: u16,
}
#[cfg_attr(test, test)]
fn main() {
let r1 = Request::V1(RequestV1 { start: 0 });
let r2 = Request::V2(RequestV2 { start: 0, end: 10 });
assert_eq!(r1.start(), r2.start());
assert_eq!(r1.end(), Err(()));
assert_eq!(r2.end(), Ok(&10));
}
For more information on this example see the Introduction.
Code generation
SuperStruct generates several types, methods and trait implementations.
You should visit each of the sub-pages in order to understand how the generated code fits together:
Example
For a full, up-to-date example of the code generated, please see the RustDoc output for
the Request
example.
Variant structs
The most basic items generated by SuperStruct are the variant structs. For each
variant listed in the top-level superstruct(variants(..)
list, a struct with
the name {BaseName}{VariantName}
will be created. For example:
#![allow(unused)] fn main() { #[superstruct(variants(Foo, Bar))] struct MyStruct { name: String, #[superstruct(only(Foo))] location: u16, } }
Here the BaseName
is MyStruct
and there are two variants called Foo
and Bar
.
The generated variant structs are:
#![allow(unused)] fn main() { struct MyStructFoo { name: String, location: u16, } struct MyStructBar { name: String, } }
Note how the only
attribute controls the presence of fields in each variant.
For more information see Struct attributes.
The variant structs are unified as part of the top-level enum.
Top-level enum
SuperStruct generates an enum that combines all of the generated variant structs.
Consider the the MyStruct
example from the previous page:
#![allow(unused)] fn main() { #[superstruct(variants(Foo, Bar))] struct MyStruct { name: String, #[superstruct(only(Foo))] location: u16, } }
The generated enum is:
#![allow(unused)] fn main() { enum MyStruct { Foo(MyStructFoo), Bar(MyStructBar), } }
The enum has one variant per variant in superstruct(variants(..))
, and each
variant contains its generated variant struct. It is named {BaseName}
.
Generation of the top-level enum can be disabled using the no_enum
attribute. For more information
see the Struct attributes.
Getters and setters
The top-level enum has getters and setters for each of the variant fields. They are named:
{field_name}()
for getters.{field_name}_mut()
for setters.
If a field is common to all variants, then the getters and setters are total and return &T
and &mut T
respectively, where T
is the type of the field.
If a field is part of some variants but not others, then the getters and
setters are partial and return Result<&T, E>
and Result<&mut T, E>
respectively.
Many aspects of the getters and setters can be configured, including their
names, whether they Copy
and which error type E
is used.
See Field attributes.
Casting methods
The top-level enum has methods to cast it to each of the variants:
as_{variantname}
returningResult<&{VariantStruct}, E>
.as_{variantname}_mut
returningResult<&mut {VariantStruct}, E>
.
The error type E
may be controlled by the cast_error
attribute.
Reference methods
The top-level enum has methods for converting it into the Ref
and RefMut
types, which
are described here.
to_ref
returning{BaseName}Ref
.to_mut
returning{BaseName}RefMut
.
From
implementations
The top-level enum has From
implementations for converting (owned) variant structs, i.e.
impl From<{VariantStruct}> for {BaseName}
for all variants
Ref
and RefMut
SuperStruct generates two reference-like structs which are designed to simplify working with nested
superstruct
types.
The immutable reference type is named {BaseName}Ref
and has all of the immutable getter methods
from the top-level enum.
The mutable reference type is named {BaseName}RefMut
and has all of the mutable getter methods
from the top-level enum.
Consider the MyStruct
example again:
#![allow(unused)] fn main() { #[superstruct(variants(Foo, Bar))] struct MyStruct { name: String, #[superstruct(only(Foo))] location: u16, } }
The generated Ref
types look like this:
#![allow(unused)] fn main() { enum MyStructRef<'a> { Foo(&'a MyStructFoo), Bar(&'a MyStructBar), } enum MyStructRefMut<'a> { Foo(&'a mut MyStructFoo), Bar(&'a mut MyStructFoo), } }
The reason these types can be useful (particularly with nesting) is that they do not require a full
reference to a MyStruct
in order to construct: a reference to a single variant struct will suffice.
Trait Implementations
Copy
Each Ref
type is Copy
, just like an ordinary &T
.
From
The Ref
type has From
implementations that allow converting from references to variants
or references to the top-level enum type, i.e.
impl From<&'a {VariantStruct}> for {BaseName}Ref<'a>
for all variants.impl From<&'a {BaseName}> for {BaseName}Ref<'a>
(same asto_ref()
).
Example
Please see examples/nested.rs
and its
generated documentation.
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.
Configuration
SuperStruct is a procedural macro, and is configured by superstruct
attributes on the
type being defined.
- Struct attributes are applied to the top-level type and configure properties relevant to that, as well as defaults for error types.
- Field attributes are applied to each struct field and determine the fields of variants, as well as the characteristics of getters and setters.
Struct attributes
The following attributes may be used in a superstruct
macro invocation on a
struct
item. All attributes are optional unless stated otherwise.
Variants
#[superstruct(variants(A, B, ...))]
Define the list of variants that this type has. See variant structs.
The variants
attribute is not optional.
Format: 1+ comma-separated identifiers.
Cast error
#[superstruct(cast_error(ty = "..", expr = ".."))]
Define the error type to be returned from casting methods.
The expression must be of the given error type, and capable of being evaluated without any context (it is not a closure).
Format: quoted type for ty
, quoted expression for expr
Partial getter error
#[superstruct(cast_error(ty = "..", expr = ".."))]
Define the error type to be returned from partial getter methods.
The expression must be of the given error type, and capable of being evaluated without any context (it is not a closure).
Format: quoted type for ty
, quoted expression for expr
Variant attributes
#[superstruct(variant_attributes(...))]
Provide a list of attributes to be applied verbatim to each variant struct definition.
This can be used to derive traits, perform conditional compilation, etc.
Format: any.
Specific variant attributes
#[superstruct(specific_variant_attributes(A(...), B(...), ...))]
Similar to variant_attributes
, but applies the attributes only to the named variants. This
is useful if e.g. one variant needs to derive a trait which the others cannot, or if another
procedural macro is being invoked on the variant struct which requires different parameters.
Format: zero or more variant names, with variant attributes nested in parens
Ref
attributes
#[superstruct(ref_attributes(...))]
Provide a list of attributes to be applied verbatim to the generated Ref
type.
Format: any.
RefMut
attributes
#[superstruct(ref_mut_attributes(...))]
Provide a list of attributes to be applied verbatim to the generated RefMut
type.
Format: any.
No enum
#[superstruct(no_enum)]
Disable generation of the top-level enum, and all code except the variant structs.
Map Into
#[map_into(ty1, ty2, ..)]
#[map_ref_into(ty1, ty2, ..)]
#[map_ref_mut_into(ty1, ty2, ..)]
Generate mapping macros from the top-level enum, the Ref
type or the RefMut
type as appropriate.
Please see the documentation on Mapping into other types for an explanation of how these macros operate.
Format: one or more superstruct
type names
Meta variants
#[superstruct(meta_variants(A, B, ...), variants(C, D, ...))]
Generate a two-dimensional superstruct. See meta variant structs.
The meta_variants
attribute is optional.
Format: 1+ comma-separated identifiers.
Field attributes
Field attributes may be applied to fields within a struct
that has a superstruct
attribute
to it at the top-level.
All attributes are optional.
Only
#[superstruct(only(A, B, ...))]
Define the list of variants that this field is a member of.
The only
attribute is currently the only way that different variants are
created.
The selected variants should be a subset of the variants defined in the top-level
variants
attribute.
Format: 1+ comma-separated identifiers.
Getter
#[superstruct(getter(copy, ..))]
#[superstruct(getter(no_mut, ..))]
#[superstruct(getter(rename = "..", ..))]
Customise the implementation of the getter functions for this field.
This attribute can only be applied to common fields (i.e. ones with no only
attribute).
All of the sub-attributes copy
, no_mut
and rename
are optional and any subset of them
may be applied in a single attribute, e.g. #[superstruct(getter(copy, no_mut))]
is valid.
copy
: returnT
rather than&T
whereT
is the type of the field.T
must beCopy
or the generated code will fail to typecheck.no_mut
: do not generate a mutating getter with_mut
suffix.rename = "name"
: rename the immutable getter toname()
and the mutable getter toname_mut()
(if enabled).
Partial getter
#[superstruct(partial_getter(copy, ..))]
#[superstruct(partial_getter(no_mut, ..))]
#[superstruct(partial_getter(rename = "..", ..))]
Customise the implementation of the partial getter functions for this field.
This attribute can only be applied to non-common fields (i.e. ones with an only
attribute).
All of the sub-attributes copy
, no_mut
and rename
are optional and any subset of them
may be applied in a single attribute, e.g. #[superstruct(partial_getter(copy, no_mut))]
is valid.
copy
: returnResult<T, E>
rather thanResult<&T, E>
whereT
is the type of the field.T
must beCopy
or the generated code will fail to typecheck.no_mut
: do not generate a mutating getter with_mut
suffix.rename = "name"
: rename the immutable partial getter toname()
and the mutable partial getter toname_mut()
(if enabled).
The error type for partial getters can currently only be configured on a per-struct basis
via the partial_getter_error
attribute, although this may
change in a future release.
No Getter
Disable the generation of (partial) getter functions for this field. This can be used for when two fields have the same name but different types:
#![allow(unused)] fn main() { #[superstruct(variants(A, B))] struct NoGetter { #[superstruct(only(A), no_getter)] pub x: u64, #[superstruct(only(B), no_getter)] pub x: String, } }
Flatten
#[superstruct(flatten)]
This attribute can only be applied to enum fields with variants that match each variant of the superstruct. This is useful for nesting superstructs whose variant types should be linked.
This will automatically create a partial getter for each variant. The following two examples are equivalent.
Using flatten
:
#![allow(unused)] fn main() { #[superstruct(variants(A, B))] struct InnerMessage { pub x: u64, pub y: u64, } #[superstruct(variants(A, B))] struct Message { #[superstruct(flatten)] pub inner: InnerMessage, } }
Equivalent without flatten
:
#![allow(unused)] fn main() { #[superstruct(variants(A, B))] struct InnerMessage { pub x: u64, pub y: u64, } #[superstruct(variants(A, B))] struct Message { #[superstruct(only(A), partial_getter(rename = "inner_a"))] pub inner: InnerMessageA, #[superstruct(only(B), partial_getter(rename = "inner_b"))] pub inner: InnerMessageB, } }
If you wish to only flatten into only a subset of variants, you can define them like so:
#![allow(unused)] fn main() { #[superstruct(variants(A, B))] struct InnerMessage { pub x: u64, pub y: u64, } #[superstruct(variants(A, B, C))] struct Message { #[superstruct(flatten(A,B))] pub inner: InnerMessage, } }