Skip to content

Commit b065d14

Browse files
authored
Add Enumeration::values() static slice for variant iteration (#49)
Partially addresses #23 (at least, the most common use case for enum attributes). Generated enums now expose `Enumeration::values()`, returning a `&'static [Self]` slice of every primary variant in proto declaration order. This subsumes the most common reason users reach for `strum::EnumIter` (the example in #23) and additionally enables `.contains()`, `.len()`, indexing, binary search, etc. ## Example For ```protobuf enum Status { UNSPECIFIED = 0; ACTIVE = 1; INACTIVE = 2; } ``` users can now write ```rust for v in Status::values() { println!("{:?} = {}", v, v.to_i32()); } assert!(Status::values().contains(&Status::ACTIVE)); assert_eq!(Status::values().len(), 3); ``` ## Behavior - **Aliases** (additional names sharing an existing value when `option allow_alias = true`) are skipped — they remain accessible as the existing `pub const` aliases on the enum, but they aren't enum variants in Rust so they don't belong in `values()`. - **Order** matches the `.proto` declaration order, which is also the order `from_i32` resolves to (so `Status::values()[i].to_i32()` lines up with `from_i32` for unique values). ## Trait change `Enumeration::values` is added with a default implementation returning an empty slice so out-of-tree consumers implementing `Enumeration` against an older buffa version continue to compile. Codegen unconditionally overrides the default with the real impl. ## Tests - New unit tests in `tests/generation.rs`: `test_enum_values_emits_static_slice_in_declaration_order` and `test_enum_values_skips_aliases`. - Workspace tests, clippy `-D warnings`, rustfmt, and markdownlint clean. - `task gen-wkt-types` and `task gen-bootstrap-types` run; the resulting drift in `buffa-types/src/generated/` and `buffa-descriptor/src/generated/` is just the new `values()` impls. ## Follow-up A separate PR will add `enum_attribute` (the symmetric inverse of `message_attribute` from #44) for users who want to inject Rust attributes onto enums but not messages.
1 parent fc7ceb5 commit b065d14

File tree

7 files changed

+234
-0
lines changed

7 files changed

+234
-0
lines changed

buffa-codegen/src/enumeration.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,11 @@ pub fn generate_enum(
116116
let mut from_i32_arms = Vec::new();
117117
let mut from_proto_name_arms: Vec<TokenStream> = Vec::new();
118118
let mut proto_name_arms = Vec::new();
119+
// Static slice for `Enumeration::values()`. Aliases are skipped — the
120+
// slice mirrors the *primary* declaration order, matching what
121+
// `from_i32` resolves to (so `MyEnum::values()[i].to_i32() ==
122+
// from_i32(...).unwrap().to_i32()` for unique values).
123+
let mut value_idents: Vec<Ident> = Vec::new();
119124
// Track the best candidate for Default: prefer value == 0 (proto3 default),
120125
// fall back to the first primary variant.
121126
let mut zero_variant: Option<Ident> = None;
@@ -162,6 +167,7 @@ pub fn generate_enum(
162167
proto_name_arms.push(quote! {
163168
Self::#variant_ident => #value_name
164169
});
170+
value_idents.push(variant_ident);
165171
}
166172
}
167173

@@ -257,6 +263,10 @@ pub fn generate_enum(
257263
_ => ::core::option::Option::None,
258264
}
259265
}
266+
267+
fn values() -> &'static [Self] {
268+
&[#(Self::#value_idents),*]
269+
}
260270
}
261271
})
262272
}

buffa-codegen/src/tests/generation.rs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,72 @@ fn test_enum_with_alias() {
153153
);
154154
}
155155

