macro_rules! unit_enum {
(write_only :: $($t:tt)*) => { ... };
(read_only $($t:tt)*) => { ... };
(write_only $($t:tt)*) => { ... };
($($t:tt)*) => { ... };
}Expand description
Declare an enum whose variants carry nothing, for every format.
The value on the wire is the variant’s name, as a string. Names default to
the Rust ones; give an explicit name with "name" => Variant when the
encoded spelling differs.
#[derive(Default, PartialEq, Debug)]
enum Level {
#[default]
Info,
Warning,
Error,
}
structio::unit_enum!(Level { Info, Warning, Error });
assert_eq!(structio::to_string(&Level::Warning), "\"Warning\"");
assert_eq!(structio::from_str::<Level>("\"Error\"").unwrap(), Level::Error);Renaming, which is how a Rust name that is not the wire name is handled:
structio::unit_enum!(Level {
"info" => Info,
"warning" => Warning,
});A variant may answer to more than one name, written after it and separated
by |, exactly as an object! field’s aliases are. The declared name is
the one written, and any of them is accepted on read, which is what renames
a variant without breaking the documents already written under the old
name.
structio::unit_enum!(Level {
"info" => Info | "INFO",
"warning" => Warning | "warn",
});/// A case rule renames the lot at once, and reads a variant name as words rather than as a snake_case string, so the capitals a Rust variant is spelled with are where it splits.
structio::unit_enum!(Mode as "kebab-case" { ReadOnly, ReadWrite });§Why it is a macro of its own
It will not compile if a variant carries a value, so the wire form is a
plain string and stays one. That promise is worth stating on its own for a
type whose encoding other people depend on, and in BEVE it pays for itself:
a value that can only be a string means a run of them is a string array,
one header for the whole run, so a Vec<Level> comes out byte for byte what
a Vec<String> of the same names would.
tagged_enum! cannot do that even for a declaration
that happens to be all unit variants, since a variant carrying a value
writes an object. Reading is unaffected either way: a sequence of enums
takes a string array or a generic one however it was written.
§Internal tagging
as tag "kind" puts the variant name inside the payload’s object rather
than in a key wrapping it:
| The variant | No tag clause | as tag "kind" |
|---|---|---|
| Carries nothing | "Empty" | {"kind":"Empty"} |
| Carries a value | {"Circle":{"radius":1}} | {"kind":"Circle","radius":1} |
structio::tagged_enum!(Shape as tag "kind" { Empty, Circle(_) });
assert_eq!(structio::to_string(&Shape::Circle(Circle { r: 1.0 })), r#"{"kind":"Circle","r":1}"#);
assert_eq!(structio::to_string(&Shape::Empty), r#"{"kind":"Empty"}"#);This is the shape most JSON APIs settled on, and the one a C++ Glaze
std::variant with a declared tag produces. It is the only form here that
a deduced variant can be made to agree with, external tagging having no
place to put the payload’s own keys.
The clause goes after the type and after a case rule, which
applies to the variant names as it does elsewhere. The tag key itself is a
literal and is never converted. A per-variant name literal
("read" => ReadFile(_)) and generics work as they do without the clause.
structio::tagged_enum!(Op as "kebab-case" tag "op" { Noop, ReadFile(_) });§The tag need not come first
A tag that comes first is read in one pass. One that comes later is
found by stepping over the members before it, which are read once the
members after it are, so a document whose keys were sorted reads the same
as one that put the tag first. An object with no tag at all is
ExpectedTag, reported against its
first key.
use structio::{ErrorCode, from_str};
assert_eq!(
from_str::<Shape>(r#"{"kind":"Circle","r":1}"#).unwrap(),
Shape::Circle(Circle { r: 1.0 }),
);
// The same members, the tag last.
assert_eq!(
from_str::<Shape>(r#"{"r":1,"kind":"Circle"}"#).unwrap(),
Shape::Circle(Circle { r: 1.0 }),
);
// No tag anywhere.
assert_eq!(
from_str::<Shape>(r#"{"r":1}"#).unwrap_err().code,
ErrorCode::ExpectedTag,
);§What a payload may be
An object, and nothing else. The variant’s members share one object with
the tag, so a payload with no members of its own to share has nowhere to
go: Sides(u32) is a compile error naming Keys, and then
WriteObject and its neighbours, rather than a
runtime surprise. Declare such a variant’s payload as a struct, or drop the
tag clause, external tagging taking any payload because it gives it an
object of its own.
A variant carrying nothing is written as the tag alone and reads back from
it. Members beside it meet the reader’s policy exactly as an unknown member
of a struct does: refused under Standard, stepped over
under SkipUnknown.
A tag that is also a payload’s field is refused at compile time. The
two share one object, so a collision would write the name twice
({"kind":"Config","kind":"debug"}), which this crate reads back and a
last-wins parser does not: it keeps the field and loses the variant. The
comparison is of wire names, so it catches a collision that only exists
after a case rule has been applied.
A declaration with no generics is checked by cargo check. A generic one
has no payload keys until it is instantiated, so it is checked when the
crate is built, which is Keys::REQUIRED’s tier.
§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, with BEVE’s Write also carrying the string array. The
names are hashed by the same KeyMap that finds a
struct’s fields.
§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!.