macro_rules! tagged_enum {
(write_only :: $($t:tt)*) => { ... };
(read_only $($t:tt)*) => { ... };
(write_only $($t:tt)*) => { ... };
($($t:tt)*) => { ... };
}Expand description
Declare an enum, for every format.
A variant that carries nothing is written as its name. A variant that carries a value is written as an object of one member, keyed by that name: the tagged-union form, with the tag being the name rather than a position, so adding or reordering variants does not change what a document means.
That is external tagging, where the name wraps the payload, and it is
what a declaration with no tag clause gets. Adding as tag "..." moves the
name inside the payload’s object instead, as a member beside the
payload’s own; see Internal tagging below.
Mark a variant that carries a value with (_). The payload’s type is not
repeated here; it is already on the enum, and stating it twice would be a
second place to keep in step.
#[derive(Default, PartialEq, Debug)]
struct Circle { radius: f64 }
structio::object!(Circle { radius });
#[derive(Default, PartialEq, Debug)]
enum Shape {
#[default]
Empty,
Circle(Circle),
Sides(u32),
}
structio::tagged_enum!(Shape {
Empty,
Circle(_),
Sides(_),
});
assert_eq!(structio::to_string(&Shape::Empty), "\"Empty\"");
assert_eq!(
structio::to_string(&Shape::Circle(Circle { radius: 2.0 })),
r#"{"Circle":{"radius":2}}"#
);
assert_eq!(
structio::from_str::<Shape>(r#"{"Sides":3}"#).unwrap(),
Shape::Sides(3)
);Names are renamed the same way a field is, they take aliases the same way
with Variant | "other_name", they take a case rule the
same way, and generics go in brackets before the type, exactly as for
object!:
structio::tagged_enum!([T: structio::ReadWrite + Default] Message<T> {
"ping" => Ping,
"data" => Data(_),
});§One payload, of a type you already declared
A variant carries at most one value, which is the shape a
std::variant<A, B, C> has and the one that composes: the payload is an
ordinary type, declared with object! or array! or built in, and the
enum adds only the tag. A Rust variant with several fields, or with named
fields, is not accepted; give it a struct or a tuple instead. Neither is
object!’s #[required] marker, which has nothing to say here: a variant
declared as carrying a value can be read only from the object form that
holds one, so its payload is required by the declaration itself.
A payload type needs Default, for the reason an Option’s payload
does: reading a variant the destination is not already holding has to build
one before it can read into it.
structio::tagged_enum!(Span { None, Range(_) });
assert_eq!(structio::to_string(&Span::Range((1, 5))), r#"{"Range":[1,5]}"#);The requirement stops at the payloads. The enum itself needs no Default,
which is worth saying because the shape that most wants a tagged enum is
the one that cannot derive it: a message union whose every variant carries
something. #[derive(Default)] reaches an enum only through a variant
marked #[default], and that variant has to be a unit one, so an enum like
the one below would have to nominate a message as the stand-in for no
message to get the derive. It does not have to. Declaring it, writing it,
and reading into a value the caller already holds ask the payloads for
their Default and the enum for nothing.
// The payloads carry their own `Default`, which is the part that is
// required. The enum below carries none, and is not asked for one.
#[derive(Default, PartialEq, Debug)]
struct Frame { len: u32 }
#[derive(Default, PartialEq, Debug)]
struct Ack { seq: u32 }
#[derive(PartialEq, Debug)]
enum Packet {
Data(Frame),
Ack(Ack),
}
structio::tagged_enum!(Packet { Data(_), Ack(_) });
let mut packet = Packet::Data(Frame { len: 0 });
structio::read_into(&mut packet, r#"{"Ack":{"seq":9}}"#).unwrap();
assert_eq!(packet, Packet::Ack(Ack { seq: 9 }));What does ask for one is a position that constructs the enum rather than
filling one that is there: from_str, which is handed
nothing but a document, and a growing container, whose new elements have to
exist before they can be read into. A Vec<Packet> therefore wants
Packet: Default and there is no spelling of the read that avoids it,
while a Box<Packet> or a [Packet; N] has no element to build and holds
the enum above as it stands. Where a placeholder is unavoidable, writing it
as a private constructor and reading into that keeps it out of the enum’s
public API, which #[derive(Default)] would not.
§What is read back
The two forms are not interchangeable, and the asymmetry runs one way. A
variant carrying nothing reads from either, so a producer that always
writes the object form still round-trips. A variant carrying a value has
only the object form: the name on its own leaves the value missing, which
is ExpectedBrace in JSON and
ExpectedObject in BEVE rather than an
unknown variant, the name having been recognized and the value under it
not being there.
use structio::{ErrorCode, from_str};
// A variant carrying nothing takes either form.
assert_eq!(from_str::<Shape>("\"Empty\"").unwrap(), Shape::Empty);
assert_eq!(from_str::<Shape>(r#"{"Empty":null}"#).unwrap(), Shape::Empty);
// A variant carrying a value takes the object form and only that.
assert_eq!(from_str::<Shape>(r#"{"Sides":6}"#).unwrap(), Shape::Sides(6));
assert_eq!(
from_str::<Shape>("\"Sides\"").unwrap_err().code,
ErrorCode::ExpectedBrace,
);What is refused is a name no variant claims, and that is refused under
every policy, including SkipUnknown. Stepping over
an unknown object key still leaves the object readable; stepping over an
unknown variant would leave the value itself undecided.
An object that is not exactly one member, {} or two tags at once, and a
value that is neither an object nor a string, are
ExpectedVariant. Under the object
form a variant carrying nothing wants null specifically, so {"Empty":0}
is ExpectedNull.
Reading reuses what the destination already holds when it is already the variant being read, so a loop that reads the same variant repeatedly keeps its payload’s buffers. Reading a different variant replaces the value, which is what changing variants means.
§What it expands to
One Variants impl carrying the name list and its
compile-time perfect hash, and then, for each format, ReadEnum and
Read/Write. Each write ends with a match over every declared
variant whose arms are empty, dead code that asks the compiler to confirm
the declaration names every variant the enum has, so extending the enum
later and forgetting to say so here is a compile error rather than a value
that silently writes nothing.
When a payload’s type cannot support both formats, declare the enum with
json_tagged_enum! or beve_tagged_enum! instead.
§Further reading
docs/enums.md is the long form: every error an enum can produce and what distinguishes it from the others, how the policies meet a tag, generics and borrowed payloads, the string array a run of unit variants becomes in BEVE, and how validation, pointers and transcoding walk through one.
A declaration that leads with write_only generates the write half alone,
so nothing in the type needs a Read impl or a Default; see
object!.