Two design decisions on one enum, each sensible on its own, that would have quietly fought each other if I’d let them. I didn’t, but only because the second one is easy to get wrong and the compiler wouldn’t have said a word either way.
Decision one: promise the list can grow
#[non_exhaustive] on the Feature enum. It tells downstream code it can’t match the enum exhaustively, so it has to keep a wildcard arm, which in turn means adding a variant later is a non-breaking, minor-version change. Nobody’s match stops compiling just because the enum grew. The doc comment says exactly that: it “keeps variant additions a minor-version change for downstream matchers.”
Decision two: hand out the whole list
A convenience all() returning every variant, because iterating over the lot is something you genuinely want to do. The tempting signature is a fixed-size array, [Feature; 11]: you know precisely how many there are, so why not put it in the type?
Why those two can’t both be true
The catch is a quirk of Rust that often trips up people arriving from other languages: the length of a fixed-size array is part of its type. [Feature; 11], an array of exactly eleven features, and [Feature; 12], exactly twelve, are not one type holding a different number of items the way they might be elsewhere. They are two genuinely different, incompatible types, about as interchangeable as i32 and i64.
So the moment you add a twelfth variant, a fixed-size all() forces an unhappy choice, and both options are bad. Bump the array to [Feature; 12] and you break every caller who wrote the old length down. Leave it at 11 and the new variant is silently dropped, leaving you a function called all that doesn’t return all of them. Either way the #[non_exhaustive] promise (adding a variant breaks nobody) is quietly cancelled by a return type that welded today’s count into the public API.
So all() returns a slice
Which is exactly what it does, and the doc comment spells out why, in crates/rtb-app/src/features.rs:
#[non_exhaustive]
pub enum Feature {
Init, Version, Update, Docs, Mcp, Doctor,
Ai, Telemetry, Config, Changelog, Credentials,
}
pub const fn all() -> &'static [Self] {
&[Self::Init, Self::Version, Self::Update, /* ...the rest... */]
}
A slice length is a value, not part of the type. Add a variant, the slice gets one longer, and not a single downstream signature changes. The promise holds.
The thing to watch for
#[non_exhaustive] is a promise about the future. A fixed-size array is a fact about the present. You can’t keep both at once, and nothing will warn you that you’ve contradicted yourself, because each decision is individually fine. The trap is always the second API surface that quietly re-bakes the flexibility the first one promised. When you mark a type “free to grow,” go and check that nothing in its public interface has secretly written down how big it is today.
