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 and RequestV2 where the end field is only present in RequestV2.
  • An enum Request with variants V1 and V2 wrapping RequestV1 and RequestV2 respectively.
  • A getter function on Request for the shared start field, e.g. r1.start().
  • A partial getter function returning Result<&u16, ()> for end, 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 define end: 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 with Copy 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:

  1. Variant structs.
  2. Top-level enum.
  3. Ref and RefMut.
  4. Mapping macros.

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} returning Result<&{VariantStruct}, E>.
  • as_{variantname}_mut returning Result<&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 as to_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 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.

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: return T rather than &T where T is the type of the field. T must be Copy 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 to name() and the mutable getter to name_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: return Result<T, E> rather than Result<&T, E> where T is the type of the field. T must be Copy 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 to name() and the mutable partial getter to name_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.

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,
}
}