Skip to main content

enumerate

Attribute Macro enumerate 

Source
#[enumerate]
Expand description

Generates enums holding the types a trait is sealed to and macros rules to work with these enums.

Reads its type list from the #[sealed(..)] attribute below it, so it must be written above; attribute macros run top down, and #[sealed] consumes itself when it expands.

// on a concrete type `into_enum` needs the trait in scope; a generic
// `S: Shape` gets it from the supertrait bound
use closed_trait::Enumerable;

struct Square;
struct Circle;

#[enumerate]
#[sealed(Square, Circle)]
trait Shape {}

impl Shape for Square {}
impl Shape for Circle {}

let shape: AnyShape = Square.into_enum();
match shape {
    AnyShape::Square(_) => {},
    AnyShape::Circle(_) => {},
}

§The three enums

All three are generated by default, each with one variant per entry named after the type’s last path segment, and each taking the trait’s visibility. A type not in upper camel case therefore gives a variant that is not either (i32 yields an i32 variant), so the enums carry #[allow(non_camel_case_types)]: the name came from a type, not from a choice the caller made. as Name is there for anyone who would rather write I32. Given the base Shape sealed trait:

enumholdsreached from
AnyShapeSquareinto_enum, or From
AnyShapeRef<'a>&'a Squareas_enum_ref, From, or owned.as_ref()
AnyShapeMut<'a>&'a mut Squareas_enum_mut, From, or owned.as_mut()

Each brings a supertrait with it: Enumerable<AnyShape> and the higher-ranked for<'a> EnumerableRef<'a, AnyShapeRef<'a>> and its Mut counterpart, which is what makes the enums reachable from a generic S: Shape without naming them.

The borrowing pair is what into_enum cannot give you: taking self, it needs the value moved in, so a &S has no route to the owned enum at all. They are also cheaper to pass, being a pointer and a discriminant rather than as wide as the largest permitted type. The shared one derives Clone and Copy.

Conversions between the three (as_ref and as_mut on the owned enum, and as_ref on the unique one, which reborrows) come as inherent methods. no_bridge leaves them out.

§Options

Written bare, an option applies to all three enums. Written inside owned(..), ref(..) or mut(..) it applies to that one, and a specific option beats a grouped one.

§name = ..

Names the enums, which are Any{Trait}, Any{Trait}Ref and Any{Trait}Mut by default.

Grouped, it is a base that each kind extends, so name = "Shapes" gives Shapes, ShapesRef and ShapesMut. Specific, it is the name itself: ref(name = "ShapeView") gives exactly ShapeView.


#[enumerate(name = "Shapes", ref(name = "ShapeView"))]
#[sealed(Square)]
trait Shape {}

impl Shape for Square {}

let mut owned: Shapes        = Shapes::Square(Square);
let mutable:   ShapesMut<'_> = owned.as_mut();
let reference: ShapeView<'_> = mutable.as_ref();

§no_bridge

Prevents generating the conversion methods written on an enum. owned(no_bridge) drops as_ref(&self) and as_mut(&mut self); mut(no_bridge) drops the reborrowing as_ref(&self); a bare no_bridge drops all three. Nothing is written on the shared enum, so ref(no_bridge) is refused rather than silently doing nothing.

§attrs = ".."

Attributes to put on a generated enum, verbatim: derives, #[non_exhaustive], #[repr(..)], anything. It is the one option that must be specific. What is valid differs between the three: the shared enum already derives Copy, and the unique one cannot derive Clone at all, so one spelling spread across them would be a trap. A bare attrs is an error saying so.


#[enumerate(owned(attrs = "#[derive(Clone, Debug, PartialEq)] #[non_exhaustive]"))]
#[sealed(Square)]
trait Shape {}

Documentation is the exception: the enum’s docs are generated and identical for every sealed trait, so a /// or #[doc = ".."] here is an error.

§crate = ".."

Where the generated code should look for Enumerable. Defaults to ::closed_trait, which is right whenever closed-trait is a direct dependency under its own name.

It is wrong in two cases, and both fail with cannot find closed_trait in the crate root even though nothing in the caller’s source names it:

  • the dependency was renamed, as in st = { package = "closed-trait" };
  • the macros are reached through a re-export, so the caller does not depend on closed-trait at all.

Point it at whatever crate re-exports Enumerable:

#[enumerate(crate = "::closed_trait")]
#[sealed(Square)]
trait Shape {}

§match_any

Generates match_any_{trait}!, a macro that expands to a match over every variant. A match on the enum already tells the variants apart; what this adds is one body that runs against the concrete type, which a closure cannot express because Rust has no generic closures.


struct Square { side: i32 }
struct Circle { radius: i32 }

#[enumerate(match_any)]
#[sealed(Square, Circle)]
trait Shape { fn area(&self) -> i32; }


let shape = AnyShape::from(Square { side: 3 });
let area = match_any_shape!(shape, s => s.area());
assert_eq!(9, area);