156+
#[test]
157+
fn test_enum_values_emits_static_slice_in_declaration_order() {
158+
let mut file = proto3_file("status.proto");
159+
file.enum_type.push(EnumDescriptorProto {
160+
name: Some("Status".to_string()),
161+
value: vec![
162+
enum_value("UNKNOWN", 0),
163+
enum_value("ACTIVE", 1),
164+
enum_value("INACTIVE", 2),
165+
],
166+
..Default::default()
167+
});
168+
169+
let files = generate(
170+
&[file],
171+
&["status.proto".to_string()],
172+
&CodeGenConfig::default(),
173+
)
174+
.expect("enum should generate");
175+
let content = &files[0].content;
176+
// Generated impl includes a `values()` returning a static slice in
177+
// proto declaration order. The generated body collapses to a single
178+
// line so we match against that exact form.
179+
assert!(
180+
content.contains("&[Self::UNKNOWN, Self::ACTIVE, Self::INACTIVE]"),
181+
"missing declaration-order values() slice: {content}"
182+
);
183+
}
184+
185+
#[test]
186+
fn test_enum_values_skips_aliases() {
187+
let mut file = proto3_file("code.proto");
188+
file.enum_type.push(EnumDescriptorProto {
189+
name: Some("Code".to_string()),
190+
value: vec![
191+
enum_value("OK", 0),
192+
enum_value("SUCCESS", 0), // alias for OK — not its own variant
193+
enum_value("ERROR", 1),
194+
],
195+
options: (crate::generated::descriptor::EnumOptions {
196+
allow_alias: Some(true),
197+
..Default::default()
198+
})
199+
.into(),
200+
..Default::default()
201+
});
202+
203+
let files = generate(
204+
&[file],
205+
&["code.proto".to_string()],
206+
&CodeGenConfig::default(),
207+
)
208+
.expect("aliased enum should generate");
209+
let content = &files[0].content;
210+
// values() lists only primary variants — aliases are `pub const` items,
211+
// not enum variants, so they don't belong in the slice.
212+
assert!(
213+
content.contains("&[Self::OK, Self::ERROR]"),
214+
"values() should mirror primary variants only: {content}"
215+
);
216+
assert!(
217+
!content.contains("Self::SUCCESS"),
218+
"alias `SUCCESS` must not appear in values(): {content}"
219+
);
220+
}
221+
156222
#[test]
157223
fn test_file_not_found_error() {
158224
let file = proto3_file("other.proto");

buffa-descriptor/src/generated/google.protobuf.compiler.plugin.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,13 @@ pub mod code_generator_response {
732732
_ => ::core::option::Option::None,
733733
}
734734
}
735+
fn values() -> &'static [Self] {
736+
&[
737+
Self::FEATURE_NONE,
738+
Self::FEATURE_PROTO3_OPTIONAL,
739+
Self::FEATURE_SUPPORTS_EDITIONS,
740+
]
741+
}
735742
}
736743
/// Represents a single generated file.
737744
#[derive(Clone, PartialEq, Default)]

