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.