Skip to main content

array

Macro array 

Source
macro_rules! array {
    ($($t:tt)*) => { ... };
}
Expand description

Declare a struct’s schema as a positional array, for every format.

The bracket counterpart of object!. Fields are encoded in declaration order with no keys at all: JSON writes them between [ and ], BEVE behind a generic-array header.

#[derive(Default, PartialEq, Debug)]
struct Vec3 {
    x: f64,
    y: f64,
    z: f64,
}

structio::array!(Vec3 [x, y, z]);

let v = Vec3 { x: 1.0, y: 2.0, z: 3.0 };
assert_eq!(structio::to_string(&v), "[1,2,3]");
assert_eq!(structio::from_str::<Vec3>("[1,2,3]").unwrap(), v);

The list holds field names and nothing else. object!’s #[required] marker has no counterpart here and is not accepted: an element is required by its position, and an array of the wrong length is refused under every policy.

Generics work as they do for object!: in brackets before the type, with 'de written out when the type borrows from the input.

#[derive(Default)]
struct Labelled<'a, T> {
    label: &'a str,
    value: T,
}
structio::array!(['de, T: structio::ReadWrite + Default] Labelled<'de, T> [label, value]);

§Homogeneous structs

When every field is the same type, name it in front of the field list, the way an array type names its element:

#[derive(Default, PartialEq, Debug)]
struct Rgb {
    r: u8,
    g: u8,
    b: u8,
}

structio::array!(Rgb [u8; r, g, b]);

JSON is unchanged by this. What it buys is BEVE, which stores a run of one type as a typed array: one header for the whole run instead of one per element, and the values as a contiguous block. Rgb goes out in five bytes rather than eight, three f64s in twenty-six rather than twenty-nine, and three bools in three rather than five, since booleans pack one per bit. The bytes are exactly what a [u8; 3] of the same values would have produced, which is also what another implementation writes for its own three-component colour.

The element type is checked: every field has to be it, and a mismatch is a compile error at the declaration. It also has to be Copy, because the payload is one contiguous run and a struct’s fields are not required to be laid out as one, so they are gathered into a block first. That bound holds whether or not the type turns out to have a typed array: one that does not, such as another struct, falls back to a generic array and is written exactly as it would have been without the element type.

Reading is unchanged either way: an array-declared struct takes a generic array or a typed one whatever it was declared as, so adding an element type changes what you write without narrowing what you accept.

Like object!, a declaration has to name every field, and .. at the end of the list says an omission is deliberate. It goes behind the element type where there is one.

#[derive(Default)]
struct Rgb { r: u8, g: u8, b: u8, label: String }
structio::array!(Rgb [u8; r, g, b, ..]);

let c = Rgb { r: 1, g: 2, b: 3, label: "red".into() };
assert_eq!(structio::to_string(&c), "[1,2,3]");

§When to reach for it

Position is cheaper than a key in every respect: nothing is hashed, nothing is compared, no KeyMap is built or stored, and the keys themselves are off the wire. For a type whose field names carry no information anyway, a coordinate or a colour or a row of a table, that is most of the per-value cost gone.

What it costs is room to move. An object can tolerate a field appearing or disappearing, since a reader matches on names and can be asked to step over a key it does not know (SkipUnknown; the default refuses it). An array cannot, under any policy: adding, removing, or reordering a field silently changes what every position means, and lengthens or shortens the array, which readers of the old shape reject. Declare a type this way when its shape is fixed by something outside your control, and object! otherwise.

§What it expands to

One Elements impl carrying the field count, and then, for each format, four small impls: ReadArray and WriteArray for the per-element dispatch, and Read/Write delegating to the array forms. There is no key list and no hash, because there is nothing to look up. An element type adds the typed-array header and payload writer to BEVE’s WriteArray, both of which fold away when it is absent.

A tuple is the same encoding without the names, and goes through the same drivers, so (f64, f64, f64) and the Vec3 above produce identical bytes in both formats.