buffa-descriptor/src/generated/google.protobuf.descriptor.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,23 @@ impl ::buffa::Enumeration for Edition {
107107
_ => ::core::option::Option::None,
108108
}
109109
}
110+
fn values() -> &'static [Self] {
111+
&[
112+
Self::EDITION_UNKNOWN,
113+
Self::EDITION_LEGACY,
114+
Self::EDITION_PROTO2,
115+
Self::EDITION_PROTO3,
116+
Self::EDITION_2023,
117+
Self::EDITION_2024,
118+
Self::EDITION_UNSTABLE,
119+
Self::EDITION_1_TEST_ONLY,
120+
Self::EDITION_2_TEST_ONLY,
121+
Self::EDITION_99997_TEST_ONLY,
122+
Self::EDITION_99998_TEST_ONLY,
123+
Self::EDITION_99999_TEST_ONLY,
124+
Self::EDITION_MAX,
125+
]
126+
}
110127
}
111128
/// Describes the 'visibility' of a symbol with respect to the proto import
112129
/// system. Symbols can only be imported when the visibility rules do not prevent
@@ -152,6 +169,9 @@ impl ::buffa::Enumeration for SymbolVisibility {
152169
_ => ::core::option::Option::None,
153170
}
154171
}
172+
fn values() -> &'static [Self] {
173+
&[Self::VISIBILITY_UNSET, Self::VISIBILITY_LOCAL, Self::VISIBILITY_EXPORT]
174+
}
155175
}
156176
/// The protocol compiler can output a FileDescriptorSet containing the .proto
157177
/// files it parses.
@@ -1809,6 +1829,9 @@ pub mod extension_range_options {
18091829
_ => ::core::option::Option::None,
18101830
}
18111831
}
1832+
fn values() -> &'static [Self] {
1833+
&[Self::DECLARATION, Self::UNVERIFIED]
1834+
}
18121835
}
18131836
#[derive(Clone, PartialEq, Default)]
18141837
pub struct Declaration {
@@ -2603,6 +2626,28 @@ pub mod field_descriptor_proto {
26032626
_ => ::core::option::Option::None,
26042627
}
26052628
}
2629+
fn values() -> &'static [Self] {
2630+
&[
2631+
Self::TYPE_DOUBLE,
2632+
Self::TYPE_FLOAT,
2633+
Self::TYPE_INT64,
2634+
Self::TYPE_UINT64,
2635+
Self::TYPE_INT32,
2636+
Self::TYPE_FIXED64,
2637+
Self::TYPE_FIXED32,
2638+
Self::TYPE_BOOL,
2639+
Self::TYPE_STRING,
2640+
Self::TYPE_GROUP,
2641+
Self::TYPE_MESSAGE,
2642+
Self::TYPE_BYTES,
2643+
Self::TYPE_UINT32,
2644+
Self::TYPE_ENUM,
2645+
Self::TYPE_SFIXED32,
2646+
Self::TYPE_SFIXED64,
2647+
Self::TYPE_SINT32,
2648+
Self::TYPE_SINT64,
2649+
]
2650+
}
26062651
}
26072652
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
26082653
#[repr(i32)]
@@ -2647,6 +2692,9 @@ pub mod field_descriptor_proto {
26472692
_ => ::core::option::Option::None,
26482693
}
26492694
}
2695+
fn values() -> &'static [Self] {
2696+
&[Self::LABEL_OPTIONAL, Self::LABEL_REPEATED, Self::LABEL_REQUIRED]
2697+
}
26502698
}
26512699
}
26522700
/// Describes a oneof.
@@ -4639,6 +4687,9 @@ pub mod file_options {
46394687
_ => ::core::option::Option::None,
46404688
}
46414689
}
4690+
fn values() -> &'static [Self] {
4691+
&[Self::SPEED, Self::CODE_SIZE, Self::LITE_RUNTIME]
4692+
}
46424693
}
46434694
}
46444695
#[derive(Clone, PartialEq, Default)]
@@ -5624,6 +5675,9 @@ pub mod field_options {
56245675
_ => ::core::option::Option::None,
56255676
}
56265677
}
5678+
fn values() -> &'static [Self] {
5679+
&[Self::STRING, Self::CORD, Self::STRING_PIECE]
5680+
}
56275681
}
56285682
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
56295683
#[repr(i32)]
@@ -5667,6 +5721,9 @@ pub mod field_options {
56675721
_ => ::core::option::Option::None,
56685722
}
56695723
}
5724+
fn values() -> &'static [Self] {
5725+
&[Self::JS_NORMAL, Self::JS_STRING, Self::JS_NUMBER]
5726+
}
56705727
}
56715728
/// If set to RETENTION_SOURCE, the option will be omitted from the binary.
56725729
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
@@ -5714,6 +5771,9 @@ pub mod field_options {
57145771
_ => ::core::option::Option::None,
57155772
}
57165773
}
5774+
fn values() -> &'static [Self] {
5775+
&[Self::RETENTION_UNKNOWN, Self::RETENTION_RUNTIME, Self::RETENTION_SOURCE]
5776+
}
57175777
}
57185778
/// This indicates the types of entities that the field may apply to when used
57195779
/// as an option. If it is unset, then the field may be freely used as an
@@ -5805,6 +5865,20 @@ pub mod field_options {
58055865
_ => ::core::option::Option::None,
58065866
}
58075867
}
5868+
fn values() -> &'static [Self] {
5869+
&[
5870+
Self::TARGET_TYPE_UNKNOWN,
5871+
Self::TARGET_TYPE_FILE,
5872+
Self::TARGET_TYPE_EXTENSION_RANGE,
5873+
Self::TARGET_TYPE_MESSAGE,
5874+
Self::TARGET_TYPE_FIELD,
5875+
Self::TARGET_TYPE_ONEOF,
5876+
Self::TARGET_TYPE_ENUM,
5877+
Self::TARGET_TYPE_ENUM_ENTRY,
5878+
Self::TARGET_TYPE_SERVICE,
5879+
Self::TARGET_TYPE_METHOD,
5880+
]
5881+
}
58085882
}
58095883
#[derive(Clone, PartialEq, Default)]
58105884
pub struct EditionDefault {
@@ -7266,6 +7340,9 @@ pub mod method_options {
72667340
_ => ::core::option::Option::None,
72677341
}
72687342
}
7343+
fn values() -> &'static [Self] {
7344+
&[Self::IDEMPOTENCY_UNKNOWN, Self::NO_SIDE_EFFECTS, Self::IDEMPOTENT]
7345+
}
72697346
}
72707347
}
72717348
/// A message representing a option the parser does not recognize. This only
@@ -8079,6 +8156,14 @@ pub mod feature_set {
80798156
_ => ::core::option::Option::None,
80808157
}
80818158
}
8159+
fn values() -> &'static [Self] {
8160+
&[
8161+
Self::FIELD_PRESENCE_UNKNOWN,
8162+
Self::EXPLICIT,
8163+
Self::IMPLICIT,
8164+
Self::LEGACY_REQUIRED,
8165+
]
8166+
}
80828167
}
80838168
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
80848169
#[repr(i32)]
@@ -8121,6 +8206,9 @@ pub mod feature_set {
81218206
_ => ::core::option::Option::None,
81228207
}
81238208
}
8209+
fn values() -> &'static [Self] {
8210+
&[Self::ENUM_TYPE_UNKNOWN, Self::OPEN, Self::CLOSED]
8211+
}
81248212
}
81258213
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
81268214
#[repr(i32)]
@@ -8167,6 +8255,9 @@ pub mod feature_set {
81678255
_ => ::core::option::Option::None,
81688256
}
81698257
}
8258+
fn values() -> &'static [Self] {
8259+
&[Self::REPEATED_FIELD_ENCODING_UNKNOWN, Self::PACKED, Self::EXPANDED]
8260+
}
81708261
}
81718262
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
81728263
#[repr(i32)]
@@ -8209,6 +8300,9 @@ pub mod feature_set {
82098300
_ => ::core::option::Option::None,
82108301
}
82118302
}
8303+
fn values() -> &'static [Self] {
8304+
&[Self::UTF8_VALIDATION_UNKNOWN, Self::VERIFY, Self::NONE]
8305+
}
82128306
}
82138307
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
82148308
#[repr(i32)]
@@ -8251,6 +8345,9 @@ pub mod feature_set {
82518345
_ => ::core::option::Option::None,
82528346
}
82538347
}
8348+
fn values() -> &'static [Self] {
8349+
&[Self::MESSAGE_ENCODING_UNKNOWN, Self::LENGTH_PREFIXED, Self::DELIMITED]
8350+
}
82548351
}
82558352
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
82568353
#[repr(i32)]
@@ -8295,6 +8392,9 @@ pub mod feature_set {
82958392
_ => ::core::option::Option::None,
82968393
}
82978394
}
8395+
fn values() -> &'static [Self] {
8396+
&[Self::JSON_FORMAT_UNKNOWN, Self::ALLOW, Self::LEGACY_BEST_EFFORT]
8397+
}
82988398
}
82998399
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
83008400
#[repr(i32)]
@@ -8337,6 +8437,9 @@ pub mod feature_set {
83378437
_ => ::core::option::Option::None,
83388438
}
83398439
}
8440+
fn values() -> &'static [Self] {
8441+
&[Self::ENFORCE_NAMING_STYLE_UNKNOWN, Self::STYLE2024, Self::STYLE_LEGACY]
8442+
}
83408443
}
83418444
#[derive(Clone, PartialEq, Default)]
83428445
pub struct VisibilityFeature {
@@ -8485,6 +8588,15 @@ pub mod feature_set {
84858588
_ => ::core::option::Option::None,
84868589
}
84878590
}
8591+
fn values() -> &'static [Self] {
8592+
&[
8593+
Self::DEFAULT_SYMBOL_VISIBILITY_UNKNOWN,
8594+
Self::EXPORT_ALL,
8595+
Self::EXPORT_TOP_LEVEL,
8596+
Self::LOCAL_ALL,
8597+
Self::STRICT,
8598+
]
8599+
}
84888600
}
84898601
}
84908602
}
@@ -9837,6 +9949,9 @@ pub mod generated_code_info {
98379949
_ => ::core::option::Option::None,
98389950
}
98399951
}
9952+
fn values() -> &'static [Self] {
9953+
&[Self::NONE, Self::SET, Self::ALIAS]
9954+
}
98409955
}
98419956
}
98429957
}

buffa-types/src/generated/google.protobuf.struct.rs

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)