The value may be given by &, by &mut or by value; match ergonomics make the binding follow it, so one macro covers all three. The binding has to be named by the caller.

It is a match, not a closure, and the difference is the point:

fn first_greater(shapes: &[AnyShape], value: i32) -> Option<i32> {
    for shape in shapes {
        // `return` leaves `first_big`, which a method taking the body could
        // never do
        match_any_shape!(shape, s => if s.area() > value { return Some(s.area()) });
    }
    None
}

The body may also move anything it owns, since only one arm ever runs, and it can be async. To handle some variants differently, match first and let the last arm fall through:

let cost = match &shape {
    AnyShape::Square(_) => 0,
    other => match_any_shape!(other, s => s.area()),
};

Two things follow from it being a macro. The enum and the trait must be in scope where it is called, because a macro_rules! body resolves paths at the call site. And the body is copied into every arm.

That copying is worth being deliberate about. It costs compile time in proportion to the number of variants, since the body is type-checked once per arm, and one mistake in it is reported once per arm too. Nesting one of these inside another squares the count.

The option takes an optional name, so match_any("match_shape") generates match_shape! instead. Whether the macro can leave the crate depends on the trait’s visibility, and so does whether it can collide, see Visibility.

§Visibility

Everything generated takes the trait’s own visibility: the three enums, the conversions between them, and the names the match macros are reached through. There is no option to change it: the enums appear in the trait’s supertrait bounds, so anything narrower would put a private type in a public interface.

It decides one thing beyond reach, though. A macro_rules! cannot leave the crate that defines it without #[macro_export], and that always plants it at the crate root. So a macro generated for a pub trait goes there under a hidden name and is aliased beside the enum, while one for any narrower trait simply stays where it was written:

the trait isthe macro isusable from another cratecan collide
pubat the crate root, aliased beside the enumyesyes
anything narrowerwhere it was writtennono

Colliding means two traits of the same name, in different modules, both asking for match_any: their hidden root names would be the same, and one of them needs match_any("other_name").

§Generics

A generic trait’s enum declares the parameters its variants are generic over, with their bounds, not those of the trait: an enum may not declare a parameter no variant uses. Where every entry pins its arguments, none is left to declare and the enum is plain:


// every entry fixes its argument, so `AnyValue` is a plain enum
#[enumerate]
#[sealed(
    i32: Value<i32>,
    f64: Value<f64>
)]
trait Value<T> {}

impl Value<i32> for i32 {}
impl Value<f64> for f64 {}

let _: AnyValue = AnyValue::i32(6);

§for<..> entries

Parameters declared in a for<..> binder are the entry’s own and never reach the enum. Each is passed to the trait as an argument, and the parameter it lands on, carrying the bounds the binder gave it, is what the enum declares:

struct Boxed<U>(U);

#[enumerate]
#[sealed(for<U> Boxed<U>: Store<U>)] // here `U` is used in place of `T` declared by Store
trait Store<T> {}

impl<V> Store<V> for Boxed<V> {}

let _: AnyStore<u8> = Boxed(1u8).into_enum();

A name never passed that way lands on no parameter, so the variant has nothing to be generic over and the entry is refused: under a trait declaring none at all, for<U> Boxed<U>: Shape leaves U free. #[sealed] accepts it, but the enum cannot.

§Pinned entries and match_any

An entry pins its arguments when it names a concrete instantiation instead of the trait’s parameters. Such an entry becomes a variant like any other, and the enum does not record which instantiation that variant belongs to.

into_enum and From exist only at the instantiations the entry named, so nothing ever builds a variant that does not belong. Pinning therefore works with enumerate.

struct Plain;

#[enumerate]
#[sealed(Plain: Store<i32>)]
pub trait Store<T> {}

impl Store<i32> for Plain {}

let _: AnyStore = Plain.into_enum();

What such an entry rules out is match_any. Nothing stops the trait from being named at an instantiation no permitted type implements, and that is precisely where a body may ask the macro to expand. This is what it would become there, written out by hand:

#[enumerate]
#[sealed(i32: Value<i32>)]
trait Value<T> {}

impl Value<i32> for i32 {}

fn describe(value: impl Value<String>) {
    // what `match_any_value!(value.into_enum(), v => takes(v))` becomes
    match value.into_enum() {
        // error: the trait bound `i32: Value<String>` is not satisfied
        AnyValue::i32(v) => takes(v),
    }
}

fn takes<V: Value<String>>(_: V) {}

The enum is fine, and so is the signature: Value<String> is a legal bound, merely one that nothing satisfies. Drop the match and it compiles on its own:

fn describe(_: impl Value<String>) {}

What cannot hold is the match_any. AnyValue::i32 hands back an i32, which is a Value<i32> and nothing else, so a body written against Value<String> cannot use it. Rather than generate that and let it fail inside the caller’s code, #[enumerate] refuses it where the list is written.

A permitted type that is not Sized cannot be held in a variant. That one is rustc’s to report rather than this macro’s, since sizedness is not visible in the tokens.