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
RequestV1andRequestV2where theendfield is only present inRequestV2. - An enum
Requestwith variantsV1andV2wrappingRequestV1andRequestV2respectively. - A getter function on
Requestfor the sharedstartfield, 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
Optionfield. In our example,Requestcould defineend: Option<u16>. - If you can achieve backwards compatible (de)serialization through clever use of
serdemacros.
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 withCopytypes, 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 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}_mutreturningResult<&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_refreturning{BaseName}Ref.to_mutreturning{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
Attributes on the enum variants
To add attributes to the enum variants, enum_variant_attributes and specific_enum_variant_attributes
can be used.
Consider a variant of the MyStruct example where you want to derive serde::Serialize. However, one
of the fields has a lifetime thus the #[serde(borrow)] attribute is required on the enum variants.
In addition, you want to change the name of one of the enum variants when it's serialized:
#![allow(unused)] fn main() { #[superstruct( variants(Foo, Bar), enum_variant_attributes(serde(borrow)), specific_enum_variant_attributes(Bar(serde(rename = "Baz"))), )] #[derive(serde::Serialize)] struct MyStruct<'a> { name: &'a str, #[superstruct(only(Foo))] location: u16, } }
The generated enum is:
#![allow(unused)] fn main() { #[derive(serde::Serialize)] enum MyStruct<'a> { #[serde(borrow)] Foo(MyStructFoo<'a>), #[serde(borrow, rename = "Baz")] Bar(MyStructBar<'a>), } }
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:inneris an instance of a variant struct, e.g.FooFirst. Note that its type changes between branches!constructoris a function from the selected variant struct type toFoo. Its type also changes between branches, and would be e.g.fn(FooFirst) -> Fooin the case of theFirstbranch.
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 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.
Meta variant structs and enums
Meta variants are an optional feature, useful for scenarios where you'd want nested
enums at the top-level. structs will be created for all combinations of meta_variants
and variants, names in the format {BaseName}{MetaVariantName}{VariantName}.
Additionally, enums will be created for each meta_variant named {BaseName}{MetaVariantName}.
For example:
#![allow(unused)] fn main() { #[superstruct(meta_variants(Baz, Qux), variants(Foo, Bar))] struct MyStruct { name: String, #[superstruct(only(Foo))] location: u16, #[superstruct(meta_only(Baz))] score: u64, #[superstruct(only(Bar), meta_only(Qux))] id: usize, } }
Here the BaseName is MyStruct and there are two variants in the meta-enum called
Baz and Qux.
The generated enums are:
#![allow(unused)] fn main() { enum MyStruct { Baz(MyStructBaz), Qux(MyStructQux), } enum MyStructBaz { Foo(MyStructBazFoo), Bar(MyStructBazBar), } enum MyStructQux { Foo(MyStructQuxFoo), Bar(MyStructQuxBar), } }
The generated variant structs are:
#![allow(unused)] fn main() { struct MyStructBazFoo { name: String, location: u16, score: u64, } struct MyStructBazBar { name: String, score: u64, } struct MyStructQuxFoo { name: String, location: u16, } struct MyStructQuxBar { name: String, id: usize, } }
Note how the only attribute still applies, and a new meta_only attribute can be used to
control the presence of fields in each meta variant.
For more information see Struct attributes.
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
Enum variant attributes
#[superstruct(enum_variant_attributes(...))]
Provide a list of attributes to be applied verbatim to each of the enum variants.
This is useful when using another proc-macro on the enum and needing to add an attribute to all enum variants.
Format: any.
Specific enum variant attributes
#[superstruct(specific_enum_variant_attributes(A(...), B(...), ...))]
Similar to enum_variant_attributes, but applies the attributes only to the named enum variants.
This is useful if e.g. one enum variant needs an attribute while the others cannot.
Format: zero or more variant names, with enum 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: returnTrather than&TwhereTis the type of the field.Tmust beCopyor the generated code will fail to typecheck.no_mut: do not generate a mutating getter with_mutsuffix.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>whereTis the type of the field.Tmust beCopyor the generated code will fail to typecheck.no_mut: do not generate a mutating getter with_mutsuffix.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, } }