Skip to main content

unit_enum

Macro unit_enum 

Source
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 variantNo tag clauseas 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!.